Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

scripts/readme/check.test.ts

Raw
import { spawnSync } from "node:child_process";
import {
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, it } from "vitest";
import { preserveReadmeDemo, readmePreamble } from "../extensions.ts";
import { checkExtension } from "./check.ts";
import { extractSurface, injectSurface, renderSurface } from "./config.ts";

const roots: string[] = [];
afterEach(() => {
	for (const root of roots.splice(0))
		rmSync(root, { recursive: true, force: true });
});
function fixture() {
	const root = mkdtempSync(join(tmpdir(), "pi-ext-readme-check-"));
	roots.push(root);
	const dir = join(root, "extensions", "fixture");
	mkdirSync(join(dir, "assets"), { recursive: true });
	writeFileSync(join(dir, "assets", "banner.svg"), "<svg />");
	writeFileSync(join(dir, "index.ts"), "export const value = 1;");
	const readme =
		'<img src="assets/banner.svg" alt="fixture" height="70">\n\n# fixture\n\n## Reference\n\n' +
		renderSurface(extractSurface(dir)) +
		"\n";
	writeFileSync(join(dir, "README.md"), readme);
	return { root, dir, readme, path: join(dir, "README.md") };
}
it("executes CLI entrypoint and rejects an unknown extension", () => {
	const result = spawnSync(
		process.execPath,
		[
			join(process.cwd(), "scripts/readme/check.ts"),
			"__missing_readme_cli_probe__",
		],
		{ cwd: process.cwd(), encoding: "utf8", timeout: 10_000 },
	);
	expect(result.error).toBeUndefined();
	expect(result.status).toBe(1);
	expect(result.stderr).toContain(
		"unknown extension(s): __missing_readme_cli_probe__",
	);
});

it("preserves banner spacing and restores a missing demo without duplicating a new caption", () => {
	const { readme } = fixture();
	const preamble = readmePreamble(readme);
	expect(preamble + "# fixture").toContain('height="70">\n\n# fixture');
	const oldDemo = "![Old caption](assets/demo.gif)";
	expect(preserveReadmeDemo("# fixture\n\nPurpose.\n", oldDemo)).toContain(
		oldDemo,
	);
	const changed = "# fixture\n\n![New caption](assets/demo.gif)\n";
	expect(preserveReadmeDemo(changed, oldDemo)).toBe(changed);
});
it.each([
	"## Debug\n\nSafe events: start, finish.\n",
	"[Debug contract](../DEBUG.md)\n",
])("rejects developer debug documentation in user READMEs: %s", (debug) => {
	const { root, readme, path } = fixture();
	writeFileSync(path, `${readme}\n${debug}`);
	expect(
		checkExtension("fixture", false, root).some(({ message }) =>
			message.includes("not a user README"),
		),
	).toBe(true);
});
it("accepts minimal docs without invented changelog history", () => {
	const { root } = fixture();
	expect(checkExtension("fixture", false, root)).toEqual([]);
});
it("checks drift without writes; write mode is idempotent", () => {
	const { root, dir, readme, path } = fixture();
	writeFileSync(
		join(dir, "index.ts"),
		'pi.registerCommand("fixture", { description: "Run fixture" });',
	);
	expect(checkExtension("fixture", false, root)[0]?.message).toContain(
		"out of date",
	);
	expect(readFileSync(path, "utf8")).toBe(readme);
	expect(checkExtension("fixture", true, root)).toEqual([]);
	const written = readFileSync(path, "utf8");
	expect(checkExtension("fixture", true, root)).toEqual([]);
	expect(readFileSync(path, "utf8")).toBe(written);
});
it("rejects duplicate or reversed markers", () => {
	const { root, readme, path } = fixture();
	writeFileSync(path, readme + "\n<!-- config:start -->");
	expect(checkExtension("fixture", true, root)[0]?.message).toContain(
		"exactly once",
	);
	expect(
		injectSurface("<!-- config:end --><!-- config:start -->", "block"),
	).toBeUndefined();
});
it("checks demo pairing, relative links, and existing changelogs", () => {
	const { root, dir, readme, path } = fixture();
	mkdirSync(join(dir, "demo"));
	writeFileSync(join(dir, "demo", "scene.json"), "{}");
	writeFileSync(join(dir, "CHANGELOG.md"), "# Changelog\n\n## Unreleased\n");
	writeFileSync(path, readme + "\n[Missing](missing.md)\n");
	const messages = checkExtension("fixture", false, root).map(
		({ message }) => message,
	);
	expect(messages).toContain(
		"demo/scene.json exists but README does not embed assets/demo.gif",
	);
	expect(messages).toContain("README does not link CHANGELOG.md");
	expect(messages).toContain("broken relative link: missing.md");
});
it("requires a rendered demo image, not merely a matching asset path", () => {
	const { root, dir, readme, path } = fixture();
	mkdirSync(join(dir, "demo"));
	writeFileSync(join(dir, "demo", "scene.json"), "{}");
	writeFileSync(join(dir, "assets", "demo.gif"), "GIF89a");
	writeFileSync(path, readme + String.raw`\n![Demo](assets/demo.gif)`);
	expect(
		checkExtension("fixture", false, root).some(({ message }) =>
			message.includes("does not embed"),
		),
	).toBe(true);
	writeFileSync(path, readme + "\n![Demo](assets/demo.gif)\n");
	expect(checkExtension("fixture", false, root)).toEqual([]);
});
it("fails closed on unresolved registrations", () => {
	const { root, dir } = fixture();
	writeFileSync(join(dir, "index.ts"), "pi.registerTool(unknownDefinition);");
	expect(
		checkExtension("fixture", false, root).some(({ message }) =>
			message.includes("cannot be resolved"),
		),
	).toBe(true);
});