/**
* Demo + visual artifact tests (W6) — deterministic, zero network, no real
* Chrome:
*
* 1. bridge-script injection (buildDemoSrcDoc) + the pure bridge-message
* reducer (reduceDemoBridgeState)
* 2. card rendering (static markup): sandboxed iframe attrs, error chip,
* failure notes, SVG image documents
* 3. the demo-builder loop with a FAKE model + FAKE headless (success with
* a fix iteration, untested-final verification, never-clean failure,
* no-Chrome failure)
* 4. the visual-maker loop (fake model + fake headless): render-and-look
* success, forced look on premature finish, never-writes failure
* 5. job runners + dispatch (demo part → demo job → steer via a fake
* deliver; visual failure path; end-to-end dispatch through
* handleChatRequest with the ack protocol)
* 6. tool registry wiring (server-executed dispatch tools, schemas)
*
* Cleanup follows tests/jobs.test.ts idioms (created chat/job files removed
* in the after() hook; overrides reset in finally blocks).
*/
import React from "react";
(globalThis as Record).React = React; // tsx classic-JSX shim (see render-cards.test.tsx)
import assert from "node:assert/strict";
import * as fs from "node:fs";
import * as path from "node:path";
import { after, describe, it } from "node:test";
import { createCanvas } from "@napi-rs/canvas";
import { Value } from "typebox/value";
import { createFauxCore, fauxAssistantMessage, fauxToolCall, type FauxResponseStep, type ToolCall } from "@earendil-works/pi-ai";
import { renderToStaticMarkup } from "react-dom/server";
import { PATHS } from "../config.ts";
import {
buildDemoSrcDoc,
DEMO_BRIDGE_SCRIPT,
DemoCard,
DemoErrorChip,
INITIAL_DEMO_BRIDGE_STATE,
reduceDemoBridgeState,
} from "../components/artifacts/demo-card.tsx";
import { svgNaturalWidth, VisualCard, VisualOutputCard } from "../components/artifacts/visual-card.tsx";
import { slugFilename } from "../components/artifacts/download.ts";
import { chartSize } from "../components/session-overlay.tsx";
import { SubagentSteerCard } from "../components/artifacts/steer-card.tsx";
import { DispatchCard } from "../components/artifacts/dispatch-card.tsx";
import { JobCard, PdfJobCard } from "../components/artifacts/job-card.tsx";
import { ToolCallCard } from "../components/artifacts/tool-call-card.tsx";
import { JobsContext } from "../components/jobs-context.ts";
import { demoInputSchema, jobTools, pdfInputSchema, visualInputSchema } from "../src/agent/tools/jobs.ts";
import { runDemoBuilder } from "../src/agent/subagents/demo-builder.ts";
import { headlessOverride, type DemoRunResult, type HeadlessApi } from "../src/agent/subagents/headless.ts";
import { createJobRunner, demoModelOverride, visualModelOverride } from "../src/agent/subagents/runner.ts";
import { runVisualMaker } from "../src/agent/subagents/visual-maker.ts";
import { buildAbortSteer, buildSubagentSteer, isSubagentFailure, setSteerUiInverted, type SteerMessage } from "../src/core/contracts.ts";
import type { JobRecord } from "../src/core/contracts.ts";
import { createTeacherTools } from "../src/agent/tools.ts";
import { chatStore } from "../src/store/chats.ts";
import { jobStore } from "../src/store/jobs.ts";
import { handleChatRequest } from "../src/server/chat-handler.ts";
import { runningTurn } from "../src/server/turn-runs.ts";
import { createTeacherAgent } from "../src/agent/teacher-agent.ts";
import { projectView as projectNative } from "../src/core/view.ts";
import type { PiEngine } from "../src/agent/models.ts";
/**
* A fake deliver seam + a runner wired like the chat-handler's, per test:
* capture the steer and mimic the pipeline's final status flip so the job
* record reads exactly as production does after the wake turn completes.
*/
function testRunner() {
const steers: SteerMessage[] = [];
const runner = createJobRunner({
chatStore,
jobStore,
deliver: async (steer) => {
steers.push(steer);
const jobId = (steer.details as { jobId: string }).jobId;
const job = jobStore.getJob(jobId);
if (job && job.status === "running") {
const failed = job.result ? isSubagentFailure(job.kind, job.result as never) : true;
jobStore.updateJob(jobId, { status: failed ? "error" : "done", activity: failed ? "failed" : "delivered" });
}
},
});
return { steers, runner };
}
import { artifactRenderers } from "../components/artifacts/index.ts";
// The key-presence turn gate (provider-key interlude, 2026-09-25): scripted
// turns pin openrouter per chat — the gate reads env-first truth, so the
// key must be present in the test process like any real setup.
process.env.OPENROUTER_API_KEY ??= "test-key-presence-gate";
const JOBS_DIR = path.join(PATHS.data, "jobs");
const createdChats: string[] = [];
const createdJobs: string[] = [];
after(() => {
for (const id of createdChats) {
try {
fs.unlinkSync(path.join(PATHS.chats, `${id}.json`));
} catch {
/* already gone */
}
}
for (const id of createdJobs) {
try {
fs.unlinkSync(path.join(JOBS_DIR, `${id}.json`));
} catch {
/* already gone */
}
}
headlessOverride.current = undefined;
demoModelOverride.current = undefined;
visualModelOverride.current = undefined;
});
/** A scripted pi engine (faux provider): the response queue replaces the old
* doGenerate turn counter — one queued response per model call (tool-call
* turn or final text), consumed in order. */
function scriptedModel(turns: Array<{ toolCalls?: Array<{ toolCallId: string; toolName: string; input: unknown }>; text?: string }>): PiEngine {
const steps: FauxResponseStep[] = turns.map((step) => {
if (step.toolCalls) {
return fauxAssistantMessage(
step.toolCalls.map((c) => fauxToolCall(c.toolName, c.input as ToolCall["arguments"], { id: c.toolCallId })),
{ stopReason: "toolUse" },
);
}
return fauxAssistantMessage(step.text ?? "");
});
const faux = createFauxCore({ tokenSize: { min: 64, max: 64 } });
faux.setResponses(steps);
return { model: faux.getModel(), streamFn: faux.streamSimple };
}
// ─── fake headless ───────────────────────────────────────────────────────────
const CLEAN_RUN: DemoRunResult = { consoleErrors: [], pageErrors: [], failedRequests: [], ok: true };
const BROKEN_RUN: DemoRunResult = {
consoleErrors: ["Uncaught ReferenceError: foo is not defined"],
pageErrors: [],
failedRequests: [],
ok: false,
};
function fakeHeadless(opts: {
/** Return BROKEN_RUN for html matching this substring, else CLEAN_RUN. */
brokenWhenIncludes?: string;
/** Throw for the first N renderSvg calls (then succeed). */
renderFailures?: number;
} = {}): HeadlessApi & { demoRuns: string[]; svgRenders: string[] } {
const api = {
demoRuns: [] as string[],
svgRenders: [] as string[],
available() {
return true;
},
unavailableReason() {
return null;
},
async runDemo(html: string): Promise {
api.demoRuns.push(html);
return opts.brokenWhenIncludes && html.includes(opts.brokenWhenIncludes) ? BROKEN_RUN : CLEAN_RUN;
},
async renderSvg(svg: string): Promise {
api.svgRenders.push(svg);
if (opts.renderFailures && api.svgRenders.length <= opts.renderFailures) {
throw new Error("XML parse error");
}
// The shared native image processor decodes renders, so a signature
// alone is no longer a successful fake raster.
const canvas = createCanvas(1, 1);
canvas.getContext("2d").fillRect(0, 0, 1, 1);
return canvas.toBuffer("image/png");
},
};
return api as HeadlessApi & { demoRuns: string[]; svgRenders: string[] };
}
function noChromeHeadless(): HeadlessApi {
return {
available: () => false,
unavailableReason: () => "No Chrome/Chromium found for headless testing. Install Google Chrome or set CHROME_PATH.",
runDemo: async () => {
throw new Error("should not run");
},
renderSvg: async () => {
throw new Error("should not render");
},
renderPdfPages: async () => {
throw new Error("should not render");
},
};
}
// ─── 1. bridge injection + reducer ───────────────────────────────────────────
describe("demo bridge", () => {
it("injects the resize + error hooks before