Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
u1: text_overrides axis in user_overrides_io u2: structure_overrides axis in user_overrides_io u3: vite allowlist for new endpoints u4: text_override_resolver u5: Step 12 text_overrides apply in phase_z2_pipeline u6: structure_override_resolver u7: text_path_stamper u8: SlideCanvas text-edit capture u9: SlideCanvas structure-edit overlay u10: userOverridesApi service extension u11: designAgent types extension u12: slidePlanUtils restore u13: user_overrides endpoint tests u14: user_overrides restore tests u15: pipeline fallback tests u16: edit-mode state + gating tests u17: slide_base print mode CSS u18: /api/connect endpoint (vite) u19: /api/export endpoint (vite) Recovery scope: 29 files (12 modified + 17 new). u20 already pushed in 9439575; this commit lands u1-u19 that were authored but not committed before #90 was externally closed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
283 lines
11 KiB
TypeScript
283 lines
11 KiB
TypeScript
// 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<string, string>,
|
|
body: "",
|
|
ended: false,
|
|
};
|
|
return {
|
|
state,
|
|
res: {
|
|
writeHead(status: number, headers?: Record<string, string>) {
|
|
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/<slug>.html on success", () => {
|
|
seedRun(daRoot, "mdx03_run", "<html>03</html>");
|
|
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("<html>03</html>");
|
|
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", "<html>04</html>");
|
|
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", "<html>05</html>");
|
|
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");
|
|
});
|
|
});
|