Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

scripts/test_extensions_static_check.mjs

Raw
#!/usr/bin/env node
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
	mkdirSync,
	mkdtempSync,
	readdirSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
	HARNESS,
	isLocalTestArtifact,
	isSecretScanIgnored,
	rootPkg,
	secretScanIgnorePaths,
	staticCheck,
} from "./extensions.mjs";
import { ROOT } from "./lib/common.mjs";

for (const path of [
	".pi/tmp/debug-fmt.log",
	"extensions/fast/__tests__/.debug-crO6jp/.test-home/.pi/agent/auth.json",
	"extensions/pfui/.pfui-warned-ABC/index.ts",
	".notes-debug-ABC/.test-state/pi-ext/debug/notes/trace.jsonl",
])
	assert(isLocalTestArtifact(path), `${path} should be blocked`);
for (const path of [
	".pi/skills/modernize/SKILL.md",
	"extensions/fast/__tests__/debug.test.ts",
	"extensions/fast/src/pi-ext-debug.ts",
])
	assert(!isLocalTestArtifact(path), `${path} should remain tracked`);

function writeExtension(root, name, overrides, index = "") {
	const ext = join(root, name);
	mkdirSync(join(ext, "__tests__"), { recursive: true });
	const pkg = {
		name: `@bugabinga/pi-ext-${name}`,
		private: true,
		type: "module",
		main: "index.ts",
		license: "MIT",
		peerDependencies: { "@earendil-works/pi-coding-agent": "*" },
		devDependencies: { [HARNESS]: "^0.6.1" },
		...overrides,
	};
	for (const [key, value] of Object.entries(overrides))
		if (value === null) delete pkg[key];
	writeFileSync(join(ext, "package.json"), JSON.stringify(pkg), "utf8");
	writeFileSync(
		join(ext, "index.ts"),
		`${index}import { dbg } from "./src/debug.ts";\ndbg?.("session.start");\n`,
		"utf8",
	);
	mkdirSync(join(ext, "src"), { recursive: true });
	writeFileSync(
		join(ext, "src", "debug.ts"),
		'import { defineDebug } from "./pi-ext-debug.ts";\nexport const { dbg, span, closeDebug } = defineDebug({ events: ["session.start"], spans: [], fields: {} } as const);\n',
		"utf8",
	);
	writeFileSync(join(ext, "README.md"), `# ${name}\n`, "utf8");
	writeFileSync(join(ext, "__tests__", "harness.test.ts"), "", "utf8");
	return ext;
}

function withTemp(fn) {
	const dir = mkdtempSync(join(tmpdir(), "pi-ext-static-"));
	try {
		fn(dir);
	} finally {
		rmSync(dir, { recursive: true, force: true });
	}
}

withTemp((root) => {
	const ext = writeExtension(root, "package-script", {
		scripts: { test: "vitest run" },
	});
	assert(
		staticCheck(ext, { root }).includes(
			"package-script: package scripts are prohibited; use extension-local Mise tasks",
		),
	);
});

withTemp((root) => {
	const ext = writeExtension(
		root,
		"typed",
		{
			peerDependencies: {
				"@earendil-works/pi-coding-agent": "*",
				typebox: "^1.0.0",
			},
			devDependencies: { [HARNESS]: "^0.6.1", typebox: "^1.0.0" },
			optionalDependencies: { typebox: "^1.0.0" },
		},
		'import { Type } from "typebox";\nvoid Type;\n',
	);
	const errors = staticCheck(ext, { root });
	assert(
		errors.includes(
			"typed: Pi package typebox peerDependency range must be '*'",
		),
	);
	assert(
		errors.includes(
			"typed: Pi package typebox belongs only in peerDependencies, not devDependencies",
		),
	);
	assert(
		errors.includes(
			"typed: Pi package typebox belongs only in peerDependencies, not optionalDependencies",
		),
	);
});

