@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
@@ -5,28 +5,37 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
5
5
  import path from "node:path";
6
6
  import os from "node:os";
7
7
 
8
- const { mockExecSync, mockExistsSync } = vi.hoisted(() => ({
8
+ const { mockExecSync, mockSpawnSync, mockExistsSync } = vi.hoisted(() => ({
9
9
  mockExecSync: vi.fn(),
10
+ mockSpawnSync: vi.fn(),
10
11
  mockExistsSync: vi.fn(),
11
12
  }));
12
13
 
13
- vi.mock("node:child_process", () => ({ execSync: mockExecSync }));
14
+ vi.mock("node:child_process", () => ({ execSync: mockExecSync, spawnSync: mockSpawnSync }));
14
15
  vi.mock("node:fs", () => ({ existsSync: mockExistsSync }));
15
16
 
16
- import { ToolResolver } from "../tool-resolver.js";
17
+ import { ToolResolver } from "../platform/binary-lookup.js";
17
18
 
18
19
  const MANAGED_BIN = path.join(os.homedir(), ".pi-dashboard", "node_modules", ".bin");
19
20
 
21
+ // On Windows, ToolResolver.which() appends ".cmd" to the binary name when
22
+ // probing managed bin / extra dirs (shim convention for npm-installed bins).
23
+ // Unix has no extension. Tests must mirror this so assertions line up with
24
+ // what the implementation actually queries.
25
+ const BIN_EXT = process.platform === "win32" ? ".cmd" : "";
26
+
20
27
  describe("ToolResolver", () => {
21
28
  beforeEach(() => {
22
29
  vi.clearAllMocks();
23
30
  mockExistsSync.mockReturnValue(false);
24
31
  mockExecSync.mockImplementation(() => { throw new Error("not found"); });
32
+ // Default: spawnSync (used by whereAllLines) reports not found.
33
+ mockSpawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
25
34
  });
26
35
 
27
36
  describe("which()", () => {
28
37
  it("finds binary in managed bin first", () => {
29
- const managedPi = path.join(MANAGED_BIN, "pi");
38
+ const managedPi = path.join(MANAGED_BIN, "pi" + BIN_EXT);
30
39
  mockExistsSync.mockImplementation((p: string) => p === managedPi);
31
40
 
32
41
  const resolver = new ToolResolver();
@@ -35,21 +44,28 @@ describe("ToolResolver", () => {
35
44
 
36
45
  it("finds binary in extra bin dirs before system PATH", () => {
37
46
  const extraDir = "/custom/bin";
38
- const extraPi = path.join(extraDir, "pi");
47
+ const extraPi = path.join(extraDir, "pi" + BIN_EXT);
39
48
  mockExistsSync.mockImplementation((p: string) => p === extraPi);
40
49
 
41
50
  const resolver = new ToolResolver({ extraBinDirs: [extraDir] });
42
51
  expect(resolver.which("pi")).toBe(extraPi);
43
52
  });
44
53
 
45
- it("falls back to system PATH via which", () => {
46
- mockExecSync.mockImplementation((cmd: string) => {
47
- if (typeof cmd === "string" && cmd.includes("which pi")) return "/usr/bin/pi\n";
48
- throw new Error("not found");
54
+ it("falls back to system PATH via which/where", () => {
55
+ // Resolver uses `where` on Windows, `which` on Unix via spawnSync
56
+ // (not execSync see whereAllLines in platform/tools.ts).
57
+ const lookupCmd = process.platform === "win32" ? "where" : "which";
58
+ const expected = process.platform === "win32" ? "C:\\Windows\\pi.exe" : "/usr/bin/pi";
59
+ mockSpawnSync.mockImplementation((cmd: string, args: string[]) => {
60
+ // argv[0] is 'where'/'which', argv[1] is the target binary.
61
+ if (cmd === lookupCmd && args?.[0] === "pi") {
62
+ return { status: 0, stdout: expected + "\n", stderr: "" };
63
+ }
64
+ return { status: 1, stdout: "", stderr: "" };
49
65
  });
50
66
 
51
67
  const resolver = new ToolResolver();
52
- expect(resolver.which("pi")).toBe("/usr/bin/pi");
68
+ expect(resolver.which("pi")).toBe(expected);
53
69
  });
54
70
 
55
71
  it("tries login shell when enabled and PATH fails", () => {
@@ -123,7 +139,7 @@ describe("ToolResolver", () => {
123
139
  });
124
140
 
125
141
  it("falls back to which(node) when no context paths", () => {
126
- const managedNode = path.join(MANAGED_BIN, "node");
142
+ const managedNode = path.join(MANAGED_BIN, "node" + BIN_EXT);
127
143
  mockExistsSync.mockImplementation((p: string) => p === managedNode);
128
144
 
129
145
  const resolver = new ToolResolver();
@@ -143,7 +159,11 @@ describe("ToolResolver", () => {
143
159
 
144
160
  it("does not duplicate managed bin if already present", () => {
145
161
  const resolver = new ToolResolver();
146
- const env = resolver.buildSpawnEnv({ PATH: `${MANAGED_BIN}:/usr/bin` });
162
+ // Use the platform's PATH delimiter (`;` on Windows, `:` on Unix) so
163
+ // MANAGED_BIN is parsed as its own PATH entry — otherwise on Windows
164
+ // `${MANAGED_BIN}:/usr/bin` is treated as one single (broken) path.
165
+ const existingPath = [MANAGED_BIN, "/usr/bin"].join(path.delimiter);
166
+ const env = resolver.buildSpawnEnv({ PATH: existingPath });
147
167
  const count = env.PATH!.split(path.delimiter).filter(p => p === MANAGED_BIN).length;
148
168
  expect(count).toBe(1);
149
169
  });
@@ -0,0 +1,133 @@
1
+ # Bootstrap Resolution Harness
2
+
3
+ In-memory test harness for the dashboard's bootstrap resolution —
4
+ `ToolRegistry` + bridge-extension registration — across install
5
+ mechanics, platforms, and HOME/path drift.
6
+
7
+ **See** `openspec/changes/bootstrap-resolution-harness/{proposal,design}.md`
8
+ for the full design rationale.
9
+
10
+ ## Why
11
+
12
+ The dashboard resolves pi, node, openspec, tsx across 5 strategies on
13
+ 3 platforms. It writes bridge registration into pi's `settings.json` at
14
+ a HOME-dependent path. Small changes in these code paths can silently
15
+ break a specific install mechanic (`npm i -g pi-dashboard` on Windows,
16
+ Electron AppImage, GUI-launched PATH, etc.). This harness captures the
17
+ full state space in a memfs-backed cube so regressions surface in ms.
18
+
19
+ ## File layout
20
+
21
+ ```
22
+ bootstrap/
23
+ ├── harness.ts ← withFakeEnv(), layer(), memfs wiring
24
+ ├── assertions.ts ← snapshotTrail, snapshotSettingsDelta
25
+ ├── scenarios.ts ← register(), skip(), cellKey(), enumerateCube()
26
+ ├── scenarios-skipped.ts ← bulk-skip manifest (everything defaults to skipped)
27
+ ├── cube.ts ← sweepCube() + formatUnclassifiedError()
28
+ ├── cube.test.ts ← fail-closed sweep (breaks CI on unclassified cells)
29
+ ├── fixtures/
30
+ │ ├── managed-install.ts ← ~/.pi-dashboard/ layout
31
+ │ ├── npm-global-layout.ts ← /usr/lib/node_modules + %APPDATA%\Roaming\npm
32
+ │ ├── electron-layout.ts ← packaged Electron resources
33
+ │ ├── dev-monorepo.ts ← workspace + hoisted deps
34
+ │ ├── settings-json.ts ← pi's settings.json variants
35
+ │ └── pi-versions.ts ← package.json stampers
36
+ └── families/
37
+ ├── index.ts ← barrel — imports every family file
38
+ ├── a-electron.test.ts ← Family A
39
+ ├── b-npm-global.test.ts ← Family B (contains ⚠ Windows bug capture)
40
+ ├── ... c through k
41
+ └── __snapshots__/ ← trail + settings-delta snapshots
42
+ ```
43
+
44
+ ## Running
45
+
46
+ ```
47
+ npm run test:bootstrap # one-shot
48
+ npm run test:bootstrap:watch # iteration mode
49
+ ```
50
+
51
+ Runs in ~2 seconds. Produces 80+ tests, 40+ trail snapshots.
52
+
53
+ ## Adding a scenario
54
+
55
+ 1. Identify the cell-key: `<platform>/<dash>/<pi>/<settings>/<env>`
56
+ (see `scenarios.ts` for axis values).
57
+
58
+ 2. Write a family test (or extend an existing one):
59
+
60
+ ```ts
61
+ const MY_CELLS = [
62
+ { platform: "win32", dash: "managed", pi: "present-valid",
63
+ settings: "valid", env: "normal" },
64
+ ] as const;
65
+ for (const cell of MY_CELLS) {
66
+ register(cell, "families/my-family.test.ts");
67
+ SKIPPED_SCENARIOS.delete(cellKey(cell));
68
+ }
69
+
70
+ describe("My family", () => {
71
+ it("demonstrates something", async () => {
72
+ await withFakeEnv(
73
+ { platform: "win32", homedir: "C:\\Users\\R",
74
+ fs: fixtures.managedInstall({ homedir: "C:\\Users\\R", platform: "win32" }) },
75
+ (ctx) => {
76
+ const registry = ctx.createRegistry();
77
+ registerDefaultTools(registry, ctx.createStrategyDeps());
78
+ const res = registry.resolve("pi");
79
+ expect(res.ok).toBe(true);
80
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
81
+ },
82
+ );
83
+ });
84
+ });
85
+ ```
86
+
87
+ 3. Add the file to `families/index.ts` so the cube sweep picks up
88
+ its registrations.
89
+
90
+ 4. Run `npm run test:bootstrap -- -u` to write the snapshot.
91
+
92
+ ## Adding a skip
93
+
94
+ Pure skip (no test):
95
+
96
+ ```ts
97
+ // in scenarios-skipped.ts, extend skipReasonFor()
98
+ if (cell.platform === "win32" && cell.env === "spaces-unicode") {
99
+ return "win32 + spaces-unicode: add when a bug reports here";
100
+ }
101
+ ```
102
+
103
+ Skips MUST have a non-empty reason — enforced by `skip()` at runtime.
104
+
105
+ ## Fail-closed invariant
106
+
107
+ `cube.test.ts` fails if any cell is neither registered nor explicitly
108
+ skipped. Adding a new axis value (e.g. a new platform or install
109
+ location) breaks the test until each resulting cell is categorized.
110
+
111
+ Cube shape: 3 platforms × 5 dash-locations × 6 pi-states × 4 settings
112
+ × 3 env = **1080 cells**.
113
+
114
+ Current state: ~30 registered, ~1050 skipped with documented reasons.
115
+
116
+ ## Snapshot stability
117
+
118
+ `normalizePath` rewrites `<HOME>`, `<NPM_ROOT>`, flips separators. This
119
+ makes snapshots stable across macOS/Linux CI. Windows CI snapshots may
120
+ shift marginally when run natively (path-join behavior); if that
121
+ surfaces, add platform-specific snapshot files.
122
+
123
+ ## Downstream handoff
124
+
125
+ - **B1 snapshot** (Windows `npm i -g pi-dashboard` → pi unresolved)
126
+ is the input for `unified-bootstrap-install` (proposal 2). When (2)
127
+ lands, the expected outcome flips from "unresolved" to "resolves
128
+ via managed after bootstrap." Update the snapshot as part of (2)'s
129
+ task list.
130
+
131
+ - **Family L cells** (lock-file scenarios) will be added by
132
+ `single-dashboard-per-home` (proposal 3). That proposal introduces
133
+ a new axis (lock state) not modelled in the current cube.
@@ -0,0 +1,370 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Family A — electron-packaged > A1 — electron-fresh (bundled dashboard, no pi) > resolves nothing for pi (darwin) 1`] = `
4
+ "name: pi
5
+ ok: false
6
+ source: —
7
+ path: —
8
+ tried:
9
+ override no override set
10
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi
11
+ where not found on PATH"
12
+ `;
13
+
14
+ exports[`Family A — electron-packaged > A1 — electron-fresh (bundled dashboard, no pi) > resolves nothing for pi (linux) 1`] = `
15
+ "name: pi
16
+ ok: false
17
+ source: —
18
+ path: —
19
+ tried:
20
+ override no override set
21
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi
22
+ where not found on PATH"
23
+ `;
24
+
25
+ exports[`Family A — electron-packaged > A1 — electron-fresh (bundled dashboard, no pi) > resolves nothing for pi (win32) 1`] = `
26
+ "name: pi
27
+ ok: false
28
+ source: —
29
+ path: —
30
+ tried:
31
+ override no override set
32
+ bare-import cannot resolve @mariozechner/pi-coding-agent/package.json
33
+ bare-import cannot resolve @oh-my-pi/pi-coding-agent/package.json
34
+ managed missing: <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
35
+ managed missing: <HOME>/.pi-dashboard/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js
36
+ npm-global missing: <NPM_ROOT>/@mariozechner/pi-coding-agent/dist/cli.js
37
+ npm-global missing: <NPM_ROOT>/@oh-my-pi/pi-coding-agent/dist/cli.js
38
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi.cmd
39
+ where not found on PATH"
40
+ `;
41
+
42
+ exports[`Family A — electron-packaged > A2 — electron-prewarmed (bundled + managed pi) > resolves pi via managed (darwin) 1`] = `
43
+ "name: pi
44
+ ok: true
45
+ source: managed
46
+ path: <HOME>/.pi-dashboard/node_modules/.bin/pi
47
+ tried:
48
+ override no override set
49
+ managed ok"
50
+ `;
51
+
52
+ exports[`Family A — electron-packaged > A2 — electron-prewarmed (bundled + managed pi) > resolves pi via managed (linux) 1`] = `
53
+ "name: pi
54
+ ok: true
55
+ source: managed
56
+ path: <HOME>/.pi-dashboard/node_modules/.bin/pi
57
+ tried:
58
+ override no override set
59
+ managed ok"
60
+ `;
61
+
62
+ exports[`Family A — electron-packaged > A2 — electron-prewarmed (bundled + managed pi) > resolves pi via managed (win32) 1`] = `
63
+ "name: pi
64
+ ok: true
65
+ source: managed
66
+ path: <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
67
+ tried:
68
+ override no override set
69
+ bare-import cannot resolve @mariozechner/pi-coding-agent/package.json
70
+ bare-import cannot resolve @oh-my-pi/pi-coding-agent/package.json
71
+ managed ok"
72
+ `;
73
+
74
+ exports[`Family A — electron-packaged > A3 — electron + pre-existing global pi > global npm pi takes precedence over managed-bin fallback (linux) 1`] = `
75
+ "name: pi
76
+ ok: true
77
+ source: system
78
+ path: /usr/local/bin/pi
79
+ tried:
80
+ override no override set
81
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi
82
+ where ok"
83
+ `;
84
+
85
+ exports[`Family B — npm-global > B1 — npm-g dash-only (⚠ captures current Windows bug) > pi unresolved on darwin 1`] = `
86
+ "name: pi
87
+ ok: false
88
+ source: —
89
+ path: —
90
+ tried:
91
+ override no override set
92
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi
93
+ where not found on PATH"
94
+ `;
95
+
96
+ exports[`Family B — npm-global > B1 — npm-g dash-only (⚠ captures current Windows bug) > pi unresolved on linux 1`] = `
97
+ "name: pi
98
+ ok: false
99
+ source: —
100
+ path: —
101
+ tried:
102
+ override no override set
103
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi
104
+ where not found on PATH"
105
+ `;
106
+
107
+ exports[`Family B — npm-global > B1 — npm-g dash-only (⚠ captures current Windows bug) > pi unresolved on win32 1`] = `
108
+ "name: pi
109
+ ok: false
110
+ source: —
111
+ path: —
112
+ tried:
113
+ override no override set
114
+ bare-import cannot resolve @mariozechner/pi-coding-agent/package.json
115
+ bare-import cannot resolve @oh-my-pi/pi-coding-agent/package.json
116
+ managed missing: <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
117
+ managed missing: <HOME>/.pi-dashboard/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js
118
+ npm-global missing: <NPM_ROOT>/@mariozechner/pi-coding-agent/dist/cli.js
119
+ npm-global missing: <NPM_ROOT>/@oh-my-pi/pi-coding-agent/dist/cli.js
120
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi.cmd
121
+ where not found on PATH"
122
+ `;
123
+
124
+ exports[`Family B — npm-global > B2 — npm-g full (pi + openspec via global npm) > pi resolves via npm-global on darwin 1`] = `
125
+ "name: pi
126
+ ok: true
127
+ source: system
128
+ path: /usr/local/bin/pi
129
+ tried:
130
+ override no override set
131
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi
132
+ where ok"
133
+ `;
134
+
135
+ exports[`Family B — npm-global > B2 — npm-g full (pi + openspec via global npm) > pi resolves via npm-global on linux 1`] = `
136
+ "name: pi
137
+ ok: true
138
+ source: system
139
+ path: /usr/local/bin/pi
140
+ tried:
141
+ override no override set
142
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi
143
+ where ok"
144
+ `;
145
+
146
+ exports[`Family B — npm-global > B2 — npm-g full (pi + openspec via global npm) > pi resolves via npm-global on win32 (with node.exe toArgv) 1`] = `
147
+ "name: pi
148
+ ok: true
149
+ source: npm-global
150
+ path: <NPM_ROOT>/@mariozechner/pi-coding-agent/dist/cli.js
151
+ tried:
152
+ override no override set
153
+ bare-import cannot resolve @mariozechner/pi-coding-agent/package.json
154
+ bare-import cannot resolve @oh-my-pi/pi-coding-agent/package.json
155
+ managed missing: <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
156
+ managed missing: <HOME>/.pi-dashboard/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js
157
+ npm-global ok
158
+ argv:
159
+ - C:/Program Files/nodejs/node.exe
160
+ - <NPM_ROOT>/@mariozechner/pi-coding-agent/dist/cli.js"
161
+ `;
162
+
163
+ exports[`Family B — npm-global > B3 — npm-g pi-installed-first (bridge needs registration) > settings.json present but lacks bridge entry (linux) 1`] = `
164
+ "settings-delta:
165
+ added:
166
+ (none)
167
+ removed:
168
+ (none)
169
+ preserved:
170
+ (none)"
171
+ `;
172
+
173
+ exports[`Family C — dev monorepo > C1 — posix (managed/where chain, no bare-import for pi) > pi chain runs on darwin 1`] = `
174
+ "name: pi
175
+ ok: false
176
+ source: —
177
+ path: —
178
+ tried:
179
+ override no override set
180
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi
181
+ where not found on PATH"
182
+ `;
183
+
184
+ exports[`Family C — dev monorepo > C1 — posix (managed/where chain, no bare-import for pi) > pi chain runs on linux 1`] = `
185
+ "name: pi
186
+ ok: false
187
+ source: —
188
+ path: —
189
+ tried:
190
+ override no override set
191
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi
192
+ where not found on PATH"
193
+ `;
194
+
195
+ exports[`Family C — dev monorepo > C2 — win32 (bare-import from workspace) > resolves pi via workspace bare-import 1`] = `
196
+ "name: pi
197
+ ok: true
198
+ source: bare-import
199
+ path: dist/cli.js
200
+ tried:
201
+ override no override set
202
+ bare-import ok"
203
+ `;
204
+
205
+ exports[`Family D — overrides > D1 — override-valid: pi resolves via override 1`] = `
206
+ "name: pi
207
+ ok: true
208
+ source: override
209
+ path: /opt/custom/bin/pi
210
+ tried:
211
+ override ok"
212
+ `;
213
+
214
+ exports[`Family D — overrides > D2 — override-invalid: path doesn't exist, chain falls through 1`] = `
215
+ "name: pi
216
+ ok: true
217
+ source: managed
218
+ path: <HOME>/.pi-dashboard/node_modules/.bin/pi
219
+ tried:
220
+ override invalid: path does not exist: /nonexistent/broken/pi
221
+ managed ok"
222
+ `;
223
+
224
+ exports[`Family E — stale / partial > E1 — stale managed pi (old version) > current strategies resolve without version gating (darwin) 1`] = `
225
+ "name: pi
226
+ ok: true
227
+ source: managed
228
+ path: <HOME>/.pi-dashboard/node_modules/.bin/pi
229
+ tried:
230
+ override no override set
231
+ managed ok"
232
+ `;
233
+
234
+ exports[`Family E — stale / partial > E1 — stale managed pi (old version) > current strategies resolve without version gating (linux) 1`] = `
235
+ "name: pi
236
+ ok: true
237
+ source: managed
238
+ path: <HOME>/.pi-dashboard/node_modules/.bin/pi
239
+ tried:
240
+ override no override set
241
+ managed ok"
242
+ `;
243
+
244
+ exports[`Family E — stale / partial > E1 — stale managed pi (old version) > current strategies resolve without version gating (win32) 1`] = `
245
+ "name: pi
246
+ ok: true
247
+ source: managed
248
+ path: <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
249
+ tried:
250
+ override no override set
251
+ bare-import cannot resolve @mariozechner/pi-coding-agent/package.json
252
+ bare-import cannot resolve @oh-my-pi/pi-coding-agent/package.json
253
+ managed ok"
254
+ `;
255
+
256
+ exports[`Family E — stale / partial > E2 — partial managed install (package.json, no dist) > strategy skips when entry file absent (linux) 1`] = `
257
+ "name: pi
258
+ ok: false
259
+ source: —
260
+ path: —
261
+ tried:
262
+ override no override set
263
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi
264
+ where not found on PATH"
265
+ `;
266
+
267
+ exports[`Family E — stale / partial > E2 — partial managed install (package.json, no dist) > strategy skips when entry file absent (win32) 1`] = `
268
+ "name: pi
269
+ ok: false
270
+ source: —
271
+ path: —
272
+ tried:
273
+ override no override set
274
+ bare-import cannot resolve @mariozechner/pi-coding-agent/package.json
275
+ bare-import cannot resolve @oh-my-pi/pi-coding-agent/package.json
276
+ managed missing: <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
277
+ managed missing: <HOME>/.pi-dashboard/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js
278
+ npm-global missing: <NPM_ROOT>/@mariozechner/pi-coding-agent/dist/cli.js
279
+ npm-global missing: <NPM_ROOT>/@oh-my-pi/pi-coding-agent/dist/cli.js
280
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi.cmd
281
+ where not found on PATH"
282
+ `;
283
+
284
+ exports[`Family F — cwd variants > F1 — resolves normally with Program Files (x86) cwd (win32) 1`] = `
285
+ "name: pi
286
+ ok: true
287
+ source: managed
288
+ path: <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
289
+ tried:
290
+ override no override set
291
+ bare-import cannot resolve @mariozechner/pi-coding-agent/package.json
292
+ bare-import cannot resolve @oh-my-pi/pi-coding-agent/package.json
293
+ managed ok"
294
+ `;
295
+
296
+ exports[`Family F — cwd variants > F1 — resolves normally with spaces in cwd (linux) 1`] = `
297
+ "name: pi
298
+ ok: true
299
+ source: managed
300
+ path: <HOME>/.pi-dashboard/node_modules/.bin/pi
301
+ tried:
302
+ override no override set
303
+ managed ok"
304
+ `;
305
+
306
+ exports[`Family F — cwd variants > F2 — resolves with Greek/Cyrillic/emoji in cwd 1`] = `
307
+ "name: pi
308
+ ok: true
309
+ source: managed
310
+ path: <HOME>/.pi-dashboard/node_modules/.bin/pi
311
+ tried:
312
+ override no override set
313
+ managed ok"
314
+ `;
315
+
316
+ exports[`Family G — Windows specifics > G1 — pi.cmd resolved + toArgv prepends node.exe (no-cmd-flash) 1`] = `
317
+ "name: pi
318
+ ok: true
319
+ source: managed
320
+ path: <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
321
+ tried:
322
+ override no override set
323
+ bare-import cannot resolve @mariozechner/pi-coding-agent/package.json
324
+ bare-import cannot resolve @oh-my-pi/pi-coding-agent/package.json
325
+ managed ok
326
+ argv:
327
+ - C:/Program Files/nodejs/node.exe
328
+ - <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js"
329
+ `;
330
+
331
+ exports[`Family G — Windows specifics > G2 — npm-g at %APPDATA%\\Roaming\\npm (argv prepends node.exe) 1`] = `
332
+ "name: pi
333
+ ok: true
334
+ source: npm-global
335
+ path: <NPM_ROOT>/@mariozechner/pi-coding-agent/dist/cli.js
336
+ tried:
337
+ override no override set
338
+ bare-import cannot resolve @mariozechner/pi-coding-agent/package.json
339
+ bare-import cannot resolve @oh-my-pi/pi-coding-agent/package.json
340
+ managed missing: <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
341
+ managed missing: <HOME>/.pi-dashboard/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js
342
+ npm-global ok
343
+ argv:
344
+ - C:/Program Files/nodejs/node.exe
345
+ - <NPM_ROOT>/@mariozechner/pi-coding-agent/dist/cli.js"
346
+ `;
347
+
348
+ exports[`Family G — Windows specifics > G4 — node.exe at C:\\Program Files\\nodejs\\node.exe 1`] = `
349
+ "name: node
350
+ ok: true
351
+ source: system
352
+ path: C:/Program Files/nodejs/node.exe
353
+ tried:
354
+ override no override set
355
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/node.cmd
356
+ where ok
357
+ argv:
358
+ - C:/Program Files/nodejs/node.exe"
359
+ `;
360
+
361
+ exports[`Family J — minimal PATH > J1 — GUI-launched minimal PATH: pi does NOT resolve on posix (limitation) 1`] = `
362
+ "name: pi
363
+ ok: false
364
+ source: —
365
+ path: —
366
+ tried:
367
+ override no override set
368
+ managed missing: <HOME>/.pi-dashboard/node_modules/.bin/pi
369
+ where not found on PATH"
370
+ `;