| 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 { useRemoteComposerSend, useRemoteComposerRuntimeActions } from "../lib/useRemoteComposerIntegration"; |
| 6 | import { useSessionOperations } from "../app-runtime/useSessionOperations"; |
| 7 | import type { RemoteSessionApi } from "../lib/useRemoteSession"; |
| 8 | import { useRemoteNavigationCommand } from "../lib/remoteNavigationCommands"; |
| 9 | import { RemoteNavigationHarness } from "./helpers/RemoteNavigationHarness"; |
| 10 | import { LocaleProvider } from "../lib/i18n"; |
| 11 | import { installDesktopHostStub } from "./desktopHostStub"; |
| 12 | |
| 13 | const dom = new JSDOM("<div id='root'></div>"); |
| 14 | Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true }); |
| 15 | const root = createRoot(document.getElementById("root")!); |
| 16 | function deferred() { let resolve!: () => void; const promise = new Promise<void>(done => { resolve = done; }); return { promise, resolve }; } |
| 17 | let gate = deferred(); |
| 18 | const calls: string[] = []; |
| 19 | installDesktopHostStub({ |
| 20 | RegisterNavigationIntent: async () => { calls.push("navigation-intent"); }, |
| 21 | OpenRemoteProjectTab: async (host: string, workspace: string, options: { newSession?: boolean }) => { calls.push(`new:${host}:${workspace}:${options.newSession}`); }, |
| 22 | }); |
| 23 | const session = { |
| 24 | setModel: async (value: string) => { calls.push(`model:${value}`); }, |
| 25 | setEffort: async (value: string) => { calls.push(`effort:${value}`); }, |
| 26 | compact: async (value: string) => { calls.push(`compact:${value}`); }, |
| 27 | runManagementCommand: async (value: string, hydrate?: boolean) => { calls.push(`manage:${value}:${hydrate}`); }, |
| 28 | pauseGoal: async () => { calls.push("remote-pause"); }, |
| 29 | resumeGoal: async () => { calls.push("remote-resume"); }, |
| 30 | retryHydration: async () => { calls.push("hydrate"); }, |
| 31 | } as unknown as RemoteSessionApi; |
| 32 | let send!: ReturnType<typeof useRemoteComposerSend>; |
| 33 | let runtime!: ReturnType<typeof useRemoteComposerRuntimeActions>; |
| 34 | let action: Promise<unknown> | undefined; |
| 35 | function Probe({ tab, generation = "1", remote = true }: { tab: string; generation?: string; remote?: boolean }) { |
| 36 | const navigateRemote = useRemoteNavigationCommand(); |
| 37 | const target = { tabId: tab, sessionKey: tab + generation }; |
| 38 | const operations = useSessionOperations({ visible: target, resources: ["A", "B"].map(tabId => ({ tabId, sessionKey: tabId + generation })) }); |
| 39 | send = useRemoteComposerSend({ hostId: "fixture", workspace: "fixture" }, tab, "goal", "", session, |
| 40 | async (display, submit) => { calls.push(`send:${tab}:${display}:${submit}`); }, |
| 41 | async (id, goal) => { calls.push(`goal:${id}:${goal}`); await gate.promise; }, |
| 42 | () => { calls.push("clear"); }, { target, operations, navigateRemote }); |
| 43 | runtime = useRemoteComposerRuntimeActions({ target, operations, remote, session, |
| 44 | runGoalAction: run => { action = Promise.resolve(run()); }, |
| 45 | pauseLocal: async id => { calls.push(`pause:${id}`); }, resumeLocal: async id => { calls.push(`resume:${id}`); }, |
| 46 | setLocalEffort: async (id, level) => { calls.push(`local-effort:${id}:${level}`); }, showError: message => { throw Error(message); } }); |
| 47 | return null; |
| 48 | } |
| 49 | const paint = (tab: string, generation = "1", remote = true) => act(async () => root.render(<LocaleProvider><RemoteNavigationHarness><Probe tab={tab} generation={generation} remote={remote} /></RemoteNavigationHarness></LocaleProvider>)); |
| 50 | try { |
| 51 | await paint("A"); |
| 52 | await send("/model model-fixture"); await send("/effort high"); await send("/compact fixture"); |
| 53 | await send("/context"); await send("/clear"); |
| 54 | assert.deepEqual(calls, ["model:model-fixture", "effort:high", "compact:fixture", "manage:/context:false", "clear"]); |
| 55 | calls.length = 0; |
| 56 | await send("/new"); |
| 57 | assert.deepEqual(calls, ["navigation-intent", "new:fixture:fixture:true"], "new-session uses the common owner; Serve lifecycle hydrates the target instead of the captured source callback"); |
| 58 | calls.length = 0; |
| 59 | const pending = send("display", " submit bytes "); |
| 60 | assert.deepEqual(calls, ["goal:A:submit bytes"]); |
| 61 | await paint("B"); gate.resolve(); await pending; |
| 62 | assert.deepEqual(calls, ["goal:A:submit bytes", "send:A:display: submit bytes "], "goal and send keep source A and preserve provider-visible submit bytes"); |
| 63 | calls.length = 0; gate = deferred(); await paint("A"); |
| 64 | const replaced = send("next"); await paint("A", "2"); gate.resolve(); await replaced; |
| 65 | assert.deepEqual(calls, ["goal:A:next"], "replacement session receives no stale post-goal submit"); |
| 66 | calls.length = 0; |
| 67 | runtime.pauseGoal(); await action; runtime.resumeGoal(); await action; |
| 68 | assert.deepEqual(calls, ["remote-pause", "remote-resume"]); |
| 69 | calls.length = 0; await paint("A", "2", false); |
| 70 | runtime.pauseGoal(); await action; runtime.resumeGoal(); await action; runtime.setEffort("max"); |
| 71 | await act(async () => {}); |
| 72 | assert.deepEqual(calls, ["pause:A", "resume:A", "local-effort:A:max"]); |
| 73 | calls.length = 0; gate = deferred(); await paint("A"); |
| 74 | const disposed = send("disposed"); await act(async () => root.unmount()); gate.resolve(); await disposed; |
| 75 | runtime.pauseGoal(); |
| 76 | assert.deepEqual(calls, ["goal:A:disposed"]); |
| 77 | console.log("remote composer commands: management routing, source Goal/send, byte preservation, runtime ports and disposal passed"); |
| 78 | } finally { dom.window.close(); } |
| 79 |