diff --git a/packages/cli/src/commands/doctor-pi.test.ts b/packages/cli/src/commands/doctor-pi.test.ts index 06006700b..5844e32db 100644 --- a/packages/cli/src/commands/doctor-pi.test.ts +++ b/packages/cli/src/commands/doctor-pi.test.ts @@ -346,6 +346,138 @@ describe("Pi doctor", () => { expect(output).toContain("WARN 2"); }); + it("skips unrelated local dev-path packages and broken trees when probing the embedding runtime", async () => { + const root = makeTempRoot(); + const cwd = makeTempRoot("mc-pi-doctor-cwd-"); + const agentDir = setEnv(root, cwd); + writeHealthyFiles(agentDir, cwd); + + // Unrelated local extension: has a package.json but is NOT the + // magic-context plugin. Must not be probed as an embedding candidate. + const unrelatedPlugin = makeTempRoot("mc-pi-doctor-unrelated-"); + writeFileSync( + join(unrelatedPlugin, "package.json"), + JSON.stringify({ name: "pi-tree-git-checkpoint", version: "0.0.0" }), + ); + // Local dev tree of the actual plugin that is missing all embedding deps. + const brokenDevTree = makeTempRoot("mc-pi-doctor-dev-"); + writeFileSync( + join(brokenDevTree, "package.json"), + JSON.stringify({ name: "@cortexkit/pi-magic-context", version: "0.0.0-dev" }), + ); + + writeFileSync( + join(agentDir, "settings.json"), + JSON.stringify({ + packages: [ + "npm:@cortexkit/pi-magic-context", + unrelatedPlugin, + brokenDevTree, + ], + }), + ); + createInstalledPiPlugin(agentDir, true); + const prompts = new MockPrompts(); + + const code = await runDoctor(baseOptions(root, cwd, prompts)); + + expect(code).toBe(0); + const output = prompts.messages.join("\n"); + expect(output).toContain( + "PASS Embedding provider: local (native runtime selected and OK)", + ); + expect(output).not.toContain( + "WARN Embedding provider: local — native runtime and WASM fallback both unavailable", + ); + }); + + it("prefers a later native-capable install over an earlier WASM fallback", async () => { + const root = makeTempRoot(); + const cwd = makeTempRoot("mc-pi-doctor-cwd-"); + const agentDir = setEnv(root, cwd); + writeHealthyFiles(agentDir, cwd); + + // Local dev tree of the actual plugin with only a WASM fallback + // (no native binding) — probing it alone would report a degraded + // runtime. + const wasmDevTree = makeTempRoot("mc-pi-doctor-wasm-dev-"); + mkdirSync(join(wasmDevTree, "node_modules", "onnxruntime-web"), { + recursive: true, + }); + writeFileSync( + join(wasmDevTree, "node_modules", "onnxruntime-web", "package.json"), + JSON.stringify({ name: "onnxruntime-web", main: "index.js" }), + ); + writeFileSync( + join(wasmDevTree, "node_modules", "onnxruntime-web", "index.js"), + "module.exports = {};\n", + ); + mkdirSync(join(wasmDevTree, "dist"), { recursive: true }); + writeFileSync( + join(wasmDevTree, "dist", "transformers-node-wasm.js"), + "export {};\n", + ); + writeFileSync( + join(wasmDevTree, "package.json"), + JSON.stringify({ name: "@cortexkit/pi-magic-context", version: "0.0.0-dev" }), + ); + + writeFileSync( + join(agentDir, "settings.json"), + JSON.stringify({ + packages: ["npm:@cortexkit/pi-magic-context", wasmDevTree], + }), + ); + createInstalledPiPlugin(agentDir, true); + const prompts = new MockPrompts(); + + const code = await runDoctor(baseOptions(root, cwd, prompts)); + + expect(code).toBe(0); + const output = prompts.messages.join("\n"); + expect(output).toContain( + "PASS Embedding provider: local (native runtime selected and OK)", + ); + expect(output).not.toContain( + "WARN Embedding provider: local — onnxruntime-node native binding failed", + ); + }); + + it("reports unverified, not a broken-runtime WARN, when only unrelated local packages are registered", async () => { + const root = makeTempRoot(); + const cwd = makeTempRoot("mc-pi-doctor-cwd-"); + const agentDir = setEnv(root, cwd); + writeHealthyFiles(agentDir, cwd); + + // Only an unrelated local extension is registered; the magic-context + // managed install tree is absent. The unrelated package must not be + // probed as an embedding candidate, so doctor reports unverified + // instead of blaming it for a missing onnxruntime. + const unrelatedPlugin = makeTempRoot("mc-pi-doctor-unrelated-"); + writeFileSync( + join(unrelatedPlugin, "package.json"), + JSON.stringify({ name: "pi-tree-git-checkpoint", version: "0.0.0" }), + ); + writeFileSync( + join(agentDir, "settings.json"), + JSON.stringify({ + packages: ["npm:@cortexkit/pi-magic-context", unrelatedPlugin], + }), + ); + const prompts = new MockPrompts(); + + const code = await runDoctor(baseOptions(root, cwd, prompts)); + + expect(code).toBe(0); + const output = prompts.messages.join("\n"); + expect(output).toContain( + "selected runtime unverified (no installed plugin tree found to inspect)", + ); + expect(output).not.toContain( + "WARN Embedding provider: local — native runtime and WASM fallback both unavailable", + ); + }); + it("reports the WASM fallback when onnxruntime-node is completely absent", async () => { const root = makeTempRoot(); const cwd = makeTempRoot("mc-pi-doctor-cwd-"); diff --git a/packages/cli/src/commands/doctor-pi.ts b/packages/cli/src/commands/doctor-pi.ts index 5f77c823a..79084f9dc 100644 --- a/packages/cli/src/commands/doctor-pi.ts +++ b/packages/cli/src/commands/doctor-pi.ts @@ -288,18 +288,35 @@ function packagesFrom(settings: Record): unknown[] { * /.pi/npm/node_modules/ (project). We collect every plausible dir * with a package.json; the resolver stays SILENT for any that don't exist. */ +/** True when the directory's package.json declares the magic-context Pi plugin. */ +function isPiMagicContextPackageDir(dir: string): boolean { + const packageJson = join(dir, "package.json"); + if (!existsSync(packageJson)) return false; + try { + const pkg = JSON.parse(readFileSync(packageJson, "utf-8")) as { + name?: unknown; + }; + return typeof pkg.name === "string" && pkg.name === PACKAGE_NAME; + } catch { + return false; + } +} + function piPluginDirCandidates(packages: unknown[], cwd: string): string[] { const dirs: string[] = []; const agentDir = getPiAgentConfigDir(); // Local dev-path entries: a string spec that is NOT an npm: specifier and // resolves to a directory on disk. Relative entries are resolved against the - // Pi agent dir (Pi's settings.packages base). + // Pi agent dir (Pi's settings.packages base). Only directories whose + // package.json names the magic-context plugin itself are candidates — other + // local extensions registered in packages[] must not be probed for the + // embedding runtime. for (const entry of packages) { const spec = typeof entry === "string" ? entry.trim() : ""; if (!spec || spec.startsWith("npm:")) continue; const resolved = isAbsolute(spec) ? spec : join(agentDir, spec); - dirs.push(resolved); + if (isPiMagicContextPackageDir(resolved)) dirs.push(resolved); } // Managed npm install roots (hoisted): /node_modules/. @@ -781,6 +798,12 @@ async function runHealthChecks(options: { // persistence-capable Node WASM fallback. Resolution starts from the // installed plugin dir and stays silent when no tree can be inspected. let runtimeReported = false; + let firstFallback: ReturnType< + typeof checkLocalEmbeddingRuntimeByResolution + > | null = null; + let firstBroken: ReturnType< + typeof checkLocalEmbeddingRuntimeByResolution + > | null = null; let runtimeUnverifiedReason = "no installed plugin tree found to inspect"; for (const pluginDir of piPluginDirCandidates(packages, options.cwd)) { const runtime = checkLocalEmbeddingRuntimeByResolution( @@ -808,23 +831,32 @@ async function runHealthChecks(options: { break; } if (runtime.state === "wasm-fallback") { - add(results, "warn", formatLocalEmbeddingRuntimeWasmFallback(runtime)); - runtimeReported = true; - break; + // Remember the best degraded candidate but keep probing: a WASM + // fallback in an earlier tree must not mask a later native-capable + // install. + firstFallback ??= runtime; + continue; } if (isLocalEmbeddingRuntimeBroken(runtime)) { - add(results, "warn", formatLocalEmbeddingRuntimeDoctorWarning(runtime)); - runtimeReported = true; - break; + // Keep probing: an earlier broken candidate (e.g. a stale local + // dev-path tree) must not mask a healthy managed install. + firstBroken ??= runtime; + continue; } if (runtime.state === "unknown") runtimeUnverifiedReason = runtime.reason; } if (!runtimeReported) { - add( - results, - "warn", - `Embedding provider ${loadedConfig.config.embedding.provider}: selected runtime unverified (${runtimeUnverifiedReason})`, - ); + if (firstFallback) { + add(results, "warn", formatLocalEmbeddingRuntimeWasmFallback(firstFallback)); + } else if (firstBroken) { + add(results, "warn", formatLocalEmbeddingRuntimeDoctorWarning(firstBroken)); + } else { + add( + results, + "warn", + `Embedding provider ${loadedConfig.config.embedding.provider}: selected runtime unverified (${runtimeUnverifiedReason})`, + ); + } } }