1import { assert, it } from "@effect/vitest";
2import {
3 ContextTransferId,
4 MessageId,
5 ProjectId,
6 ProviderInstanceId,
7 ProviderThreadId,
8 RunId,
9 ThreadId,
10 TurnItemId,
11 type OrchestrationV2AppThread,
12 type OrchestrationV2TurnItem,
13} from "@t3tools/contracts";
14import * as DateTime from "effect/DateTime";
15import * as Effect from "effect/Effect";
16import * as Layer from "effect/Layer";
17β
18import * as ContextHandoffService from "./ContextHandoffService.ts";
19import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
20import { ProjectionStoreThreadNotFoundError } from "./ProjectionStore.ts";
21β
22const layerTest = ContextHandoffService.layer.pipe(Layer.provide(IdAllocator.layer));
23β
24function importedItem(
25 input:
26 | {
27 readonly role: "user";
28 readonly id: string;
29 readonly text: string;
30 readonly ordinal: number;
31 }
32 | {
33 readonly role: "assistant";
34 readonly id: string;
35 readonly text: string;
36 readonly ordinal: number;
37 },
38): OrchestrationV2TurnItem {
39 const now = DateTime.makeUnsafe("2026-01-01T00:00:00.000Z");
40 const base = {
41 id: TurnItemId.make(`turn-item:${input.id}`),
42 threadId: ThreadId.make("thread:legacy-context"),
43 runId: null,
44 nodeId: null,
45 providerThreadId: null,
46 providerTurnId: null,
47 nativeItemRef: null,
48 parentItemId: null,
49 ordinal: input.ordinal,
50 status: "completed" as const,
51 title: null,
52 startedAt: now,
53 completedAt: now,
54 updatedAt: now,
55 messageId: MessageId.make(`message:${input.id}`),
56 text: input.text,
57 };
58 return input.role === "user"
59 ? {
60 ...base,
61 createdBy: "user",
62 creationSource: "server",
63 type: "user_message",
64 inputIntent: "turn_start",
65 attachments: [],
66 }
67 : {
68 ...base,
69 type: "assistant_message",
70 streaming: false,
71 };
72}
73β
74it.layer(layerTest)("ContextHandoffService legacy import", (it) => {
75 it.effect("prepares imported history for the first native v2 turn", () =>
76 Effect.gen(function* () {
77 const service = yield* ContextHandoffService.ContextHandoffServiceV2;
78 const handoff = yield* service.prepareLegacyImport({
79 threadId: ThreadId.make("thread:legacy-context"),
80 targetRunId: RunId.make("run:first-v2"),
81 toProviderThreadId: ProviderThreadId.make("provider-thread:first-v2"),
82 toProviderInstanceId: ProviderInstanceId.make("codex"),
83 items: [
84 importedItem({ role: "user", id: "one", text: "What did we decide?", ordinal: 1 }),
85 importedItem({
86 role: "assistant",
87 id: "two",
88 text: "We decided to keep the migration lightweight.",
89 ordinal: 2,
90 }),
91 ],
92 createdAt: DateTime.makeUnsafe("2026-01-02T00:00:00.000Z"),
93 });
94β
95 assert.equal(handoff.strategy, "manual_context");
96 assert.deepStrictEqual(handoff.fromProviderThreadIds, []);
97 assert.include(handoff.summaryText, "What did we decide?");
98 assert.include(handoff.summaryText, "keep the migration lightweight");
99 }),
100 );
101β
102 it.effect("preserves role attribution when truncating imported history", () =>
103 Effect.gen(function* () {
104 const service = yield* ContextHandoffService.ContextHandoffServiceV2;
105 const handoff = yield* service.prepareLegacyImport({
106 threadId: ThreadId.make("thread:legacy-context"),
107 targetRunId: RunId.make("run:first-v2"),
108 toProviderThreadId: ProviderThreadId.make("provider-thread:first-v2"),
109 toProviderInstanceId: ProviderInstanceId.make("codex"),
110 items: [
111 importedItem({
112 role: "user",
113 id: "long",
114 text: `${"x".repeat(35_000)} retained final words`,
115 ordinal: 1,
116 }),
117 ],
118 createdAt: DateTime.makeUnsafe("2026-01-02T00:00:00.000Z"),
119 });
120β
121 assert.isAtMost(handoff.summaryText.length, 32_000);
122 assert.include(handoff.summaryText, "User:\n... retained final words");
123 assert.notMatch(handoff.summaryText, /\n+x+ retained final words/);
124 }),
125 );
126β
127 it.effect("retains the newest oversized import even when it has no whitespace", () =>
128 Effect.gen(function* () {
129 const service = yield* ContextHandoffService.ContextHandoffServiceV2;
130 const handoff = yield* service.prepareLegacyImport({
131 threadId: ThreadId.make("thread:legacy-context"),
132 targetRunId: RunId.make("run:first-v2"),
133 toProviderThreadId: ProviderThreadId.make("provider-thread:first-v2"),
134 toProviderInstanceId: ProviderInstanceId.make("codex"),
135 items: [
136 importedItem({
137 role: "assistant",
138 id: "older",
139 text: "older message",
140 ordinal: 1,
141 }),
142 importedItem({
143 role: "user",
144 id: "long-single-token",
145 text: `${"π§ͺ".repeat(20_000)}LATEST_SINGLE_TOKEN`,
146 ordinal: 2,
147 }),
148 ],
149 createdAt: DateTime.makeUnsafe("2026-01-02T00:00:00.000Z"),
150 });
151β
152 assert.include(
153 handoff.history?.omittedItemIds ?? [],
154 TurnItemId.make("turn-item:long-single-token"),
155 );
156 assert.isAtMost(handoff.summaryText.length, 32_000);
157 assert.include(handoff.summaryText, "User:\n... ");
158 assert.include(handoff.summaryText, "LATEST_SINGLE_TOKEN");
159 assert.notInclude(handoff.summaryText, "\ufffd");
160 }),
161 );
162});
163β
164it.effect("caps the side chat context block without summarizing discarded parent history", () =>
165 Effect.gen(function* () {
166 const parentThreadId = ThreadId.make("thread:side-chat-parent");
167 let summarizedItems = 0;
168 const parentItems = Array.from({ length: 60 }, (_, index) => ({
169 ...importedItem({
170 role: index % 2 === 0 ? "user" : "assistant",
171 id: `parent-${index}`,
172 text: `item-${index} ${"x".repeat(228)}π${"x".repeat(70)}`,
173 ordinal: index,
174 }),
175 get text() {
176 summarizedItems++;
177 return `item-${index} ${"x".repeat(228)}π${"x".repeat(70)}`;
178 },
179 }));
180 const service = yield* ContextHandoffService.ContextHandoffServiceV2;
181 const handoff = yield* service.prepareSideChatContext({
182 parentThreadId,
183 threadId: ThreadId.make("thread:side-chat"),
184 targetRunId: RunId.make("run:side-chat"),
185 transferId: ContextTransferId.make("transfer:side-chat"),
186 toProviderThreadId: ProviderThreadId.make("provider-thread:side-chat"),
187 fromProviderInstanceId: ProviderInstanceId.make("codex"),
188 toProviderInstanceId: ProviderInstanceId.make("codex"),
189 coveredRunOrdinals: { from: 1, to: 1 },
190 parentItems,
191 createdAt: DateTime.makeUnsafe(0),
192 });
193 const context = handoff.summaryText;
194 assert.isBelow(summarizedItems, parentItems.length);
195β
196 assert.include(context, "- Earlier items truncated.");
197 assert.notInclude(context, "item-0 ");
198 assert.include(context, "- Assistant: item-59 ");
199 assert.isAtMost(context.length, 8_000);
200 assert.isTrue(context.isWellFormed());
201 assert.isTrue(
202 context.endsWith(
203 `Use t3_thread_read on thread ${parentThreadId} to see the main thread's live state.`,
204 ),
205 );
206β
207 const framingOnly = ContextHandoffService.makeSideChatContext({
208 parentThreadId,
209 digest: [],
210 });
211 assert.isTrue(framingOnly.startsWith("You are in a side chat beside the main thread."));
212 assert.notInclude(framingOnly, "Main thread activity");
213β
214 const multiFilePatch = ContextHandoffService.makeSideChatContext({
215 parentThreadId,
216 digest: [
217 {
218 id: TurnItemId.make("turn-item:patch"),
219 threadId: parentThreadId,
220 runId: null,
221 nodeId: null,
222 providerThreadId: null,
223 providerTurnId: null,
224 nativeItemRef: null,
225 parentItemId: null,
226 ordinal: 60,
227 status: "completed",
228 title: null,
229 startedAt: null,
230 completedAt: null,
231 updatedAt: DateTime.makeUnsafe(0),
232 type: "file_change",
233 fileName: "src/a.ts",
234 changes: [
235 { operation: "update", path: "src/a.ts" },
236 { operation: "add", path: "src/b.ts" },
237 { operation: "update", path: "src/new.ts", oldPath: "src/old.ts" },
238 ],
239 },
240 ],
241 });
242 assert.include(multiFilePatch, "- File change: src/a.ts, src/b.ts, src/new.ts");
243 }).pipe(Effect.provide(layerTest)),
244);
245β
246it("covers the run range of a very long delta", () => {
247 const runs = [
248 { id: RunId.make("run:long-delta-early"), ordinal: 2 },
249 { id: RunId.make("run:long-delta-late"), ordinal: 5 },
250 ];
251 const items = Array.from({ length: 150_000 }, (_, index) => ({
252 runId: runs[index % 2]!.id,
253 }));
254β
255 assert.deepEqual(
256 ContextHandoffService.visibleDeltaRunOrdinals({ runs }, [...items, { runId: null }]),
257 { from: 2, to: 5 },
258 );
259});
260β
261it.effect("inherited history stops at a missing fork ancestor", () =>
262 Effect.gen(function* () {
263 const now = DateTime.makeUnsafe("2026-01-01T00:00:00.000Z");
264 const threadId = ThreadId.make("thread:fork-of-missing");
265 const thread: OrchestrationV2AppThread = {
266 createdBy: "user",
267 creationSource: "web",
268 id: threadId,
269 projectId: ProjectId.make("project:fork-of-missing"),
270 title: "Fork of a missing thread",
271 providerInstanceId: ProviderInstanceId.make("codex"),
272 modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
273 runtimeMode: "full-access",
274 interactionMode: "default",
275 branch: null,
276 worktreePath: null,
277 activeProviderThreadId: null,
278 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
279 forkedFrom: {
280 type: "run",
281 threadId: ThreadId.make("thread:missing-ancestor"),
282 runId: RunId.make("run:missing-ancestor"),
283 },
284 createdAt: now,
285 updatedAt: now,
286 archivedAt: null,
287 settledOverride: null,
288 settledAt: null,
289 lastVisitedAt: null,
290 snoozedUntil: null,
291 snoozedAt: null,
292 deletedAt: null,
293 };
294 const items = yield* ContextHandoffService.readInheritedHandoffItems(
295 {
296 getThreadRecords: (missing) =>
297 Effect.fail(new ProjectionStoreThreadNotFoundError({ threadId: missing })),
298 },
299 thread,
300 );
301 assert.deepEqual(items, []);
302 }),
303);