import { describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { join } from "@std/path"; import { spy } from "@std/testing/mock"; import { FakeTime } from "@std/testing/time"; import { nothing } from "lit"; import { urlToAppView } from "@commonfabric/navigation"; import { ConsoleLive, consoleLiveAddress, consoleLiveAtTail, consoleLiveEntries, type ConsoleLiveEntry, consoleLivePieceHref, consoleLivePolicyMark, consoleLiveRunReads, consoleLiveState, consoleLiveToolLine, stepWithheldAnything, } from "../../../console/src/live-view.ts"; import "../../../console/src/live.ts"; import { readConsoleRun } from "../../../console/run-store.ts"; import type { ConsoleRunDetail } from "../../../console/run-store.ts"; import type { ConsoleChatEventEnvelope, ConsoleChatStructuredEvent, } from "../../../console/turn-result.ts"; import type { ConsoleStep } from "../../../console/steps.ts"; import type { BrowserToolAction } from "../../../src/tools/browser.ts"; import { HARNESS_CHAT_EVENT_TYPE, HARNESS_CHAT_PROTOCOL_VERSION, } from "../../../src/contracts/interactive-chat.ts"; import { CfHarnessEngine } from "../../../src/engine.ts"; import { HarnessInteractiveChatService } from "../../../src/interactive-chat-service.ts"; import { CfHarnessPromptLoop } from "../../../src/prompt-loop.ts"; import { directPromptSlotBindingFor } from "../../support/prompt-slot-binding.ts"; import { templateText } from "./template-text.ts"; describe("console/src/live-view", () => { /** The log a page reads, numbered in the order the events were emitted. */ const log = ( ...events: readonly ( | ConsoleChatStructuredEvent | { turnId: string; event: ConsoleChatStructuredEvent } )[] ): readonly ConsoleChatEventEnvelope[] => events.map((entry, index) => { const tagged = "event" in entry ? entry : { turnId: "turn-1", event: entry }; return { type: HARNESS_CHAT_EVENT_TYPE, protocolVersion: HARNESS_CHAT_PROTOCOL_VERSION, sessionId: "session-1", turnId: tagged.turnId, sequence: index + 1, emittedAt: "2026-01-01T00:00:00.000Z", event: tagged.event, }; }); /** A turn that ended having named one piece, in the space it built it in. */ const COMPLETED_WITH_PIECE: ConsoleChatStructuredEvent = { kind: "turn_completed", turnId: "turn-1", result: { outcome: "completed" as const, sessionId: "session-1", continuable: true, looms: [], pieces: [{ slug: "reading-list", url: "http://localhost:8000/did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK/reading-list", space: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", }], spaceName: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", finalText: "built it", }, }; /** The result a completed turn carries when it named no piece. */ const EMPTY_RESULT = { outcome: "completed" as const, sessionId: "session-1", continuable: true, looms: [], pieces: [], spaceName: "console-test", finalText: "", }; const turnStarted: ConsoleChatStructuredEvent = { kind: "turn_started", turn: { turnId: "turn-1", status: "running", startedAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }, }; const toolStarted = ( toolCallId: string, toolId: string, ): ConsoleChatStructuredEvent => ({ kind: "tool_started", tool: { toolCallId, toolId }, }); const toolCompleted = ( toolCallId: string, toolId: string, resultSummary?: string, ): ConsoleChatStructuredEvent => ({ kind: "tool_completed", tool: { toolCallId, toolId }, status: "completed", ...(resultSummary === undefined ? {} : { resultSummary }), }); const toolEntry = ( entries: readonly ConsoleLiveEntry[], toolCallId: string, ): Extract => { const entry = entries.find((candidate) => candidate.kind === "tool" && candidate.toolCallId === toolCallId ); if (entry === undefined || entry.kind !== "tool") { throw new Error(`no tool entry for ${toolCallId}`); } return entry; }; describe("the page's entry point", () => { it("registers the element the live page's markup names", () => { // `live.ts` is what the built script runs, and registering the element // is the whole of what it does; an unregistered `` is an // empty pane with no error anywhere. expect(customElements.get("console-live")).toBe(ConsoleLive); }); }); describe("consoleLiveAddress()", () => { it("returns the session the live address names", () => { expect(consoleLiveAddress("/live/session-1")).toEqual({ sessionId: "session-1", }); }); it("returns the session behind the prefix a host fronts the console at", () => { // loom's daemon serves the console at /harness-console on its origin; // the session is still the last segment. expect(consoleLiveAddress("/harness-console/live/session-1")).toEqual({ sessionId: "session-1", }); }); it("returns the session named with a trailing slash", () => { expect(consoleLiveAddress("/live/session-1/").sessionId).toBe( "session-1", ); }); it("returns the session id an address escaped, decoded", () => { expect(consoleLiveAddress("/live/session%2F1").sessionId).toBe( "session/1", ); }); it("returns no session for an escape the address got wrong", () => { expect(consoleLiveAddress("/live/session%E0%A4%A")).toEqual({}); }); it("returns no session for a path below the session segment", () => { expect(consoleLiveAddress("/live/session-1/turn-1")).toEqual({}); }); it("returns no session for the console's own page", () => { expect(consoleLiveAddress("/")).toEqual({}); }); it("returns no session for a live address naming none", () => { expect(consoleLiveAddress("/live/")).toEqual({}); }); it("returns the turn a focused address names", () => { expect(consoleLiveAddress("/live/session-1", "?turn=turn-1")).toEqual({ sessionId: "session-1", turnId: "turn-1", }); }); it("returns the piece base a host renders pieces at", () => { expect(consoleLiveAddress( "/live/session-1", "?piecesBase=http%3A%2F%2Flocalhost%3A9901%2Fpattern-pane", )).toEqual({ sessionId: "session-1", piecesBase: "http://localhost:9901/pattern-pane", }); }); it("returns a piece base without the trailing slash it was given", () => { // The composition adds its own separator, and two would name a path // segment that is not there. expect( consoleLiveAddress( "/live/session-1", "?piecesBase=https%3A%2F%2Fhost.test%2Fpieces%2F%2F", ).piecesBase, ).toBe("https://host.test/pieces"); }); it("refuses a piece base that could run as script", () => { const address = consoleLiveAddress( "/live/session-1", "?piecesBase=javascript%3Aalert(1)", ); expect(address.piecesBase).toBeUndefined(); expect(address.piecesBaseRefused).toBe(true); }); it("refuses a piece base that names no host", () => { const address = consoleLiveAddress( "/live/session-1", "?piecesBase=%2Fpattern-pane", ); expect(address.piecesBase).toBeUndefined(); expect(address.piecesBaseRefused).toBe(true); }); it("refuses a piece base on a scheme a link must not carry", () => { expect( consoleLiveAddress("/live/session-1", "?piecesBase=file%3A%2F%2F%2Fetc") .piecesBase, ).toBeUndefined(); }); it("returns no refusal for an address naming no piece base at all", () => { expect(consoleLiveAddress("/live/session-1").piecesBaseRefused) .toBeUndefined(); }); it("returns no turn for a `turn` the address left empty", () => { expect(consoleLiveAddress("/live/session-1", "?turn=")).toEqual({ sessionId: "session-1", }); }); }); describe("consoleLiveEntries()", () => { it("returns one line per tool call however many events it produced", () => { const entries = consoleLiveEntries(log( turnStarted, toolStarted("call-1", "run_pattern"), { kind: "tool_progress", toolCallId: "call-1", message: "compiling" }, toolCompleted("call-1", "run_pattern", '{"status":"ok"}'), )); expect(entries.filter((entry) => entry.kind === "tool")).toHaveLength(1); expect(toolEntry(entries, "call-1").status).toBe("completed"); expect(toolEntry(entries, "call-1").progress).toBe("compiling"); }); it("returns what a turn already under way did, from the replayed log", () => { // The whole of the backfill: a pane opened mid-turn reads the durable // log from sequence zero, and what it renders is every step that has // already happened rather than only the ones that follow. const entries = consoleLiveEntries(log( turnStarted, toolStarted("call-1", "search_patterns"), toolCompleted("call-1", "search_patterns"), { kind: "assistant_completed", text: "found one" }, toolStarted("call-2", "run_pattern"), )); expect(entries.map((entry) => entry.kind)).toEqual([ "turn", "tool", "assistant", "tool", ]); expect(toolEntry(entries, "call-2").status).toBe("running"); }); it("returns the events out of order in the order they were emitted", () => { const [started, tool] = log(turnStarted, toolStarted("call-1", "x")); expect(consoleLiveEntries([tool, started]).map((entry) => entry.kind)) .toEqual(["turn", "tool"]); }); it("returns a line for an event that belongs to no turn", () => { const [envelope] = log(toolStarted("call-1", "read_file")); const sessionWide = { ...envelope, turnId: undefined }; expect(toolEntry(consoleLiveEntries([sessionWide]), "call-1").turnId) .toBeUndefined(); }); it("returns only the turn a focused feed names", () => { const entries = consoleLiveEntries( log( { turnId: "turn-1", event: toolStarted("call-1", "run_pattern") }, { turnId: "turn-2", event: toolStarted("call-2", "assign_slug") }, ), "turn-2", ); expect(entries).toHaveLength(1); expect(toolEntry(entries, "call-2").toolName).toBe("assign_slug"); }); it("returns the whole assistant message the completed event settled", () => { const entries = consoleLiveEntries(log( { kind: "assistant_delta", text: "half a thought" }, { kind: "assistant_completed", text: "half a thought" }, )); expect(entries).toHaveLength(1); expect(entries[0].kind === "assistant" && entries[0].text).toBe( "half a thought", ); }); it("returns the deltas of a message no completed event settled", () => { const entries = consoleLiveEntries(log( { kind: "assistant_delta", text: "half " }, { kind: "assistant_delta", text: "a thought" }, )); expect(entries[0].kind === "assistant" && entries[0].text).toBe( "half a thought", ); }); it("returns a completed turn's answer in place of the block it streamed as, when the turn named no piece", () => { const entries = consoleLiveEntries(log( { kind: "assistant_completed", text: "Bought cfh:v:22222." }, { kind: "turn_completed", turnId: "turn-1", finalText: "Bought cfh:v:22222.", result: { outcome: "completed" as const, sessionId: "session-1", continuable: true, looms: [], pieces: [], spaceName: "s", finalText: "Bought **cfh:v:22222**.", revealed: { "cfh:v:22222": "https://shop.example/item/7" }, }, }, )); expect(entries).toEqual([{ kind: "ended", key: "2", turnId: "turn-1", status: "completed", outcome: "completed", answer: "Bought **cfh:v:22222**.", revealed: { "cfh:v:22222": "https://shop.example/item/7" }, pieces: [], }]); }); it("keeps the block a turn streamed when its answer came from finish_task", () => { const entries = consoleLiveEntries(log( { kind: "assistant_completed", text: "Checking the forecast." }, { kind: "turn_completed", turnId: "turn-1", finalText: "It is sunny.", outcome: "completed", answer: "It is sunny.", result: { ...EMPTY_RESULT, outcome: "completed" as const, answer: "It is sunny.", finalText: "It is sunny.", }, }, )); expect(entries.map((entry) => entry.kind)).toEqual([ "assistant", "ended", ]); expect(entries[0].kind === "assistant" && entries[0].text).toBe( "Checking the forecast.", ); expect(entries[1].kind === "ended" && entries[1].text).toBe( "It is sunny.", ); }); it("returns a model's reasoning as a thought under the subagent it came from", () => { const subagent = { parentToolCallId: "call-1", profile: "browser" as const, childRunId: "turn-1.subagent.1", }; const entries = consoleLiveEntries(log( { kind: "assistant_reasoning", text: "**Planning** the search." }, { kind: "assistant_reasoning", text: "Open the form first.", subagent }, )); expect(entries).toEqual([{ kind: "thought", key: "1", turnId: "turn-1", text: "**Planning** the search.", }, { kind: "thought", key: "2", turnId: "turn-1", text: "Open the form first.", subagent: { parentToolCallId: "call-1", profile: "browser" }, }]); }); it("returns the piece links a completed turn handed back, with its final text as the answer", () => { const entries = consoleLiveEntries(log({ kind: "turn_completed", turnId: "turn-1", finalText: "built it", result: { outcome: "completed" as const, sessionId: "session-1", continuable: true, looms: [], pieces: [{ slug: "reading-list", url: "http://localhost:8000/s/r" }], spaceName: "s", finalText: "built it", }, })); expect(entries[0]).toEqual({ kind: "ended", key: "1", turnId: "turn-1", status: "completed", outcome: "completed", answer: "built it", pieces: [{ slug: "reading-list", url: "http://localhost:8000/s/r" }], }); }); it("returns the error a failed turn reported", () => { const entries = consoleLiveEntries(log({ kind: "turn_failed", turnId: "turn-1", error: { code: "internal_error", message: "the sandbox is down" }, })); expect(entries[0].kind === "ended" && entries[0].text).toBe( "the sandbox is down", ); }); it("returns a line for a call whose start the feed never carried", () => { // A pane that resumed mid-call is handed the completion without the // start, and the step still belongs in the feed. const entries = consoleLiveEntries(log( toolCompleted("call-1", "assign_slug", '{"slug":"reading-list"}'), )); expect(entries).toHaveLength(1); expect(toolEntry(entries, "call-1").status).toBe("completed"); expect(toolEntry(entries, "call-1").resultSummary).toBe( '{"slug":"reading-list"}', ); }); it("returns the reason a canceled turn gave", () => { const entries = consoleLiveEntries(log({ kind: "turn_canceled", turnId: "turn-1", reason: "the operator stopped it", })); expect(entries[0]).toEqual({ kind: "ended", key: "1", turnId: "turn-1", status: "canceled", text: "the operator stopped it", pieces: [], }); }); it("returns a canceled turn that gave no reason", () => { const entries = consoleLiveEntries(log({ kind: "turn_canceled", turnId: "turn-1", })); expect(entries[0].kind === "ended" && entries[0].text).toBeUndefined(); }); it("returns the child a delegated call belongs to on its lines", () => { const subagent = { parentToolCallId: "call-1", profile: "pattern-author" as const, childRunId: "turn-1.subagent.1", }; const entries = consoleLiveEntries(log( { kind: "tool_started", tool: { toolCallId: "call-2", toolId: "x" }, subagent, }, )); expect(toolEntry(entries, "call-2").subagent).toEqual({ parentToolCallId: "call-1", profile: "pattern-author", }); }); it("ignores an event kind the feed draws nothing for", () => { expect(consoleLiveEntries(log({ kind: "file_changed", change: { kind: "create", path: "/workspace/a.tsx" }, }))).toEqual([]); }); it("returns a subagent line carrying its profile and its verdict", () => { const subagent = { parentToolCallId: "call-1", profile: "pattern-author" as const, goal: "write the card", }; const entries = consoleLiveEntries(log( { kind: "subagent_started", subagent }, { kind: "subagent_completed", subagent, status: "failed" }, )); expect(entries).toEqual([{ kind: "subagent", key: "1", turnId: "turn-1", parentToolCallId: "call-1", profile: "pattern-author", goal: "write the card", status: "failed", }]); }); }); describe("consoleLiveRunReads()", () => { it("returns the turn's own run for a call that completed", () => { expect(consoleLiveRunReads( log(toolCompleted("call-1", "run_pattern"))[0], )).toEqual(["turn-1"]); }); it("returns the turn's own run for a turn that completed", () => { expect(consoleLiveRunReads( log({ kind: "turn_completed", turnId: "turn-1", result: EMPTY_RESULT, })[0], )).toEqual(["turn-1"]); }); it("returns the turn's own run for a turn that failed", () => { expect(consoleLiveRunReads( log({ kind: "turn_failed", turnId: "turn-1", error: { code: "internal_error", message: "down" }, })[0], )).toEqual(["turn-1"]); }); it("returns the child's run beside the turn's for a delegated call", () => { // The parent's run does not record what a child called, so the child's // own run is what carries the step the feed is about to enrich. expect(consoleLiveRunReads( log({ kind: "tool_completed", tool: { toolCallId: "call-2", toolId: "run_pattern" }, status: "completed", subagent: { parentToolCallId: "call-1", profile: "pattern-author", childRunId: "turn-1.subagent.1", }, })[0], )).toEqual(["turn-1", "turn-1.subagent.1"]); }); it("returns the child's run when the child reported it on completing", () => { expect(consoleLiveRunReads( log({ kind: "subagent_completed", status: "completed", subagent: { parentToolCallId: "call-1", profile: "pattern-author", childRunId: "turn-1.subagent.1", }, })[0], )).toEqual(["turn-1.subagent.1"]); }); it("returns no run for a call that only started", () => { expect(consoleLiveRunReads(log(toolStarted("call-1", "run_pattern"))[0])) .toEqual([]); }); it("returns no run for a child whose delegation named none", () => { expect(consoleLiveRunReads( log({ kind: "subagent_completed", status: "failed", subagent: { parentToolCallId: "call-1", profile: "pattern-author" }, })[0], )).toEqual([]); }); }); describe("consoleLivePieceHref()", () => { const space = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; const piece = { slug: "reading-list", url: `http://localhost:8000/${space}/reading-list`, space, } as const; it("returns the address the run recorded when no base is named", () => { expect(consoleLivePieceHref(piece, undefined)).toBe(piece.url); }); it("returns the piece's address under the base a host renders pieces at", () => { expect(consoleLivePieceHref(piece, "http://localhost:9901/pattern-pane")) .toBe(`http://localhost:9901/pattern-pane/${space}/reading-list`); // Written as the shell writes it, so the shell reads back this piece. expect( urlToAppView( new URL(consoleLivePieceHref(piece, "http://localhost:9901")), ), ).toEqual({ spaceDid: space, pieceSlug: "reading-list" }); }); it("takes nothing of the recorded address but the space and slug", () => { const queried = { ...piece, url: `http://localhost:8000/api/v1/${space}/reading-list?t=1#top`, }; expect(consoleLivePieceHref(queried, "http://host.test/p")) .toBe(`http://host.test/p/${space}/reading-list`); }); it("returns the recorded address for a piece that recorded no space", () => { const { space: _space, ...unplaced } = piece; expect(consoleLivePieceHref(unplaced, "http://host.test/p")).toBe( piece.url, ); }); }); describe("stepWithheldAnything()", () => { const step = ( withheld: ConsoleStep["withheld"], policy?: ConsoleStep["policy"], ): ConsoleStep => ({ index: 0, kind: "tool", toolName: "run_pattern", toolCallId: "call-1", handlesIntroduced: [], handlesInScope: [], status: "ok", policyEvents: [], withheld, ...(policy === undefined ? {} : { policy }), }); it("returns `true` for a release the boundary withheld from", () => { expect(stepWithheldAnything(step({ status: "unrecorded", locations: [], }, { decision: "withheld", reasonCodes: ["cfc_release_withheld"], }))).toBe(true); }); it("returns `true` for a result whose record holds a withheld position", () => { expect(stepWithheldAnything(step({ status: "recorded", locations: [{ rule: "artifact-only", artifactPath: "/run/output.json", jsonPointer: "/rawValue", available: true, }], }))).toBe(true); }); it("returns `true` for a record the console could not read", () => { expect(stepWithheldAnything(step({ status: "record-unreadable", locations: [], }))).toBe(true); }); it("returns `true` for a record holding no entry for this result", () => { expect(stepWithheldAnything(step({ status: "record-entry-missing", locations: [], }))).toBe(true); }); it("returns `false` for a result no omission rule applied to", () => { // A narrow pane marks what was held back, not every result that // recorded holding nothing back. expect(stepWithheldAnything(step({ status: "recorded", locations: [] }))) .toBe(false); }); }); describe("consoleLiveAtTail()", () => { it("returns `true` for a feed scrolled to its bottom", () => { expect(consoleLiveAtTail({ scrollHeight: 1000, scrollTop: 700, clientHeight: 300, })).toBe(true); }); it("returns `true` for a feed a row's rounding short of its bottom", () => { expect(consoleLiveAtTail({ scrollHeight: 1000, scrollTop: 693, clientHeight: 300, })).toBe(true); }); it("returns `false` for a reader who scrolled up to an earlier step", () => { expect(consoleLiveAtTail({ scrollHeight: 1000, scrollTop: 200, clientHeight: 300, })).toBe(false); }); }); describe("consoleLivePolicyMark()", () => { const step = ( policy: ConsoleStep["policy"], withheld: ConsoleStep["withheld"] = { status: "recorded", locations: [], }, ): ConsoleStep => ({ index: 0, kind: "tool", toolName: "browser", toolCallId: "call-1", handlesIntroduced: [], handlesInScope: [], status: "ok", ...(policy === undefined ? {} : { policy }), policyEvents: [], withheld, }); it("returns a quiet shield, and no word, for an allowance that held nothing back", () => { const text = templateText(consoleLivePolicyMark(step({ decision: "allowed", effectClass: "side-effect", reasonCodes: ["cfc_enforce_strict_direct_command"], }))); expect(text).toContain("live-policy ok"); expect(text).toContain('Allowed'); expect(text).not.toContain('