// IMP-56 (#90) u18 — vitest coverage for the vite POST /api/connect // middleware and its supporting mirrorDirRecursive helper. // // Scope: // 1) mirrorDirRecursive (pure helper): // - absent src → returns 0 (no-throw, no dst creation). // - file-only src → flat copy + count. // - nested src → recursive copy + count. // - overwrites pre-existing dst files (cel mirror semantics). // 2) handleConnectMirror (POST): // - method != POST → false (chain continues; next middleware may handle). // - invalid JSON / non-object body → 400. // - missing run_id or slug → 400. // - invalid run_id or slug (key gate / path traversal) → 400. // - final.html missing → 404. // - success without run-assets dir → 200, assets_copied: 0, html copy ok. // - success with run-assets dir → 200, assets_copied = file count, dst dir // populated. // - dstSlidesDir auto-created when celRoot/public/slides missing. // // Tests exercise the pure handler with mock req/res — no real vite server. import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { EventEmitter } from "node:events"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { handleConnectMirror, mirrorDirRecursive, } from "../../vite.config"; function makeMockRes() { const state = { statusCode: 0, headers: {} as Record, body: "", ended: false, }; return { state, res: { writeHead(status: number, headers?: Record) { state.statusCode = status; if (headers) state.headers = headers; }, end(body?: string) { state.body = body ?? ""; state.ended = true; }, }, }; } function makeMockReq(opts: { method?: string; }): EventEmitter & { method?: string; send: (body: string) => void } { const ee = new EventEmitter() as EventEmitter & { method?: string; send: (body: string) => void; }; ee.method = opts.method; ee.send = (body: string) => { if (body.length > 0) ee.emit("data", Buffer.from(body, "utf-8")); ee.emit("end"); }; return ee; } function seedRun(daRoot: string, runId: string, htmlBody: string): string { const runDir = path.join(daRoot, "data", "runs", runId, "phase_z2"); fs.mkdirSync(runDir, { recursive: true }); const html = path.join(runDir, "final.html"); fs.writeFileSync(html, htmlBody, "utf-8"); return runDir; } describe("mirrorDirRecursive (IMP-56 #90 u18)", () => { let tmp: string; beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), "imp90-u18-mirror-")); }); afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); }); it("returns 0 and does not throw when src absent", () => { const dst = path.join(tmp, "dst"); const n = mirrorDirRecursive(path.join(tmp, "missing"), dst); expect(n).toBe(0); expect(fs.existsSync(dst)).toBe(false); }); it("returns 0 when src exists but is a file (not a directory)", () => { const srcFile = path.join(tmp, "src.txt"); fs.writeFileSync(srcFile, "x", "utf-8"); const dst = path.join(tmp, "dst"); const n = mirrorDirRecursive(srcFile, dst); expect(n).toBe(0); expect(fs.existsSync(dst)).toBe(false); }); it("flat-copies file entries and returns the file count", () => { const src = path.join(tmp, "src"); fs.mkdirSync(src); fs.writeFileSync(path.join(src, "a.css"), "/*a*/", "utf-8"); fs.writeFileSync(path.join(src, "b.png"), "PNG", "utf-8"); const dst = path.join(tmp, "dst"); const n = mirrorDirRecursive(src, dst); expect(n).toBe(2); expect(fs.readFileSync(path.join(dst, "a.css"), "utf-8")).toBe("/*a*/"); expect(fs.readFileSync(path.join(dst, "b.png"), "utf-8")).toBe("PNG"); }); it("recurses into nested directories and counts only files", () => { const src = path.join(tmp, "src"); fs.mkdirSync(path.join(src, "nested", "deep"), { recursive: true }); fs.writeFileSync(path.join(src, "root.txt"), "r", "utf-8"); fs.writeFileSync(path.join(src, "nested", "n.txt"), "n", "utf-8"); fs.writeFileSync(path.join(src, "nested", "deep", "d.txt"), "d", "utf-8"); const dst = path.join(tmp, "dst"); const n = mirrorDirRecursive(src, dst); expect(n).toBe(3); expect(fs.readFileSync(path.join(dst, "nested", "deep", "d.txt"), "utf-8")) .toBe("d"); }); it("overwrites pre-existing files in dst (cel mirror semantics)", () => { const src = path.join(tmp, "src"); fs.mkdirSync(src); fs.writeFileSync(path.join(src, "a.css"), "NEW", "utf-8"); const dst = path.join(tmp, "dst"); fs.mkdirSync(dst); fs.writeFileSync(path.join(dst, "a.css"), "OLD", "utf-8"); mirrorDirRecursive(src, dst); expect(fs.readFileSync(path.join(dst, "a.css"), "utf-8")).toBe("NEW"); }); }); describe("handleConnectMirror (IMP-56 #90 u18)", () => { let daRoot: string; let celRoot: string; beforeEach(() => { daRoot = fs.mkdtempSync(path.join(os.tmpdir(), "imp90-u18-da-")); celRoot = fs.mkdtempSync(path.join(os.tmpdir(), "imp90-u18-cel-")); }); afterEach(() => { fs.rmSync(daRoot, { recursive: true, force: true }); fs.rmSync(celRoot, { recursive: true, force: true }); }); it("returns false (next chained) when method != POST", () => { const req = makeMockReq({ method: "GET" }); const { res, state } = makeMockRes(); const handled = handleConnectMirror(req, res, daRoot, celRoot); expect(handled).toBe(false); expect(state.ended).toBe(false); }); it("returns 400 on invalid JSON body", () => { const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); const handled = handleConnectMirror(req, res, daRoot, celRoot); expect(handled).toBe(true); req.send("{not-json}"); expect(state.statusCode).toBe(400); expect(JSON.parse(state.body).error).toBe("invalid JSON"); }); it("returns 400 when body is not a JSON object (array root)", () => { const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleConnectMirror(req, res, daRoot, celRoot); req.send(JSON.stringify(["not", "an", "object"])); expect(state.statusCode).toBe(400); expect(JSON.parse(state.body).error).toBe("body must be a JSON object"); }); it("returns 400 when run_id or slug is missing", () => { const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleConnectMirror(req, res, daRoot, celRoot); req.send(JSON.stringify({ run_id: "abc" })); // slug missing expect(state.statusCode).toBe(400); expect(JSON.parse(state.body).error).toBe("missing run_id or slug"); }); it("returns 400 when run_id contains path traversal", () => { const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleConnectMirror(req, res, daRoot, celRoot); req.send(JSON.stringify({ run_id: "../escape", slug: "03" })); expect(state.statusCode).toBe(400); expect(JSON.parse(state.body).error).toBe("invalid run_id or slug"); }); it("returns 400 when slug contains a forward slash", () => { const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleConnectMirror(req, res, daRoot, celRoot); req.send(JSON.stringify({ run_id: "valid_id", slug: "03/etc" })); expect(state.statusCode).toBe(400); expect(JSON.parse(state.body).error).toBe("invalid run_id or slug"); }); it("returns 404 when final.html does not exist for run_id", () => { const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleConnectMirror(req, res, daRoot, celRoot); req.send(JSON.stringify({ run_id: "ghost_run", slug: "03" })); expect(state.statusCode).toBe(404); expect(JSON.parse(state.body).error).toBe("final.html not found"); }); it("copies final.html to cel/public/slides/.html on success", () => { seedRun(daRoot, "mdx03_run", "03"); const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleConnectMirror(req, res, daRoot, celRoot); req.send(JSON.stringify({ run_id: "mdx03_run", slug: "03" })); expect(state.statusCode).toBe(200); const dstHtml = path.join(celRoot, "public", "slides", "03.html"); expect(fs.existsSync(dstHtml)).toBe(true); expect(fs.readFileSync(dstHtml, "utf-8")).toBe("03"); const body = JSON.parse(state.body); expect(body.success).toBe(true); expect(body.run_id).toBe("mdx03_run"); expect(body.slug).toBe("03"); expect(body.assets_copied).toBe(0); expect(body.html_target).toBe(dstHtml); }); it("auto-creates cel/public/slides when missing", () => { seedRun(daRoot, "mdx04_run", "04"); expect(fs.existsSync(path.join(celRoot, "public", "slides"))).toBe(false); const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleConnectMirror(req, res, daRoot, celRoot); req.send(JSON.stringify({ run_id: "mdx04_run", slug: "04" })); expect(state.statusCode).toBe(200); expect(fs.existsSync(path.join(celRoot, "public", "slides", "04.html"))).toBe(true); }); it("mirrors assets/ recursively when present in the run dir", () => { const runDir = seedRun(daRoot, "mdx05_run", "05"); fs.mkdirSync(path.join(runDir, "assets", "css"), { recursive: true }); fs.writeFileSync(path.join(runDir, "assets", "main.css"), "*{}", "utf-8"); fs.writeFileSync(path.join(runDir, "assets", "css", "extra.css"), "p{}", "utf-8"); const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleConnectMirror(req, res, daRoot, celRoot); req.send(JSON.stringify({ run_id: "mdx05_run", slug: "05" })); expect(state.statusCode).toBe(200); expect(JSON.parse(state.body).assets_copied).toBe(2); expect(fs.readFileSync(path.join(celRoot, "public", "slides", "assets", "main.css"), "utf-8")) .toBe("*{}"); expect(fs.readFileSync(path.join(celRoot, "public", "slides", "assets", "css", "extra.css"), "utf-8")) .toBe("p{}"); }); it("overwrites pre-existing cel slide html (re-Connect semantics)", () => { seedRun(daRoot, "mdx03_run", "NEW"); const dstSlidesDir = path.join(celRoot, "public", "slides"); fs.mkdirSync(dstSlidesDir, { recursive: true }); fs.writeFileSync(path.join(dstSlidesDir, "03.html"), "OLD", "utf-8"); const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleConnectMirror(req, res, daRoot, celRoot); req.send(JSON.stringify({ run_id: "mdx03_run", slug: "03" })); expect(state.statusCode).toBe(200); expect(fs.readFileSync(path.join(dstSlidesDir, "03.html"), "utf-8")).toBe("NEW"); }); });