| 1 | import React, { act } from "react"; |
| 2 | import { createRoot } from "react-dom/client"; |
| 3 | import { RemoteSessionSurface } from "../../components/RemoteSessionSurface"; |
| 4 | import { LocaleProvider } from "../../lib/i18n"; |
| 5 | import { useRemoteSession, type RemoteSessionApi } from "../../lib/useRemoteSession"; |
| 6 | import type { AppBindings } from "../../lib/bridge"; |
| 7 | import type { TabMeta } from "../../lib/types"; |
| 8 | |
| 9 | export async function runRemoteRuntimeCases({ commands, emitRemote: __emitMockRemoteTab, remoteTab, ok, tape, flush, setSnapshotHistory }: { |
| 10 | commands: AppBindings; |
| 11 | emitRemote: (tabId: string, channel: "state" | "event", payload: unknown) => void; |
| 12 | remoteTab: TabMeta; |
| 13 | ok: (value: boolean, label: string) => void; |
| 14 | tape: string[]; |
| 15 | flush: () => Promise<void>; |
| 16 | setSnapshotHistory: (history: unknown[]) => void; |
| 17 | }) { |
| 18 | // Only the ordered Follow stream settles transcript activity. Ancillary |
| 19 | // status cannot replace its content or settle a newer turn. |
| 20 | const { runtimeStateStore } = await import("../../lib/runtimeStateStore"); |
| 21 | let runtimeProbe: RemoteSessionApi; |
| 22 | function RuntimeProbe() { |
| 23 | runtimeProbe = useRemoteSession("tab-runtime", "ready", "/runtime-session"); |
| 24 | return <RemoteSessionSurface tab={{ ...remoteTab, id: "tab-runtime" }} session={runtimeProbe} />; |
| 25 | } |
| 26 | const runtimeNode = document.createElement("div"); |
| 27 | document.body.append(runtimeNode); |
| 28 | const runtimeRoot = createRoot(runtimeNode); |
| 29 | await act(async () => { runtimeRoot.render(<LocaleProvider><RuntimeProbe /></LocaleProvider>); await flush(); }); |
| 30 | let runtimeRevision = 0; |
| 31 | async function publishRuntime(phase: "idle" | "executing" | "finishing", turnId = "lost-turn", freshness: "synced" | "unknown" = "synced", sessionPath = "/runtime-session") { |
| 32 | await act(async () => { |
| 33 | runtimeStateStore.commit({ epoch: "runtime-app", revision: ++runtimeRevision, topics: [], sessions: [{ |
| 34 | tabId: "tab-runtime", scope: "project", workspaceRoot: "~/app", topicId: "", sessionPath, |
| 35 | sessionGeneration: 1, open: true, remote: true, freshness, |
| 36 | state: { schemaVersion: 1, runtimeEpoch: "runtime-controller", revision: runtimeRevision, |
| 37 | phase, running: phase !== "idle", turnId, turnStatus: phase === "idle" ? "completed" : "in_progress", |
| 38 | turnEventSeq: runtimeRevision, pendingPrompt: false, cancelRequested: false, |
| 39 | cancellable: phase === "executing", backgroundJobs: 0, activity: "" }, |
| 40 | }] }); |
| 41 | await flush(); |
| 42 | }); |
| 43 | } |
| 44 | await act(async () => { |
| 45 | __emitMockRemoteTab("tab-runtime", "event", { kind: "turn_started", turnId: "lost-turn" }); |
| 46 | __emitMockRemoteTab("tab-runtime", "event", { kind: "text", messageId: "runtime-answer", text: "partial runtime answer" }); |
| 47 | await flush(); |
| 48 | }); |
| 49 | const beforeRuntimeHistory = tape.filter(entry => entry === "snapshot:tab-runtime").length; |
| 50 | await publishRuntime("idle", "lost-turn", "unknown"); |
| 51 | ok(runtimeProbe!.transcript.running, "unknown runtime does not settle a stream"); |
| 52 | await publishRuntime("finishing"); |
| 53 | ok(runtimeProbe!.transcript.running, "finishing does not settle the transcript before completion"); |
| 54 | await publishRuntime("idle", "previous-turn"); |
| 55 | ok(runtimeProbe!.transcript.running, "idle evidence for a previous turn cannot settle the current turn"); |
| 56 | await publishRuntime("idle", "lost-turn", "synced", "/other-session"); |
| 57 | ok(runtimeProbe!.transcript.running, "another selected session cannot settle the current transcript"); |
| 58 | await act(async () => { |
| 59 | __emitMockRemoteTab("tab-runtime", "event", { kind: "retrying", retryAttempt: 2, retryMax: 4 }); |
| 60 | await flush(); |
| 61 | }); |
| 62 | ok(runtimeProbe!.transcript.retry !== undefined, "lost-completion fixture includes an active retry"); |
| 63 | setSnapshotHistory([{ role: "assistant", content: "obsolete metadata history" }]); |
| 64 | await publishRuntime("idle"); |
| 65 | ok(runtimeProbe!.transcript.running && runtimeProbe!.transcript.live?.text === "partial runtime answer", |
| 66 | "ancillary idle cannot settle or erase an active Follow stream"); |
| 67 | ok(tape.filter(entry => entry === "snapshot:tab-runtime").length === beforeRuntimeHistory, |
| 68 | "runtime metadata never initiates a completion history rebase"); |
| 69 | await act(async () => { |
| 70 | __emitMockRemoteTab("tab-runtime", "event", { kind: "message", messageId: "runtime-answer", text: "complete durable runtime answer" }); |
| 71 | __emitMockRemoteTab("tab-runtime", "event", { kind: "turn_done", turnId: "lost-turn" }); |
| 72 | await flush(); |
| 73 | }); |
| 74 | ok(!runtimeProbe!.transcript.running && !runtimeProbe!.transcript.turnActive && runtimeProbe!.transcript.retry === undefined, |
| 75 | "Follow completion settles activity and retry together"); |
| 76 | ok(runtimeNode.textContent?.includes("complete durable runtime answer") === true, |
| 77 | "committed final content remains visible after Follow completion"); |
| 78 | await act(async () => { |
| 79 | __emitMockRemoteTab("tab-runtime", "event", { kind: "turn_started", turnId: "next-turn" }); |
| 80 | __emitMockRemoteTab("tab-runtime", "event", { kind: "text", text: "next live answer" }); |
| 81 | await flush(); |
| 82 | }); |
| 83 | await publishRuntime("idle", "lost-turn"); |
| 84 | ok(runtimeProbe!.transcript.running && runtimeProbe!.transcript.live?.text === "next live answer", |
| 85 | "old idle cannot settle the next Follow turn"); |
| 86 | const beforeStatus = tape.filter(entry => entry === "snapshot:tab-runtime").length; |
| 87 | await act(async () => { await commands.RemoteTabStatus("tab-runtime"); await flush(); }); |
| 88 | ok(tape.filter(entry => entry === "snapshot:tab-runtime").length === beforeStatus |
| 89 | && !runtimeNode.textContent?.includes("obsolete metadata history"), |
| 90 | "late status cannot replace the transcript with unrelated history"); |
| 91 | await act(async () => runtimeRoot.unmount()); |
| 92 | runtimeNode.remove(); |
| 93 | } |
| 94 |