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");