1import { assert, it } from "@effect/vitest";
2import { RunId, ThreadId, TurnItemId } from "@t3tools/contracts";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as SqlClient from "effect/sql/SqlClient";
6
7import * as SqlitePersistence from "../persistence/Sqlite.ts";
8import * as TurnItemPositionStore from "./TurnItemPositionStore.ts";
9
10const layerTest = TurnItemPositionStore.layer.pipe(
11 Layer.provideMerge(SqlitePersistence.layerMemory),
12);
13
14it.effect("reuses an item's position without attempting another allocation insert", () =>
15 Effect.gen(function* () {
16 const sql = yield* SqlClient.SqlClient;
17 const positions = yield* TurnItemPositionStore.TurnItemPositionStoreV2;
18 const input = {
19 threadId: ThreadId.make("thread:position-reuse"),
20 turnItemId: TurnItemId.make("item:streaming"),
21 runId: null,
22 };
23 const first = yield* positions.allocate(input);
24 assert.equal(first, 1);
25
26 // BEFORE INSERT runs even when ON CONFLICT would discard the allocation.
27 yield* sql`
28 CREATE TEMP TRIGGER reject_redundant_position_insert
29 BEFORE INSERT ON orchestration_v2_turn_item_positions
30 WHEN EXISTS (
31 SELECT 1 FROM orchestration_v2_turn_item_positions
32 WHERE thread_id = NEW.thread_id AND turn_item_id = NEW.turn_item_id
33 )
34 BEGIN
35 SELECT RAISE(ABORT, 'Existing item positions must be reused');
36 END
37 `;
38
39 const next = yield* positions.allocate({
40 ...input,
41 turnItemId: TurnItemId.make("item:next"),
42 });
43 assert.equal(next, first + 1);
44 assert.equal(yield* positions.allocate(input), first);
45 assert.equal(
46 yield* positions.allocate({
47 ...input,
48 runId: RunId.make("run:moved"),
49 runOrdinal: 2,
50 }),
51 first,
52 );
53 assert.equal(
54 yield* positions.allocate({ ...input, threadId: ThreadId.make("thread:other") }),
55 1,
56 );
57 }).pipe(Effect.provide(layerTest)),
58);
59
60it.effect("places a run-start item ahead of the run's items, then appends once taken", () =>
61 Effect.gen(function* () {
62 const positions = yield* TurnItemPositionStore.TurnItemPositionStoreV2;
63 const threadId = ThreadId.make("thread:run-start");
64 const allocate = (id: string, placement?: "run_start") =>
65 positions.allocate({
66 threadId,
67 turnItemId: TurnItemId.make(id),
68 runId: RunId.make("run:2"),
69 runOrdinal: 2,
70 ...(placement === undefined ? {} : { placement }),
71 });
72 const message = yield* allocate("item:message");
73 const recovery = yield* allocate("item:recovery", "run_start");
74 assert.isBelow(recovery, message);
75 assert.isAbove(recovery, 1_999_999);
76 assert.equal(yield* allocate("item:retry-recovery", "run_start"), message + 1);
77 assert.equal(yield* allocate("item:reply"), message + 2);
78 }).pipe(Effect.provide(layerTest)),
79);