@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
@@ -1,17 +1,53 @@
1
1
  import { describe, it, expect } from "vitest";
2
+ import { buildJitiRegisterUrl, resolveJitiImport } from "../resolve-jiti.js";
3
+
4
+ describe("buildJitiRegisterUrl", () => {
5
+ // Pure function: given a jiti package.json path, return the file:// URL of
6
+ // its register hook. The URL contract is the critical invariant — Node's
7
+ // --import on Windows rejects raw drive-letter paths (parses "C:" as a
8
+ // URL scheme). See change: fix-windows-server-parity.
9
+
10
+ it("returns a file:// URL", () => {
11
+ const url = buildJitiRegisterUrl("/usr/lib/node_modules/@mariozechner/jiti/package.json");
12
+ expect(url.startsWith("file://")).toBe(true);
13
+ });
14
+
15
+ it("URL is parseable by new URL() without throwing", () => {
16
+ const url = buildJitiRegisterUrl("/usr/lib/node_modules/@mariozechner/jiti/package.json");
17
+ expect(() => new URL(url)).not.toThrow();
18
+ });
19
+
20
+ it("points at lib/jiti-register.mjs under the package dir", () => {
21
+ const url = buildJitiRegisterUrl("/usr/lib/node_modules/@mariozechner/jiti/package.json");
22
+ expect(url.endsWith("/lib/jiti-register.mjs")).toBe(true);
23
+ });
24
+
25
+ it("handles Windows drive-letter paths (regression for ERR_UNSUPPORTED_ESM_URL_SCHEME)", () => {
26
+ // This is the exact shape that crashed pre-fix: a raw path with a
27
+ // drive letter was passed to `node --import` and Node parsed "B:" as
28
+ // a URL scheme. A file:// URL sidesteps the parser entirely.
29
+ const url = buildJitiRegisterUrl("B:\\Dev\\Nodejs\\global\\node_modules\\@mariozechner\\jiti\\package.json");
30
+ expect(url.startsWith("file:///")).toBe(true);
31
+ expect(() => new URL(url)).not.toThrow();
32
+ expect(new URL(url).protocol).toBe("file:");
33
+ // The drive letter survives as part of the pathname, not as a protocol
34
+ expect(url.toLowerCase()).toContain("/b:/");
35
+ expect(url.endsWith("/lib/jiti-register.mjs")).toBe(true);
36
+ });
37
+
38
+ });
2
39
 
