import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import assert from "node:assert/strict"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { homedir, tmpdir } from "node:os"; import { join, parse } from "node:path"; import test from "node:test"; import pluginInitExtension, { findMissingPluginInitializers, initializeMissingPlugins, validatePluginInitCwd, } from "../extensions/plugin-init.ts"; test("initializes missing CodeGraph and Hippo state after preflighting both CLIs", async () => { const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-")); const calls: Array<{ command: string; args: string[]; cwd: string }> = []; try { const targets = await findMissingPluginInitializers(cwd); assert.deepEqual(targets.map(({ name }) => name), ["CodeGraph", "Hippo"]); await initializeMissingPlugins(cwd, targets, async (command, args, options) => { calls.push({ command, args, cwd: options.cwd }); return { code: 0, stdout: "ok", stderr: "" }; }); assert.deepEqual(calls, [ { command: "codegraph", args: ["--version"], cwd }, { command: "hippo", args: ["--version"], cwd }, { command: "codegraph", args: ["init", cwd], cwd }, { command: "hippo", args: ["init"], cwd }, ]); } finally { await rm(cwd, { recursive: true, force: true }); } }); test("uses the CLIs' database files as authoritative initialization markers", async () => { const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-")); const calls: string[] = []; try { await mkdir(join(cwd, ".codegraph")); await mkdir(join(cwd, ".hippo")); assert.deepEqual( (await findMissingPluginInitializers(cwd)).map(({ name }) => name), ["CodeGraph", "Hippo"], "bare state directories must not count as initialized", ); await writeFile(join(cwd, ".codegraph", "codegraph.db"), ""); assert.deepEqual((await findMissingPluginInitializers(cwd)).map(({ name }) => name), ["Hippo"]); await writeFile(join(cwd, ".hippo", "hippo.db"), ""); const targets = await findMissingPluginInitializers(cwd); assert.deepEqual(targets, []); await initializeMissingPlugins(cwd, targets, async (command, args) => { calls.push(`${command} ${args.join(" ")}`); return { code: 0, stdout: "ok", stderr: "" }; }); assert.deepEqual(calls, ["codegraph --version", "hippo --version"]); } finally { await rm(cwd, { recursive: true, force: true }); } }); test("rejects unsafe project roots", () => { assert.throws(() => validatePluginInitCwd(parse(homedir()).root), /文件系统根目录/); assert.throws(() => validatePluginInitCwd(homedir()), /用户主目录/); }); test("preflights both CLIs before creating project state", async () => { const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-")); const calls: string[] = []; try { const targets = await findMissingPluginInitializers(cwd); await assert.rejects( initializeMissingPlugins(cwd, targets, async (command, args) => { calls.push(`${command} ${args.join(" ")}`); return command === "hippo" ? { code: 1, stdout: "", stderr: "not installed" } : { code: 0, stdout: "ok", stderr: "" }; }), /Hippo 检测失败:not installed/, ); assert.deepEqual(calls, ["codegraph --version", "hippo --version"]); } finally { await rm(cwd, { recursive: true, force: true }); } }); test("plugin_init confirms the target and hot-reloads after initialization", async () => { let command: { handler: (args: string, ctx: any) => Promise } | undefined; const calls: string[] = []; const pi = { registerCommand(name: string, definition: typeof command) { assert.equal(name, "plugin_init"); command = definition; }, exec: async (program: string, args: string[]) => { calls.push(`${program} ${args.join(" ")}`); return { code: 0, stdout: "ok", stderr: "" }; }, } as unknown as ExtensionAPI; pluginInitExtension(pi); assert.ok(command); const registeredCommand = command!; const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-")); let reloaded = false; let confirmation = ""; try { await registeredCommand.handler("", { cwd, hasUI: true, ui: { confirm: async (_title: string, message: string) => { confirmation = message; return true; }, notify() {}, }, reload: async () => { reloaded = true; }, }); assert.match(confirmation, new RegExp(cwd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.deepEqual(calls, [ "codegraph --version", "hippo --version", `codegraph init ${cwd}`, "hippo init", ]); assert.equal(reloaded, true); } finally { await rm(cwd, { recursive: true, force: true }); } }); test("plugin_init reports retained partial state and does not reload when a later initializer fails", async () => { let command: { handler: (args: string, ctx: any) => Promise } | undefined; const pi = { registerCommand(_name: string, definition: typeof command) { command = definition; }, exec: async (program: string, args: string[]) => ({ code: program === "hippo" && args[0] === "init" ? 1 : 0, stdout: "", stderr: "mock failure", }), } as unknown as ExtensionAPI; pluginInitExtension(pi); assert.ok(command); const registeredCommand = command!; const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-")); let reloaded = false; let error = ""; try { await registeredCommand.handler("", { cwd, hasUI: true, ui: { confirm: async () => true, notify(message: string, level: string) { if (level === "error") error = message; }, }, reload: async () => { reloaded = true; }, }); assert.match(error, /Hippo 初始化失败:mock failure/); assert.match(error, /已完成的 CodeGraph 初始化会保留,不会自动回滚/); assert.equal(reloaded, false); } finally { await rm(cwd, { recursive: true, force: true }); } }); test("plugin_init stops without executing or reloading when confirmation is cancelled", async () => { let command: { handler: (args: string, ctx: any) => Promise } | undefined; let executed = false; const pi = { registerCommand(_name: string, definition: typeof command) { command = definition; }, exec: async () => { executed = true; return { code: 0, stdout: "", stderr: "" }; }, } as unknown as ExtensionAPI; pluginInitExtension(pi); const registeredCommand = command!; const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-")); let reloaded = false; try { await registeredCommand.handler("", { cwd, hasUI: true, ui: { confirm: async () => false, notify() {} }, reload: async () => { reloaded = true; }, }); assert.equal(executed, false); assert.equal(reloaded, false); } finally { await rm(cwd, { recursive: true, force: true }); } });