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