| 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import { |
| 3 | MessageId, |
| 4 | ProviderInstanceId, |
| 5 | ProviderThreadId, |
| 6 | RunId, |
| 7 | ThreadId, |
| 8 | TurnItemId, |
| 9 | type OrchestrationV2TurnItem, |
| 10 | } from "@t3tools/contracts"; |
| 11 | import * as DateTime from "effect/DateTime"; |
| 12 | import * as Effect from "effect/Effect"; |
| 13 | import * as Layer from "effect/Layer"; |
| 14 | |
| 15 | import * as ContextHandoffService from "./ContextHandoffService.ts"; |
| 16 | import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; |
| 17 | |
| 18 | const layerTest = ContextHandoffService.layer.pipe(Layer.provide(IdAllocator.layer)); |
| 19 | |
| 20 | function importedItem( |
| 21 | input: |
| 22 | | { |
| 23 | readonly role: "user"; |
| 24 | readonly id: string; |
| 25 | readonly text: string; |
| 26 | readonly ordinal: number; |
| 27 | } |
| 28 | | { |
| 29 | readonly role: "assistant"; |
| 30 | readonly id: string; |
| 31 | readonly text: string; |
| 32 | readonly ordinal: number; |
| 33 | }, |
| 34 | ): OrchestrationV2TurnItem { |
| 35 | const now = DateTime.makeUnsafe("2026-01-01T00:00:00.000Z"); |
| 36 | const base = { |
| 37 | id: TurnItemId.make(`turn-item:${input.id}`), |
| 38 | threadId: ThreadId.make("thread:legacy-context"), |
| 39 | runId: null, |
| 40 | nodeId: null, |
| 41 | providerThreadId: null, |
| 42 | providerTurnId: null, |
| 43 | nativeItemRef: null, |
| 44 | parentItemId: null, |
| 45 | ordinal: input.ordinal, |
| 46 | status: "completed" as const, |
| 47 | title: null, |
| 48 | startedAt: now, |
| 49 | completedAt: now, |
| 50 | updatedAt: now, |
| 51 | messageId: MessageId.make(`message:${input.id}`), |
| 52 | text: input.text, |
| 53 | }; |
| 54 | return input.role === "user" |
| 55 | ? { |
| 56 | ...base, |
| 57 | createdBy: "user", |
| 58 | creationSource: "server", |
| 59 | type: "user_message", |
| 60 | inputIntent: "turn_start", |
| 61 | attachments: [], |
| 62 | } |
| 63 | : { |
| 64 | ...base, |
| 65 | type: "assistant_message", |
| 66 | streaming: false, |
| 67 | }; |
| 68 | } |
| 69 | |
| 70 | it.layer(layerTest)("ContextHandoffService legacy import", (it) => { |
| 71 | it.effect("prepares imported history for the first native v2 turn", () => |
| 72 | Effect.gen(function* () { |
| 73 | const service = yield* ContextHandoffService.ContextHandoffServiceV2; |
| 74 | const handoff = yield* service.prepareLegacyImport({ |
| 75 | threadId: ThreadId.make("thread:legacy-context"), |
| 76 | targetRunId: RunId.make("run:first-v2"), |
| 77 | toProviderThreadId: ProviderThreadId.make("provider-thread:first-v2"), |
| 78 | toProviderInstanceId: ProviderInstanceId.make("codex"), |
| 79 | items: [ |
| 80 | importedItem({ role: "user", id: "one", text: "What did we decide?", ordinal: 1 }), |
| 81 | importedItem({ |
| 82 | role: "assistant", |
| 83 | id: "two", |
| 84 | text: "We decided to keep the migration lightweight.", |
| 85 | ordinal: 2, |
| 86 | }), |
| 87 | ], |
| 88 | createdAt: DateTime.makeUnsafe("2026-01-02T00:00:00.000Z"), |
| 89 | }); |
| 90 | |
| 91 | assert.equal(handoff.strategy, "manual_context"); |
| 92 | assert.deepStrictEqual(handoff.fromProviderThreadIds, []); |
| 93 | assert.include(handoff.summaryText, "What did we decide?"); |
| 94 | assert.include(handoff.summaryText, "keep the migration lightweight"); |
| 95 | const providerMessage = ContextHandoffService.providerMessageWithContextHandoff({ |
| 96 | handoff, |
| 97 | userText: "Continue from there.", |
| 98 | }); |
| 99 | assert.include(providerMessage, handoff.summaryText); |
| 100 | assert.include(providerMessage, "User message:\nContinue from there."); |
| 101 | }), |
| 102 | ); |
| 103 | |
| 104 | it.effect("preserves role attribution when truncating imported history", () => |
| 105 | Effect.gen(function* () { |
| 106 | const service = yield* ContextHandoffService.ContextHandoffServiceV2; |
| 107 | const handoff = yield* service.prepareLegacyImport({ |
| 108 | threadId: ThreadId.make("thread:legacy-context"), |
| 109 | targetRunId: RunId.make("run:first-v2"), |
| 110 | toProviderThreadId: ProviderThreadId.make("provider-thread:first-v2"), |
| 111 | toProviderInstanceId: ProviderInstanceId.make("codex"), |
| 112 | items: [ |
| 113 | importedItem({ |
| 114 | role: "user", |
| 115 | id: "long", |
| 116 | text: `${"x".repeat(35_000)} retained final words`, |
| 117 | ordinal: 1, |
| 118 | }), |
| 119 | ], |
| 120 | createdAt: DateTime.makeUnsafe("2026-01-02T00:00:00.000Z"), |
| 121 | }); |
| 122 | |
| 123 | assert.isAtMost(handoff.summaryText.length, 32_000); |
| 124 | assert.include(handoff.summaryText, "User:\n... retained final words"); |
| 125 | assert.notMatch(handoff.summaryText, /\n+x+ retained final words/); |
| 126 | }), |
| 127 | ); |
| 128 | |
| 129 | it.effect("retains the newest oversized import even when it has no whitespace", () => |
| 130 | Effect.gen(function* () { |
| 131 | const service = yield* ContextHandoffService.ContextHandoffServiceV2; |
| 132 | const handoff = yield* service.prepareLegacyImport({ |
| 133 | threadId: ThreadId.make("thread:legacy-context"), |
| 134 | targetRunId: RunId.make("run:first-v2"), |
| 135 | toProviderThreadId: ProviderThreadId.make("provider-thread:first-v2"), |
| 136 | toProviderInstanceId: ProviderInstanceId.make("codex"), |
| 137 | items: [ |
| 138 | importedItem({ |
| 139 | role: "assistant", |
| 140 | id: "older", |
| 141 | text: "older message", |
| 142 | ordinal: 1, |
| 143 | }), |
| 144 | importedItem({ |
| 145 | role: "user", |
| 146 | id: "long-single-token", |
| 147 | text: `${"🧪".repeat(20_000)}LATEST_SINGLE_TOKEN`, |
| 148 | ordinal: 2, |
| 149 | }), |
| 150 | ], |
| 151 | createdAt: DateTime.makeUnsafe("2026-01-02T00:00:00.000Z"), |
| 152 | }); |
| 153 | |
| 154 | assert.include( |
| 155 | handoff.history?.omittedItemIds ?? [], |
| 156 | TurnItemId.make("turn-item:long-single-token"), |
| 157 | ); |
| 158 | assert.isAtMost(handoff.summaryText.length, 32_000); |
| 159 | assert.include(handoff.summaryText, "User:\n... "); |
| 160 | assert.include(handoff.summaryText, "LATEST_SINGLE_TOKEN"); |
| 161 | assert.notInclude(handoff.summaryText, "\ufffd"); |
| 162 | }), |
| 163 | ); |
| 164 | }); |