diff --git a/.changeset/funny-bats-itch.md b/.changeset/funny-bats-itch.md new file mode 100644 index 0000000..a845151 --- /dev/null +++ b/.changeset/funny-bats-itch.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/test/api.test.ts b/test/api.test.ts index f474074..4c3a2ad 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -1262,6 +1262,142 @@ test("mcp endpoint requires bearer when token configured", async () => { assert.equal(ok.status, 200); }); +test("mcp upload_asset stores base64 bytes and returns id + url + kind", async () => { + const app = makeApp(); + const data = Buffer.from("\x89PNG\r\n\x1a\n pixels"); + const res = (await ( + await app.request( + "/mcp", + mcpCall(1, "tools/call", { + name: "upload_asset", + arguments: { + data: data.toString("base64"), + contentType: "image/png", + filename: "shot.png", + kind: "image", + }, + }), + ) + ).json()) as any; + assert.equal(res.result.isError, undefined); + const asset = JSON.parse(res.result.content[0].text); + assert.ok(asset.id); + assert.equal(asset.kind, "image"); + assert.equal(asset.contentType, "image/png"); + assert.equal(asset.byteLength, data.length); + assert.ok(asset.url.includes(`/a/${asset.id}`)); + // the blob is retrievable at the asset route, with matching bytes + const blob = await app.request(`/a/${asset.id}`); + assert.equal(blob.status, 200); + assert.equal((await blob.arrayBuffer()).byteLength, data.length); +}); + +test("mcp upload_asset without data fails with a clear error", async () => { + const app = makeApp(); + const res = (await ( + await app.request( + "/mcp", + mcpCall(1, "tools/call", { name: "upload_asset", arguments: { contentType: "image/png" } }), + ) + ).json()) as any; + assert.equal(res.result.isError, true); + assert.match(res.result.content[0].text, /upload_asset needs base64 `data`/); +}); + +test("mcp upload_asset with an explicit session attaches the asset to it", async () => { + const app = makeApp(); + const session = (await (await app.request("/api/sessions", json({ agent: "m" }))).json()) as any; + const res = (await ( + await app.request( + "/mcp", + mcpCall(1, "tools/call", { + name: "upload_asset", + arguments: { + data: Buffer.from("hi").toString("base64"), + contentType: "text/plain", + session: session.id, + }, + }), + ) + ).json()) as any; + const asset = JSON.parse(res.result.content[0].text); + assert.equal(asset.sessionId, session.id); +}); + +test("mcp get_design_guide returns the guide text", async () => { + const app = makeApp(); + const res = (await ( + await app.request("/mcp", mcpCall(1, "tools/call", { name: "get_design_guide", arguments: {} })) + ).json()) as any; + assert.equal(res.result.isError, undefined); + assert.equal(res.result.content[0].text, "# guide"); +}); + +test("mcp publish_post with no surfaces fails with a clear error", async () => { + const app = makeApp(); + const res = (await ( + await app.request( + "/mcp", + mcpCall(1, "tools/call", { name: "publish_post", arguments: { surfaces: [] } }), + ) + ).json()) as any; + assert.equal(res.result.isError, true); + assert.match(res.result.content[0].text, /a post needs at least one surface/); +}); + +test("mcp update_snippet revises via the legacy html argument", async () => { + const app = makeApp(); + const pub = (await ( + await app.request( + "/mcp", + mcpCall(1, "tools/call", { name: "publish_snippet", arguments: { html: "

v1

" } }), + ) + ).json()) as any; + const id = JSON.parse(pub.result.content[0].text).id; + const res = (await ( + await app.request( + "/mcp", + mcpCall(2, "tools/call", { name: "update_snippet", arguments: { id, html: "

v2

" } }), + ) + ).json()) as any; + const out = JSON.parse(res.result.content[0].text); + assert.equal(out.id, id); + assert.equal(out.version, 2); +}); + +test("mcp endpoint: malformed JSON body is a -32700 parse error", async () => { + const app = makeApp(); + const res = await app.request("/mcp", { method: "POST", body: "{not valid json" }); + assert.equal(res.status, 400); + const body = (await res.json()) as any; + assert.equal(body.error.code, -32700); + assert.equal(body.id, null); +}); + +test("mcp endpoint: a JSON-RPC batch is rejected with -32600", async () => { + const app = makeApp(); + const res = await app.request("/mcp", json([mcpCall(1, "tools/list").body])); + assert.equal(res.status, 400); + const body = (await res.json()) as any; + assert.equal(body.error.code, -32600); + assert.match(body.error.message, /batch/); +}); + +test("mcp endpoint: a notification (no id) is acknowledged 202 with no body", async () => { + const app = makeApp(); + const res = await app.request( + "/mcp", + json({ jsonrpc: "2.0", method: "notifications/initialized" }), + ); + assert.equal(res.status, 202); +}); + +test("mcp endpoint: ping responds with an empty result", async () => { + const app = makeApp(); + const res = (await (await app.request("/mcp", mcpCall(1, "ping"))).json()) as any; + assert.deepEqual(res.result, {}); +}); + test("agent writes piggyback unseen user comments, delivered once", async () => { const app = makeApp(); const s = (await ( diff --git a/test/cli.test.ts b/test/cli.test.ts index f383ecd..b3ce3a6 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; import { execFile, spawn } from "node:child_process"; import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -347,3 +348,530 @@ test("hook stays silent when no sideshow session owns the cwd", async () => { await server.close(); } }); + +// --------------------------------------------------------------------------- +// Core publish → comment → revise loop and the rich-surface commands. +// The CLI is a first-class integration tier ("agents with only a shell can use +// this"); these exercise the command bodies that hit the network. +// --------------------------------------------------------------------------- + +// A throwaway file under a temp dir; returns its absolute path. +function tmpFile(name: string, content: string) { + const dir = mkdtempSync(join(tmpdir(), "sideshow-cli-file-")); + const file = join(dir, name); + writeFileSync(file, content); + return file; +} + +// Create a session on the test server and return { id, url, close, session }. +async function serveSession() { + const server = await serveApp(); + const session = await post(`${server.url}/api/sessions`, { agent: "cli-test", title: "CLI" }); + return { ...server, session }; +} + +// Run a CLI command against a running server, pinning the session via env so +// state-file resolution never interferes across tests. +function cli(server: { url: string; session: { id: string } }, ...args: string[]) { + return runWith( + { env: { SIDESHOW_URL: server.url, SIDESHOW_SESSION: server.session.id } }, + ...args, + ); +} + +// --- publish (html + combined surfaces) ----------------------------------- + +test("publish posts an html file and prints id + url + kinds", async () => { + const server = await serveSession(); + try { + const file = tmpFile("card.html", "

hello

"); + const { code, stdout } = await cli(server, "publish", file, "--title", "Card"); + assert.equal(code, 0); + const out = JSON.parse(stdout); + assert.equal(out.title, "Card"); + assert.equal(out.sessionId, server.session.id); + assert.deepEqual(out.kinds, ["html"]); + assert.equal(out.url, `${server.url}/s/${out.id}`); + assert.equal(out.version, 1); + } finally { + await server.close(); + } +}); + +test("publish reads html from stdin with '-'", async () => { + const server = await serveSession(); + try { + const { code, stdout } = await runWith( + { + env: { SIDESHOW_URL: server.url, SIDESHOW_SESSION: server.session.id }, + stdin: "

piped

", + }, + "publish", + "-", + ); + assert.equal(code, 0); + const out = JSON.parse(stdout); + assert.deepEqual(out.kinds, ["html"]); + const full = (await fetch(`${server.url}/api/surfaces/${out.id}`).then((r) => r.json())) as any; + assert.equal(full.surfaces[0].html, "

piped

"); + } finally { + await server.close(); + } +}); + +test("publish combines html with --md, --code, --terminal, --mermaid surfaces", async () => { + const server = await serveSession(); + try { + const html = tmpFile("h.html", "
x
"); + const md = tmpFile("m.md", "# heading"); + const code = tmpFile("snippet.ts", "const x = 1;"); + const term = tmpFile("t.log", "$ echo hi"); + const mermaid = tmpFile("d.mmd", "graph TD; A-->B"); + const { code: exit, stdout } = await cli( + server, + "publish", + html, + "--md", + md, + "--code", + code, + "--terminal", + term, + "--mermaid", + mermaid, + ); + assert.equal(exit, 0); + const out = JSON.parse(stdout); + // The publish command appends in a fixed order: html, md, mermaid, diff, + // terminal, json, code, image — independent of flag order on the command line. + assert.deepEqual(out.kinds, ["html", "markdown", "mermaid", "terminal", "code"]); + } finally { + await server.close(); + } +}); + +test("publish --code infers the language from the filename", async () => { + const server = await serveSession(); + try { + const html = tmpFile("h.html", "

"); + const code = tmpFile("app.py", "print('hi')"); + const { stdout } = await cli(server, "publish", html, "--code", code); + const out = JSON.parse(stdout); + assert.deepEqual(out.kinds, ["html", "code"]); + const full = (await fetch(`${server.url}/api/surfaces/${out.id}`).then((r) => r.json())) as any; + const codeSurface = full.surfaces.find((s: any) => s.kind === "code"); + assert.equal(codeSurface.language, "python"); + assert.equal(codeSurface.title, "app.py"); + } finally { + await server.close(); + } +}); + +test("publish --json with invalid JSON fails with a clear error", async () => { + const server = await serveSession(); + try { + const html = tmpFile("h.html", "

"); + const bad = tmpFile("bad.json", "{not json"); + const { code, stderr } = await cli(server, "publish", html, "--json", bad); + assert.notEqual(code, 0); + assert.match(stderr, /--json: invalid JSON/); + } finally { + await server.close(); + } +}); + +test("publish --diff with --layout split carries the layout on the diff surface", async () => { + const server = await serveSession(); + try { + const html = tmpFile("h.html", "

"); + const patch = tmpFile("p.patch", "--- a/f.txt\n+++ b/f.txt\n@@ -1 +1 @@\n-old\n+new\n"); + const { stdout } = await cli(server, "publish", html, "--diff", patch, "--layout", "split"); + const out = JSON.parse(stdout); + assert.deepEqual(out.kinds, ["html", "diff"]); + const full = (await fetch(`${server.url}/api/surfaces/${out.id}`).then((r) => r.json())) as any; + assert.equal(full.surfaces.find((s: any) => s.kind === "diff").layout, "split"); + } finally { + await server.close(); + } +}); + +// --- single-surface commands (thin wrappers around the publish path) ------ + +test("diff publishes a diff-only post from a patch", async () => { + const server = await serveSession(); + try { + const patch = tmpFile("p.patch", "--- a/f.txt\n+++ b/f.txt\n@@ -1 +1 @@\n-old\n+new\n"); + const { code, stdout } = await cli(server, "diff", patch, "--title", "Fix"); + assert.equal(code, 0); + assert.deepEqual(JSON.parse(stdout).kinds, ["diff"]); + } finally { + await server.close(); + } +}); + +test("markdown publishes a markdown-only post", async () => { + const server = await serveSession(); + try { + const md = tmpFile("m.md", "# hello\n\nbody"); + const { code, stdout } = await cli(server, "markdown", md); + assert.equal(code, 0); + assert.deepEqual(JSON.parse(stdout).kinds, ["markdown"]); + } finally { + await server.close(); + } +}); + +test("code --line-start and --filename and --language are honored", async () => { + const server = await serveSession(); + try { + const src = tmpFile("x.txt", "a\nb\nc"); + const { code, stdout } = await cli( + server, + "code", + src, + "--filename", + "src/lib.rs", + "--language", + "rust", + "--line-start", + "42", + ); + assert.equal(code, 0); + const full = (await fetch(`${server.url}/api/surfaces/${JSON.parse(stdout).id}`).then((r) => + r.json(), + )) as any; + const surface = full.surfaces[0]; + assert.equal(surface.kind, "code"); + assert.equal(surface.language, "rust"); + assert.equal(surface.title, "src/lib.rs"); + assert.equal(surface.lineStart, 42); + } finally { + await server.close(); + } +}); + +test("terminal --cols and --term-title are honored", async () => { + const server = await serveSession(); + try { + const t = tmpFile("t.log", "$ run\nok"); + const { code, stdout } = await cli( + server, + "terminal", + t, + "--cols", + "120", + "--term-title", + "build", + ); + assert.equal(code, 0); + const full = (await fetch(`${server.url}/api/surfaces/${JSON.parse(stdout).id}`).then((r) => + r.json(), + )) as any; + const surface = full.surfaces[0]; + assert.equal(surface.kind, "terminal"); + assert.equal(surface.cols, 120); + assert.equal(surface.title, "build"); + } finally { + await server.close(); + } +}); + +test("json publishes a parsed JSON surface", async () => { + const server = await serveSession(); + try { + const f = tmpFile("d.json", '{"a": 1, "b": [2, 3]}'); + const { code, stdout } = await cli(server, "json", f); + assert.equal(code, 0); + const full = (await fetch(`${server.url}/api/surfaces/${JSON.parse(stdout).id}`).then((r) => + r.json(), + )) as any; + assert.equal(full.surfaces[0].kind, "json"); + assert.deepEqual(full.surfaces[0].data, { a: 1, b: [2, 3] }); + } finally { + await server.close(); + } +}); + +test("json with invalid JSON fails with a clear error", async () => { + const server = await serveSession(); + try { + const f = tmpFile("bad.json", "{nope"); + const { code, stderr } = await cli(server, "json", f); + assert.notEqual(code, 0); + assert.match(stderr, /invalid JSON/); + } finally { + await server.close(); + } +}); + +test("mermaid publishes a mermaid-only post", async () => { + const server = await serveSession(); + try { + const m = tmpFile("d.mmd", "graph TD; A-->B"); + const { code, stdout } = await cli(server, "mermaid", m); + assert.equal(code, 0); + assert.deepEqual(JSON.parse(stdout).kinds, ["mermaid"]); + } finally { + await server.close(); + } +}); + +// --- update (revise → new version, same card) ----------------------------- + +test("update revises a post to a new version on the same card", async () => { + const server = await serveSession(); + try { + const file = tmpFile("v1.html", "

v1

"); + const pub = await cli(server, "publish", file); + const id = JSON.parse(pub.stdout).id; + + const next = tmpFile("v2.html", "

v2

"); + const { code, stdout } = await cli(server, "update", id, next, "--title", "Renamed"); + assert.equal(code, 0); + const out = JSON.parse(stdout); + assert.equal(out.id, id); + assert.equal(out.version, 2); + assert.equal(out.title, "Renamed"); + } finally { + await server.close(); + } +}); + +test("update without an id fails with a usage error", async () => { + const server = await serveSession(); + try { + const { code, stderr } = await cli(server, "update"); + assert.notEqual(code, 0); + assert.match(stderr, /usage: sideshow update/); + } finally { + await server.close(); + } +}); + +// --- wait (blocking feedback long-poll) ----------------------------------- + +test("wait returns a pending user comment immediately", async () => { + const server = await serveSession(); + try { + const file = tmpFile("c.html", "

x

"); + const pub = await cli(server, "publish", file); + const id = JSON.parse(pub.stdout).id; + // a user comment is already waiting when wait runs + await post(`${server.url}/api/comments`, { surface: id, text: "ship it", author: "user" }); + + const { code, stdout } = await cli(server, "wait", "--timeout", "5"); + assert.equal(code, 0); + const out = JSON.parse(stdout); + assert.equal(out.comments.length, 1); + assert.equal(out.comments[0].text, "ship it"); + } finally { + await server.close(); + } +}); + +test("wait with no comments returns timedOut", async () => { + const server = await serveSession(); + try { + const { code, stdout } = await cli(server, "wait", "--timeout", "1"); + assert.equal(code, 0); + const out = JSON.parse(stdout); + assert.equal(out.timedOut, true); + assert.deepEqual(out.comments, []); + } finally { + await server.close(); + } +}); + +test("wait --after with a non-number fails fast", async () => { + const server = await serveSession(); + try { + const { code, stderr } = await cli(server, "wait", "--after", "abc"); + assert.notEqual(code, 0); + assert.match(stderr, /--after must be a number/); + } finally { + await server.close(); + } +}); + +// --- comment (agent replies to the user) ---------------------------------- + +test("comment replies on a post; --author overrides the default agent name", async () => { + const server = await serveSession(); + try { + const file = tmpFile("c.html", "

x

"); + const id = JSON.parse((await cli(server, "publish", file)).stdout).id; + + // default author falls back to "agent" when no --author/--agent/env is set + const def = await cli(server, "comment", "on it", "--post", id); + assert.equal(def.code, 0); + assert.equal(JSON.parse(def.stdout).author, "agent"); + + // --author sets the reply's author explicitly + const named = await cli(server, "comment", "on it", "--post", id, "--author", "bot7"); + assert.equal(named.code, 0); + const out = JSON.parse(named.stdout); + assert.equal(out.text, "on it"); + assert.equal(out.postId, id); + assert.equal(out.author, "bot7"); + } finally { + await server.close(); + } +}); + +test("comment without --post fails with a usage error", async () => { + const server = await serveSession(); + try { + const { code, stderr } = await cli(server, "comment", "hello"); + assert.notEqual(code, 0); + assert.match(stderr, /a comment must target a post/); + } finally { + await server.close(); + } +}); + +test("comment --surface is a back-compat alias for --post", async () => { + const server = await serveSession(); + try { + const file = tmpFile("c.html", "

x

"); + const id = JSON.parse((await cli(server, "publish", file)).stdout).id; + const { code, stdout } = await cli(server, "comment", "via alias", "--surface", id); + assert.equal(code, 0); + assert.equal(JSON.parse(stdout).postId, id); + } finally { + await server.close(); + } +}); + +// --- list / sessions ------------------------------------------------------ + +test("list prints the posts in the active session", async () => { + const server = await serveSession(); + try { + await cli(server, "publish", tmpFile("a.html", "

a

"), "--title", "A"); + await cli(server, "publish", tmpFile("b.html", "

b

"), "--title", "B"); + const { code, stdout } = await cli(server, "list"); + assert.equal(code, 0); + const posts = JSON.parse(stdout); + assert.equal(posts.length, 2); + assert.deepEqual( + posts.map((p: any) => p.title), + ["A", "B"], + ); + } finally { + await server.close(); + } +}); + +test("list --all folds every session's posts into one dump", async () => { + const server = await serveSession(); + try { + await cli(server, "publish", tmpFile("a.html", "

a

")); + // a second session, created directly via the API + const other = await post(`${server.url}/api/sessions`, { agent: "other", title: "Other" }); + await post(`${server.url}/api/surfaces`, { + parts: [{ kind: "html", html: "

z

" }], + session: other.id, + title: "Z", + }); + + const { code, stdout } = await cli(server, "list", "--all"); + assert.equal(code, 0); + const sessions = JSON.parse(stdout); + assert.equal(sessions.length, 2); + assert.ok(sessions.some((s: any) => s.surfaces.some((p: any) => p.title === "Z"))); + } finally { + await server.close(); + } +}); + +test("sessions prints the workspace's sessions", async () => { + const server = await serveSession(); + try { + const { code, stdout } = await cli(server, "sessions"); + assert.equal(code, 0); + const sessions = JSON.parse(stdout); + assert.equal(sessions.length, 1); + assert.equal(sessions[0].id, server.session.id); + } finally { + await server.close(); + } +}); + +// --- assets (image / upload / asset-url) ---------------------------------- + +test("image uploads bytes and publishes an image post", async () => { + const server = await serveSession(); + try { + // minimal PNG header — the server only needs non-empty bytes; kind=image + // is passed explicitly by the image command. + const png = tmpFile( + "pic.png", + String.fromCharCode(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a), + ); + const { code, stdout } = await cli(server, "image", png, "--title", "Shot", "--caption", "hi"); + assert.equal(code, 0); + const out = JSON.parse(stdout); + assert.deepEqual(out.kinds, ["image"]); + const full = (await fetch(`${server.url}/api/surfaces/${out.id}`).then((r) => r.json())) as any; + assert.equal(full.surfaces[0].caption, "hi"); + assert.ok(full.surfaces[0].assetId); + } finally { + await server.close(); + } +}); + +test("upload stores an asset and prints its id and url", async () => { + const server = await serveSession(); + try { + const png = tmpFile("up.png", String.fromCharCode(0x89, 0x50, 0x4e, 0x47)); + const { code, stdout } = await cli(server, "upload", png, "--kind", "image"); + assert.equal(code, 0); + const out = JSON.parse(stdout); + assert.ok(out.id); + assert.equal(out.url, `${server.url}/a/${out.id}`); + assert.equal(out.kind, "image"); + } finally { + await server.close(); + } +}); + +test("asset-url prints the content-hash id and url without hitting the server", async () => { + const bytes = "asset-url-payload"; + const file = tmpFile("f.bin", bytes); + const expected = createHash("sha256").update(bytes).digest("hex"); + // No server needed — asset-url is a pure local hash. Point BASE at a dummy. + const { code, stdout } = await runWith( + { env: { SIDESHOW_URL: "http://127.0.0.1:1" } }, + "asset-url", + file, + ); + assert.equal(code, 0); + const out = JSON.parse(stdout); + assert.equal(out.id, expected); + assert.equal(out.url, `http://127.0.0.1:1/a/${expected}`); +}); + +// --- error paths ---------------------------------------------------------- + +test("an unreachable server fails with a one-line error, not a stack trace", async () => { + const { code, stdout, stderr } = await runWith( + { env: { SIDESHOW_URL: "http://127.0.0.1:1" } }, + "publish", + tmpFile("x.html", "

"), + ); + assert.notEqual(code, 0); + assert.equal(stdout, ""); + assert.match(stderr, /^sideshow: server not reachable/); +}); + +test("a server error is surfaced as the server's error message", async () => { + const server = await serveSession(); + try { + // update a post that doesn't exist → 404 from the server + const { code, stderr } = await cli(server, "update", "no-such-id", tmpFile("v.html", "

")); + assert.notEqual(code, 0); + assert.match(stderr, /not found|no such/i); + } finally { + await server.close(); + } +}); diff --git a/test/richRender.test.ts b/test/richRender.test.ts new file mode 100644 index 0000000..4b42c66 --- /dev/null +++ b/test/richRender.test.ts @@ -0,0 +1,146 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { renderCode, renderDiff, renderMarkdown, renderTerminal } from "../server/richRender.ts"; +import type { + CodeSurface, + DiffSurface, + MarkdownSurface, + TerminalSurface, +} from "../server/types.ts"; + +// richRender builds the sandboxed HTML served from /s/:id for the rich surface +// kinds. It has no unit tests today (only e2e), so branch coverage sits at ~61%. +// These exercise the fallbacks and optional-field branches that produce HTML. + +test("renderMarkdown: links get target=_blank and rel=noopener noreferrer", async () => { + const md: MarkdownSurface = { kind: "markdown", markdown: "[ex](https://example.com)" }; + const { body } = await renderMarkdown(md); + assert.match(body, /target="_blank"/); + assert.match(body, /rel="noopener noreferrer"/); + assert.match(body, /href="https:\/\/example\.com"/); +}); + +test("renderMarkdown: code blocks are highlighted and inline code escaped", async () => { + const md: MarkdownSurface = { kind: "markdown", markdown: " `let x = 1`" }; + const { body } = await renderMarkdown(md); + assert.match(body, //); +}); + +test("renderCode: an empty language falls back to the plain pre renderer", async () => { + // language: "" defeats the `?? "text"` default, so highlight() sees a falsy + // lang and returns null → plainHtml. (A literal "text" is a real shiki lang + // and gets highlighted; "" is the path that exercises the !lang branch.) + const code: CodeSurface = { kind: "code", code: "a\nb\nc", language: "" }; + const { body } = await renderCode(code); + assert.match(body, /

/);
+  assert.equal(body.match(//g)?.length, 3);
+});
+
+test("renderCode: an unknown language falls back to plain (highlight try/catch)", async () => {
+  // shiki can't load "klingon"; codeToHtml throws → highlight() catches → plainHtml.
+  const code: CodeSurface = { kind: "code", code: "blah", language: "klingon" };
+  const { body } = await renderCode(code);
+  assert.match(body, /
/);
+});
+
+test("renderCode: lineStart injects a counter-reset so line numbers start later", async () => {
+  const code: CodeSurface = { kind: "code", code: "x\ny", language: "typescript", lineStart: 42 };
+  const { body } = await renderCode(code);
+  assert.match(body, /counter-reset:line 41/);
+});
+
+test("renderCode: a known language highlights (shiki pre, not plain)", async () => {
+  const code: CodeSurface = { kind: "code", code: "const x = 1;", language: "typescript" };
+  const { body } = await renderCode(code);
+  assert.match(body, /]*class="shiki/);
+  assert.doesNotMatch(body, /
/);
+});
+
+test("renderCode: a filename/title populates the header bar", async () => {
+  const code: CodeSurface = { kind: "code", code: "x", language: "typescript", title: "app.ts" };
+  const { body } = await renderCode(code);
+  assert.match(body, /code-filename/);
+  assert.match(body, /app\.ts/);
+  assert.match(body, /code-lang/);
+});
+
+test("renderCode: the copy button embeds the source escaped for JS strings", async () => {
+  const code: CodeSurface = { kind: "code", code: "let s = '';", language: "javascript" };
+  const { body } = await renderCode(code);
+  // the < in the embedded JSON string is escaped to \u003c so it can't break out
+  assert.match(body, /\\u003c/);
+  assert.match(body, /__codeCopy/);
+});
+
+test("renderDiff: explicit before/after file pairs render without a patch", async () => {
+  const diff: DiffSurface = {
+    kind: "diff",
+    files: [{ filename: "f.txt", before: "old line", after: "new line" }],
+  };
+  const { body } = await renderDiff(diff);
+  assert.match(body, //);
+  // the rendered shadow root is non-empty
+  assert.ok(
+    body.replace(/|]*>|<\/template>|<\/diffs-container>/g, "").length >
+      0,
+  );
+});
+
+test("renderDiff: a multi-file patch renders one diffs-container per file", async () => {
+  const patch = [
+    "--- a/one.txt",
+    "+++ b/one.txt",
+    "@@ -1 +1 @@",
+    "-a",
+    "+b",
+    "--- a/two.txt",
+    "+++ b/two.txt",
+    "@@ -1 +1 @@",
+    "-c",
+    "+d",
+  ].join("\n");
+  const { body } = await renderDiff({ kind: "diff", patch });
+  assert.equal(body.match(//g)?.length, 2);
+});
+
+test("renderDiff: a patch with no recognizable file headers hits the processFile fallback", async () => {
+  // No --- a/ +++ b/ headers, so parsePatchFiles yields nothing and buildFileDiffs
+  // falls back to processFile. Either it produces a diff or renderDiff throws
+  // "No diff content." — both exercise the fallback branch; assert the path is
+  // reached without an unrelated failure.
+  const headerless = ["@@ -1 +1 @@", "-old", "+new"].join("\n");
+  try {
+    const { body } = await renderDiff({ kind: "diff", patch: headerless });
+    assert.match(body, //);
+  } catch (err) {
+    assert.match((err as Error).message, /No diff content/);
+  }
+});
+
+test("renderDiff: an empty patch and no files throws No diff content", async () => {
+  await assert.rejects(() => renderDiff({ kind: "diff", patch: "" }), /No diff content/);
+});
+
+test("renderTerminal: ANSI codes are converted and a window bar is rendered", async () => {
+  const term: TerminalSurface = {
+    kind: "terminal",
+    text: "\x1b[32mok\x1b[0m done",
+    title: "build",
+    cols: 80,
+  };
+  const { body } = await renderTerminal(term);
+  assert.match(body, /term-bar/);
+  assert.match(body, /build/);
+  // ansi_up turns the green SGR into a span with a color style
+  assert.match(body, / {
+  const term: TerminalSurface = { kind: "terminal", text: "plain output" };
+  const { body } = await renderTerminal(term);
+  // the bar is always rendered; a missing title defaults to "terminal", and no
+  // cols means the body 
 carries no width style
+  assert.match(body, /term-bar/);
+  assert.match(body, /terminal<\/span>/);
+  assert.match(body, /
plain output<\/pre>/);
+});