| 1 | import assert from "node:assert/strict"; |
| 2 | import React, { act } from "react"; |
| 3 | import { createRoot } from "react-dom/client"; |
| 4 | import { JSDOM } from "jsdom"; |
| 5 | import { useSessionSubmission } from "../lib/useSessionSubmission"; |
| 6 | import { useSessionOperations } from "../app-runtime/useSessionOperations"; |
| 7 | import type { SubmissionPorts, SubmissionResource } from "../app-runtime/sessionSubmissionOwner"; |
| 8 | |
| 9 | const dom = new JSDOM("<div id='root'></div>"); |
| 10 | Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true }); |
| 11 | const root = createRoot(document.getElementById("root")!); |
| 12 | const { createSubmissionPorts } = await import("../app-runtime/desktopSubmissionAdapter"); |
| 13 | function deferred() { |
| 14 | let resolve!: () => void; let reject!: (error: Error) => void; |
| 15 | const promise = new Promise<void>((yes, no) => { resolve = yes; reject = no; }); |
| 16 | return { promise, resolve, reject }; |
| 17 | } |
| 18 | let goalGate: ReturnType<typeof deferred> | undefined, profileGate: ReturnType<typeof deferred> | undefined; |
| 19 | const calls: unknown[][] = []; |
| 20 | const ports: SubmissionPorts = { |
| 21 | send: async (...args) => { calls.push(["send", ...args]); }, |
| 22 | clearUndo: tab => { calls.push(["undo", tab]); }, |
| 23 | setGoal: async (...args) => { calls.push(["goal", ...args]); await goalGate?.promise; }, |
| 24 | patchGoal: (...args) => { calls.push(["patch", ...args]); }, |
| 25 | profile: async tab => { calls.push(["profile", tab]); await profileGate?.promise; return true; }, |
| 26 | }; |
| 27 | let commands!: ReturnType<typeof useSessionSubmission>; |
| 28 | function Probe({ tab, gen, draft, readOnly }: { tab: string; gen: number; draft: boolean; readOnly: boolean }) { |
| 29 | const resources: SubmissionResource[] = ["A", "B"].map(tabId => ({ target: { tabId, sessionKey: tabId + gen }, |
| 30 | ready: true, remote: false, unavailable: readOnly ? "read-only" : "", goalDraft: draft, collaboration: "normal", approval: "ask" })); |
| 31 | const target = resources.find(source => source.target.tabId === tab)!.target; |
| 32 | const operations = useSessionOperations({ visible: target, resources: resources.map(source => source.target) }); |
| 33 | commands = useSessionSubmission({ target, resources, operations, ports, missingSource: "missing" }); |
| 34 | return null; |
| 35 | } |
| 36 | const paint = (tab = "A", gen = 1, draft = false, readOnly = false) => act(async () => root.render(<Probe tab={tab} gen={gen} draft={draft} readOnly={readOnly} />)); |
| 37 | try { |
| 38 | const adapterCalls: unknown[][] = []; |
| 39 | const adapter = createSubmissionPorts({ |
| 40 | send: async (...args) => { adapterCalls.push(["send", ...args]); }, |
| 41 | setGoal: async (...args) => { adapterCalls.push(["set", ...args]); }, |
| 42 | clearGoal: async (...args) => { adapterCalls.push(["clear", ...args]); }, |
| 43 | clearUndo: () => {}, patchGoal: () => {}, profile: async () => true, |
| 44 | }); |
| 45 | await adapter.setGoal("B", "goal", false); await adapter.setGoal("A", "", false); |
| 46 | await adapter.send("B", "display", " raw bytes ", undefined, { goal: "goal", collaborationMode: "normal", toolApprovalMode: "ask" }); |
| 47 | assert.deepEqual(adapterCalls, [["set", "B", "goal"], ["clear", "A"], ["send", "B", "display", " raw bytes ", undefined, undefined, |
| 48 | { goal: "goal", collaborationMode: "normal", toolApprovalMode: "ask" }]], "runtime adapter preserves explicit Controller targets, original-text slot and atomic Goal payload"); |
| 49 | await paint(); profileGate = deferred(); |
| 50 | const first = commands.submit("A", " display ", " provider bytes "); |
| 51 | await paint("B"); profileGate.resolve(); await first; |
| 52 | assert.deepEqual(calls, [["profile", "A"], ["undo", "A"], ["send", "A", "display", "provider bytes", undefined, undefined]], "ordinary continuation uses the source resource and preserves established trim semantics"); |
| 53 | |
| 54 | calls.length = 0; profileGate = deferred(); await paint(); |
| 55 | const replaced = commands.submit("A", "stale"); |
| 56 | await paint("A", 2); profileGate.resolve(); await replaced; |
| 57 | assert.deepEqual(calls, [["profile", "A"]], "replacement receives no stale undo invalidation or submit"); |
| 58 | |
| 59 | calls.length = 0; profileGate = undefined; goalGate = deferred(); await paint(); |
| 60 | const goal = commands.submit("A", "/goal source goal"); |
| 61 | await paint("A", 2); goalGate.resolve(); await goal; |
| 62 | assert.deepEqual(calls, [["goal", "A", "source goal", false]], "old Goal completion cannot patch or submit to a replacement"); |
| 63 | |
| 64 | calls.length = 0; goalGate = deferred(); await paint(); |
| 65 | const rejected = commands.applyGoal("bad goal"); |
| 66 | goalGate.reject(Error("activation failed")); await assert.rejects(rejected, /activation failed/); |
| 67 | assert.deepEqual(calls, [["goal", "A", "bad goal", false]], "failed activation leaves UI profile and undo untouched"); |
| 68 | |
| 69 | calls.length = 0; goalGate = undefined; await paint("A", 1, true); |
| 70 | const structured = { display: "skill", input: "/skill input", invocations: [{ name: "skill", kind: "skill" as const, offset: 0 }] }; |
| 71 | await commands.submit("A", " goal text ", " /skill input ", structured); |
| 72 | assert.deepEqual(calls, [["undo", "A"], ["send", "A", "goal text", "/skill input", structured, |
| 73 | { goal: "goal text", collaborationMode: "normal", toolApprovalMode: "ask" }], ["patch", "A", "goal text"]], "structured Goal uses one atomic source send and unchanged invocation bytes"); |
| 74 | calls.length = 0; |
| 75 | await commands.submit("A", " goal text ", " task bytes "); |
| 76 | assert.equal(calls[1][3], "/goal task bytes", "ordinary first Goal retains its existing prefix"); |
| 77 | |
| 78 | calls.length = 0; await paint(); |
| 79 | await commands.submit("A", "/goal pause"); await commands.submit("A", "/goal resume"); |
| 80 | assert.deepEqual(calls.filter(call => call[0] === "goal" || call[0] === "patch"), [], "pause and resume preserve Goal before backend command handling"); |
| 81 | assert.deepEqual(calls.filter(call => call[0] === "send").map(call => call[3]), ["/goal pause", "/goal resume"]); |
| 82 | calls.length = 0; await commands.applyGoalForTab("B", " target goal "); await commands.applyGoalForTab("B", ""); |
| 83 | assert.deepEqual(calls, [["goal", "B", "target goal", false], ["patch", "B", "target goal"], ["goal", "B", "", false], ["patch", "B", ""]], "activation and clear patch only their explicit source after backend success"); |
| 84 | |
| 85 | calls.length = 0; await paint(); |
| 86 | await commands.submit("A", "/goal --deep --research preserve flags"); |
| 87 | assert.deepEqual(calls, [["patch", "A", "preserve flags"], ["undo", "A"], |
| 88 | ["send", "A", "/goal --deep --research preserve flags", "/goal --deep --research preserve flags", undefined, undefined]], "legacy Goal flags remain backend-visible and do not invoke separate activation"); |
| 89 | |
| 90 | calls.length = 0; await paint("A", 1, false, true); |
| 91 | await assert.rejects(commands.commitThenSend("A", "direct"), /read-only/); |
| 92 | assert.deepEqual(calls, [], "read-only source preserves undo and sends nothing"); |
| 93 | await paint(); profileGate = deferred(); |
| 94 | const disposed = commands.submit("A", "disposed"); |
| 95 | await act(async () => root.unmount()); profileGate.resolve(); await disposed; |
| 96 | commands.commitThenSend("A", "after-unmount"); |
| 97 | assert.deepEqual(calls, [["profile", "A"]], "unmount revokes both continuation and committed direct entry"); |
| 98 | console.log("session submission lifecycle: source continuations, Goal atomicity/bytes, replacement, failure, read-only and disposal passed"); |
| 99 | } finally { dom.window.close(); } |
| 100 |