withTemp((root) => {
	writeExtension(
		root,
		"old-pi-ai",
		{
			peerDependencies: {
				"@earendil-works/pi-ai": "*",
				"@earendil-works/pi-coding-agent": "*",
			},
			devDependencies: { [HARNESS]: "^0.6.1" },
		},
		'import { complete, StringEnum } from "@earendil-works/pi-ai";\nvoid [complete, StringEnum];\n',
	);
	const errors = staticCheck(join(root, "old-pi-ai"), { root });
	assert(
		errors.includes(
			"old-pi-ai: moved pi-ai root API import complete in index.ts; use @earendil-works/pi-ai/compat or a specific API entrypoint",
		),
	);
});

withTemp((root) => {
	const ext = writeExtension(
		root,
		"raw-settings",
		{},
		'export default (pi: any, m: any) => { pi.registerFlag("x", {}); pi.getFlag("x"); m.getGlobalSettings(); m?.getProjectSettings(); };\n',
	);
	writeFileSync(
		join(ext, "src", "pi-ext-settings.ts"),
		"export const ok = (pi: any, m: any) => [pi.registerFlag(), pi.getFlag(), m.getGlobalSettings(), m.getProjectSettings()];\n",
		"utf8",
	);
	const errors = staticCheck(ext, { root }).filter((error) =>
		error.includes("direct "),
	);
	assert.deepEqual(errors, [
		"raw-settings: direct registerFlag() in index.ts; register flags through registerSettingFlag from src/pi-ext-settings.ts",
		"raw-settings: direct getFlag() in index.ts; read flags through resolveSetting from src/pi-ext-settings.ts",
		"raw-settings: direct getGlobalSettings() in index.ts; read settings through resolveSetting from src/pi-ext-settings.ts",
		"raw-settings: direct getProjectSettings() in index.ts; read settings through resolveSetting from src/pi-ext-settings.ts",
	]);
});

withTemp((root) => {
	const ext = writeExtension(root, "missing-debug", {});
	rmSync(join(ext, "src", "debug.ts"));
	assert(
		staticCheck(ext, { root }).includes(
			"missing-debug: missing handwritten src/debug.ts schema",
		),
	);
});

withTemp((root) => {
	const ext = writeExtension(root, "bad-debug", {});
	writeFileSync(
		join(ext, "src", "debug.ts"),
		"// defineDebug is not bound.\nexport const dbg = undefined;\n",
	);
	assert(
		staticCheck(ext, { root }).includes(
			"bad-debug: src/debug.ts must export dbg and closeDebug bound by defineDebug",
		),
	);
});

withTemp((root) => {
	const ext = writeExtension(root, "direct-debug", {});
	writeFileSync(
		join(ext, "index.ts"),
		'import * as direct from "./src/pi-ext-debug.js";\nexport { dbg } from "./src/pi-ext-debug.ts";\nvoid import("./src/pi-ext-debug.ts");\nvoid direct;\n',
	);
	const errors = staticCheck(ext, { root });
	assert(
		errors.some((error) =>
			error.includes("runtime import of generated debug module"),
		),
	);
	assert(
		errors.includes(
			"direct-debug: runtime code must import its handwritten src/debug.ts schema",
		),
	);
});

withTemp((root) => {
	const ext = writeExtension(root, "browser", {});
	mkdirSync(join(root, "test"));
	writeFileSync(
		join(root, "test", "playwright.base.ts"),
		"export default {};\n",
	);
	writeFileSync(
		join(ext, "playwright.config.ts"),
		'import base from "../test/playwright.base.ts";\nvoid base;\n',
	);
	assert.deepEqual(staticCheck(ext, { root }), []);
});

withTemp((root) => {
	writeExtension(
		root,
		"bad-config",
		{ devDependencies: { [HARNESS]: "^0.6.1" } },
		[
			'import path from "node:path";',
			'const a = path.join(process.cwd(), "settings.json");',
			"const d = (pi as SettingsAPI).settingsManager?.getSettings?.();",
			"void [a, d];",
		].join("\n"),
	);
	const errors = staticCheck(join(root, "bad-config"), { root });
	assert(errors.some((e) => e.includes("workspace-root settings.json")));
	assert(errors.some((e) => e.includes("private settingsManager access")));
});

