@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +67 -116
- package/README.md +93 -7
- package/docs/architecture.md +408 -9
- package/package.json +6 -4
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/bridge.ts +69 -2
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +71 -27
- package/packages/server/package.json +16 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-registry.test.ts +28 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +13 -7
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +8 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +256 -32
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +19 -4
- package/packages/server/src/editor-pid-registry.ts +9 -8
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +7 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/package-manager-wrapper.ts +207 -47
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-core-updater.ts +7 -1
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +196 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +130 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/openspec-routes.ts +25 -1
- package/packages/server/src/routes/pi-core-routes.ts +24 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -8
- package/packages/server/src/routes/provider-routes.ts +43 -0
- package/packages/server/src/routes/recommended-routes.ts +10 -12
- package/packages/server/src/routes/system-routes.ts +20 -33
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +211 -10
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +61 -20
- package/packages/server/src/tunnel.ts +42 -28
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +56 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +71 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +63 -46
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +15 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/recommended-extensions.ts +18 -2
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +26 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/tool-registry/definitions.ts +342 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the standard tool definitions (strategies + registration).
|
|
3
|
+
*
|
|
4
|
+
* We inject fake `exists` / `which` / `npmRootGlobal` so tests are
|
|
5
|
+
* deterministic across platforms and don't depend on the test host's
|
|
6
|
+
* real filesystem or PATH.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import {
|
|
12
|
+
ToolRegistry,
|
|
13
|
+
registerDefaultTools,
|
|
14
|
+
OverridesStore,
|
|
15
|
+
} from "../tool-registry/index.js";
|
|
16
|
+
|
|
17
|
+
function freshRegistry(opts: {
|
|
18
|
+
exists?: (p: string) => boolean;
|
|
19
|
+
which?: (name: string) => string | null;
|
|
20
|
+
npmRootGlobal?: () => string;
|
|
21
|
+
overrides?: Record<string, string>;
|
|
22
|
+
platform?: NodeJS.Platform;
|
|
23
|
+
}) {
|
|
24
|
+
const store = new OverridesStore({
|
|
25
|
+
filePath: path.join(os.tmpdir(), `tool-registry-test-${Math.random()}.json`),
|
|
26
|
+
warn: () => {},
|
|
27
|
+
});
|
|
28
|
+
for (const [k, v] of Object.entries(opts.overrides ?? {})) store.set(k, v);
|
|
29
|
+
|
|
30
|
+
const r = new ToolRegistry({
|
|
31
|
+
overrides: store,
|
|
32
|
+
platform: opts.platform ?? "linux",
|
|
33
|
+
});
|
|
34
|
+
registerDefaultTools(r, {
|
|
35
|
+
exists: opts.exists ?? (() => false),
|
|
36
|
+
which: opts.which ?? (() => null),
|
|
37
|
+
npmRootGlobal: opts.npmRootGlobal ?? (() => ""),
|
|
38
|
+
});
|
|
39
|
+
return r;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("pi binary definition", () => {
|
|
43
|
+
it("chain order: override → managed → where", () => {
|
|
44
|
+
const r = freshRegistry({ which: (n) => (n === "pi" ? "/usr/bin/pi" : null) });
|
|
45
|
+
const res = r.resolve("pi");
|
|
46
|
+
expect(res.tried.map((t) => t.strategy)).toEqual([
|
|
47
|
+
"override",
|
|
48
|
+
"managed",
|
|
49
|
+
"where",
|
|
50
|
+
]);
|
|
51
|
+
expect(res.ok).toBe(true);
|
|
52
|
+
expect(res.path).toBe("/usr/bin/pi");
|
|
53
|
+
expect(res.source).toBe("system");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("managed wins over system when MANAGED_BIN/pi exists", () => {
|
|
57
|
+
const managed = path.join(os.homedir(), ".pi-dashboard", "node_modules", ".bin", "pi");
|
|
58
|
+
const r = freshRegistry({
|
|
59
|
+
exists: (p) => p === managed,
|
|
60
|
+
which: () => "/usr/bin/pi",
|
|
61
|
+
platform: "linux",
|
|
62
|
+
});
|
|
63
|
+
const res = r.resolve("pi");
|
|
64
|
+
expect(res.ok).toBe(true);
|
|
65
|
+
expect(res.path).toBe(managed);
|
|
66
|
+
expect(res.source).toBe("managed");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("picks .cmd extension on Windows", () => {
|
|
70
|
+
const managed = path.join(os.homedir(), ".pi-dashboard", "node_modules", ".bin", "pi.cmd");
|
|
71
|
+
const r = freshRegistry({
|
|
72
|
+
exists: (p) => p === managed,
|
|
73
|
+
platform: "win32",
|
|
74
|
+
});
|
|
75
|
+
const res = r.resolve("pi");
|
|
76
|
+
expect(res.ok).toBe(true);
|
|
77
|
+
expect(res.path).toBe(managed);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("override wins when set and path exists", () => {
|
|
81
|
+
const custom = "/opt/custom/pi";
|
|
82
|
+
const r = freshRegistry({
|
|
83
|
+
overrides: { pi: custom },
|
|
84
|
+
exists: (p) => p === custom, // validate() passes
|
|
85
|
+
});
|
|
86
|
+
const res = r.resolve("pi");
|
|
87
|
+
expect(res.ok).toBe(true);
|
|
88
|
+
expect(res.path).toBe(custom);
|
|
89
|
+
expect(res.source).toBe("override");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("invalid override falls through to next strategy with 'invalid:' reason", () => {
|
|
93
|
+
const r = freshRegistry({
|
|
94
|
+
overrides: { pi: "/does/not/exist" },
|
|
95
|
+
which: () => "/usr/bin/pi",
|
|
96
|
+
exists: (p) => p === "/usr/bin/pi", // override path fails validate
|
|
97
|
+
});
|
|
98
|
+
const res = r.resolve("pi");
|
|
99
|
+
expect(res.ok).toBe(true);
|
|
100
|
+
expect(res.source).toBe("system");
|
|
101
|
+
expect(res.tried[0].strategy).toBe("override");
|
|
102
|
+
expect(res.tried[0].result).toMatch(/^invalid:/);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("pi-coding-agent module definition", () => {
|
|
107
|
+
it("probes both @mariozechner and @oh-my-pi alias names", () => {
|
|
108
|
+
const r = freshRegistry({ exists: () => false });
|
|
109
|
+
const res = r.resolve("pi-coding-agent");
|
|
110
|
+
const names = res.tried.map((t) => t.strategy);
|
|
111
|
+
// First strategy: override. Then two bare-import (one per alias),
|
|
112
|
+
// then two managed, then two npm-global.
|
|
113
|
+
expect(names[0]).toBe("override");
|
|
114
|
+
expect(names.filter((n) => n === "bare-import").length).toBe(2);
|
|
115
|
+
expect(names.filter((n) => n === "managed").length).toBe(2);
|
|
116
|
+
expect(names.filter((n) => n === "npm-global").length).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("managed strategy hits ~/.pi-dashboard/node_modules/<pkg>/dist/index.js", () => {
|
|
120
|
+
const managed = path.join(
|
|
121
|
+
os.homedir(), ".pi-dashboard", "node_modules",
|
|
122
|
+
"@mariozechner", "pi-coding-agent", "dist", "index.js",
|
|
123
|
+
);
|
|
124
|
+
const r = freshRegistry({ exists: (p) => p === managed });
|
|
125
|
+
const res = r.resolve("pi-coding-agent");
|
|
126
|
+
expect(res.ok).toBe(true);
|
|
127
|
+
expect(res.path).toBe(managed);
|
|
128
|
+
expect(res.source).toBe("managed");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("npm-global strategy uses <npm root -g>/<pkg>/dist/index.js", () => {
|
|
132
|
+
const npmRoot = "/npm/global/root";
|
|
133
|
+
const entry = path.join(npmRoot, "@mariozechner", "pi-coding-agent", "dist", "index.js");
|
|
134
|
+
const r = freshRegistry({
|
|
135
|
+
exists: (p) => p === entry,
|
|
136
|
+
npmRootGlobal: () => npmRoot,
|
|
137
|
+
});
|
|
138
|
+
const res = r.resolve("pi-coding-agent");
|
|
139
|
+
expect(res.ok).toBe(true);
|
|
140
|
+
expect(res.path).toBe(entry);
|
|
141
|
+
expect(res.source).toBe("npm-global");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("fails cleanly when no strategy succeeds", () => {
|
|
145
|
+
const r = freshRegistry({
|
|
146
|
+
exists: () => false,
|
|
147
|
+
npmRootGlobal: () => "",
|
|
148
|
+
});
|
|
149
|
+
const res = r.resolve("pi-coding-agent");
|
|
150
|
+
expect(res.ok).toBe(false);
|
|
151
|
+
expect(res.path).toBeNull();
|
|
152
|
+
expect(res.source).toBeNull();
|
|
153
|
+
// Trail should include override + 2 bare-import + 2 managed + 2 npm-global.
|
|
154
|
+
expect(res.tried.length).toBeGreaterThanOrEqual(5);
|
|
155
|
+
expect(res.tried.some((t) => t.strategy === "npm-global")).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("openspec binary definition", () => {
|
|
160
|
+
it("finds openspec.cmd under managed bin on Windows", () => {
|
|
161
|
+
const managed = path.join(os.homedir(), ".pi-dashboard", "node_modules", ".bin", "openspec.cmd");
|
|
162
|
+
const r = freshRegistry({ exists: (p) => p === managed, platform: "win32" });
|
|
163
|
+
const res = r.resolve("openspec");
|
|
164
|
+
expect(res.ok).toBe(true);
|
|
165
|
+
expect(res.path).toBe(managed);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("falls through managed → where on Unix when managed is absent", () => {
|
|
169
|
+
const r = freshRegistry({
|
|
170
|
+
exists: () => false,
|
|
171
|
+
which: (n) => (n === "openspec" ? "/usr/local/bin/openspec" : null),
|
|
172
|
+
platform: "darwin",
|
|
173
|
+
});
|
|
174
|
+
const res = r.resolve("openspec");
|
|
175
|
+
expect(res.ok).toBe(true);
|
|
176
|
+
expect(res.source).toBe("system");
|
|
177
|
+
expect(res.path).toBe("/usr/local/bin/openspec");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("registered tool set", () => {
|
|
182
|
+
it("registers pi, pi-coding-agent, openspec, npm, node, git, zrok, wt", () => {
|
|
183
|
+
const r = freshRegistry({});
|
|
184
|
+
for (const name of ["pi", "pi-coding-agent", "openspec", "npm", "node", "git", "zrok", "wt"]) {
|
|
185
|
+
expect(r.has(name)).toBe(true);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("wt resolves via where when found", () => {
|
|
190
|
+
const r = freshRegistry({
|
|
191
|
+
platform: "win32",
|
|
192
|
+
which: (name) => (name === "wt" ? "C:\\WindowsApps\\wt.exe" : null),
|
|
193
|
+
});
|
|
194
|
+
const res = r.resolve("wt");
|
|
195
|
+
expect(res.ok).toBe(true);
|
|
196
|
+
expect(res.path).toBe("C:\\WindowsApps\\wt.exe");
|
|
197
|
+
expect(res.source).toBe("system");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("wt unavailable returns ok:false without error", () => {
|
|
201
|
+
const r = freshRegistry({ platform: "win32", which: () => null });
|
|
202
|
+
const res = r.resolve("wt");
|
|
203
|
+
expect(res.ok).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("does NOT register tsx (it's a loader, not a spawn target)", () => {
|
|
207
|
+
const r = freshRegistry({});
|
|
208
|
+
expect(r.has("tsx")).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("registers Windows-only process utilities on win32, NOT ps/pgrep", () => {
|
|
212
|
+
const r = freshRegistry({ platform: "win32" });
|
|
213
|
+
expect(r.has("tasklist")).toBe(true);
|
|
214
|
+
expect(r.has("taskkill")).toBe(true);
|
|
215
|
+
expect(r.has("wmic")).toBe(true);
|
|
216
|
+
expect(r.has("powershell")).toBe(true);
|
|
217
|
+
// ps/pgrep are POSIX-only; they'd always show "not found" on Windows
|
|
218
|
+
// and pollute the Tools UI with red rows the code never calls.
|
|
219
|
+
expect(r.has("ps")).toBe(false);
|
|
220
|
+
expect(r.has("pgrep")).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("registers POSIX process utilities on linux/darwin, NOT tasklist etc.", () => {
|
|
224
|
+
for (const platform of ["linux", "darwin"] as NodeJS.Platform[]) {
|
|
225
|
+
const r = freshRegistry({ platform });
|
|
226
|
+
expect(r.has("ps")).toBe(true);
|
|
227
|
+
expect(r.has("pgrep")).toBe(true);
|
|
228
|
+
expect(r.has("tasklist")).toBe(false);
|
|
229
|
+
expect(r.has("taskkill")).toBe(false);
|
|
230
|
+
expect(r.has("wmic")).toBe(false);
|
|
231
|
+
expect(r.has("powershell")).toBe(false);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("does NOT register pi-dashboard (it's the package this code is part of)", () => {
|
|
236
|
+
const r = freshRegistry({});
|
|
237
|
+
expect(r.has("pi-dashboard")).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for OverridesStore (packages/shared/src/tool-registry/overrides.ts).
|
|
3
|
+
*
|
|
4
|
+
* Covered scenarios:
|
|
5
|
+
* - Absent file → empty map
|
|
6
|
+
* - Malformed file → warn + empty map
|
|
7
|
+
* - set/clear round-trip with atomic write
|
|
8
|
+
* - File schema shape (version + overrides[name].path)
|
|
9
|
+
* - invalidate() forces reload from disk
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import { OverridesStore } from "../tool-registry/overrides.js";
|
|
16
|
+
|
|
17
|
+
function freshPath(): string {
|
|
18
|
+
return path.join(
|
|
19
|
+
os.tmpdir(),
|
|
20
|
+
`tool-overrides-unit-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("OverridesStore.list", () => {
|
|
25
|
+
it("returns empty map when file is absent", () => {
|
|
26
|
+
const fp = freshPath();
|
|
27
|
+
const s = new OverridesStore({ filePath: fp, warn: () => {} });
|
|
28
|
+
expect(s.list()).toEqual({});
|
|
29
|
+
expect(fs.existsSync(fp)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns empty map and warns when file is malformed JSON", () => {
|
|
33
|
+
const fp = freshPath();
|
|
34
|
+
fs.writeFileSync(fp, "{ this is not json");
|
|
35
|
+
const warnings: string[] = [];
|
|
36
|
+
const s = new OverridesStore({ filePath: fp, warn: (m) => warnings.push(m) });
|
|
37
|
+
expect(s.list()).toEqual({});
|
|
38
|
+
expect(warnings.length).toBe(1);
|
|
39
|
+
expect(warnings[0]).toMatch(/failed to read/);
|
|
40
|
+
fs.unlinkSync(fp);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns empty map when file has valid JSON but wrong schema", () => {
|
|
44
|
+
const fp = freshPath();
|
|
45
|
+
fs.writeFileSync(fp, JSON.stringify({ version: 1 })); // no overrides key
|
|
46
|
+
const warnings: string[] = [];
|
|
47
|
+
const s = new OverridesStore({ filePath: fp, warn: (m) => warnings.push(m) });
|
|
48
|
+
expect(s.list()).toEqual({});
|
|
49
|
+
expect(warnings[0]).toMatch(/malformed/);
|
|
50
|
+
fs.unlinkSync(fp);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("skips individual entries with wrong shape but keeps well-formed ones", () => {
|
|
54
|
+
const fp = freshPath();
|
|
55
|
+
fs.writeFileSync(fp, JSON.stringify({
|
|
56
|
+
version: 1,
|
|
57
|
+
overrides: {
|
|
58
|
+
good: { path: "/x" },
|
|
59
|
+
bad1: "string not object",
|
|
60
|
+
bad2: { wrong: "field" },
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
const s = new OverridesStore({ filePath: fp, warn: () => {} });
|
|
64
|
+
expect(s.list()).toEqual({ good: "/x" });
|
|
65
|
+
fs.unlinkSync(fp);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("OverridesStore.set / clear", () => {
|
|
70
|
+
let fp: string;
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
fp = freshPath();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("set writes the file with the documented schema", () => {
|
|
76
|
+
const s = new OverridesStore({ filePath: fp, warn: () => {} });
|
|
77
|
+
s.set("pi", "/custom/pi");
|
|
78
|
+
const raw = fs.readFileSync(fp, "utf-8");
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
expect(parsed).toEqual({
|
|
81
|
+
version: 1,
|
|
82
|
+
overrides: { pi: { path: "/custom/pi" } },
|
|
83
|
+
});
|
|
84
|
+
fs.unlinkSync(fp);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("set + list round-trips", () => {
|
|
88
|
+
const s = new OverridesStore({ filePath: fp, warn: () => {} });
|
|
89
|
+
s.set("pi", "/a");
|
|
90
|
+
s.set("openspec", "/b");
|
|
91
|
+
expect(s.list()).toEqual({ pi: "/a", openspec: "/b" });
|
|
92
|
+
fs.unlinkSync(fp);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("clear removes an entry and persists", () => {
|
|
96
|
+
const s = new OverridesStore({ filePath: fp, warn: () => {} });
|
|
97
|
+
s.set("pi", "/a");
|
|
98
|
+
s.set("openspec", "/b");
|
|
99
|
+
s.clear("pi");
|
|
100
|
+
expect(s.list()).toEqual({ openspec: "/b" });
|
|
101
|
+
const parsed = JSON.parse(fs.readFileSync(fp, "utf-8"));
|
|
102
|
+
expect(parsed.overrides).toEqual({ openspec: { path: "/b" } });
|
|
103
|
+
fs.unlinkSync(fp);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("clear is a no-op when the name is absent", () => {
|
|
107
|
+
const s = new OverridesStore({ filePath: fp, warn: () => {} });
|
|
108
|
+
s.clear("pi"); // nothing to clear; must not throw
|
|
109
|
+
expect(s.list()).toEqual({});
|
|
110
|
+
expect(fs.existsSync(fp)).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("writes are atomic (tmp file renamed, not left behind)", () => {
|
|
114
|
+
const s = new OverridesStore({ filePath: fp, warn: () => {} });
|
|
115
|
+
s.set("pi", "/a");
|
|
116
|
+
expect(fs.existsSync(fp)).toBe(true);
|
|
117
|
+
expect(fs.existsSync(fp + ".tmp")).toBe(false);
|
|
118
|
+
fs.unlinkSync(fp);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("OverridesStore.invalidate", () => {
|
|
123
|
+
it("forces a reload from disk on next list()", () => {
|
|
124
|
+
const fp = freshPath();
|
|
125
|
+
fs.writeFileSync(fp, JSON.stringify({ version: 1, overrides: { pi: { path: "/a" } } }));
|
|
126
|
+
const s = new OverridesStore({ filePath: fp, warn: () => {} });
|
|
127
|
+
expect(s.list()).toEqual({ pi: "/a" });
|
|
128
|
+
|
|
129
|
+
// Mutate the file underneath.
|
|
130
|
+
fs.writeFileSync(fp, JSON.stringify({ version: 1, overrides: { pi: { path: "/b" } } }));
|
|
131
|
+
expect(s.list()).toEqual({ pi: "/a" }); // still cached
|
|
132
|
+
|
|
133
|
+
s.invalidate();
|
|
134
|
+
expect(s.list()).toEqual({ pi: "/b" }); // reloaded
|
|
135
|
+
fs.unlinkSync(fp);
|
|
136
|
+
});
|
|
137
|
+
});
|