@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.
Files changed (197) hide show
  1. package/AGENTS.md +67 -116
  2. package/README.md +93 -7
  3. package/docs/architecture.md +408 -9
  4. package/package.json +6 -4
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
  7. package/packages/extension/src/__tests__/git-info.test.ts +67 -55
  8. package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
  9. package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
  10. package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
  11. package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
  12. package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
  13. package/packages/extension/src/bridge.ts +69 -2
  14. package/packages/extension/src/dev-build.ts +1 -1
  15. package/packages/extension/src/git-info.ts +9 -19
  16. package/packages/extension/src/pi-env.d.ts +1 -0
  17. package/packages/extension/src/process-scanner.ts +72 -38
  18. package/packages/extension/src/provider-register.ts +304 -16
  19. package/packages/extension/src/server-auto-start.ts +27 -1
  20. package/packages/extension/src/server-launcher.ts +71 -27
  21. package/packages/server/package.json +16 -2
  22. package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
  23. package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
  24. package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
  25. package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
  26. package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
  27. package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
  28. package/packages/server/src/__tests__/config-api.test.ts +68 -0
  29. package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
  30. package/packages/server/src/__tests__/directory-service.test.ts +234 -8
  31. package/packages/server/src/__tests__/editor-registry.test.ts +28 -15
  32. package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
  33. package/packages/server/src/__tests__/extension-register.test.ts +3 -1
  34. package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
  35. package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
  36. package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
  37. package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
  38. package/packages/server/src/__tests__/home-lock.test.ts +308 -0
  39. package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
  40. package/packages/server/src/__tests__/node-guard.test.ts +85 -0
  41. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
  42. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
  43. package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
  44. package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
  45. package/packages/server/src/__tests__/process-manager.test.ts +45 -18
  46. package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
  47. package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
  48. package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
  49. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
  50. package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
  51. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
  52. package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
  53. package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
  54. package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
  55. package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
  56. package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
  57. package/packages/server/src/__tests__/tunnel.test.ts +13 -7
  58. package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
  59. package/packages/server/src/bootstrap-queue.ts +130 -0
  60. package/packages/server/src/bootstrap-state.ts +131 -0
  61. package/packages/server/src/browse.ts +8 -3
  62. package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
  63. package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
  64. package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
  65. package/packages/server/src/cli.ts +256 -32
  66. package/packages/server/src/config-api.ts +16 -0
  67. package/packages/server/src/directory-service.ts +270 -39
  68. package/packages/server/src/editor-detection.ts +12 -9
  69. package/packages/server/src/editor-manager.ts +19 -4
  70. package/packages/server/src/editor-pid-registry.ts +9 -8
  71. package/packages/server/src/editor-registry.ts +22 -25
  72. package/packages/server/src/git-operations.ts +1 -1
  73. package/packages/server/src/headless-pid-registry.ts +7 -20
  74. package/packages/server/src/home-lock-release.ts +72 -0
  75. package/packages/server/src/home-lock.ts +389 -0
  76. package/packages/server/src/node-guard.ts +52 -0
  77. package/packages/server/src/package-manager-wrapper.ts +207 -47
  78. package/packages/server/src/pi-core-checker.ts +1 -1
  79. package/packages/server/src/pi-core-updater.ts +7 -1
  80. package/packages/server/src/pi-resource-scanner.ts +5 -8
  81. package/packages/server/src/pi-version-skew.ts +196 -0
  82. package/packages/server/src/preferences-store.ts +17 -3
  83. package/packages/server/src/process-manager.ts +403 -222
  84. package/packages/server/src/provider-probe.ts +234 -0
  85. package/packages/server/src/restart-helper.ts +130 -0
  86. package/packages/server/src/routes/bootstrap-routes.ts +88 -0
  87. package/packages/server/src/routes/openspec-routes.ts +25 -1
  88. package/packages/server/src/routes/pi-core-routes.ts +24 -1
  89. package/packages/server/src/routes/provider-auth-routes.ts +8 -8
  90. package/packages/server/src/routes/provider-routes.ts +43 -0
  91. package/packages/server/src/routes/recommended-routes.ts +10 -12
  92. package/packages/server/src/routes/system-routes.ts +20 -33
  93. package/packages/server/src/routes/tool-routes.ts +153 -0
  94. package/packages/server/src/server-pid.ts +5 -9
  95. package/packages/server/src/server.ts +211 -10
  96. package/packages/server/src/session-api.ts +77 -8
  97. package/packages/server/src/session-bootstrap.ts +17 -3
  98. package/packages/server/src/session-diff.ts +21 -21
  99. package/packages/server/src/terminal-manager.ts +61 -20
  100. package/packages/server/src/tunnel.ts +42 -28
  101. package/packages/shared/package.json +10 -3
  102. package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
  103. package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
  104. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
  105. package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
  106. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
  107. package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
  108. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
  109. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
  110. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
  111. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
  112. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
  113. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
  114. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
  115. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
  116. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
  117. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
  118. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
  119. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
  120. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
  121. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
  122. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
  123. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
  124. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
  125. package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
  126. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
  127. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
  128. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
  129. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
  130. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
  131. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
  132. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
  133. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
  134. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
  135. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
  136. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
  137. package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
  138. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
  139. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
  140. package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
  141. package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
  142. package/packages/shared/src/__tests__/config.test.ts +56 -0
  143. package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
  144. package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
  145. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
  146. package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
  147. package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
  148. package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
  149. package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
  150. package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
  151. package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
  152. package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
  153. package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
  154. package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
  155. package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
  156. package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
  157. package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
  158. package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
  159. package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
  160. package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
  161. package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
  162. package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
  163. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
  164. package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
  165. package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
  166. package/packages/shared/src/bootstrap-install.ts +212 -0
  167. package/packages/shared/src/bridge-register.ts +87 -20
  168. package/packages/shared/src/browser-protocol.ts +71 -1
  169. package/packages/shared/src/config.ts +87 -15
  170. package/packages/shared/src/managed-paths.ts +31 -4
  171. package/packages/shared/src/openspec-poller.ts +63 -46
  172. package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
  173. package/packages/shared/src/platform/commands.ts +100 -0
  174. package/packages/shared/src/platform/detached-spawn.ts +305 -0
  175. package/packages/shared/src/platform/exec.ts +220 -0
  176. package/packages/shared/src/platform/git.ts +155 -0
  177. package/packages/shared/src/platform/index.ts +15 -0
  178. package/packages/shared/src/platform/npm.ts +162 -0
  179. package/packages/shared/src/platform/openspec.ts +91 -0
  180. package/packages/shared/src/platform/paths.ts +276 -0
  181. package/packages/shared/src/platform/process-identify.ts +126 -0
  182. package/packages/shared/src/platform/process-scan.ts +94 -0
  183. package/packages/shared/src/platform/process.ts +168 -0
  184. package/packages/shared/src/platform/runner.ts +369 -0
  185. package/packages/shared/src/platform/shell.ts +44 -0
  186. package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
  187. package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
  188. package/packages/shared/src/recommended-extensions.ts +18 -2
  189. package/packages/shared/src/resolve-jiti.ts +62 -3
  190. package/packages/shared/src/rest-api.ts +26 -0
  191. package/packages/shared/src/semaphore.ts +83 -0
  192. package/packages/shared/src/tool-registry/definitions.ts +342 -0
  193. package/packages/shared/src/tool-registry/index.ts +56 -0
  194. package/packages/shared/src/tool-registry/overrides.ts +118 -0
  195. package/packages/shared/src/tool-registry/registry.ts +262 -0
  196. package/packages/shared/src/tool-registry/strategies.ts +198 -0
  197. 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
+ });