import { describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { nothing } from "lit"; import { clampSelection, ConsoleSteps, stepPolicyView, withheldSummary, withheldView, } from "../../../console/src/steps-view.ts"; import { type ConsoleHandle, consoleRunSteps, type ConsoleStep, } from "../../../console/steps.ts"; import type { HarnessTranscriptMessage } from "../../../src/contracts/transcript.ts"; import { createToolOutputId } from "../../../src/contracts/tool-result.ts"; import { templateText } from "./template-text.ts"; describe("console/src/steps-view", () => { describe("clampSelection", () => { it("keeps a selection the run is long enough to hold", () => { expect(clampSelection(3, 9)).toBe(3); }); it("moves a selection past a shorter run's end to its last step", () => { expect(clampSelection(8, 3)).toBe(2); }); it("answers zero for a run that recorded no steps", () => { expect(clampSelection(8, 0)).toBe(0); }); it("answers zero for a step before the first", () => { expect(clampSelection(-1, 9)).toBe(0); }); }); describe("stepPolicyView", () => { const step = (policy?: ConsoleStep["policy"]): ConsoleStep => ({ index: 1, kind: "tool", toolName: "read_file", handlesIntroduced: [], handlesInScope: [], status: "ok", policyEvents: [], withheld: { status: "recorded", locations: [] }, ...(policy === undefined ? {} : { policy }), }); it("renders nothing for a step whose run recorded no CFC decision", () => { // A step with no decision, no event and no labels has no CFC pane to // draw, and an empty one would read as a decision that was not made. expect(stepPolicyView(step())).toBe(nothing); }); it("renders the decision and its reasons for a step that carries one", () => { const text = templateText(stepPolicyView(step({ decision: "allowed", effectClass: "side-effect", reasonCodes: ["cfc_enforce_explicit_direct_command"], }))); expect(text).toContain("allowed"); expect(text).toContain("side-effect"); expect(text).toContain("cfc_enforce_explicit_direct_command"); }); it("lists the prompt slot's influence as integrity on the path it names", () => { const text = templateText(stepPolicyView({ ...step(), invocation: { type: "cf-harness.cfc-invocation-context", version: 1, sequence: 1, runId: "r", createdAt: "2026-01-01T00:00:00.000Z", toolId: "bash", operation: "shell", cfcEnforcementMode: "enforce-strict", cwd: "/workspace", runManifest: { present: false }, inputs: {}, promptSlotInfluenceLabels: { version: 1, entries: [{ path: ["command"], label: { integrity: [{ type: "https://commonfabric.org/cfc/atom/PromptSlotInfluence", version: 1, }], }, }], }, }, })); expect(text).toContain("command"); expect(text).toContain("PromptSlotInfluence"); // An integrity-only row carries no taint and says nothing about its // absence. expect(text).not.toContain("no confidentiality atom"); }); }); describe("withheldSummary", () => { const step = (withheld: ConsoleStep["withheld"]): ConsoleStep => ({ index: 1, kind: "tool", handlesIntroduced: [], handlesInScope: [], status: "ok", policyEvents: [], withheld, }); it("labels a legacy result as having no omission record", () => { expect(withheldSummary(step({ status: "unrecorded", locations: [] }))) .toBe("model omissions · no record"); }); it("labels a recorded result with its withheld position count", () => { expect(withheldSummary(step({ status: "recorded", locations: [{ rule: "artifact-only", artifactPath: "/run/tool-outputs/result.json", jsonPointer: "/rawValue", value: "retained", available: true, }], }))).toBe("kept on the artifact, not sent to the model · 1"); }); it("labels policy-denied observations by their rule", () => { expect(withheldSummary(step({ status: "recorded", locations: [{ rule: "observation-denied", artifactPath: "/run/tool-outputs/result.json", jsonPointer: "/stdout", redaction: "[redacted by CFC]", available: true, }], }))).toBe("withheld by policy · 1"); }); it("distinguishes an unreadable record from a missing result entry", () => { expect(withheldSummary(step({ status: "record-unreadable", locations: [], }))).toContain("record unreadable"); expect(withheldSummary(step({ status: "record-entry-missing", locations: [], }))).toContain("entry missing"); }); }); describe("withheldView", () => { const step = (withheld: ConsoleStep["withheld"]): ConsoleStep => ({ index: 1, kind: "tool", handlesIntroduced: [], handlesInScope: [], status: "ok", policyEvents: [], withheld, }); it("explains why a legacy result cannot be reconstructed", () => { const text = templateText(withheldView(step({ status: "unrecorded", locations: [], }))); expect(text).toContain("model omissions · no record"); expect(text).toContain("Legacy runs cannot be"); expect(text).toContain("reconstructed honestly"); }); it("states when a recorded result has no omissions", () => { const text = templateText(withheldView(step({ status: "recorded", locations: [], }))); expect(text).toContain("No omission rule applied to this result"); expect(text).not.toContain("withheld by policy"); expect(text).not.toContain("kept on the artifact"); }); it("does not describe a bad or incomplete current record as legacy", () => { const unreadable = templateText(withheldView(step({ status: "record-unreadable", locations: [], }))); const incomplete = templateText(withheldView(step({ status: "record-entry-missing", locations: [], }))); expect(unreadable).toContain("exists but is unreadable"); expect(incomplete).toContain("exists but has no entry"); expect(unreadable).not.toContain("Legacy"); expect(incomplete).not.toContain("Legacy"); }); it("renders values, redaction markers, and unavailable positions", () => { const text = templateText(withheldView(step({ status: "recorded", locations: [{ rule: "artifact-only", artifactPath: "/run/output.json", jsonPointer: "/rawValue", value: { retained: true }, available: true, }, { rule: "observation-denied", artifactPath: "/run/output.json", jsonPointer: "/stderr", redaction: "[redacted by CFC]", available: true, }, { rule: "model-context-truncation", artifactPath: "/run/missing.json", jsonPointer: "/stdout", available: false, }], }))); expect(text).toContain("kept on the artifact, not sent to the model"); expect(text).toContain("withheld by policy"); expect(text).toContain("model-context-truncation"); expect(text).toContain('{\n "retained": true\n}'); expect(text).toContain("[redacted by CFC]"); expect(text).toContain("recorded artifact position is unavailable"); expect(text).not.toContain("Model received:"); }); it("never renders an available denied value through an overlapping rule", () => { const outputId = createToolOutputId("run", "bash", 1); const artifactPath = "/run/tool-outputs/bash.json"; const transcript: HarnessTranscriptMessage[] = [{ role: "assistant", content: "", toolCalls: [{ id: "call-1", type: "function", function: { name: "bash", arguments: '{"command":"echo"}' }, }], }, { role: "tool", toolCallId: "call-1", toolName: "bash", content: "model-facing", resultRef: { type: "cf-harness.tool-result-ref", outputId, toolId: "bash", runId: "run", artifactPath, }, }]; const locations = [{ artifactPath, jsonPointer: "/stdout" }]; const joined = consoleRunSteps( transcript, [], [], [], { type: "cf-harness.transcript-omissions", version: 1, results: [{ transcriptIndex: 1, toolCallId: "call-1", toolId: "bash", outputId, rules: [{ rule: "artifact-only", locations }, { rule: "observation-denied", locations, }], }], }, [{ artifactPath, value: { stdout: "PLANTED-SECRET" } }], ); const text = templateText(withheldView(joined[0])); expect(text.match(/\[redacted by CFC\]/g)).toHaveLength(2); expect(text).not.toContain("PLANTED-SECRET"); expect(text).toContain("Model received: model-facing"); }); }); describe("ConsoleSteps", () => { class TestConsoleSteps extends ConsoleSteps { view() { return this.render(); } } it("renders a step's reasoning as plain text in a thinking pane, for prose and for a call", () => { const steps = consoleRunSteps([ { role: "assistant", content: "", reasoning: "**Read** [it](https://elsewhere.example/) first.", toolCalls: [{ id: "c1", type: "function" as const, function: { name: "read_file", arguments: '{"path":"a"}' }, }], }, { role: "tool", toolCallId: "c1", toolName: "read_file", content: JSON.stringify({ outputId: "o1", content: "a" }), }, { role: "assistant", content: "done", reasoning: "It says *done*." }, ]); const view = new TestConsoleSteps(); view.steps = steps; const shown = [0, 1].map((selected) => { view.selected = selected; return templateText(view.view()); }); expect(shown[0]).toContain('