import assert from "node:assert/strict"; import { readdir, readFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; import { getGlobalConfigPath, getGlobalLogsDir, getHardGuardConfigPath, } from "../pi-permission-system/src/config-paths.ts"; import { getToolSearchCatalogPath, getToolSearchConfigPath, } from "../pi-tool-search/extensions/config.ts"; import { CONFIG_PATH as RTK_CONFIG_PATH, resolveConfigPath as resolveRtkConfigPath, } from "../pi-rtk-optimizer/src/constants.ts"; import { CONFIG_PATH as COMPACTION_CONFIG_PATH } from "../pi-compaction/src/config.ts"; import { WORKTREE_CONFIG_PATH } from "../pi-worktree/src/settings.ts"; import { resolvePreamblePath } from "../extensions/system-prompt-config.ts"; const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const bundleConfigDir = join(repositoryRoot, "config"); // pi-hermes-memory's config module uses runtime "./x.js" specifiers that // Node's type stripping cannot resolve to sibling ".ts" files, so it is // loaded through jiti instead of a plain import. const { createJiti } = await import("jiti"); const jiti = createJiti(import.meta.url); const { DEFAULT_CONFIG_PATH: HERMES_CONFIG_PATH } = (await jiti.import( join(repositoryRoot, "pi-hermes-memory", "src", "config.ts"), )) as { DEFAULT_CONFIG_PATH: string }; /** Bundle config files every extension reads directly at runtime. */ const BUNDLE_CONFIG_FILES = [ "pi-permission-policy.jsonc", "pi-permission-hard-guards.jsonc", "tool-search.json", "rtk-optimizer.json", "pi-compaction.json", "pi-worktree.json", "pi-hermes-memory.json", "prompt/preamble.md", "prompt/tools.jsonc", "prompt/routing.jsonc", "prompt/sections.jsonc", "pi-hashline-edit.json", "lsp.json", "p10k.zsh", ]; /** Strip JSONC comments (line and block) — same approach as the seed test. */ function stripJsonc(raw: string): string { return raw.replace(/^\s*\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); } test("every bundle config file exists and is non-empty", async () => { for (const name of BUNDLE_CONFIG_FILES) { const content = await readFile(join(bundleConfigDir, name), "utf8"); assert.ok(content.trim().length > 0, `config/${name} must not be empty`); } }); test("every bundle JSON/JSONC config file parses", async () => { for (const name of BUNDLE_CONFIG_FILES) { if (!name.endsWith(".json") && !name.endsWith(".jsonc")) continue; const raw = await readFile(join(bundleConfigDir, name), "utf8"); assert.doesNotThrow( () => JSON.parse(stripJsonc(raw)), `config/${name} must parse as JSONC`, ); } }); test("each extension resolves its config path inside the bundle config/ dir", () => { const savedToolSearch = process.env.PI_TOOL_SEARCH_CONFIG_PATH; const savedRtk = process.env.PI_RTK_CONFIG_PATH; delete process.env.PI_TOOL_SEARCH_CONFIG_PATH; delete process.env.PI_RTK_CONFIG_PATH; try { const inBundleConfig = (resolved: string, file: string) => assert.equal( resolved, join(bundleConfigDir, file), `expected the bundle config path for ${file}`, ); inBundleConfig(getGlobalConfigPath(), "pi-permission-policy.jsonc"); inBundleConfig(getHardGuardConfigPath(), "pi-permission-hard-guards.jsonc"); inBundleConfig(getToolSearchConfigPath(), "tool-search.json"); inBundleConfig(RTK_CONFIG_PATH, "rtk-optimizer.json"); inBundleConfig(resolveRtkConfigPath(), "rtk-optimizer.json"); inBundleConfig(COMPACTION_CONFIG_PATH, "pi-compaction.json"); inBundleConfig(WORKTREE_CONFIG_PATH, "pi-worktree.json"); inBundleConfig(HERMES_CONFIG_PATH, "pi-hermes-memory.json"); inBundleConfig(resolvePreamblePath(), "prompt/preamble.md"); } finally { if (savedToolSearch === undefined) delete process.env.PI_TOOL_SEARCH_CONFIG_PATH; else process.env.PI_TOOL_SEARCH_CONFIG_PATH = savedToolSearch; if (savedRtk === undefined) delete process.env.PI_RTK_CONFIG_PATH; else process.env.PI_RTK_CONFIG_PATH = savedRtk; } }); test("disposable runtime state stays under the agent-dir my-pi tree", () => { const agentDir = "/agent"; assert.ok( getGlobalLogsDir(agentDir).endsWith("my-pi/permission-system/logs"), ); assert.ok( getToolSearchCatalogPath(agentDir).endsWith( "my-pi/tool-search/catalog-v1.json", ), ); }); test("every package-local @earendil-works/* devDependency is pinned to latest", async () => { for (const entry of await readdir(repositoryRoot, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const manifestPath = join(repositoryRoot, entry.name, "package.json"); let manifest: { devDependencies?: Record }; try { manifest = JSON.parse(await readFile(manifestPath, "utf8")); } catch { continue; // not a package directory } for (const [name, range] of Object.entries(manifest.devDependencies ?? {})) { if (!name.startsWith("@earendil-works/")) continue; assert.equal( range, "latest", `${entry.name} devDependency ${name} must track "latest" (refresh locks via scripts/update-pi-devdeps.sh)`, ); } } }); test("subagent prompt profile: preamble non-blank, agent names match filename stems", async () => { const subagentDir = join(bundleConfigDir, "prompt", "subagent"); const preamble = await readFile(join(subagentDir, "preamble.md"), "utf8"); assert.ok( preamble.trim().length > 0, "config/prompt/subagent/preamble.md must not be blank", ); const agentsDir = join(subagentDir, "agents"); const agentFiles = (await readdir(agentsDir)).filter((name) => name.endsWith(".md")); assert.ok(agentFiles.length > 0, "config/prompt/subagent/agents/ must ship agent files"); for (const file of agentFiles) { const raw = await readFile(join(agentsDir, file), "utf8"); const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(frontmatter, `${file} must open with a frontmatter block`); const name = frontmatter[1].match(/^name:[ \t]*["']?([^"'\r\n]+?)["']?[ \t]*$/m)?.[1]; assert.equal( name, file.slice(0, -".md".length), `config/prompt/subagent/agents/${file} frontmatter name must match its filename stem`, ); } });