@blackbelt-technology/pi-agent-dashboard 0.2.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/AGENTS.md +64 -8
  2. package/README.md +308 -101
  3. package/docs/architecture.md +515 -16
  4. package/package.json +14 -7
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
  7. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
  8. package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -0
  9. package/packages/extension/src/__tests__/git-info.test.ts +67 -55
  10. package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
  11. package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
  12. package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
  13. package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
  14. package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
  15. package/packages/extension/src/ask-user-tool.ts +289 -20
  16. package/packages/extension/src/bridge.ts +107 -6
  17. package/packages/extension/src/command-handler.ts +34 -39
  18. package/packages/extension/src/dev-build.ts +1 -1
  19. package/packages/extension/src/git-info.ts +9 -19
  20. package/packages/extension/src/pi-env.d.ts +1 -0
  21. package/packages/extension/src/process-scanner.ts +72 -38
  22. package/packages/extension/src/prompt-expander.ts +25 -4
  23. package/packages/extension/src/provider-register.ts +304 -16
  24. package/packages/extension/src/server-auto-start.ts +27 -1
  25. package/packages/extension/src/server-launcher.ts +71 -27
  26. package/packages/server/package.json +17 -2
  27. package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
  28. package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -2
  29. package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
  30. package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
  31. package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
  32. package/packages/server/src/__tests__/browse-endpoint.test.ts +246 -10
  33. package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -0
  34. package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
  35. package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
  36. package/packages/server/src/__tests__/config-api.test.ts +68 -0
  37. package/packages/server/src/__tests__/cors.test.ts +34 -2
  38. package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
  39. package/packages/server/src/__tests__/directory-service.test.ts +234 -8
  40. package/packages/server/src/__tests__/editor-manager-pid-registry.test.ts +168 -0
  41. package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
  42. package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
  43. package/packages/server/src/__tests__/editor-registry.test.ts +29 -15
  44. package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
  45. package/packages/server/src/__tests__/extension-register.test.ts +3 -1
  46. package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
  47. package/packages/server/src/__tests__/fix-pty-permissions.test.ts +59 -0
  48. package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
  49. package/packages/server/src/__tests__/git-operations.test.ts +9 -7
  50. package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
  51. package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
  52. package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
  53. package/packages/server/src/__tests__/home-lock.test.ts +308 -0
  54. package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
  55. package/packages/server/src/__tests__/node-guard.test.ts +85 -0
  56. package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +178 -0
  57. package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
  58. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
  59. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
  60. package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
  61. package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
  62. package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
  63. package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
  64. package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
  65. package/packages/server/src/__tests__/process-manager.test.ts +45 -18
  66. package/packages/server/src/__tests__/provider-auth-routes.test.ts +13 -3
  67. package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
  68. package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
  69. package/packages/server/src/__tests__/recommended-routes.test.ts +389 -0
  70. package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
  71. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
  72. package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
  73. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
  74. package/packages/server/src/__tests__/session-file-dedup.test.ts +10 -10
  75. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
  76. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
  77. package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
  78. package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
  79. package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
  80. package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
  81. package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
  82. package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
  83. package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
  84. package/packages/server/src/__tests__/tunnel.test.ts +103 -6
  85. package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
  86. package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
  87. package/packages/server/src/bootstrap-queue.ts +130 -0
  88. package/packages/server/src/bootstrap-state.ts +131 -0
  89. package/packages/server/src/browse.ts +108 -9
  90. package/packages/server/src/browser-gateway.ts +16 -3
  91. package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
  92. package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
  93. package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
  94. package/packages/server/src/cli.ts +256 -32
  95. package/packages/server/src/config-api.ts +16 -0
  96. package/packages/server/src/directory-service.ts +270 -39
  97. package/packages/server/src/editor-detection.ts +12 -9
  98. package/packages/server/src/editor-manager.ts +39 -5
  99. package/packages/server/src/editor-pid-registry.ts +199 -0
  100. package/packages/server/src/editor-registry.ts +22 -25
  101. package/packages/server/src/fix-pty-permissions.ts +44 -0
  102. package/packages/server/src/git-operations.ts +1 -1
  103. package/packages/server/src/headless-pid-registry.ts +16 -20
  104. package/packages/server/src/home-lock-release.ts +72 -0
  105. package/packages/server/src/home-lock.ts +389 -0
  106. package/packages/server/src/node-guard.ts +52 -0
  107. package/packages/server/src/npm-search-proxy.ts +71 -0
  108. package/packages/server/src/openspec-tasks.ts +158 -0
  109. package/packages/server/src/package-manager-wrapper.ts +225 -34
  110. package/packages/server/src/pi-core-checker.ts +290 -0
  111. package/packages/server/src/pi-core-updater.ts +172 -0
  112. package/packages/server/src/pi-gateway.ts +7 -0
  113. package/packages/server/src/pi-resource-scanner.ts +5 -8
  114. package/packages/server/src/pi-version-skew.ts +196 -0
  115. package/packages/server/src/preferences-store.ts +17 -3
  116. package/packages/server/src/process-manager.ts +403 -222
  117. package/packages/server/src/provider-probe.ts +234 -0
  118. package/packages/server/src/restart-helper.ts +130 -0
  119. package/packages/server/src/routes/bootstrap-routes.ts +88 -0
  120. package/packages/server/src/routes/file-routes.ts +30 -3
  121. package/packages/server/src/routes/openspec-routes.ts +107 -1
  122. package/packages/server/src/routes/pi-core-routes.ts +140 -0
  123. package/packages/server/src/routes/provider-auth-routes.ts +12 -10
  124. package/packages/server/src/routes/provider-routes.ts +55 -2
  125. package/packages/server/src/routes/recommended-routes.ts +225 -0
  126. package/packages/server/src/routes/system-routes.ts +30 -34
  127. package/packages/server/src/routes/tool-routes.ts +153 -0
  128. package/packages/server/src/server-pid.ts +5 -9
  129. package/packages/server/src/server.ts +363 -26
  130. package/packages/server/src/session-api.ts +77 -8
  131. package/packages/server/src/session-bootstrap.ts +17 -3
  132. package/packages/server/src/session-diff.ts +21 -21
  133. package/packages/server/src/terminal-manager.ts +65 -20
  134. package/packages/server/src/test-env-guard.ts +26 -0
  135. package/packages/server/src/test-support/test-server.ts +63 -0
  136. package/packages/server/src/tunnel.ts +172 -34
  137. package/packages/shared/package.json +10 -3
  138. package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
  139. package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
  140. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
  141. package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
  142. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
  143. package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
  144. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
  145. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
  146. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
  147. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
  148. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
  149. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
  150. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
  151. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
  152. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
  153. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
  154. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
  155. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
  156. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
  157. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
  158. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
  159. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
  160. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
  161. package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
  162. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
  163. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
  164. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
  165. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
  166. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
  167. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
  168. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
  169. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
  170. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
  171. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
  172. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
  173. package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
  174. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
  175. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
  176. package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
  177. package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
  178. package/packages/shared/src/__tests__/config.test.ts +59 -3
  179. package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
  180. package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
  181. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
  182. package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
  183. package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
  184. package/packages/shared/src/__tests__/openspec-poller.test.ts +44 -0
  185. package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
  186. package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
  187. package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
  188. package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
  189. package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
  190. package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
  191. package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
  192. package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
  193. package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
  194. package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
  195. package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
  196. package/packages/shared/src/__tests__/recommended-extensions.test.ts +156 -0
  197. package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
  198. package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
  199. package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
  200. package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
  201. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
  202. package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
  203. package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
  204. package/packages/shared/src/bootstrap-install.ts +212 -0
  205. package/packages/shared/src/bridge-register.ts +87 -20
  206. package/packages/shared/src/browser-protocol.ts +93 -1
  207. package/packages/shared/src/config.ts +87 -15
  208. package/packages/shared/src/managed-paths.ts +31 -4
  209. package/packages/shared/src/openspec-poller.ts +71 -49
  210. package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
  211. package/packages/shared/src/platform/commands.ts +100 -0
  212. package/packages/shared/src/platform/detached-spawn.ts +305 -0
  213. package/packages/shared/src/platform/exec.ts +220 -0
  214. package/packages/shared/src/platform/git.ts +155 -0
  215. package/packages/shared/src/platform/index.ts +15 -0
  216. package/packages/shared/src/platform/npm.ts +162 -0
  217. package/packages/shared/src/platform/openspec.ts +91 -0
  218. package/packages/shared/src/platform/paths.ts +276 -0
  219. package/packages/shared/src/platform/process-identify.ts +126 -0
  220. package/packages/shared/src/platform/process-scan.ts +94 -0
  221. package/packages/shared/src/platform/process.ts +168 -0
  222. package/packages/shared/src/platform/runner.ts +369 -0
  223. package/packages/shared/src/platform/shell.ts +44 -0
  224. package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
  225. package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
  226. package/packages/shared/src/recommended-extensions.ts +196 -0
  227. package/packages/shared/src/resolve-jiti.ts +62 -3
  228. package/packages/shared/src/rest-api.ts +97 -0
  229. package/packages/shared/src/semaphore.ts +83 -0
  230. package/packages/shared/src/source-matching.ts +126 -0
  231. package/packages/shared/src/test-support/setup-home.ts +74 -0
  232. package/packages/shared/src/tool-registry/definitions.ts +342 -0
  233. package/packages/shared/src/tool-registry/index.ts +56 -0
  234. package/packages/shared/src/tool-registry/overrides.ts +118 -0
  235. package/packages/shared/src/tool-registry/registry.ts +262 -0
  236. package/packages/shared/src/tool-registry/strategies.ts +198 -0
  237. package/packages/shared/src/tool-registry/types.ts +180 -0
  238. package/packages/shared/src/types.ts +7 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Tests for packages/shared/src/platform/npm.ts — Recipe argv + parse.
