| 1 | import assert from "node:assert/strict"; |
| 2 | import test from "node:test"; |
| 3 | import { createTurnSubmissionId, historyMessagesToItems, initialState, reducer, type State } from "../lib/useController"; |
| 4 | import { submitTurn, type ManagementReceipt } from "../lib/turnSubmit"; |
| 5 | import type { AppBindings } from "../lib/bridge"; |
| 6 | import type { RuntimeState } from "../lib/runtimeStateStore"; |
| 7 | import { canonicalMessage } from "../lib/canonicalTranscriptBackend"; |
| 8 | import { isCompactCommand, isCompactSubmission } from "../lib/sessionMaintenanceOperation"; |
| 9 | import { TranscriptStore, type TranscriptBackend } from "../lib/transcriptStore"; |
| 10 | import type { TranscriptSnapshot } from "../lib/transcriptProtocol"; |
| 11 | |
| 12 | const operation = { operationId: "compact-A", kind: "compact", activity: "running", status: "running", operationRevision: 1, runtimeEpoch: "epoch-A" }; |
| 13 | const runtime = (revision: number, maintenance: RuntimeState["maintenance"]): RuntimeState => ({ |
| 14 | schemaVersion: 1, projectionEpoch: "projection-A", runtimeEpoch: "epoch-A", activityRevision: revision, revision, |
| 15 | phase: maintenance ? "executing" : "idle", running: Boolean(maintenance), turnId: "", turnStatus: "", turnEventSeq: 0, |
| 16 | pendingPrompt: false, cancelRequested: false, cancellable: Boolean(maintenance), backgroundJobs: 0, activity: maintenance ? "maintenance" : "", maintenance, |
| 17 | }); |
| 18 | const pending = (state = initialState, submissionId = "submit") => reducer(state, { type: "user", text: "/compact", seq: state.seq, submissionId }); |
| 19 | const confirm = (state: State, receipt?: ManagementReceipt, submissionId = "submit") => reducer(state, { type: "management_confirmed", submissionId, receipt }); |
| 20 | const running = () => reducer(initialState, { type: "runtime_snapshot", snapshot: runtime(1, operation) }); |
| 21 | const terminal = { ...operation, status: "completed", activity: "finalizing", operationRevision: 3, applied: true, summary: "durable summary" }; |
| 22 | const busyReceipt = { operationId: operation.operationId, errorCode: "maintenance_busy" }; |
| 23 | |
| 24 | test("management refusal retains operation identity through the submit adapter; legacy success and transport errors keep their meaning", async () => { |
| 25 | for (const managementErrorCode of [undefined, "maintenance_busy", "maintenance_recovery_required", "future_code"]) { |
| 26 | const bridge = { StartTurnForTab: async () => ({ disposition: "management_handled", operationId: operation.operationId, managementErrorCode }) } as unknown as AppBindings; |
| 27 | assert.deepEqual(await submitTurn(bridge, "A", "submit", "/compact", "/compact", ""), [2, { operationId: operation.operationId, errorCode: managementErrorCode }]); |
| 28 | } |
| 29 | const bridge = { StartTurnForTab: async () => { throw new Error("connection closed"); } } as unknown as AppBindings; |
| 30 | await assert.rejects(submitTurn(bridge, "A", "submit", "/compact", "/compact", ""), /connection closed/); |
| 31 | }); |
| 32 | |
| 33 | test("duplicate compact has no chat echo, preserves runtime-owned Stop state and deduplicates the busy notice", () => { |
| 34 | let state = confirm(running(), busyReceipt); |
| 35 | assert.deepEqual(state.localSubmissionOrder, []); |
| 36 | assert.equal(state.running, false, "management does not fabricate a conversational turn"); |
| 37 | assert.equal(state.turnActive, false); |
| 38 | assert.equal(state.runtimeStateSnapshot?.cancellable, true); |
| 39 | assert.equal(state.items.filter(item => item.kind === "compaction").length, 1); |
| 40 | assert.equal(state.items.some(item => item.kind === "notice" && item.level === "warn"), false); |
| 41 | state = confirm(state, busyReceipt); |
| 42 | assert.equal(state.items.filter(item => item.kind === "notice").length, 1); |
| 43 | }); |
| 44 | |
| 45 | test("a late management receipt cannot clear a newer submission or reopen completed compaction", () => { |
| 46 | let state = running(); |
| 47 | state = reducer(state, { type: "event", e: { kind: "session_operation", sessionOperation: terminal } }); |
| 48 | state = reducer(state, { type: "runtime_snapshot", snapshot: runtime(3, undefined) }); |
| 49 | state = confirm(state, busyReceipt); |
| 50 | assert.equal(state.running, false); |
| 51 | assert.equal(state.items.some(item => item.kind === "notice"), false); |
| 52 | assert.equal(state.items.find(item => item.kind === "compaction")?.status, "completed"); |
| 53 | state = pending(pending(running()), "newer"); |
| 54 | const later = confirm(state, busyReceipt); |
| 55 | assert.equal(later.pendingSubmissionId, "newer"); |
| 56 | assert.equal(later.localSubmissions.submit, undefined); |
| 57 | assert.equal(later.localSubmissions.newer.status, "sending"); |
| 58 | }); |
| 59 | |
| 60 | test("recovery and unknown command refusals remain visible without a failed conversational bubble", () => { |
| 61 | for (const errorCode of ["maintenance_recovery_required", "future_code"]) { |
| 62 | const state = confirm(running(), { operationId: operation.operationId, errorCode }); |
| 63 | assert.deepEqual(state.localSubmissionOrder, []); |
| 64 | assert.equal(state.items.filter(item => item.kind === "notice" && item.level === "warn").length, 1); |
| 65 | } |
| 66 | }); |
| 67 | |
| 68 | test("switching away during compaction and restoring durable history rebuilds one completed summary", () => { |
| 69 | const background = confirm(running(), busyReceipt); |
| 70 | const other = reducer(initialState, { type: "history", messages: [{ role: "user", content: "unrelated session", messageId: "B" }] }); |
| 71 | const completed = reducer(background, { type: "event", e: { kind: "session_operation", sessionOperation: terminal } }); |
| 72 | const body = { role: "compaction", id: `maintenance:${operation.operationId}`, content: JSON.stringify(terminal) }; |
| 73 | const message = canonicalMessage({ messageId: body.id, role: body.role, position: 2, eventSequence: 3, version: 1, visibleTurn: 1 }, body); |
| 74 | const snapshot: TranscriptSnapshot = { protocolVersion: 1, snapshotId: "completed-A", identity: { |
| 75 | sessionId: "A", runtimeEpoch: "epoch-A", rewriteEpoch: 0, headId: "" }, projectionRevision: 3, |
| 76 | coveredThroughSeq: 3, records: [{ id: body.id, order: 0, message, refs: [] }], activeRecords: [], activeAttempts: [], |
| 77 | runtime: { status: "completed", pendingEvents: [] }, before: 0, hasOlder: false, totalRecords: 1, totalTurns: 1, stale: false }; |
| 78 | const projection = { items: historyMessagesToItems([message], "snapshot:").items, removeIds: [], startTurn: 0, endTurn: 1, |
| 79 | totalTurns: 1, hasOlder: false, hasNewer: false, revision: 3, revisionKnown: true, digest: "completed-A" }; |
| 80 | for (const state of [completed, reducer(completed, { type: "reset" })]) { |
| 81 | let restored = reducer(state, { type: "transcript_v2_snapshot", snapshot, projection }); |
| 82 | restored = reducer(restored, { type: "runtime_snapshot", snapshot: runtime(3, undefined) }); |
| 83 | const cards = restored.items.filter(item => item.kind === "compaction"); |
| 84 | assert.equal(cards.length, 1); |
| 85 | assert.equal(cards[0].status, "completed"); |
| 86 | assert.equal(cards[0].summary, terminal.summary); |
| 87 | assert.equal(cards[0].pending, false); |
| 88 | assert.deepEqual(restored.localSubmissionOrder, []); |
| 89 | } |
| 90 | assert.equal(other.items.some(item => item.kind === "compaction"), false); |
| 91 | }); |
| 92 | |
| 93 | test("only compact and its focus form use the management route", () => { |
| 94 | for (const text of ["/compact", " /compact keep decisions "]) assert.equal(isCompactCommand(text), true); |
| 95 | for (const text of ["/compactly", "continue", "explain /compact", "/context"]) assert.equal(isCompactCommand(text), false); |
| 96 | }); |
| 97 | |
| 98 | test("expanded and edited compact focus uses the typed command route without rewriting instructions", async () => { |
| 99 | const calls: unknown[][] = []; |
| 100 | const bridge = { |
| 101 | StartTurnForTab: async (...args: unknown[]) => { calls.push(args); return { disposition: "management_handled", operationId: operation.operationId, managementErrorCode: "maintenance_busy" }; }, |
| 102 | SubmitDisplayToTabWithID: async () => { throw Error("display route bypassed management admission"); }, |
| 103 | SubmitEditedDisplayToTabWithID: async () => { throw Error("edit route bypassed management admission"); }, |
| 104 | } as unknown as AppBindings; |
| 105 | const focus = `/compact preserve decisions\n${"source details ".repeat(200)}`; |
| 106 | for (const original of ["", "previous draft"]) { |
| 107 | assert.equal(isCompactSubmission(focus), true); |
| 108 | assert.deepEqual(await submitTurn(bridge, "A", "request", "/compact [Pasted text #1]", focus, original), [2, busyReceipt]); |
| 109 | } |
| 110 | assert.deepEqual(calls, [["A", focus, "request"], ["A", focus, "request"]]); |
| 111 | assert.equal(isCompactSubmission(focus, { display: focus, input: focus, invocations: [] }), false); |
| 112 | assert.equal(isCompactSubmission(focus, undefined, { goal: focus }), false); |
| 113 | }); |
| 114 | |
| 115 | test("every receipt/completion/idle ordering settles without pinning a phantom turn", () => { |
| 116 | const permutations = [["receipt", "terminal", "idle"], ["receipt", "idle", "terminal"], |
| 117 | ["terminal", "receipt", "idle"], ["terminal", "idle", "receipt"], |
| 118 | ["idle", "receipt", "terminal"], ["idle", "terminal", "receipt"]]; |
| 119 | for (const order of permutations) { |
| 120 | let state: State = { ...running(), transcriptProtocol: 2 }; |
| 121 | for (const step of order) { |
| 122 | if (step === "receipt") state = confirm(state, busyReceipt); |
| 123 | if (step === "terminal") { |
| 124 | state = reducer(state, { type: "event", e: { kind: "session_operation", sessionOperation: terminal } }); |
| 125 | state = reducer(state, { type: "transcript_runtime", runtime: { status: "completed", turnId: "previous-turn", submissionId: "previous-submission", pendingEvents: [], samplingCount: 0, toolCount: 0 } }); |
| 126 | } |
| 127 | if (step === "idle") state = reducer(state, { type: "runtime_snapshot", snapshot: runtime(3, undefined) }); |
| 128 | } |
| 129 | assert.equal(state.running, false, order.join(" -> ")); |
| 130 | assert.equal(state.cancellable, false); |
| 131 | assert.equal(state.pendingSubmissionId, undefined); |
| 132 | assert.equal(state.runtimeStateSnapshot?.running, false); |
| 133 | assert.equal(state.items.find(item => item.kind === "compaction")?.status, "completed"); |
| 134 | const store = new TranscriptStore({} as TranscriptBackend); |
| 135 | store.setPinned("A", Boolean(state.running || state.turnActive || state.live)); |
| 136 | assert.equal(store.tabIsPinned("A"), false); |
| 137 | } |
| 138 | }); |
| 139 | |
| 140 | test("a late compact receipt preserves a newer active chat and pending prompt", () => { |
| 141 | const state: State = { ...pending(initialState, "new-chat"), running: true, turnActive: true, |
| 142 | pendingPrompt: true, activeTurnId: "new-turn", cancellable: true, cancelRequested: true }; |
| 143 | const next = confirm(state, busyReceipt, "old-compact"); |
| 144 | for (const key of ["running", "turnActive", "pendingPrompt", "activeTurnId", "cancellable", "cancelRequested", "pendingSubmissionId", "localSubmissions"] as const) { |
| 145 | assert.equal(next[key], state[key], key); |
| 146 | } |
| 147 | }); |
| 148 | |
| 149 | test("management requests reserve distinct submission identities without starting chat lifecycle", () => { |
| 150 | let state = initialState; |
| 151 | const ids: string[] = []; |
| 152 | for (let i = 0; i < 3; i++) { |
| 153 | ids.push(createTurnSubmissionId("A", state.sessionGen, state.seq)); |
| 154 | state = reducer(state, { type: "management_requested" }); |
| 155 | assert.equal(state.running, false); |
| 156 | assert.deepEqual(state.localSubmissionOrder, []); |
| 157 | } |
| 158 | const chatId = createTurnSubmissionId("A", state.sessionGen, state.seq); |
| 159 | assert.equal(new Set([...ids, chatId]).size, 4); |
| 160 | state = pending(state, chatId); |
| 161 | for (const id of ids) state = confirm(state, busyReceipt, id); |
| 162 | assert.equal(state.pendingSubmissionId, chatId); |
| 163 | assert.equal(state.running, true); |
| 164 | }); |
| 165 | |
| 166 | test("legacy compact bindings and conversational display submissions retain their contracts", async () => { |
| 167 | const calls: unknown[][] = []; |
| 168 | const bridge = { |
| 169 | SubmitToTabWithID: async (...args: unknown[]) => { calls.push(["legacy", ...args]); }, |
| 170 | SubmitDisplayToTabWithID: async (...args: unknown[]) => { calls.push(["display", ...args]); }, |
| 171 | SubmitInvocationsToTabWithID: async (...args: unknown[]) => { calls.push(["structured", ...args]); }, |
| 172 | SubmitInitialGoalToTabWithID: async (...args: unknown[]) => { calls.push(["goal", ...args]); return ["approval"]; }, |
| 173 | } as unknown as AppBindings; |
| 174 | assert.deepEqual(await submitTurn(bridge, "A", "legacy", "/compact [paste]", "/compact decisions", ""), [0]); |
| 175 | await submitTurn(bridge, "A", "chat", "visible", "hidden context\nvisible", ""); |
| 176 | await submitTurn(bridge, "A", "structured", "/compact", "/compact", "", { display: "tool task", input: "tool input", invocations: [] }); |
| 177 | assert.deepEqual(await submitTurn(bridge, "A", "goal", "task", "/compact", "", undefined, |
| 178 | { goal: "task", collaborationMode: "goal", toolApprovalMode: "ask" }), [1, ["approval"]]); |
| 179 | assert.deepEqual(calls.map(call => call[0]), ["legacy", "display", "structured", "goal"]); |
| 180 | assert.deepEqual(calls[0], ["legacy", "A", "/compact decisions", "legacy"]); |
| 181 | assert.deepEqual(calls[1], ["display", "A", "visible", "hidden context\nvisible", "chat"]); |
| 182 | }); |
| 183 |