import { afterEach, describe, expect, test } from "bun:test" import { promises as fs } from "fs" import os from "os" import path from "path" const serverScript = path.join( import.meta.dir, "..", "..", "skills", "ce-brainstorm", "scripts", "visual-probe-server.js", ) type RunResult = { exitCode: number stdout: string stderr: string } const rootsToStop: string[] = [] async function readJsonLine(stream: ReadableStream | null): Promise> { expect(stream).not.toBeNull() const reader = stream!.getReader() const decoder = new TextDecoder() let text = "" const deadline = Date.now() + 3000 while (Date.now() < deadline) { const { done, value } = await reader.read() if (done) break text += decoder.decode(value, { stream: true }) const newline = text.indexOf("\n") if (newline !== -1) { return JSON.parse(text.slice(0, newline)) } } throw new Error(`Timed out waiting for server JSON. Received: ${text}`) } async function runServerCommand(args: string[]): Promise { const proc = Bun.spawn(["node", serverScript, ...args], { stdout: "pipe", stderr: "pipe", }) const [exitCode, stdout, stderr] = await Promise.all([ proc.exited, new Response(proc.stdout).text(), new Response(proc.stderr).text(), ]) return { exitCode, stdout, stderr } } async function startServer( root: string, extraArgs: string[] = [], env: Record = {}, ): Promise> { const proc = Bun.spawn(["node", serverScript, "start", "--root", root, "--port", "0", ...extraArgs], { stdout: "pipe", stderr: "pipe", env: { ...process.env, ...env }, }) const [exitCode, stdout, stderr] = await Promise.all([ proc.exited, new Response(proc.stdout).text(), new Response(proc.stderr).text(), ]) const result = { exitCode, stdout, stderr } expect(result.exitCode, result.stderr).toBe(0) rootsToStop.push(root) return JSON.parse(result.stdout.trim()) } afterEach(async () => { while (rootsToStop.length > 0) { const root = rootsToStop.pop()! await runServerCommand(["stop", "--root", root]) } }) describe("ce-brainstorm visual-probe-server.js", () => { test("start serves the newest screen from a display-only local server", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "ce-visual-probe-server-")) const info = await startServer(root) expect(info.status).toBe("started") expect(info.url).toMatch(/^http:\/\/localhost:\d+$/) expect(info.screen_dir).toBe(path.join(root, "screens")) expect(info.state_dir).toBe(path.join(root, "state")) const screenDir = String(info.screen_dir) await fs.writeFile(path.join(screenDir, "001-first.html"), "

First sketch

") let response = await fetch(String(info.url)) let html = await response.text() expect(html).toContain("First sketch") expect(html).toContain('fetch("/version"') expect(html).not.toContain("setTimeout(function(){ location.reload(); }, 1000)") expect(html).not.toContain("WebSocket") expect(html).not.toContain("data-choice") expect(html).not.toContain("events") response = await fetch(`${String(info.url)}/version`) let version = await response.json() expect(version.screen).toBe("001-first.html") await new Promise((resolve) => setTimeout(resolve, 20)) await fs.writeFile(path.join(screenDir, "002-second.html"), "

Second sketch

") response = await fetch(String(info.url)) html = await response.text() expect(html).toContain("Second sketch") expect(html).not.toContain("First sketch") response = await fetch(`${String(info.url)}/version`) version = await response.json() expect(version.screen).toBe("002-second.html") }) test("serves full HTML documents with refresh polling injected before body close", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "ce-visual-probe-full-doc-")) const info = await startServer(root) await fs.writeFile( path.join(String(info.screen_dir), "001-full.html"), "

Full sketch

", ) const response = await fetch(String(info.url)) const html = await response.text() expect(html).toContain("

Full sketch

") expect(html).toContain('fetch("/version"') expect(html.indexOf('fetch("/version"')).toBeLessThan(html.indexOf("")) expect(html).not.toContain("CE Brainstorm Visual Probe - directional sketch") }) test("/files stays inside the screens directory", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "ce-visual-probe-files-")) const info = await startServer(root) await fs.writeFile(path.join(root, "secret.txt"), "outside") await fs.writeFile(path.join(String(info.screen_dir), "asset.html"), "

asset

") let response = await fetch(`${String(info.url)}/files/asset.html`) expect(response.status).toBe(200) expect(response.headers.get("content-type")).toContain("text/html") expect(await response.text()).toBe("

asset

") response = await fetch(`${String(info.url)}/files/../secret.txt`) expect(response.status).toBe(404) }) test("status and stop use the root state directory", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "ce-visual-probe-status-")) await startServer(root) let result = await runServerCommand(["status", "--root", root]) expect(result.exitCode, result.stderr).toBe(0) let status = JSON.parse(result.stdout.trim()) expect(status.status).toBe("running") expect(status.root).toBe(root) result = await runServerCommand(["stop", "--root", root]) expect(result.exitCode, result.stderr).toBe(0) status = JSON.parse(result.stdout.trim()) expect(status.status).toBe("stopped") result = await runServerCommand(["status", "--root", root]) expect(result.exitCode, result.stderr).toBe(0) status = JSON.parse(result.stdout.trim()) expect(status.status).toBe("stopped") }) test("foreground start serves until stopped", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "ce-visual-probe-foreground-")) const proc = Bun.spawn(["node", serverScript, "start", "--root", root, "--port", "0", "--foreground"], { stdout: "pipe", stderr: "pipe", }) rootsToStop.push(root) const info = await readJsonLine(proc.stdout) expect(info.status).toBe("running") expect(info.url).toMatch(/^http:\/\/localhost:\d+$/) await fs.writeFile(path.join(String(info.screen_dir), "001-foreground.html"), "

Foreground

") const response = await fetch(String(info.url)) expect(await response.text()).toContain("Foreground") const result = await runServerCommand(["stop", "--root", root]) expect(result.exitCode, result.stderr).toBe(0) await proc.exited }) test("stale pid files do not make status trust an unrelated process", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "ce-visual-probe-stale-pid-")) await fs.mkdir(path.join(root, "state"), { recursive: true }) await fs.writeFile(path.join(root, "state", "server.pid"), `${process.pid}\n`) await fs.writeFile(path.join(root, "state", "display-info.json"), "{}\n") const result = await runServerCommand(["status", "--root", root]) expect(result.exitCode, result.stderr).toBe(0) const status = JSON.parse(result.stdout.trim()) expect(status.status).toBe("stopped") }) test("/version polling does not keep an otherwise idle server alive", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "ce-visual-probe-idle-")) const info = await startServer(root, [], { CE_VISUAL_PROBE_IDLE_TIMEOUT_MS: "250", CE_VISUAL_PROBE_LIFECYCLE_CHECK_MS: "50", }) await fs.writeFile(path.join(String(info.screen_dir), "001-first.html"), "

First sketch

") await fetch(String(info.url)) const deadline = Date.now() + 700 while (Date.now() < deadline) { try { await fetch(`${String(info.url)}/version`) } catch { break } await new Promise((resolve) => setTimeout(resolve, 50)) } const result = await runServerCommand(["status", "--root", root]) expect(result.exitCode, result.stderr).toBe(0) const status = JSON.parse(result.stdout.trim()) expect(status.status).toBe("stopped") }) test("server exits when its owner process exits", async () => { const owner = Bun.spawn(["node", "-e", "setInterval(() => {}, 1000)"], { stdout: "ignore", stderr: "ignore", }) const root = await fs.mkdtemp(path.join(os.tmpdir(), "ce-visual-probe-owner-")) try { const info = await startServer(root, ["--owner-pid", String(owner.pid)], { CE_VISUAL_PROBE_IDLE_TIMEOUT_MS: "5000", CE_VISUAL_PROBE_LIFECYCLE_CHECK_MS: "50", }) expect(info.owner_pid).toBe(owner.pid) owner.kill() await owner.exited let status = { status: "running" } for (let i = 0; i < 20; i++) { const result = await runServerCommand(["status", "--root", root]) expect(result.exitCode, result.stderr).toBe(0) status = JSON.parse(result.stdout.trim()) if (status.status === "stopped") break await new Promise((resolve) => setTimeout(resolve, 50)) } expect(status.status).toBe("stopped") } finally { owner.kill() } }) })