3
+ * Live npm calls are out of scope (npm may or may not be on PATH in CI).
4
+ *
5
+ * See change: platform-command-executor.
6
+ */
7
+ import { describe, it, expect, beforeEach } from "vitest";
8
+ import {
9
+ NPM_ROOT_GLOBAL,
10
+ NPM_OUTDATED,
11
+ NPM_OUTDATED_GLOBAL,
12
+ NPM_INSTALL,
13
+ NPM_INSTALL_GLOBAL,
14
+ NPM_VIEW_VERSION,
15
+ NPM_RECIPES,
16
+ _resetNpmRootCache,
17
+ } from "../platform/npm.js";
18
+
19
+ beforeEach(() => {
20
+ _resetNpmRootCache();
21
+ });
22
+
23
+ describe("NPM_ROOT_GLOBAL", () => {
24
+ it("produces `npm root -g`", () => {
25
+ expect(NPM_ROOT_GLOBAL.argv({})).toEqual(["npm", "root", "-g"]);
26
+ });
27
+
28
+ it("trims stdout", () => {
29
+ expect(NPM_ROOT_GLOBAL.parse("/usr/lib/node_modules\n", {})).toBe("/usr/lib/node_modules");
30
+ });
31
+ });
32
+
33
+ describe("NPM_OUTDATED", () => {
34
+ it("project-wide form", () => {
35
+ expect(NPM_OUTDATED.argv({})).toEqual(["npm", "outdated", "--json"]);
36
+ });
37
+
38
+ it("scoped to a single package", () => {
39
+ expect(NPM_OUTDATED.argv({ pkg: "pi-web-access" })).toEqual([
40
+ "npm", "outdated", "pi-web-access", "--json",
41
+ ]);
42
+ });
43
+
44
+ it("parses JSON stdout", () => {
45
+ const json = '{"pi-flows":{"current":"1.0.0","wanted":"1.1.0"}}';
46
+ expect(NPM_OUTDATED.parse(json, {})).toEqual({
47
+ "pi-flows": { current: "1.0.0", wanted: "1.1.0" },
48
+ });
49
+ });
50
+
51
+ it("returns null for empty or malformed stdout", () => {
52
+ expect(NPM_OUTDATED.parse("", {})).toBeNull();
53
+ expect(NPM_OUTDATED.parse("not json", {})).toBeNull();
54
+ });
55
+
56
+ it("tolerates exit code 1 (npm exits 1 when updates exist)", () => {
57
+ expect(NPM_OUTDATED.tolerate).toContain(1);
58
+ });
59
+ });
60
+
61
+ describe("NPM_OUTDATED_GLOBAL", () => {
62
+ it("includes `-g` flag", () => {
63
+ expect(NPM_OUTDATED_GLOBAL.argv({})).toEqual(["npm", "outdated", "-g", "--json"]);
64
+ });
65
+
66
+ it("scoped form", () => {
67
+ expect(NPM_OUTDATED_GLOBAL.argv({ pkg: "typescript" })).toEqual([
68
+ "npm", "outdated", "-g", "typescript", "--json",
69
+ ]);
70
+ });
71
+
72
+ it("tolerates exit 1", () => {
73
+ expect(NPM_OUTDATED_GLOBAL.tolerate).toContain(1);
74
+ });
75
+ });
76
+
77
+ describe("NPM_INSTALL", () => {
78
+ it("installs latest when version omitted", () => {
79
+ expect(NPM_INSTALL.argv({ pkg: "pi-flows" })).toEqual(["npm", "install", "pi-flows"]);
80
+ });
81
+
82
+ it("installs pinned version", () => {
83
+ expect(NPM_INSTALL.argv({ pkg: "pi-flows", version: "1.2.3" })).toEqual([
84
+ "npm", "install", "pi-flows@1.2.3",
85
+ ]);
86
+ });
87
+
88
+ it("has a long timeout for install", () => {
89
+ expect(NPM_INSTALL.timeout).toBeGreaterThanOrEqual(60_000);
90
+ });
91
+ });
92
+
93
+ describe("NPM_INSTALL_GLOBAL", () => {
94
+ it("includes `-g` flag with version", () => {
95
+ expect(NPM_INSTALL_GLOBAL.argv({ pkg: "typescript", version: "5.0.0" })).toEqual([
96
+ "npm", "install", "-g", "typescript@5.0.0",
97
+ ]);
98
+ });
99
+
100
+ it("omits version suffix when not given", () => {
101
+ expect(NPM_INSTALL_GLOBAL.argv({ pkg: "typescript" })).toEqual([
102
+ "npm", "install", "-g", "typescript",
103
+ ]);
104
+ });
105
+ });
106
+
107
+ describe("NPM_VIEW_VERSION", () => {
108
+ it("produces `npm view <pkg> version`", () => {
109
+ expect(NPM_VIEW_VERSION.argv({ pkg: "@blackbelt-technology/pi-agent-dashboard" })).toEqual([
110
+ "npm", "view", "@blackbelt-technology/pi-agent-dashboard", "version",
111
+ ]);
112
+ });
113
+
114
+ it("trims the version string", () => {
115
+ expect(NPM_VIEW_VERSION.parse("1.2.3\n", { pkg: "x" })).toBe("1.2.3");
116
+ });
117
+ });
118
+
119
+ describe("NPM_RECIPES registry", () => {
120
+ it("enumerates all 6 recipes", () => {
121
+ expect(Object.keys(NPM_RECIPES).sort()).toEqual([
122
+ "NPM_INSTALL",
123
+ "NPM_INSTALL_GLOBAL",
124
+ "NPM_OUTDATED",
125
+ "NPM_OUTDATED_GLOBAL",
126
+ "NPM_ROOT_GLOBAL",
127
+ "NPM_VIEW_VERSION",
128
+ ]);
129
+ });
130
+
131
+ it("every recipe has argv and parse functions", () => {
132
+ for (const [name, recipe] of Object.entries(NPM_RECIPES)) {
133
+ expect(typeof recipe.argv, `${name}.argv`).toBe("function");
134
+ expect(typeof recipe.parse, `${name}.parse`).toBe("function");
135
+ }
136
+ });
137
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Tests for packages/shared/src/platform/openspec.ts.
3
+ *
4
+ * Pure argv + parse tests; integration with a live openspec binary is
5
+ * out of scope for unit tests (openspec may not be on PATH in CI).
6
+ *
7
+ * See change: platform-command-executor.
8
+ */
9
+ import { describe, it, expect } from "vitest";
10
+ import {
11
+ OPENSPEC_LIST,
12
+ OPENSPEC_STATUS,
13
+ OPENSPEC_ARCHIVE_COMPLETED,
14
+ OPENSPEC_RECIPES,
15
+ } from "../platform/openspec.js";
16
+
17
+ describe("OPENSPEC_LIST", () => {
18
+ it("produces `openspec list --json`", () => {
19
+ expect(OPENSPEC_LIST.argv({ cwd: "/tmp" })).toEqual(["openspec", "list", "--json"]);
20
+ });
21
+
22
+ it("parses valid JSON output", () => {
23
+ const input = { cwd: "/tmp" };
24
+ const out = OPENSPEC_LIST.parse('{"changes":[{"name":"x"}]}', input);
25
+ expect(out).toEqual({ changes: [{ name: "x" }] });
26
+ });
27
+
28
+ it("returns null for empty stdout", () => {
29
+ expect(OPENSPEC_LIST.parse("", { cwd: "/tmp" })).toBeNull();
30
+ expect(OPENSPEC_LIST.parse(" \n", { cwd: "/tmp" })).toBeNull();
31
+ });
32
+
33
+ it("returns null for malformed JSON", () => {
34
+ expect(OPENSPEC_LIST.parse("not json", { cwd: "/tmp" })).toBeNull();
35
+ expect(OPENSPEC_LIST.parse("{broken", { cwd: "/tmp" })).toBeNull();
36
+ });
37
+ });
38
+
39
+ describe("OPENSPEC_STATUS", () => {
40
+ it("produces `openspec status --change <name> --json`", () => {
41
+ expect(OPENSPEC_STATUS.argv({ cwd: "/tmp", change: "add-feature" })).toEqual([
42
+ "openspec", "status", "--change", "add-feature", "--json",
43
+ ]);
44
+ });
45
+
46
+ it("parses status result JSON", () => {
47
+ const input = { cwd: "/tmp", change: "x" };
48
+ const out = OPENSPEC_STATUS.parse('{"artifacts":[{"id":"proposal","status":"done"}]}', input);
49
+ expect(out).toEqual({ artifacts: [{ id: "proposal", status: "done" }] });
50
+ });
51
+
52
+ it("accepts change names with special characters verbatim (no shell escaping needed)", () => {
53
+ // argv is an array, so special chars flow through unchanged
54
+ expect(OPENSPEC_STATUS.argv({ cwd: "/tmp", change: "a b/c" })).toEqual([
55
+ "openspec", "status", "--change", "a b/c", "--json",
56
+ ]);
57
+ });
58
+ });
59
+
60
+ describe("OPENSPEC_ARCHIVE_COMPLETED", () => {
61
+ it("produces `openspec archive --completed`", () => {
62
+ expect(OPENSPEC_ARCHIVE_COMPLETED.argv({ cwd: "/tmp" })).toEqual([
63
+ "openspec", "archive", "--completed",
64
+ ]);
65
+ });
66
+
67
+ it("returns stdout verbatim (no JSON parsing)", () => {
68
+ expect(OPENSPEC_ARCHIVE_COMPLETED.parse("Archived 3 changes\n", { cwd: "/tmp" }))
69
+ .toBe("Archived 3 changes\n");
70
+ });
71
+
72
+ it("has a longer timeout than list/status (archive can be slow)", () => {
73
+ expect(OPENSPEC_ARCHIVE_COMPLETED.timeout).toBeGreaterThanOrEqual(15_000);
74
+ });
75
+ });
76
+
77
+ describe("OPENSPEC_RECIPES registry", () => {
78
+ it("enumerates all exported recipes", () => {
79
+ expect(Object.keys(OPENSPEC_RECIPES).sort()).toEqual([
80
+ "OPENSPEC_ARCHIVE_COMPLETED",
81
+ "OPENSPEC_LIST",
82
+ "OPENSPEC_STATUS",
83
+ ]);
84
+ });
85
+
86
+ it("every recipe has argv and parse functions", () => {
87
+ for (const [name, recipe] of Object.entries(OPENSPEC_RECIPES)) {
88
+ expect(typeof recipe.argv, `${name}.argv`).toBe("function");
89
+ expect(typeof recipe.parse, `${name}.parse`).toBe("function");
90
+ }
91
+ });
92
+ });
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Tests for packages/shared/src/platform/paths.ts.
3
+ *
4
+ * Every test explicitly passes a `platform: NodeJS.Platform` argument so
5
+ * both Windows and Unix branches run on every CI host. No mutation of
6
+ * `process.platform`, no `vi.mock`.
7
+ *
8
+ * See change: platform-path-normalization.
9
+ */
10
+ import { describe, it, expect } from "vitest";
11
+ import {
12
+ normalizePath,
13
+ samePath,
14
+ parsePathInput,
15
+ withTrailingSep,
16
+ joinForDisplay,
17
+ isFilesystemRoot,
18
+ } from "../platform/paths.js";
19
+
20
+ // ── normalizePath ───────────────────────────────────────────────────────────
21
+
22
+ describe("normalizePath — Windows", () => {
23
+ it("strips trailing separator from non-root path", () => {
24
+ expect(normalizePath("C:\\Dev\\BB\\pi-agent-dashboard\\", "win32"))
25
+ .toBe("C:\\Dev\\BB\\pi-agent-dashboard");
26
+ });
27
+
28
+ it("canonicalizes mixed separators to backslash", () => {
29
+ expect(normalizePath("C:/Dev\\BB/pi-agent-dashboard", "win32"))
30
+ .toBe("C:\\Dev\\BB\\pi-agent-dashboard");
31
+ });
32
+
33
+ it("preserves drive root trailing separator", () => {
34
+ expect(normalizePath("C:\\", "win32")).toBe("C:\\");
35
+ });
36
+
37
+ it("preserves UNC root", () => {
38
+ expect(normalizePath("\\\\server\\share\\path\\", "win32"))
39
+ .toBe("\\\\server\\share\\path");
40
+ });
41
+
42
+ it("resolves .. and . segments within a drive", () => {
43
+ expect(normalizePath("C:\\Dev\\BB\\..\\.\\pi-agent-dashboard", "win32"))
44
+ .toBe("C:\\Dev\\pi-agent-dashboard");
45
+ });
46
+
47
+ it("preserves case (no lowercasing)", () => {
48
+ expect(normalizePath("C:\\Dev\\BB", "win32")).toBe("C:\\Dev\\BB");
49
+ expect(normalizePath("b:\\Dev\\BB", "win32")).toBe("b:\\Dev\\BB");
50
+ });
51
+
52
+ it("preserves different drive letters independently", () => {
53
+ expect(normalizePath("A:\\Foo\\Bar", "win32")).toBe("A:\\Foo\\Bar");
54
+ expect(normalizePath("B:\\Foo\\Bar", "win32")).toBe("B:\\Foo\\Bar");
55
+ expect(normalizePath("Z:\\Something", "win32")).toBe("Z:\\Something");
56
+ });
57
+
58
+ it("treats bare drive letter as drive root, not cwd-relative", () => {
59
+ // Must NOT fall through to path.win32.resolve which would return
60
+ // <cwd-on-B-drive>, leaking process.cwd() into the result.
61
+ expect(normalizePath("B:", "win32")).toBe("B:\\");
62
+ expect(normalizePath("Z:", "win32")).toBe("Z:\\");
63
+ });
64
+
65
+ it("treats drive-relative typed form as drive-rooted", () => {
66
+ // "B:Dev" → treat as "B:\Dev", NOT as <B-drive-cwd>\Dev
67
+ expect(normalizePath("B:Dev", "win32")).toBe("B:\\Dev");
68
+ expect(normalizePath("C:Users\\me", "win32")).toBe("C:\\Users\\me");
69
+ });
70
+
71
+ it("drops duplicate separators", () => {
72
+ expect(normalizePath("D:\\\\", "win32")).toBe("D:\\");
73
+ expect(normalizePath("C:\\\\Users\\\\me", "win32")).toBe("C:\\Users\\me");
74
+ });
75
+ });
76
+
77
+ describe("normalizePath — POSIX", () => {
78
+ it("strips trailing separator from non-root path", () => {
79
+ expect(normalizePath("/Users/me/Projects/", "linux"))
80
+ .toBe("/Users/me/Projects");
81
+ });
82
+
83
+ it("preserves root", () => {
84
+ expect(normalizePath("/", "linux")).toBe("/");
85
+ expect(normalizePath("/", "darwin")).toBe("/");
86
+ });
87
+
88
+ it("resolves .. and . segments", () => {
89
+ expect(normalizePath("/Users/me/Dev/../Projects", "linux"))
90
+ .toBe("/Users/me/Projects");
91
+ });
92
+
93
+ it("collapses duplicate slashes", () => {
94
+ expect(normalizePath("/Users//me///Projects", "linux"))
95
+ .toBe("/Users/me/Projects");
96
+ });
97
+
98
+ it("preserves case", () => {
99
+ expect(normalizePath("/Users/Robson/Dev", "linux")).toBe("/Users/Robson/Dev");
100
+ });
101
+ });
102
+
103
+ // ── samePath ────────────────────────────────────────────────────────────────
104
+
105
+ describe("samePath — Windows (case-insensitive)", () => {
106
+ it("matches identical paths", () => {
107
+ expect(samePath("C:\\Dev", "C:\\Dev", "win32")).toBe(true);
108
+ });
109
+
110
+ it("matches with different case", () => {
111
+ expect(samePath("C:\\Dev\\BB", "c:\\dev\\bb", "win32")).toBe(true);
112
+ });
113
+
114
+ it("matches with different separator style", () => {
115
+ expect(samePath("C:\\Dev\\BB", "C:/Dev/BB", "win32")).toBe(true);
116
+ });
117
+
118
+ it("matches with trailing-separator drift", () => {
119
+ expect(samePath("C:\\Dev\\BB", "C:\\Dev\\BB\\", "win32")).toBe(true);
120
+ });
121
+
122
+ it("matches drive-letter case drift alone", () => {
123
+ expect(samePath("B:\\Dev\\BB", "b:\\Dev\\BB", "win32")).toBe(true);
124
+ });
125
+
126
+ it("DOES NOT merge different drive letters", () => {
127
+ expect(samePath("A:\\Foo", "B:\\Foo", "win32")).toBe(false);
128
+ expect(samePath("C:\\Users\\me\\Dev", "D:\\Users\\me\\Dev", "win32")).toBe(false);
129
+ });
130
+
131
+ it("DOES NOT merge UNC path with drive-letter path", () => {
132
+ expect(samePath("\\\\server\\share\\x", "B:\\x", "win32")).toBe(false);
133
+ });
134
+
135
+ it("returns false for genuinely different paths", () => {
136
+ expect(samePath("C:\\a", "C:\\b", "win32")).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe("samePath — macOS (case-insensitive, HFS+ default)", () => {
141
+ it("matches with different case", () => {
142
+ expect(samePath("/Users/me/Dev", "/Users/me/dev", "darwin")).toBe(true);
143
+ });
144
+
145
+ it("matches with trailing-separator drift", () => {
146
+ expect(samePath("/Users/me/Dev", "/Users/me/Dev/", "darwin")).toBe(true);
147
+ });
148
+ });
149
+
150
+ describe("samePath — Linux (case-sensitive)", () => {
151
+ it("does NOT match on case drift", () => {
152
+ expect(samePath("/Users/me/Dev", "/users/me/dev", "linux")).toBe(false);
153
+ });
154
+
155
+ it("matches identical paths", () => {
156
+ expect(samePath("/Users/me/Dev", "/Users/me/Dev", "linux")).toBe(true);
157
+ });
158
+
159
+ it("matches with trailing-separator drift", () => {
160
+ expect(samePath("/Users/me/Dev", "/Users/me/Dev/", "linux")).toBe(true);
161
+ });
162
+
163
+ it("returns false for different paths", () => {
164
+ expect(samePath("/a/b", "/a/c", "linux")).toBe(false);
165
+ });
166
+ });
167
+
168
+ // ── parsePathInput ──────────────────────────────────────────────────────────
169
+
170
+ describe("parsePathInput — Windows", () => {
171
+ it("splits path ending in separator", () => {
172
+ expect(parsePathInput("C:\\Users\\mboto\\", "win32"))
173
+ .toEqual({ parent: "C:\\Users\\mboto", partial: "" });
174
+ });
175
+
176
+ it("splits path with partial trailing segment", () => {
177
+ expect(parsePathInput("C:\\Users\\mboto\\Dev", "win32"))
178
+ .toEqual({ parent: "C:\\Users\\mboto", partial: "Dev" });
179
+ });
180
+
181
+ it("treats drive root with trailing separator as root", () => {
182
+ expect(parsePathInput("C:\\", "win32"))
183
+ .toEqual({ parent: "C:\\", partial: "" });
184
+ });
185
+
186
+ it("treats bare drive letter as drive root", () => {
187
+ // Critical: must NOT leak cwd via path.win32.resolve fallback.
188
+ expect(parsePathInput("B:", "win32"))
189
+ .toEqual({ parent: "B:\\", partial: "" });
190
+ expect(parsePathInput("Z:", "win32"))
191
+ .toEqual({ parent: "Z:\\", partial: "" });
192
+ });
193
+
194
+ it("treats drive-relative typed form as drive root + partial", () => {
195
+ expect(parsePathInput("B:Dev", "win32"))
196
+ .toEqual({ parent: "B:\\", partial: "Dev" });
197
+ expect(parsePathInput("C:Users", "win32"))
198
+ .toEqual({ parent: "C:\\", partial: "Users" });
199
+ });
200
+
201
+ it("splits drive root + partial when separator present", () => {
202
+ expect(parsePathInput("C:\\Us", "win32"))
203
+ .toEqual({ parent: "C:\\", partial: "Us" });
204
+ });
205
+
206
+ it("handles UNC path with trailing separator", () => {
207
+ expect(parsePathInput("\\\\server\\share\\dir\\", "win32"))
208
+ .toEqual({ parent: "\\\\server\\share\\dir", partial: "" });
209
+ });
210
+
211
+ it("tolerates mixed separators", () => {
212
+ expect(parsePathInput("C:\\Users\\mboto/Dev", "win32"))
213
+ .toEqual({ parent: "C:\\Users\\mboto", partial: "Dev" });
214
+ });
215
+
216
+ it("is drive-letter symmetric", () => {
217
+ // Same shape regardless of drive letter.
218
+ for (const d of ["A", "B", "C", "D", "Z"]) {
219
+ expect(parsePathInput(`${d}:\\Foo\\B`, "win32"))
220
+ .toEqual({ parent: `${d}:\\Foo`, partial: "B" });
221
+ }
222
+ });
223
+ });
224
+
225
+ describe("parsePathInput — POSIX", () => {
226
+ it("splits absolute path with trailing separator", () => {
227
+ expect(parsePathInput("/Users/me/", "linux"))
228
+ .toEqual({ parent: "/Users/me", partial: "" });
229
+ });
230
+
231
+ it("splits absolute path with partial", () => {
232
+ expect(parsePathInput("/Users/me/Dev", "linux"))
233
+ .toEqual({ parent: "/Users/me", partial: "Dev" });
234
+ });
235
+
236
+ it("treats root alone as root", () => {
237
+ expect(parsePathInput("/", "linux"))
238
+ .toEqual({ parent: "/", partial: "" });
239
+ });
240
+
241
+ it("treats partial-under-root as such", () => {
242
+ expect(parsePathInput("/U", "linux"))
243
+ .toEqual({ parent: "/", partial: "U" });
244
+ });
245
+ });
246
+
247
+ // ── withTrailingSep & joinForDisplay ────────────────────────────────────────
248
+
249
+ describe("withTrailingSep", () => {
250
+ it("appends \\ on Windows", () => {
251
+ expect(withTrailingSep("C:\\Users\\me", "win32")).toBe("C:\\Users\\me\\");
252
+ });
253
+ it("appends / on POSIX", () => {
254
+ expect(withTrailingSep("/Users/me", "linux")).toBe("/Users/me/");
255
+ });
256
+ it("does not double-append when already terminated", () => {
257
+ expect(withTrailingSep("C:\\Users\\me\\", "win32")).toBe("C:\\Users\\me\\");
258
+ expect(withTrailingSep("/Users/me/", "linux")).toBe("/Users/me/");
259
+ });
260
+ });
261
+
262
+ describe("joinForDisplay", () => {
263
+ it("joins Windows paths with backslash", () => {
264
+ expect(joinForDisplay("C:\\Users", "me", "win32")).toBe("C:\\Users\\me");
265
+ });
266
+ it("joins POSIX paths with forward slash", () => {
267
+ expect(joinForDisplay("/Users", "me", "linux")).toBe("/Users/me");
268
+ });
269
+ });
270
+
271
+ // ── isFilesystemRoot ────────────────────────────────────────────────────────
272
+
273
+ describe("isFilesystemRoot", () => {
274
+ it("recognises Windows drive roots", () => {
275
+ expect(isFilesystemRoot("C:\\", "win32")).toBe(true);
276
+ expect(isFilesystemRoot("B:\\", "win32")).toBe(true);
277
+ expect(isFilesystemRoot("C:\\Users", "win32")).toBe(false);
278
+ });
279
+
280
+ it("recognises Unix root", () => {
281
+ expect(isFilesystemRoot("/", "linux")).toBe(true);
282
+ expect(isFilesystemRoot("/Users", "linux")).toBe(false);
283
+ });
284
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tests for packages/shared/src/platform/process-scan.ts.
3
+ * Platform behavior is exercised via injected `platform` + `exec`.
4
+ * See change: consolidate-platform-handlers.
5
+ */
6
+ import { describe, it, expect, vi } from "vitest";
7
+ import { parseEtime, isProcessRunning } from "../platform/process-scan.js";
8
+
9
+ describe("parseEtime", () => {
10
+ it("parses mm:ss format", () => expect(parseEtime("02:15")).toBe(135_000));
11
+ it("parses hh:mm:ss format", () => expect(parseEtime("01:30:00")).toBe(5_400_000));
12
+ it("parses dd-hh:mm:ss format", () => expect(parseEtime("2-03:00:00")).toBe(183_600_000));
13
+ it("parses 1-00:00:00 as 1 day", () => expect(parseEtime("1-00:00:00")).toBe(86_400_000));
14
+ it("parses 00:05 as 5 seconds", () => expect(parseEtime("00:05")).toBe(5_000));
15
+ it("returns 0 for empty", () => expect(parseEtime("")).toBe(0));
16
+ it("returns 0 for whitespace", () => expect(parseEtime(" ")).toBe(0));
17
+ it("returns 0 for garbage", () => expect(parseEtime("not-a-time")).toBe(0));
18
+ it("returns 0 for single number (not a time)", () => expect(parseEtime("42")).toBe(0));
19
+ });
20
+
21
+ describe("isProcessRunning", () => {
22
+ it("uses tasklist on Windows and matches image name", () => {
23
+ const exec = vi.fn().mockReturnValue(
24
+ "Code.exe 12345 Console 1 50,000 K\n",
25
+ );
26
+ expect(isProcessRunning("Code.exe", { platform: "win32", exec })).toBe(true);
27
+ expect(exec.mock.calls[0][0]).toMatch(/tasklist\s+\/FI\s+"IMAGENAME eq Code\.exe"/);
28
+ });
29
+
30
+ it("returns false on Windows when image name is missing from output", () => {
31
+ const exec = vi.fn().mockReturnValue("INFO: No tasks are running.\n");
32
+ expect(isProcessRunning("Missing.exe", { platform: "win32", exec })).toBe(false);
33
+ });
34
+
35
+ it("uses pgrep on Unix and returns true when exit code is 0", () => {
36
+ const exec = vi.fn().mockReturnValue("12345\n");
37
+ expect(isProcessRunning("/Applications/Zed.app", { platform: "darwin", exec })).toBe(true);
38
+ expect(exec.mock.calls[0][0]).toMatch(/pgrep\s+-f\s+"\/Applications\/Zed\.app"/);
39
+ });
40
+
41
+ it("returns false on Unix when pgrep throws (no match)", () => {
42
+ const exec = vi.fn().mockImplementation(() => {
43
+ throw new Error("exit code 1");
44
+ });
45
+ expect(isProcessRunning("nothing", { platform: "linux", exec })).toBe(false);
46
+ });
47
+
48
+ it("returns false on any platform when exec throws unexpectedly", () => {
49
+ const exec = vi.fn().mockImplementation(() => {
50
+ throw new Error("boom");
51
+ });
52
+ expect(isProcessRunning("Code.exe", { platform: "win32", exec })).toBe(false);
53
+ expect(isProcessRunning("zed", { platform: "linux", exec })).toBe(false);
54
+ });
55
+ });