mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-19 08:10:08 +02:00
test(make-pdf)+feat(diagram): review-wave test pins + skill transport hardening
Tests: indented-fence byte-for-byte replay + no-extraction-in-lists,
drive-letter local-path routing, $-pattern slot immunity, base64 source
round-trip ('A --> B' exact), existing-style merge preservation, DOCX
rasterize-failure surfaces source, srcSha256 + font-stack drift guards,
landscape veto asserted as some-portrait/no-landscape (layout-order-proof),
judge rubric cap lowered to 5 so it actually fails, vacuous error-shape test
removed honestly, tmpdir cleanup.
/diagram skill: base64 transport (template literals corrupted backticks/${
in sources), content-addressed staging with hash verification, and --tab-id
pinned on every browse call so a concurrent /qa session can't be clobbered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -110,11 +110,19 @@ describe("rasterizeDiagramFigures (mock tab)", () => {
|
||||
expect(out).toContain('src="data:image/png;base64,AAAA"');
|
||||
});
|
||||
|
||||
test("figure rasterization failure keeps the figure (never silent loss)", () => {
|
||||
test("figure rasterization failure surfaces the SOURCE as text (never silent loss)", () => {
|
||||
// Returning the figure unchanged would make the diagram vanish in DOCX
|
||||
// (the converter drops <figure>/<svg>) — the failure must be visible.
|
||||
const { tab } = mockTab(() => { throw new RenderCallError("tainted"); });
|
||||
const warnings: string[] = [];
|
||||
const out = rasterizeDiagramFigures(figure, tab, 6.5, (m) => warnings.push(m));
|
||||
expect(out).toContain('<figure class="diagram"');
|
||||
const srcFigure = figure.replace(
|
||||
'<figure class="diagram"',
|
||||
`<figure class="diagram" data-gstack-source="${Buffer.from("graph LR\n A --> B").toString("base64")}"`,
|
||||
);
|
||||
const out = rasterizeDiagramFigures(srcFigure, tab, 6.5, (m) => warnings.push(m));
|
||||
expect(out).toContain("could not be rasterized");
|
||||
expect(out).toContain("A --> B"); // source visible (escaped), not dropped
|
||||
expect(out).not.toContain("<figure");
|
||||
expect(warnings[0]).toContain("rasterization failed");
|
||||
});
|
||||
|
||||
@@ -196,25 +204,17 @@ describe("pure-function stragglers", () => {
|
||||
fs.unlinkSync(tmp);
|
||||
}
|
||||
});
|
||||
test("resolveBundlePath error names every candidate and the fix", () => {
|
||||
let message = "";
|
||||
try {
|
||||
resolveBundlePath({ GSTACK_DIAGRAM_BUNDLE: "/definitely/not/here.html", HOME: "/nonexistent-home" } as NodeJS.ProcessEnv);
|
||||
} catch (err: any) {
|
||||
message = err.message;
|
||||
}
|
||||
// The repo-relative candidates exist in this checkout, so force a miss is
|
||||
// not possible portably — accept either a path return or the error shape.
|
||||
if (message) {
|
||||
expect(message).toContain("diagram-render bundle not found");
|
||||
expect(message).toContain("build:diagram-render");
|
||||
}
|
||||
});
|
||||
// NOTE: resolveBundlePath's not-found error shape is untestable from inside
|
||||
// this checkout (the repo-relative candidate always exists), and a vacuous
|
||||
// if-guarded assertion was worse than none. The env-override test above is
|
||||
// the honest coverage; the error path is exercised manually via
|
||||
// GSTACK_DIAGRAM_BUNDLE pointing at a missing file outside a repo.
|
||||
|
||||
test("screenCss is media-scoped and readable-width", () => {
|
||||
const css = screenCss();
|
||||
expect(css).toContain("@media screen");
|
||||
expect(css).toContain("max-width: 52em");
|
||||
// 42em at 12pt ≈ 70-75 chars/line — the readable ceiling (design review).
|
||||
expect(css).toContain("max-width: 42em");
|
||||
expect(css).toContain(".watermark { display: none; }");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* byte-level image dimension prober. No browse daemon required — the tab
|
||||
* factory returns null so downscale paths are exercised as no-ops.
|
||||
*/
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
inlineLocalImages,
|
||||
parseInfoString,
|
||||
substituteSlots,
|
||||
decodeFigureSource,
|
||||
} from "../src/diagram-prepass";
|
||||
import { imageDims } from "../src/image-size";
|
||||
|
||||
@@ -146,6 +147,18 @@ describe("diagnostic + figure blocks", () => {
|
||||
expect(fig).toContain('aria-label="Auth flow"');
|
||||
expect(fig).toContain("diagram-caption");
|
||||
});
|
||||
test("embedded source round-trips mermaid arrows exactly", () => {
|
||||
const source = "graph LR\n A --> B\n B -->|label with $& and `ticks`| C";
|
||||
const fig = buildDiagramFigure({ ...fence, source }, "<svg></svg>");
|
||||
expect(decodeFigureSource(fig)).toBe(source);
|
||||
});
|
||||
test("slot substitution is immune to $-replacement patterns in labels", () => {
|
||||
const slotHtml = `<figure>label says $' and $& here</figure>`;
|
||||
const out = substituteSlots("<p>tok-x</p><p>tail</p>", new Map([["tok-x", slotHtml]]));
|
||||
expect(out).toContain("label says $' and $& here");
|
||||
expect(out).toContain("<p>tail</p>");
|
||||
expect(out).not.toContain("tailtail"); // $' expansion would duplicate the tail
|
||||
});
|
||||
});
|
||||
|
||||
// ─── image dimension probing ──────────────────────────────────────────
|
||||
@@ -229,6 +242,9 @@ describe("content width", () => {
|
||||
describe("inlineLocalImages", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "prepass-img-"));
|
||||
fs.writeFileSync(path.join(dir, "ok.png"), tinyPng(40, 20));
|
||||
afterAll(() => {
|
||||
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
const base = {
|
||||
inputDir: dir,
|
||||
@@ -290,6 +306,30 @@ describe("inlineLocalImages", () => {
|
||||
expect(out).toContain('data-gstack-px-height="44"');
|
||||
});
|
||||
|
||||
test("Windows drive-letter src is treated as a local path, not a URL scheme", () => {
|
||||
// C:/x.png matches the single-letter-scheme regex — it must reach the
|
||||
// local-path branch (and the missing-file placeholder), never silently
|
||||
// pass through as an unknown URL.
|
||||
const warnings: string[] = [];
|
||||
const out = inlineLocalImages(`<img src="C:/missing/x.png">`, { ...base, warn: (m) => warnings.push(m) });
|
||||
expect(out).toContain("image-missing");
|
||||
expect(warnings.length).toBe(1);
|
||||
});
|
||||
|
||||
test("indented fences inside lists replay byte-for-byte (no list splitting)", () => {
|
||||
const md = "- item\n\n ```js\n code();\n ```\n\n- next";
|
||||
const { markdown, fences } = extractDiagramFences(md);
|
||||
expect(fences).toHaveLength(0);
|
||||
expect(markdown).toBe(md);
|
||||
});
|
||||
|
||||
test("indented mermaid fences are NOT extracted (column-0 placeholder would split the list)", () => {
|
||||
const md = "- item\n\n ```mermaid\n graph LR\n ```\n";
|
||||
const { markdown, fences } = extractDiagramFences(md);
|
||||
expect(fences).toHaveLength(0);
|
||||
expect(markdown).toBe(md);
|
||||
});
|
||||
|
||||
test("oversized raster without a tab inlines at full size with no downscale", () => {
|
||||
// 6000px-wide PNG header (body irrelevant for probing; file must exist)
|
||||
fs.writeFileSync(path.join(dir, "wide.png"), tinyPng(6000, 100));
|
||||
|
||||
@@ -91,15 +91,17 @@ describe("landscape promotion gate", () => {
|
||||
expect(portrait.length).toBeGreaterThanOrEqual(2);
|
||||
expect(isLandscape(boxes[0])).toBe(false);
|
||||
|
||||
// The veto'd diagram rendered (its labels exist) on a PORTRAIT page.
|
||||
// The veto'd diagram rendered on SOME portrait page and NO landscape
|
||||
// page — the actual invariant. (Asserting a specific page index breaks
|
||||
// spuriously when font metrics shift pagination.)
|
||||
const pdftotext = resolvePopplerTool("pdftotext")!;
|
||||
const lastPortrait = portrait[portrait.length - 1];
|
||||
const vetoText = execFileSync(
|
||||
pdftotext,
|
||||
["-f", String(lastPortrait.page), "-l", String(lastPortrait.page), outputPdf, "-"],
|
||||
{ encoding: "utf8", timeout: CHILD_TIMEOUT_MS },
|
||||
);
|
||||
expect(vetoText).toContain("vetoalpha");
|
||||
const pageText = (page: number) =>
|
||||
execFileSync(pdftotext, ["-f", String(page), "-l", String(page), outputPdf, "-"], {
|
||||
encoding: "utf8",
|
||||
timeout: CHILD_TIMEOUT_MS,
|
||||
});
|
||||
expect(portrait.some((b) => pageText(b.page).includes("vetoalpha"))).toBe(true);
|
||||
expect(landscape.some((b) => pageText(b.page).includes("vetoalpha"))).toBe(false);
|
||||
} finally {
|
||||
try { fs.rmSync(workDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
@@ -75,6 +75,14 @@ describe("width styles", () => {
|
||||
const { html } = applyImagePolicy(img(`src="x" data-gstack-width="3in"`), OPTS);
|
||||
expect(html).toContain("width: 3in");
|
||||
});
|
||||
test("width directive merges with an existing style attribute, preserving it", () => {
|
||||
const { html } = applyImagePolicy(
|
||||
img(`src="x" style="border: 1px solid" data-gstack-width="50%"`),
|
||||
OPTS,
|
||||
);
|
||||
expect(html).toContain("border: 1px solid");
|
||||
expect(html).toContain("width: 50%");
|
||||
});
|
||||
test("no directive → no inline style (CSS max-width owns the default)", () => {
|
||||
const { html } = applyImagePolicy(img(`src="x" data-gstack-px-width="40" data-gstack-px-height="20"`), OPTS);
|
||||
expect(html).not.toContain("style=");
|
||||
|
||||
Reference in New Issue
Block a user