withTemp((root) => {
	writeExtension(
		root,
		"flat-prompt",
		{ devDependencies: { [HARNESS]: "^0.6.1" } },
		[
			'pi.on("before_agent_start", (event) => {',
			'  event.systemPromptOptions.forceSystemPrompt = "forced";',
			'  return { systemPrompt: event.systemPrompt + " flat" };',
			"});",
		].join("\n"),
	);
	const errors = staticCheck(join(root, "flat-prompt"), { root });
	assert(
		errors.includes(
			"flat-prompt: flat before_agent_start system prompt override is forbidden; mutate event.systemPromptOptions.sections in index.ts",
		),
	);
});

withTemp((root) => {
	mkdirSync(join(root, "scripts"));
	writeFileSync(
		join(root, "scripts", "extensions.json"),
		JSON.stringify({
			secretScanIgnore: ["extensions/leaks/__tests__/ux.test.ts"],
		}),
		"utf8",
	);
	assert.deepEqual([...secretScanIgnorePaths(root)].sort(), [
		"extensions/leaks/__tests__/ux.test.ts",
	]);
	assert.equal(
		isSecretScanIgnored(
			join(root, "extensions/leaks/__tests__/ux.test.ts"),
			new Set(["extensions/leaks/__tests__/ux.test.ts"]),
			root,
		),
		true,
	);
	assert.equal(
		isSecretScanIgnored(
			join(root, "extensions/leaks/index.ts"),
			new Set(["extensions/leaks/__tests__/ux.test.ts"]),
			root,
		),
		false,
	);
});

// The workspace root exports nothing; Pi loads generated dist/<name> packages.
assert.deepEqual(rootPkg().pi, {});

const cliFailure = spawnSync(
	process.execPath,
	["scripts/extensions.mjs", "check", "__missing_extension__"],
	{ cwd: ROOT, encoding: "utf8" },
);
assert.equal(cliFailure.status, 1);
assert.match(cliFailure.stderr, /unknown extension/);

assert.deepEqual(
	JSON.parse(
		readFileSync(join(ROOT, "extensions/the-system/package.json"), "utf8"),
	).pi,
	{
		extensions: ["index.ts"],
	},
);

const legacyReferences = [
	["extensions", "super"].join("/"),
	["pi-ext", "super"].join("-"),
];
const textExtensions = new Set([
	".html",
	".json",
	".md",
	".mjs",
	".toml",
	".ts",
	".yaml",
	".yml",
]);

function liveTextFiles(dir = ROOT) {
	const files = [];
	for (const entry of readdirSync(dir, { withFileTypes: true })) {
		const path = join(dir, entry.name);
		if (entry.isDirectory()) {
			if (
				entry.name === ".git" ||
				entry.name === ".jj" ||
				entry.name === ".worktrees" ||
				entry.name === "node_modules" ||
				path === join(ROOT, "docs", "super")
			)
				continue;
			files.push(...liveTextFiles(path));
		} else if (
			entry.isFile() &&
			entry.name !== "CHANGELOG.md" &&
			textExtensions.has(entry.name.slice(entry.name.lastIndexOf(".")))
		) {
			files.push(path);
		}
	}
	return files;
}

withTemp((root) => {
	mkdirSync(join(root, ".worktrees", "branch"), { recursive: true });
	writeFileSync(
		join(root, ".worktrees", "branch", "ignored.md"),
		legacyReferences.join("\n"),
	);
	assert.deepEqual(liveTextFiles(root), []);
});

const legacyReferenceViolations = liveTextFiles().flatMap((path) => {
	const content = readFileSync(path, "utf8");
	return legacyReferences
		.filter((reference) => content.includes(reference))
		.map((reference) => `${path}: forbidden live reference ${reference}`);
});
assert.deepEqual(legacyReferenceViolations, []);

withTemp((root) => {
	const ext = writeExtension(root, "test-secret", {});
	writeFileSync(
		join(ext, "__tests__", "harness.test.ts"),
		'const apiKey = "not-a-real-secret-value";\n',
		"utf8",
	);
	assert(
		staticCheck(ext, { root }).includes(
			"test-secret: possible secret in __tests__/harness.test.ts",
		),
	);
});

console.log("OK static-check tests");