3
40
  describe("resolveJitiImport", () => {
4
- it("throws with clear error when pi-coding-agent is not resolvable", async () => {
5
- // In test context (vitest, not inside pi's jiti loader),
6
- // peer deps are not resolvable should throw
7
- const { resolveJitiImport } = await import("../resolve-jiti.js");
41
+ // Integration-lite: in vitest context (not inside pi's jiti loader),
42
+ // process.argv[1] points at the test runner, not pi so peer-dep
43
+ // resolution fails and the function throws a helpful error. The
44
+ // URL-contract behavior is covered by buildJitiRegisterUrl above.
8
45
 
46
+ it("throws with clear error when pi-coding-agent is not resolvable", () => {
9
47
  expect(() => resolveJitiImport()).toThrow("Cannot find pi's TypeScript loader");
10
48
  });
11
49
 
12
- it("error message mentions pi-coding-agent", async () => {
13
- const { resolveJitiImport } = await import("../resolve-jiti.js");
14
-
50
+ it("error message mentions pi-coding-agent", () => {
15
51
  expect(() => resolveJitiImport()).toThrow("pi-coding-agent");
16
52
  });
17
53
  });
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createSemaphore } from "../semaphore.js";
3
+
4
+ function defer<T = void>(): { promise: Promise<T>; resolve: (v: T) => void; reject: (e: any) => void } {
5
+ let resolve!: (v: T) => void;
6
+ let reject!: (e: any) => void;
7
+ const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; });
8
+ return { promise, resolve, reject };
9
+ }
10
+
11
+ describe("createSemaphore", () => {
12
+ it("throws when max < 1", () => {
13
+ expect(() => createSemaphore(0)).toThrow();
14
+ expect(() => createSemaphore(-1)).toThrow();
15
+ });
16
+
17
+ it("runs tasks immediately up to the cap", async () => {
18
+ const sem = createSemaphore(2);
19
+ const a = defer<string>();
20
+ const b = defer<string>();
21
+ const pa = sem.run(() => a.promise);
22
+ const pb = sem.run(() => b.promise);
23
+ expect(sem.size()).toBe(2);
24
+ a.resolve("a");
25
+ b.resolve("b");
26
+ expect(await pa).toBe("a");
27
+ expect(await pb).toBe("b");
28
+ expect(sem.size()).toBe(0);
29
+ });
30
+
31
+ it("caps concurrency: third task waits", async () => {
32
+ const sem = createSemaphore(2);
33
+ const a = defer<string>();
34
+ const b = defer<string>();
35
+ const c = defer<string>();
36
+
37
+ let cStarted = false;
38
+ const pa = sem.run(() => a.promise);
39
+ const pb = sem.run(() => b.promise);
40
+ const pc = sem.run(() => { cStarted = true; return c.promise; });
41
+
42
+ // Wait a microtask for queue placement
43
+ await Promise.resolve();
44
+ expect(cStarted).toBe(false);
45
+ expect(sem.size()).toBe(3); // active + queued
46
+
47
+ a.resolve("a");
48
+ await pa;
49
+ // c should now be started
50
+ await Promise.resolve();
51
+ expect(cStarted).toBe(true);
52
+ b.resolve("b");
53
+ c.resolve("c");
54
+ expect(await pb).toBe("b");
55
+ expect(await pc).toBe("c");
56
+ });
57
+
58
+ it("FIFO order of queued tasks", async () => {
59
+ const sem = createSemaphore(1);
60
+ const order: string[] = [];
61
+ const blockers = [defer<void>(), defer<void>(), defer<void>()];
62
+ const ps = blockers.map((d, i) =>
63
+ sem.run(async () => { order.push(`start-${i}`); await d.promise; order.push(`end-${i}`); }),
64
+ );
65
+ await Promise.resolve();
66
+ blockers[0].resolve(); await ps[0];
67
+ blockers[1].resolve(); await ps[1];
68
+ blockers[2].resolve(); await ps[2];
69
+ expect(order).toEqual(["start-0", "end-0", "start-1", "end-1", "start-2", "end-2"]);
70
+ });
71
+
72
+ it("releases slot on reject so queued tasks still run", async () => {
73
+ const sem = createSemaphore(1);
74
+ const failed = sem.run(async () => { throw new Error("boom"); });
75
+ await expect(failed).rejects.toThrow("boom");
76
+ const ok = sem.run(async () => "ok");
77
+ expect(await ok).toBe("ok");
78
+ });
79
+
80
+ it("setMax increases cap and drains queued tasks immediately", async () => {
81
+ const sem = createSemaphore(1);
82
+ const a = defer<void>();
83
+ const b = defer<void>();
84
+ let bStarted = false;
85
+ const pa = sem.run(() => a.promise);
86
+ const pb = sem.run(() => { bStarted = true; return b.promise; });
87
+ await Promise.resolve();
88
+ expect(bStarted).toBe(false);
89
+
90
+ sem.setMax(2);
91
+ await Promise.resolve();
92
+ expect(bStarted).toBe(true);
93
+
94
+ a.resolve(); b.resolve();
95
+ await pa; await pb;
96
+ });
97
+
98
+ it("setMax shrinking does not interrupt in-flight tasks but caps new ones", async () => {
99
+ const sem = createSemaphore(3);
100
+ const a = defer<void>();
101
+ const b = defer<void>();
102
+ const pa = sem.run(() => a.promise);
103
+ const pb = sem.run(() => b.promise);
104
+
105
+ sem.setMax(1);
106
+ const c = defer<void>();
107
+ let cStarted = false;
108
+ const pc = sem.run(() => { cStarted = true; return c.promise; });
109
+ await Promise.resolve();
110
+ expect(cStarted).toBe(false);
111
+
112
+ a.resolve(); b.resolve();
113
+ await pa; await pb;
114
+ await Promise.resolve();
115
+ expect(cStarted).toBe(true);
116
+ c.resolve();
117
+ await pc;
118
+ });
119
+ });
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseSourceKey, sourcesMatch } from "../source-matching.js";
3
+
4
+ describe("parseSourceKey", () => {
5
+ it("parses npm:<name>", () => {
6
+ expect(parseSourceKey("npm:pi-web-access")).toEqual({ kind: "npm", name: "pi-web-access" });
7
+ });
8
+
9
+ it("parses npm:<name>@<version>", () => {
10
+ expect(parseSourceKey("npm:pi-web-access@0.10.6")).toEqual({
11
+ kind: "npm",
12
+ name: "pi-web-access",
13
+ });
14
+ });
15
+
16
+ it("parses scoped npm name without version", () => {
17
+ expect(parseSourceKey("npm:@tintinweb/pi-subagents")).toEqual({
18
+ kind: "npm",
19
+ name: "@tintinweb/pi-subagents",
20
+ });
21
+ });
22
+
23
+ it("parses scoped npm name with version", () => {
24
+ expect(parseSourceKey("npm:@tintinweb/pi-subagents@0.5.2")).toEqual({
25
+ kind: "npm",
26
+ name: "@tintinweb/pi-subagents",
27
+ });
28
+ });
29
+
30
+ it("parses git SSH sources", () => {
31
+ expect(parseSourceKey("git@github.com:BlackBeltTechnology/pi-flows.git")).toEqual({
32
+ kind: "git",
33
+ host: "github.com",
34
+ owner: "BlackBeltTechnology",
35
+ repo: "pi-flows",
36
+ });
37
+ });
38
+
39
+ it("parses git HTTPS sources", () => {
40
+ expect(parseSourceKey("https://github.com/BlackBeltTechnology/pi-flows.git")).toEqual({
41
+ kind: "git",
42
+ host: "github.com",
43
+ owner: "BlackBeltTechnology",
44
+ repo: "pi-flows",
45
+ });
46
+ });
47
+
48
+ it("parses git:<host>/... sources", () => {
49
+ expect(parseSourceKey("git:github.com/BlackBeltTechnology/pi-flows#main")).toEqual({
50
+ kind: "git",
51
+ host: "github.com",
52
+ owner: "BlackBeltTechnology",
53
+ repo: "pi-flows",
54
+ });
55
+ });
56
+
57
+ it("returns raw for local paths", () => {
58
+ expect(parseSourceKey("../pi-flows")).toEqual({ kind: "raw", source: "../pi-flows" });
59
+ expect(parseSourceKey("/abs/path")).toEqual({ kind: "raw", source: "/abs/path" });
60
+ });
61
+ });
62
+
63
+ describe("sourcesMatch", () => {
64
+ it("matches npm by name regardless of version", () => {
65
+ expect(sourcesMatch("npm:pi-web-access@0.10.6", "npm:pi-web-access")).toBe(true);
66
+ });
67
+
68
+ it("matches scoped npm names", () => {
69
+ expect(
70
+ sourcesMatch("npm:@tintinweb/pi-subagents@0.5.2", "npm:@tintinweb/pi-subagents"),
71
+ ).toBe(true);
72
+ });
73
+
74
+ it("matches git SSH vs HTTPS forms", () => {
75
+ expect(
76
+ sourcesMatch(
77
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
78
+ "https://github.com/BlackBeltTechnology/pi-flows.git",
79
+ ),
80
+ ).toBe(true);
81
+ });
82
+
83
+ it("is case-insensitive for git host/owner/repo", () => {
84
+ expect(
85
+ sourcesMatch(
86
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
87
+ "git@github.com:blackbelttechnology/pi-flows.git",
88
+ ),
89
+ ).toBe(true);
90
+ });
91
+
92
+ it("distinguishes different repos", () => {
93
+ expect(
94
+ sourcesMatch(
95
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
96
+ "git@github.com:BlackBeltTechnology/pi-anthropic-messages.git",
97
+ ),
98
+ ).toBe(false);
99
+ });
100
+
101
+ it("distinguishes different npm names", () => {
102
+ expect(sourcesMatch("npm:pi-web-access", "npm:pi-agent-browser")).toBe(false);
103
+ });
104
+
105
+ it("cross-matches git URL against local path whose basename is the repo", () => {
106
+ expect(
107
+ sourcesMatch(
108
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
109
+ "../pi-flows",
110
+ ),
111
+ ).toBe(true);
112
+ expect(
113
+ sourcesMatch(
114
+ "../pi-anthropic-messages/",
115
+ "git@github.com:BlackBeltTechnology/pi-anthropic-messages.git",
116
+ ),
117
+ ).toBe(true);
118
+ expect(
119
+ sourcesMatch(
120
+ "git@github.com:Org/pi-flows.git",
121
+ "/abs/path/to/pi-flows.git",
122
+ ),
123
+ ).toBe(true);
124
+ });
125
+
126
+ it("does not cross-match a git URL against an unrelated local path", () => {
127
+ expect(
128
+ sourcesMatch(
129
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
130
+ "../pi-web-access",
131
+ ),
132
+ ).toBe(false);
133
+ });
134
+
135
+ it("does not cross-match a git URL against a deep local path", () => {
136
+ expect(
137
+ sourcesMatch(
138
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
139
+ "../pi-flows/packages/core",
140
+ ),
141
+ ).toBe(false);
142
+ });
143
+ });
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Tests for platform/spawn-mechanism.ts pure selector + argv builders.
3
+ *
4
+ * Every test passes `platform` explicitly. Never mutates process.platform.
5
+ */
6
+ import { describe, it, expect } from "vitest";
7
+ import {
8
+ selectMechanism,
9
+ buildWtArgs,
10
+ sessionFlagsToArgv,
11
+ type SpawnMechanismContext,
12
+ } from "../platform/spawn-mechanism.js";
13
+
14
+ function ctx(overrides: Partial<SpawnMechanismContext> = {}): SpawnMechanismContext {
15
+ return {
16
+ platform: "linux",
17
+ userStrategy: "tmux",
18
+ electronMode: false,
19
+ available: { tmux: false, wt: false, wslTmux: false },
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ describe("selectMechanism", () => {
25
+ it("electron mode always returns headless", () => {
26
+ expect(selectMechanism(ctx({ electronMode: true, platform: "win32", available: { tmux: false, wt: true, wslTmux: true } }))).toBe("headless");
27
+ expect(selectMechanism(ctx({ electronMode: true, platform: "linux", available: { tmux: true, wt: false, wslTmux: false } }))).toBe("headless");
28
+ expect(selectMechanism(ctx({ electronMode: true, platform: "darwin", available: { tmux: true, wt: false, wslTmux: false } }))).toBe("headless");
29
+ });
30
+
31
+ it("userStrategy headless always returns headless", () => {
32
+ expect(selectMechanism(ctx({ userStrategy: "headless", platform: "win32", available: { tmux: false, wt: true, wslTmux: true } }))).toBe("headless");
33
+ expect(selectMechanism(ctx({ userStrategy: "headless", platform: "linux", available: { tmux: true, wt: false, wslTmux: false } }))).toBe("headless");
34
+ });
35
+
36
+ it("Linux with tmux returns tmux", () => {
37
+ expect(selectMechanism(ctx({ platform: "linux", available: { tmux: true, wt: false, wslTmux: false } }))).toBe("tmux");
38
+ });
39
+
40
+ it("macOS with tmux returns tmux", () => {
41
+ expect(selectMechanism(ctx({ platform: "darwin", available: { tmux: true, wt: false, wslTmux: false } }))).toBe("tmux");
42
+ });
43
+
44
+ it("Linux without tmux returns headless", () => {
45
+ expect(selectMechanism(ctx({ platform: "linux", available: { tmux: false, wt: false, wslTmux: false } }))).toBe("headless");
46
+ });
47
+
48
+ it("Windows with wt returns wt", () => {
49
+ expect(selectMechanism(ctx({ platform: "win32", available: { tmux: false, wt: true, wslTmux: false } }))).toBe("wt");
50
+ });
51
+
52
+ it("Windows with wt AND wsl-tmux prefers wt", () => {
53
+ expect(selectMechanism(ctx({ platform: "win32", available: { tmux: false, wt: true, wslTmux: true } }))).toBe("wt");
54
+ });
55
+
56
+ it("Windows with only wsl-tmux returns wsl-tmux", () => {
57
+ expect(selectMechanism(ctx({ platform: "win32", available: { tmux: false, wt: false, wslTmux: true } }))).toBe("wsl-tmux");
58
+ });
59
+
60
+ it("Windows with nothing available returns headless", () => {
61
+ expect(selectMechanism(ctx({ platform: "win32", available: { tmux: false, wt: false, wslTmux: false } }))).toBe("headless");
62
+ });
63
+
64
+ it("unknown platform falls back to headless", () => {
65
+ expect(selectMechanism(ctx({ platform: "openbsd" as NodeJS.Platform, available: { tmux: true, wt: false, wslTmux: false } }))).toBe("headless");
66
+ });
67
+ });
68
+
69
+ describe("buildWtArgs", () => {
70
+ it("produces argv in expected order", () => {
71
+ const argv = buildWtArgs({
72
+ cwd: "C:\\proj",
73
+ title: "proj",
74
+ piArgv: ["C:\\node.exe", "cli.js", "--mode", "rpc"],
75
+ });
76
+ expect(argv).toEqual([
77
+ "-w", "0",
78
+ "new-tab",
79
+ "-d", "C:\\proj",
80
+ "--title", "proj",
81
+ "--",
82
+ "C:\\node.exe", "cli.js", "--mode", "rpc",
83
+ ]);
84
+ });
85
+
86
+ it("preserves cwd with spaces as a single argv element", () => {
87
+ const argv = buildWtArgs({
88
+ cwd: "C:\\Users\\Bob's Project (2)",
89
+ title: "x",
90
+ piArgv: ["pi"],
91
+ });
92
+ expect(argv).toContain("C:\\Users\\Bob's Project (2)");
93
+ expect(argv.filter(a => a.includes("Bob"))).toHaveLength(1);
94
+ });
95
+
96
+ it("places piArgv after -- sentinel with --fork intact", () => {
97
+ const argv = buildWtArgs({
98
+ cwd: "C:\\proj",
99
+ title: "proj",
100
+ piArgv: ["node.exe", "cli.js", "--fork", "C:\\x\\session.jsonl"],
101
+ });
102
+ const sentinelIdx = argv.indexOf("--");
103
+ expect(sentinelIdx).toBeGreaterThan(0);
104
+ expect(argv.slice(sentinelIdx + 1)).toEqual(["node.exe", "cli.js", "--fork", "C:\\x\\session.jsonl"]);
105
+ });
106
+
107
+ it("never includes -p profile flag", () => {
108
+ const argv = buildWtArgs({ cwd: "C:\\x", title: "y", piArgv: ["pi"] });
109
+ expect(argv).not.toContain("-p");
110
+ });
111
+ });
112
+
113
+ describe("sessionFlagsToArgv", () => {
114
+ it("returns --session file for continue mode", () => {
115
+ expect(sessionFlagsToArgv({ sessionFile: "/s/abc.jsonl", mode: "continue" })).toEqual(["--session", "/s/abc.jsonl"]);
116
+ });
117
+
118
+ it("returns --fork file for fork mode", () => {
119
+ expect(sessionFlagsToArgv({ sessionFile: "C:\\s\\abc.jsonl", mode: "fork" })).toEqual(["--fork", "C:\\s\\abc.jsonl"]);
120
+ });
121
+
122
+ it("returns empty array with no file", () => {
123
+ expect(sessionFlagsToArgv({})).toEqual([]);
124
+ expect(sessionFlagsToArgv({ mode: "continue" })).toEqual([]);
125
+ expect(sessionFlagsToArgv({ mode: "fork" })).toEqual([]);
126
+ });
127
+
128
+ it("returns empty array with file but no mode", () => {
129
+ expect(sessionFlagsToArgv({ sessionFile: "/s/x.jsonl" })).toEqual([]);
130
+ });
131
+ });