| 1 | import assert from "node:assert/strict"; |
| 2 | import { createElement } from "react"; |
| 3 | import { renderToStaticMarkup } from "react-dom/server"; |
| 4 | import { WorkspaceDockRegion } from "../app-shell/WorkspaceDockRegion"; |
| 5 | import type { Translator } from "../lib/i18n"; |
| 6 | import { projectConversation, projectConversationLayout, projectNavigationSurfaceTarget } from "../app-runtime/conversationProjection"; |
| 7 | import { initialState } from "../lib/useController"; |
| 8 | import type { RemoteSessionApi } from "../lib/useRemoteSession"; |
| 9 | import type { BackgroundRuntimeView } from "../lib/types"; |
| 10 | |
| 11 | const local = { ...initialState, running: true, activeTurnId: "local-turn", turnTokens: 999, |
| 12 | turnStartAt: 111, sessionTokens: 999, sessionCost: 999, sessionCurrency: "USD", |
| 13 | context: { used: 999, window: 999, sessionTokens: 999 }, balance: { available: true, display: "LOCAL" }, |
| 14 | meta: { ready: true, eventChannel: "local-events", cwd: "local-cwd", label: "local-model", workspaceName: "local-project", gitBranch: "local-branch", |
| 15 | imageInputEnabled: true, visionFallbackEnabled: true, pinnedFiles: [{ path: "local-file", sizeBytes: 10, tokenEstimate: 5 }] }, |
| 16 | } as typeof initialState; |
| 17 | const remote: Pick<RemoteSessionApi, "transcript" | "running" | "modelLabel" | "commands" | "composerProfile" | "goalView" | "goalRuntime" | "effort"> = { |
| 18 | transcript: { ...initialState, turnTokens: 7, turnStartAt: 222, sessionTokens: 8, sessionCost: 2, sessionCurrency: "CNY" }, |
| 19 | running: false, modelLabel: "remote-model", commands: [], |
| 20 | goalView: { id: "goal-remote", revision: 3, objective: "resume remote work", phase: "active", maxGoalRounds: null, |
| 21 | roundsStarted: 7, createdAt: "2026-09-13T10:00:00Z", updatedAt: "2026-09-13T10:10:00Z", activation: "disarmed" }, |
| 22 | }; |
| 23 | const tab = { id: "remote", label: "remote-tab", remote: { hostId: "fixture", workspace: "remote-cwd" }, workspaceName: "remote-project" }; |
| 24 | const background = [{ id: "local-runtime" }] as unknown as BackgroundRuntimeView[]; |
| 25 | const view = projectConversation({ local, remote, tab, activeTabId: tab.id, backgroundRuntimes: background, connectingLabel: "connecting" }); |
| 26 | assert.equal(view.runtime, remote.transcript, "projection shares the canonical state and message arrays"); |
| 27 | assert.equal(view.context.items, remote.transcript.items); |
| 28 | assert.equal(view.context.tabId, undefined, "remote context cannot fetch local telemetry"); |
| 29 | assert.equal(view.composer.modelLabel, "remote-model"); |
| 30 | assert.equal(view.composer.cwd, "remote-cwd"); |
| 31 | assert.equal(view.composer.turnTokens, 7); |
| 32 | assert.equal(view.composer.turnStartAt, 222); |
| 33 | assert.equal(view.composer.goalView?.id, "goal-remote"); |
| 34 | assert.equal(view.composer.currency, "CNY"); |
| 35 | assert.equal(view.composer.pinnedFiles, undefined); |
| 36 | assert.equal(view.composer.attachmentInputEnabled, false); |
| 37 | assert.equal(view.composer.imageInputEnabled, false); |
| 38 | assert.equal(view.composer.imageUnderstandingEnabled, false); |
| 39 | assert.equal(view.composer.localDurableGuidance, false); |
| 40 | assert.equal(view.composer.turnId, undefined); |
| 41 | assert.equal(view.composer.context, remote.transcript.context); |
| 42 | assert.equal(view.composer.balance, remote.transcript.balance); |
| 43 | assert.equal(view.status.context, remote.transcript.context); |
| 44 | assert.equal(view.status.balance, remote.transcript.balance); |
| 45 | assert.deepEqual(view.status.backgroundRuntimes, []); |
| 46 | assert.equal(view.status.gitBranch, undefined); |
| 47 | assert.equal(view.status.workspaceName, "remote-project"); |
| 48 | assert.equal(view.status.cost, 2); |
| 49 | assert.equal(view.status.sessionTokens, 8); |
| 50 | const localView = projectConversation({ local, activeTabId: "local", backgroundRuntimes: background, connectingLabel: "connecting" }); |
| 51 | assert.equal(localView.runtime, local); |
| 52 | assert.equal(localView.status.backgroundRuntimes, background); |
| 53 | assert.equal(localView.composer.attachmentInputEnabled, true); |
| 54 | assert.equal(localView.composer.localDurableGuidance, true); |
| 55 | assert.equal(localView.context.tabId, "local"); |
| 56 | const readableBeforeRuntime = projectNavigationSurfaceTarget({ |
| 57 | activeTabId: "local", |
| 58 | sessionKey: "local:session", |
| 59 | local: { |
| 60 | ...local, |
| 61 | meta: { ...local.meta!, ready: false, runtime: { phase: "starting", epoch: "runtime-1" } }, |
| 62 | backendActivationPending: true, |
| 63 | hydrating: false, |
| 64 | hydrateError: undefined, |
| 65 | }, |
| 66 | }); |
| 67 | assert.equal(readableBeforeRuntime.ready, true, "local navigation paint is released by readable history rather than runtime readiness"); |
| 68 | assert.equal(readableBeforeRuntime.backendActivationPending, false, "local runtime activation does not block transcript paint"); |
| 69 | const localHistoryPending = projectNavigationSurfaceTarget({ |
| 70 | activeTabId: "local", |
| 71 | sessionKey: "local:session", |
| 72 | local: { ...local, backendActivationPending: true, hydrating: true, hydrateError: undefined }, |
| 73 | }); |
| 74 | assert.equal(localHistoryPending.ready, false, "local cache miss keeps only the target history skeleton pending"); |
| 75 | for (const chatVisible of [false, true]) for (const localToolsEnabled of [false, true]) for (const dockMode of ["files", "changed", "remote", "context"]) { |
| 76 | const layout = projectConversationLayout({ chatVisible, localToolsEnabled, dockMode, dockRenderable: true, |
| 77 | dockGridOpen: true, dockOverlay: true, dockOpen: true, dockMaximized: true, terminalOpen: true }); |
| 78 | const permitted = chatVisible && (localToolsEnabled || !["files", "changed"].includes(dockMode)); |
| 79 | assert.equal(layout.dockVisible, permitted); |
| 80 | assert.equal(layout.dockGridOpen, permitted); |
| 81 | assert.equal(layout.dockOverlay, permitted); |
| 82 | assert.equal(layout.terminalOpen, chatVisible && localToolsEnabled); |
| 83 | assert.equal(layout.dockMaximized, chatVisible, "automation masks stored maximization without modifying the preference"); |
| 84 | if (!localToolsEnabled && (dockMode === "files" || dockMode === "changed")) { |
| 85 | const noop = () => {}; |
| 86 | const markup = renderToStaticMarkup(createElement(WorkspaceDockRegion, { |
| 87 | visible: layout.dockVisible, overlay: layout.dockOverlay, mode: dockMode, |
| 88 | showContext: true, t: ((key: string) => key) as Translator, |
| 89 | onPickEntry: noop, remote: { onClose: noop }, context: view.context, |
| 90 | workspaceKey: "fixture", workspace: { open: layout.dockVisible, maximized: false, onClose: noop, onToggleMaximized: noop }, |
| 91 | })); |
| 92 | assert.equal(markup, "", "actual dock region never mounts local Files/Changes for a remote source"); |
| 93 | } |
| 94 | } |
| 95 | console.log("conversation projection: shared source identity, remote telemetry isolation and local-tool layout policy passed"); |
| 96 |