@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,157 @@
1
+ /**
2
+ * Family B — npm-global install scenarios.
3
+ *
4
+ * B1: npm-g dash only (⚠ Windows bug) — pi UNRESOLVED.
5
+ * B2: npm-g full — pi + openspec via global npm.
6
+ * B3: npm-g pi-installed-first — pi present, settings lacks bridge;
7
+ * dashboard registers on boot (delta assertion).
8
+ *
9
+ * B1 is the key scenario: current behavior when a Windows user runs
10
+ * `npm i -g pi-dashboard` without first installing pi. This snapshot
11
+ * captures the broken state; `unified-bootstrap-install` will flip it.
12
+ */
13
+ import { describe, expect, it } from "vitest";
14
+ import { withFakeEnv, layer } from "../harness.js";
15
+ import { registerDefaultTools } from "../../../tool-registry/definitions.js";
16
+ import * as fixtures from "../fixtures/index.js";
17
+ import { snapshotTrail, snapshotSettingsDelta } from "../assertions.js";
18
+ import { register, SKIPPED_SCENARIOS, cellKey } from "../scenarios.js";
19
+
20
+ const B1 = [
21
+ { platform: "linux", dash: "npm-g", pi: "absent", settings: "missing", env: "normal" },
22
+ { platform: "darwin", dash: "npm-g", pi: "absent", settings: "missing", env: "normal" },
23
+ { platform: "win32", dash: "npm-g", pi: "absent", settings: "missing", env: "normal" },
24
+ ] as const;
25
+ const B2 = [
26
+ { platform: "linux", dash: "npm-g", pi: "present-valid", settings: "valid", env: "normal" },
27
+ { platform: "darwin", dash: "npm-g", pi: "present-valid", settings: "valid", env: "normal" },
28
+ { platform: "win32", dash: "npm-g", pi: "present-valid", settings: "valid", env: "normal" },
29
+ ] as const;
30
+ const B3 = [
31
+ { platform: "linux", dash: "npm-g", pi: "present-no-ext", settings: "empty", env: "normal" },
32
+ ] as const;
33
+ for (const cell of [...B1, ...B2, ...B3]) {
34
+ register(cell, "families/b-npm-global.test.ts");
35
+ SKIPPED_SCENARIOS.delete(cellKey(cell));
36
+ }
37
+
38
+ describe("Family B — npm-global", () => {
39
+ describe("B1 — npm-g dash-only (⚠ captures current Windows bug)", () => {
40
+ it.each(["linux", "darwin", "win32"] as const)(
41
+ "pi unresolved on %s",
42
+ async (platform) => {
43
+ const homedir = platform === "win32" ? "C:\\Users\\R" : "/home/r";
44
+ await withFakeEnv(
45
+ {
46
+ platform,
47
+ homedir,
48
+ env: platform === "win32"
49
+ ? { PATH: "C:\\Users\\R\\AppData\\Roaming\\npm" }
50
+ : { PATH: "/usr/local/bin" },
51
+ fs: platform === "win32"
52
+ // On Windows we ship dash only — no pi, no openspec.
53
+ ? fixtures.npmGlobalWindowsAppData(homedir, {
54
+ dashboard: true,
55
+ pi: false,
56
+ openspec: false,
57
+ })
58
+ : fixtures.npmGlobalUnix({ pi: false, openspec: false }),
59
+ },
60
+ (ctx) => {
61
+ const registry = ctx.createRegistry();
62
+ registerDefaultTools(registry, ctx.createStrategyDeps());
63
+ const res = registry.resolve("pi");
64
+ expect(res.ok).toBe(false);
65
+ // FIXED-BY: unified-bootstrap-install.
66
+ // When that proposal lands, update the snapshot: pi should
67
+ // resolve via managed after first-run bootstrap.
68
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
69
+ },
70
+ );
71
+ },
72
+ );
73
+ });
74
+
75
+ describe("B2 — npm-g full (pi + openspec via global npm)", () => {
76
+ it.each(["linux", "darwin"] as const)(
77
+ "pi resolves via npm-global on %s",
78
+ async (platform) => {
79
+ const homedir = "/home/r";
80
+ await withFakeEnv(
81
+ {
82
+ platform,
83
+ homedir,
84
+ env: { PATH: "/usr/local/bin" },
85
+ fs: fixtures.npmGlobalUnix({}),
86
+ },
87
+ (ctx) => {
88
+ // On Unix, the pi strategy chain only has override → managed-bin
89
+ // → where. `where` finds /usr/local/bin/pi. So source === "system"
90
+ // not "npm-global" — the current strategy chain doesn't know
91
+ // about npm-g on Unix at the executor level.
92
+ const registry = ctx.createRegistry();
93
+ registerDefaultTools(registry, ctx.createStrategyDeps());
94
+ const res = registry.resolve("pi");
95
+ expect(res.ok).toBe(true);
96
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
97
+ },
98
+ );
99
+ },
100
+ );
101
+
102
+ it("pi resolves via npm-global on win32 (with node.exe toArgv)", async () => {
103
+ const homedir = "C:\\Users\\R";
104
+ await withFakeEnv(
105
+ {
106
+ platform: "win32",
107
+ homedir,
108
+ env: {
109
+ PATH: "C:\\Users\\R\\AppData\\Roaming\\npm;C:\\Program Files\\nodejs",
110
+ },
111
+ fs: layer(
112
+ fixtures.npmGlobalWindowsAppData(homedir, { dashboard: true }),
113
+ { "C:\\Program Files\\nodejs\\node.exe": "\x7fELF" },
114
+ ),
115
+ },
116
+ (ctx) => {
117
+ const registry = ctx.createRegistry();
118
+ registerDefaultTools(registry, ctx.createStrategyDeps());
119
+ const executor = registry.resolveExecutor("pi");
120
+ expect(executor.ok).toBe(true);
121
+ expect(executor.source).toBe("npm-global");
122
+ // No-cmd-flash invariant for npm-g on Windows: argv[0] is
123
+ // node.exe, argv[1] is cli.js. The snapshot locks this in.
124
+ expect(executor.argv[0]).toBe("C:\\Program Files\\nodejs\\node.exe");
125
+ expect(snapshotTrail(executor, ctx)).toMatchSnapshot();
126
+ },
127
+ );
128
+ });
129
+ });
130
+
131
+ describe("B3 — npm-g pi-installed-first (bridge needs registration)", () => {
132
+ it("settings.json present but lacks bridge entry (linux)", async () => {
133
+ const homedir = "/home/r";
134
+ await withFakeEnv(
135
+ {
136
+ platform: "linux",
137
+ homedir,
138
+ fs: layer(
139
+ fixtures.npmGlobalUnix({}),
140
+ fixtures.settingsJson({ homedir, platform: "linux", packages: [] }),
141
+ ),
142
+ },
143
+ (ctx) => {
144
+ // Input-side assertion: the fixture correctly produces a
145
+ // settings.json without the bridge entry. Full round-trip
146
+ // (calling `registerBridgeExtension` and observing the
147
+ // mutation) requires `bridge-register` to accept an
148
+ // injectable fs — future task, cross-proposal.
149
+ const before = ctx.readSettings() as { packages?: string[] } | null;
150
+ expect(before).toEqual({ packages: [] });
151
+ const after = before; // no mutation via harness
152
+ expect(snapshotSettingsDelta(before, after, ctx)).toMatchSnapshot();
153
+ },
154
+ );
155
+ });
156
+ });
157
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Family C — dev monorepo scenarios.
3
+ *
4
+ * C1: mac/linux — bare-import resolves pi via workspace node_modules.
5
+ * C2: win32 — same, plus node.exe toArgv invariant.
6
+ *
7
+ * The bare-import strategy only runs for pi on win32 (see pi's
8
+ * winStrategies in definitions.ts). On Unix, pi on Unix still resolves
9
+ * via managed-bin → where. So the interesting "dev monorepo resolves
10
+ * via bare-import" path is Windows-only today. C1 asserts what Unix
11
+ * actually does (no bare-import in its chain).
12
+ */
13
+ import { describe, expect, it } from "vitest";
14
+ import { withFakeEnv, layer } from "../harness.js";
15
+ import { registerDefaultTools } from "../../../tool-registry/definitions.js";
16
+ import * as fixtures from "../fixtures/index.js";
17
+ import { snapshotTrail } from "../assertions.js";
18
+ import { register, SKIPPED_SCENARIOS, cellKey } from "../scenarios.js";
19
+
20
+ // C1 posix — dev dash, pi present via workspace; settings typically empty
21
+ // at dev time.
22
+ const C1 = [
23
+ { platform: "linux", dash: "dev", pi: "present-valid", settings: "empty", env: "normal" },
24
+ { platform: "darwin", dash: "dev", pi: "present-valid", settings: "empty", env: "normal" },
25
+ ] as const;
26
+
27
+ // C2 — Windows dev layout is already skipped in scenarios-skipped.ts
28
+ // with reason "rare; capture if reported". We still add a test case
29
+ // that registers the cell so the bare-import + toArgv invariant is
30
+ // locked in — developers on Windows do exist.
31
+ const C2 = [
32
+ { platform: "win32", dash: "dev", pi: "present-valid", settings: "empty", env: "normal" },
33
+ ] as const;
34
+
35
+ for (const cell of [...C1, ...C2]) {
36
+ register(cell, "families/c-dev-monorepo.test.ts");
37
+ SKIPPED_SCENARIOS.delete(cellKey(cell));
38
+ }
39
+
40
+ describe("Family C — dev monorepo", () => {
41
+ describe("C1 — posix (managed/where chain, no bare-import for pi)", () => {
42
+ it.each(["linux", "darwin"] as const)(
43
+ "pi chain runs on %s",
44
+ async (platform) => {
45
+ const root = "/home/dev/pi-agent-dashboard";
46
+ const homedir = "/home/dev";
47
+ await withFakeEnv(
48
+ {
49
+ platform,
50
+ homedir,
51
+ cwd: root,
52
+ env: { PATH: "/usr/bin" },
53
+ fs: fixtures.devMonorepo({ root, platform }),
54
+ },
55
+ (ctx) => {
56
+ const registry = ctx.createRegistry();
57
+ registerDefaultTools(registry, ctx.createStrategyDeps());
58
+ const res = registry.resolve("pi");
59
+ // Nothing on PATH, nothing in ~/.pi-dashboard — unresolved.
60
+ // The workspace node_modules is only reachable via
61
+ // bare-import, which isn't in pi's Unix chain.
62
+ expect(res.ok).toBe(false);
63
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
64
+ },
65
+ );
66
+ },
67
+ );
68
+ });
69
+
70
+ describe("C2 — win32 (bare-import from workspace)", () => {
71
+ it("resolves pi via workspace bare-import", async () => {
72
+ const root = "C:\\dev\\pi-agent-dashboard";
73
+ const homedir = "C:\\Users\\Dev";
74
+ await withFakeEnv(
75
+ {
76
+ platform: "win32",
77
+ homedir,
78
+ cwd: root,
79
+ env: { PATH: "C:\\Windows\\System32" },
80
+ fs: layer(fixtures.devMonorepo({ root, platform: "win32" })),
81
+ },
82
+ (ctx) => {
83
+ // Build deps with a resolveModule anchor at the workspace
84
+ // root so bareImportCliStrategy finds pi-coding-agent.
85
+ const baseDeps = ctx.createStrategyDeps();
86
+ const rootAnchor = `${root}\\packages\\shared\\src\\index.ts`;
87
+ const deps = {
88
+ ...baseDeps,
89
+ resolveModule: (id: string, _from: string) =>
90
+ baseDeps.resolveModule(id, rootAnchor),
91
+ };
92
+ const registry = ctx.createRegistry();
93
+ registerDefaultTools(registry, deps);
94
+ const res = registry.resolve("pi");
95
+ expect(res.ok).toBe(true);
96
+ expect(res.source).toBe("bare-import");
97
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
98
+ },
99
+ );
100
+ });
101
+ });
102
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Family D — overrides.
3
+ *
4
+ * D1: override-valid — pi resolves via override, source = "override".
5
+ * D2: override-invalid — broken path; falls through to next strategy.
6
+ *
7
+ * Overrides override platform. We test on linux; Windows is covered
8
+ * structurally because overrideStrategy is first in every chain.
9
+ */
10
+ import { describe, expect, it } from "vitest";
11
+ import { withFakeEnv } from "../harness.js";
12
+ import { registerDefaultTools } from "../../../tool-registry/definitions.js";
13
+ import * as fixtures from "../fixtures/index.js";
14
+ import { snapshotTrail } from "../assertions.js";
15
+ import { register, SKIPPED_SCENARIOS, cellKey } from "../scenarios.js";
16
+
17
+ // Overrides are orthogonal to the cube's pi-state axis — treat as
18
+ // "present-valid" scenarios for taxonomy purposes.
19
+ const D = [
20
+ { platform: "linux", dash: "managed", pi: "present-valid", settings: "empty", env: "normal" },
21
+ { platform: "darwin", dash: "managed", pi: "present-valid", settings: "empty", env: "normal" },
22
+ ] as const;
23
+ for (const cell of D) {
24
+ register(cell, "families/d-overrides.test.ts");
25
+ SKIPPED_SCENARIOS.delete(cellKey(cell));
26
+ }
27
+
28
+ describe("Family D — overrides", () => {
29
+ it("D1 — override-valid: pi resolves via override", async () => {
30
+ const homedir = "/home/r";
31
+ const overridePath = "/opt/custom/bin/pi";
32
+ await withFakeEnv(
33
+ {
34
+ platform: "linux",
35
+ homedir,
36
+ fs: {
37
+ [overridePath]: "#!/bin/sh\nexec custom-pi",
38
+ // Also managed, to prove override wins.
39
+ ...fixtures.managedInstall({ homedir, platform: "linux" }),
40
+ },
41
+ overrides: { pi: overridePath },
42
+ },
43
+ (ctx) => {
44
+ const registry = ctx.createRegistry();
45
+ registerDefaultTools(registry, ctx.createStrategyDeps());
46
+ const res = registry.resolve("pi");
47
+ expect(res.ok).toBe(true);
48
+ expect(res.source).toBe("override");
49
+ expect(res.path).toBe(overridePath);
50
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
51
+ },
52
+ );
53
+ });
54
+
55
+ it("D2 — override-invalid: path doesn't exist, chain falls through", async () => {
56
+ const homedir = "/home/r";
57
+ await withFakeEnv(
58
+ {
59
+ platform: "linux",
60
+ homedir,
61
+ fs: fixtures.managedInstall({ homedir, platform: "linux" }),
62
+ overrides: { pi: "/nonexistent/broken/pi" },
63
+ },
64
+ (ctx) => {
65
+ const registry = ctx.createRegistry();
66
+ registerDefaultTools(registry, ctx.createStrategyDeps());
67
+ const res = registry.resolve("pi");
68
+ // Override strategy returns `invalid: ...` as reason; next
69
+ // strategy (managed-bin) succeeds.
70
+ expect(res.ok).toBe(true);
71
+ expect(res.source).toBe("managed");
72
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
73
+ },
74
+ );
75
+ });
76
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Family E — stale / partial managed installs.
3
+ *
4
+ * E1: stale-managed — managed pi version 0.0.1. TODAY the strategies
5
+ * don't do version gating; resolution succeeds. Snapshot locks in
6
+ * current behavior so version-skew detection (from proposal 2)
7
+ * will force a snapshot update.
8
+ * E2: managed-partial — only package.json, no dist/cli.js. Strategy
9
+ * returns not-found; next strategy runs.
10
+ */
11
+ import { describe, expect, it } from "vitest";
12
+ import { withFakeEnv } from "../harness.js";
13
+ import { registerDefaultTools } from "../../../tool-registry/definitions.js";
14
+ import * as fixtures from "../fixtures/index.js";
15
+ import { snapshotTrail } from "../assertions.js";
16
+ import { register, SKIPPED_SCENARIOS, cellKey } from "../scenarios.js";
17
+
18
+ const E1 = [
19
+ // present-stale-ext as cube axis for "installed but bridge registration is stale"
20
+ { platform: "linux", dash: "electron", pi: "present-stale-ext", settings: "valid", env: "normal" },
21
+ { platform: "darwin", dash: "electron", pi: "present-stale-ext", settings: "valid", env: "normal" },
22
+ { platform: "win32", dash: "electron", pi: "present-stale-ext", settings: "valid", env: "normal" },
23
+ ] as const;
24
+ // E2 maps to "malformed" pi-state — the managed install is present but
25
+ // broken (incomplete).
26
+ const E2 = [
27
+ { platform: "linux", dash: "electron", pi: "malformed", settings: "empty", env: "normal" },
28
+ { platform: "win32", dash: "electron", pi: "malformed", settings: "empty", env: "normal" },
29
+ ] as const;
30
+ for (const cell of [...E1, ...E2]) {
31
+ register(cell, "families/e-stale-partial.test.ts");
32
+ SKIPPED_SCENARIOS.delete(cellKey(cell));
33
+ }
34
+
35
+ describe("Family E — stale / partial", () => {
36
+ describe("E1 — stale managed pi (old version)", () => {
37
+ it.each(["linux", "darwin", "win32"] as const)(
38
+ "current strategies resolve without version gating (%s)",
39
+ async (platform) => {
40
+ const homedir = platform === "win32" ? "C:\\Users\\R" : "/home/r";
41
+ await withFakeEnv(
42
+ {
43
+ platform,
44
+ homedir,
45
+ fs: fixtures.managedInstall({
46
+ homedir,
47
+ platform,
48
+ pi: { version: "0.0.1" }, // very old
49
+ }),
50
+ },
51
+ (ctx) => {
52
+ const registry = ctx.createRegistry();
53
+ registerDefaultTools(registry, ctx.createStrategyDeps());
54
+ const res = registry.resolve("pi");
55
+ expect(res.ok).toBe(true);
56
+ // NOTE: version-skew detection lands in
57
+ // `unified-bootstrap-install` — resolution still
58
+ // succeeds; the detection happens downstream in
59
+ // dependency-detector. Snapshot will shift when that
60
+ // detection is added to trail.
61
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
62
+ },
63
+ );
64
+ },
65
+ );
66
+ });
67
+
68
+ describe("E2 — partial managed install (package.json, no dist)", () => {
69
+ it.each(["linux", "win32"] as const)(
70
+ "strategy skips when entry file absent (%s)",
71
+ async (platform) => {
72
+ const homedir = platform === "win32" ? "C:\\Users\\R" : "/home/r";
73
+ await withFakeEnv(
74
+ {
75
+ platform,
76
+ homedir,
77
+ fs: fixtures.managedInstall({
78
+ homedir,
79
+ platform,
80
+ piPartial: true, // package.json only, no dist/cli.js, no .bin shim
81
+ }),
82
+ },
83
+ (ctx) => {
84
+ const registry = ctx.createRegistry();
85
+ registerDefaultTools(registry, ctx.createStrategyDeps());
86
+ const res = registry.resolve("pi");
87
+ expect(res.ok).toBe(false);
88
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
89
+ },
90
+ );
91
+ },
92
+ );
93
+ });
94
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Family F — cwd variants.
3
+ *
4
+ * F1: cwd-with-spaces — cwd contains spaces. Resolution unaffected.
5
+ * F2: cwd-unicode — cwd contains non-ASCII. Resolution unaffected.
6
+ *
7
+ * Today, cwd is NOT used by any strategy in the chain. Asserting that
8
+ * invariant via snapshots means if someone later adds cwd-sensitive
9
+ * resolution (e.g. workspace-local binaries), the change surfaces
10
+ * loudly in these snapshots.
11
+ */
12
+ import { describe, expect, it } from "vitest";
13
+ import { withFakeEnv } from "../harness.js";
14
+ import { registerDefaultTools } from "../../../tool-registry/definitions.js";
15
+ import * as fixtures from "../fixtures/index.js";
16
+ import { snapshotTrail } from "../assertions.js";
17
+ import { register, SKIPPED_SCENARIOS, cellKey } from "../scenarios.js";
18
+
19
+ const F = [
20
+ { platform: "linux", dash: "managed", pi: "present-valid", settings: "empty", env: "spaces-unicode" },
21
+ { platform: "win32", dash: "managed", pi: "present-valid", settings: "empty", env: "spaces-unicode" },
22
+ ] as const;
23
+ for (const cell of F) {
24
+ register(cell, "families/f-cwd-variants.test.ts");
25
+ SKIPPED_SCENARIOS.delete(cellKey(cell));
26
+ }
27
+
28
+ describe("Family F — cwd variants", () => {
29
+ it("F1 — resolves normally with spaces in cwd (linux)", async () => {
30
+ const homedir = "/home/r";
31
+ await withFakeEnv(
32
+ {
33
+ platform: "linux",
34
+ homedir,
35
+ cwd: "/home/r/My Project With Spaces",
36
+ fs: fixtures.managedInstall({ homedir, platform: "linux" }),
37
+ },
38
+ (ctx) => {
39
+ const registry = ctx.createRegistry();
40
+ registerDefaultTools(registry, ctx.createStrategyDeps());
41
+ const res = registry.resolve("pi");
42
+ expect(res.ok).toBe(true);
43
+ expect(res.source).toBe("managed");
44
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
45
+ },
46
+ );
47
+ });
48
+
49
+ it("F1 — resolves normally with Program Files (x86) cwd (win32)", async () => {
50
+ const homedir = "C:\\Users\\R";
51
+ await withFakeEnv(
52
+ {
53
+ platform: "win32",
54
+ homedir,
55
+ cwd: "C:\\Program Files (x86)\\Pi Dashboard",
56
+ fs: fixtures.managedInstall({ homedir, platform: "win32" }),
57
+ },
58
+ (ctx) => {
59
+ const registry = ctx.createRegistry();
60
+ registerDefaultTools(registry, ctx.createStrategyDeps());
61
+ const res = registry.resolve("pi");
62
+ expect(res.ok).toBe(true);
63
+ expect(res.source).toBe("managed");
64
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
65
+ },
66
+ );
67
+ });
68
+
69
+ it("F2 — resolves with Greek/Cyrillic/emoji in cwd", async () => {
70
+ const homedir = "/home/r";
71
+ await withFakeEnv(
72
+ {
73
+ platform: "linux",
74
+ homedir,
75
+ cwd: "/home/r/πρότζεκτ_тест_🚀",
76
+ fs: fixtures.managedInstall({ homedir, platform: "linux" }),
77
+ },
78
+ (ctx) => {
79
+ const registry = ctx.createRegistry();
80
+ registerDefaultTools(registry, ctx.createStrategyDeps());
81
+ const res = registry.resolve("pi");
82
+ expect(res.ok).toBe(true);
83
+ expect(snapshotTrail(res, ctx)).toMatchSnapshot();
84
+ },
85
+ );
86
+ });
87
+ });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Family G — Windows specifics.
3
+ *
4
+ * G1: win-cmd-shim — pi.cmd found; `toArgv` MUST prepend node.exe.
5
+ * G2: win-appdata-roaming — npm-g installed at %APPDATA%\Roaming\npm.
6
+ * G3: win-programfiles-cwd — cwd under "C:\Program Files (x86)\..."
7
+ * (covered in F1-win; add a G-variant with
8
+ * pi resolution via npm-g).
9
+ * G4: win-programfiles-node — node.exe at "C:\Program Files\nodejs".
10
+ */
11
+ import { describe, expect, it } from "vitest";
12
+ import { withFakeEnv, layer } from "../harness.js";
13
+ import { registerDefaultTools } from "../../../tool-registry/definitions.js";
14
+ import * as fixtures from "../fixtures/index.js";
15
+ import { snapshotTrail } from "../assertions.js";
16
+ import { register, SKIPPED_SCENARIOS, cellKey } from "../scenarios.js";
17
+
18
+ // All Family G cells are win32-only.
19
+ const G = [
20
+ // G1 is already covered by B2 (npm-g on win32); this family focuses
21
+ // on specific layout variants.
22
+ { platform: "win32", dash: "managed", pi: "present-valid", settings: "valid", env: "normal" },
23
+ { platform: "win32", dash: "npm-g", pi: "present-valid", settings: "valid", env: "normal" },
24
+ ] as const;
25
+ for (const cell of G) {
26
+ register(cell, "families/g-windows-specifics.test.ts");
27
+ SKIPPED_SCENARIOS.delete(cellKey(cell));
28
+ }
29
+
30
+ describe("Family G — Windows specifics", () => {
31
+ it("G1 — pi.cmd resolved + toArgv prepends node.exe (no-cmd-flash)", async () => {
32
+ // Managed install with pi-coding-agent at a real module path, NOT
33
+ // the .bin/pi.cmd shim. This forces resolution through
34
+ // managedModuleStrategy (matches `@mariozechner/pi-coding-agent/
35
+ // dist/cli.js`), which is a Node script — `toArgv` prepends
36
+ // node.exe. That's the no-cmd-flash invariant.
37
+ const homedir = "C:\\Users\\R";
38
+ await withFakeEnv(
39
+ {
40
+ platform: "win32",
41
+ homedir,
42
+ env: { PATH: "C:\\Program Files\\nodejs" },
43
+ fs: layer(
44
+ fixtures.managedInstall({ homedir, platform: "win32" }),
45
+ {
46
+ // node.exe must be resolvable for toArgv to prepend it.
47
+ "C:\\Program Files\\nodejs\\node.exe": "\x7fELF",
48
+ },
49
+ ),
50
+ },
51
+ (ctx) => {
52
+ const registry = ctx.createRegistry();
53
+ registerDefaultTools(registry, ctx.createStrategyDeps());
54
+ const executor = registry.resolveExecutor("pi");
55
+ expect(executor.ok).toBe(true);
56
+ expect(executor.path?.endsWith("cli.js")).toBe(true);
57
+ // No-cmd-flash invariant: argv[0] MUST be node.exe, NOT the
58
+ // .cmd shim or cmd.exe. The snapshot locks this in.
59
+ expect(executor.argv[0]).toBe("C:\\Program Files\\nodejs\\node.exe");
60
+ expect(executor.argv).toHaveLength(2);
61
+ expect(snapshotTrail(executor, ctx)).toMatchSnapshot();
62
+ },
63
+ );
64
+ });
65
+
66
+ it("G2 — npm-g at %APPDATA%\\Roaming\\npm (argv prepends node.exe)", async () => {
67
+ const homedir = "C:\\Users\\R";
68
+ await withFakeEnv(
69
+ {
70
+ platform: "win32",
71
+ homedir,
72
+ env: {
73
+ PATH: "C:\\Users\\R\\AppData\\Roaming\\npm;C:\\Program Files\\nodejs",
74
+ APPDATA: "C:\\Users\\R\\AppData\\Roaming",
75
+ },
76
+ fs: layer(
77
+ fixtures.npmGlobalWindowsAppData(homedir, { dashboard: false }),
78
+ { "C:\\Program Files\\nodejs\\node.exe": "\x7fELF" },
79
+ ),
80
+ },
81
+ (ctx) => {
82
+ const registry = ctx.createRegistry();
83
+ registerDefaultTools(registry, ctx.createStrategyDeps());
84
+ const executor = registry.resolveExecutor("pi");
85
+ expect(executor.ok).toBe(true);
86
+ expect(executor.source).toBe("npm-global");
87
+ // Same no-cmd-flash invariant: even for npm-g, argv routes
88
+ // through node.exe.
89
+ expect(executor.argv[0]).toBe("C:\\Program Files\\nodejs\\node.exe");
90
+ expect(snapshotTrail(executor, ctx)).toMatchSnapshot();
91
+ },
92
+ );
93
+ });
94
+
95
+ it("G3 — cwd under Program Files (x86) does not affect resolution", async () => {
96
+ // Spec requires this cell tested; covered structurally by F1-win,
97
+ // but a dedicated block documents the invariant alongside the
98
+ // other G cells.
99
+ const homedir = "C:\\Users\\R";
100
+ await withFakeEnv(
101
+ {
102
+ platform: "win32",
103
+ homedir,
104
+ cwd: "C:\\Program Files (x86)\\Pi Dashboard",
105
+ fs: fixtures.managedInstall({ homedir, platform: "win32" }),
106
+ },
107
+ (ctx) => {
108
+ const registry = ctx.createRegistry();
109
+ registerDefaultTools(registry, ctx.createStrategyDeps());
110
+ const res = registry.resolve("pi");
111
+ expect(res.ok).toBe(true);
112
+ expect(res.source).toBe("managed");
113
+ },
114
+ );
115
+ });
116
+
117
+ it("G4 — node.exe at C:\\Program Files\\nodejs\\node.exe", async () => {
118
+ const homedir = "C:\\Users\\R";
119
+ await withFakeEnv(
120
+ {
121
+ platform: "win32",
122
+ homedir,
123
+ env: { PATH: "C:\\Program Files\\nodejs" },
124
+ fs: layer(
125
+ fixtures.managedInstall({ homedir, platform: "win32" }),
126
+ {
127
+ "C:\\Program Files\\nodejs\\node.exe": "\x7fELF",
128
+ },
129
+ ),
130
+ },
131
+ (ctx) => {
132
+ const registry = ctx.createRegistry();
133
+ registerDefaultTools(registry, ctx.createStrategyDeps());
134
+ const nodeRes = registry.resolveExecutor("node");
135
+ expect(nodeRes.ok).toBe(true);
136
+ expect(nodeRes.path).toBe("C:\\Program Files\\nodejs\\node.exe");
137
+ // Binary-kind tool: argv = [path] (no interpreter prepended).
138
+ expect(nodeRes.argv).toEqual(["C:\\Program Files\\nodejs\\node.exe"]);
139
+ expect(snapshotTrail(nodeRes, ctx)).toMatchSnapshot();
140
+ },
141
+ );
142
+ });
143
+ });