@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,100 @@
1
+ /**
2
+ * Tests for fork entryId timing fix.
3
+ *
4
+ * Verifies that the bridge's message_end entryId enrichment captures the
5
+ * correct leaf ID (after pi core persists the entry), not the stale one
6
+ * (before appendMessage runs).
7
+ *
8
+ * The bug: pi core emits message_end via _emit() BEFORE calling
9
+ * sessionManager.appendMessage(), so getLeafId() returns the previous leaf.
10
+ * The fix: the bridge defers getLeafId() for message_end using queueMicrotask,
11
+ * allowing appendMessage to run first.
12
+ */
13
+ import { describe, it, expect, vi } from "vitest";
14
+
15
+ /**
16
+ * Simulates the pi core + bridge interaction for entryId enrichment.
17
+ *
18
+ * Pi core's _processAgentEvent does:
19
+ * 1. _emit(event) — bridge handler called (async, not awaited)
20
+ * 2. appendMessage(msg) — updates leafId synchronously
21
+ *
22
+ * The bridge handler (async) should yield via queueMicrotask before reading
23
+ * getLeafId(), so that appendMessage has already run.
24
+ */
25
+ describe("message_end entryId timing", () => {
26
+ it("deferred getLeafId() captures the post-persist entry ID", async () => {
27
+ // Simulate sessionManager with mutable leafId
28
+ let leafId = "user-entry-100"; // stale leaf before appendMessage
29
+ const sessionManager = {
30
+ getLeafId: () => leafId,
31
+ };
32
+
33
+ let capturedEntryId: string | undefined;
34
+
35
+ // Simulate the bridge handler (with the fix: defers via queueMicrotask)
36
+ const bridgeHandler = async () => {
37
+ // This is what the fixed bridge does for message_end:
38
+ await new Promise<void>(resolve => queueMicrotask(resolve));
39
+ capturedEntryId = sessionManager.getLeafId();
40
+ };
41
+
42
+ // Simulate pi core's _processAgentEvent:
43
+ // 1. _emit calls handler (async, NOT awaited)
44
+ const handlerPromise = bridgeHandler();
45
+ // 2. appendMessage runs synchronously, updating leafId
46
+ leafId = "assistant-entry-101";
47
+
48
+ // Wait for the deferred handler to complete
49
+ await handlerPromise;
50
+
51
+ expect(capturedEntryId).toBe("assistant-entry-101");
52
+ });
53
+
54
+ it("immediate getLeafId() would capture the stale entry ID (demonstrates the bug)", async () => {
55
+ let leafId = "user-entry-100";
56
+ const sessionManager = {
57
+ getLeafId: () => leafId,
58
+ };
59
+
60
+ let capturedEntryId: string | undefined;
61
+
62
+ // Simulate the OLD (buggy) bridge handler: reads getLeafId() immediately
63
+ const buggyBridgeHandler = async () => {
64
+ // No deferral — reads leafId before appendMessage runs
65
+ capturedEntryId = sessionManager.getLeafId();
66
+ };
67
+
68
+ // Simulate pi core's _processAgentEvent:
69
+ const handlerPromise = buggyBridgeHandler();
70
+ leafId = "assistant-entry-101"; // too late — handler already read it
71
+
72
+ await handlerPromise;
73
+
74
+ // Bug: captures the stale leaf, not the assistant's entry
75
+ expect(capturedEntryId).toBe("user-entry-100");
76
+ });
77
+
78
+ it("message_start should still capture entryId immediately (no deferral)", async () => {
79
+ let leafId = "previous-assistant-entry-99";
80
+ const sessionManager = {
81
+ getLeafId: () => leafId,
82
+ };
83
+
84
+ let capturedEntryId: string | undefined;
85
+
86
+ // Simulate bridge handler for message_start (immediate, no deferral)
87
+ const messageStartHandler = async () => {
88
+ capturedEntryId = sessionManager.getLeafId();
89
+ };
90
+
91
+ const handlerPromise = messageStartHandler();
92
+ // User entry gets written after message_start
93
+ leafId = "user-entry-100";
94
+
95
+ await handlerPromise;
96
+
97
+ // message_start should capture the leaf BEFORE the user entry is written
98
+ expect(capturedEntryId).toBe("previous-assistant-entry-99");
99
+ });
100
+ });
@@ -1,112 +1,124 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { gatherGitInfo, detectBranch, detectRemoteUrl, detectPrNumber } from "../git-info.js";
3
-
4
- const { execSyncMock } = vi.hoisted(() => ({
5
- execSyncMock: vi.fn(),
1
+ /**
2
+ * Tests for git-info.ts.
3
+ *
4
+ * The file now delegates to `@blackbelt-technology/pi-dashboard-shared/platform/git.js`
5
+ * (the Recipe-based tool module). We mock that module so the tests focus
6
+ * on the git-info orchestration logic (branch detection, detached HEAD
7
+ * fallback, PR detection) without spawning git.
8
+ *
9
+ * See change: platform-command-executor.
10
+ */
11
+ import { describe, it, expect, vi, beforeEach } from "vitest";
12
+
13
+ const { currentBranchOr, headShaOr, remoteUrlOr, prNumberOr } = vi.hoisted(() => ({
14
+ currentBranchOr: vi.fn(),
15
+ headShaOr: vi.fn(),
16
+ remoteUrlOr: vi.fn(),
17
+ prNumberOr: vi.fn(),
6
18
  }));
7
19
 
8
- vi.mock("node:child_process", () => ({
9
- default: { execSync: execSyncMock },
10
- execSync: execSyncMock,
20
+ vi.mock("@blackbelt-technology/pi-dashboard-shared/platform/git.js", () => ({
21
+ currentBranchOr,
22
+ headShaOr,
23
+ remoteUrlOr,
24
+ prNumberOr,
11
25
  }));
12
26
 
27
+ import { gatherGitInfo, detectBranch, detectRemoteUrl, detectPrNumber } from "../git-info.js";
28
+
13
29
  describe("git-info", () => {
14
30
  beforeEach(() => {
15
- execSyncMock.mockReset();
31
+ currentBranchOr.mockReset();
32
+ headShaOr.mockReset();
33
+ remoteUrlOr.mockReset();
34
+ prNumberOr.mockReset();
16
35
  });
17
36
 
18
37
  describe("detectBranch", () => {
19
38
  it("returns branch name", () => {
20
- execSyncMock.mockReturnValue("main\n");
39
+ currentBranchOr.mockReturnValue("main");
21
40
  expect(detectBranch("/test")).toBe("main");
22
41
  });
23
42
 
24
43
  it("returns undefined when not a git repo", () => {
25
- execSyncMock.mockImplementation(() => { throw new Error("not a git repo"); });
44
+ currentBranchOr.mockReturnValue(undefined);
26
45
  expect(detectBranch("/test")).toBeUndefined();
27
46
  });
28
47
 
29
48
  it("returns short SHA for detached HEAD", () => {
30
- execSyncMock
31
- .mockReturnValueOnce("HEAD\n") // rev-parse --abbrev-ref HEAD
32
- .mockReturnValueOnce("abc1234\n"); // rev-parse --short HEAD
49
+ currentBranchOr.mockReturnValue("HEAD");
50
+ headShaOr.mockReturnValue("abc1234");
33
51
  expect(detectBranch("/test")).toBe("abc1234");
34
52
  });
35
53
 
36
54
  it("returns 'HEAD' as fallback if short SHA fails", () => {
37
- execSyncMock
38
- .mockReturnValueOnce("HEAD\n")
39
- .mockImplementationOnce(() => { throw new Error("fail"); });
55
+ currentBranchOr.mockReturnValue("HEAD");
56
+ headShaOr.mockReturnValue(undefined);
40
57
  expect(detectBranch("/test")).toBe("HEAD");
41
58
  });
42
59
  });
43
60
 
44
61
  describe("detectRemoteUrl", () => {
45
- it("returns remote URL", () => {
46
- execSyncMock.mockReturnValue("git@github.com:user/repo.git\n");
47
- expect(detectRemoteUrl("/test")).toBe("git@github.com:user/repo.git");
62
+ it("returns origin remote URL", () => {
63
+ remoteUrlOr.mockReturnValue("git@github.com:org/repo.git");
64
+ expect(detectRemoteUrl("/test")).toBe("git@github.com:org/repo.git");
48
65
  });
49
66
 
50
- it("returns undefined when no origin", () => {
51
- execSyncMock.mockImplementation(() => { throw new Error("no remote"); });
67
+ it("returns undefined when no remote is configured", () => {
68
+ remoteUrlOr.mockReturnValue(undefined);
52
69
  expect(detectRemoteUrl("/test")).toBeUndefined();
53
70
  });
54
71
  });
55
72
 
56
73
  describe("detectPrNumber", () => {
57
- it("returns PR number from gh CLI", () => {
58
- execSyncMock.mockReturnValue("42\n");
74
+ it("returns PR number when gh finds one", () => {
75
+ prNumberOr.mockReturnValue(42);
59
76
  expect(detectPrNumber("/test")).toBe(42);
60
77
  });
61
78
 
62
- it("returns undefined when gh CLI fails", () => {
63
- execSyncMock.mockImplementation(() => { throw new Error("gh not found"); });
79
+ it("returns undefined when gh is missing or no PR exists", () => {
80
+ prNumberOr.mockReturnValue(undefined);
64
81
  expect(detectPrNumber("/test")).toBeUndefined();
65
82
  });
66
83
  });
67
84
 
68
85
  describe("gatherGitInfo", () => {
69
- it("returns full git info with links", () => {
70
- execSyncMock
71
- .mockReturnValueOnce("feat/foo\n") // branch
72
- .mockReturnValueOnce("git@github.com:user/repo.git\n") // remote
73
- .mockReturnValueOnce("7\n"); // PR
74
-
75
- const info = gatherGitInfo("/test");
76
- expect(info).toEqual({
77
- gitBranch: "feat/foo",
78
- gitBranchUrl: "https://github.com/user/repo/tree/feat%2Ffoo",
79
- gitPrNumber: 7,
80
- gitPrUrl: "https://github.com/user/repo/pull/7",
81
- });
82
- });
83
-
84
86
  it("returns undefined when not a git repo", () => {
85
- execSyncMock.mockImplementation(() => { throw new Error("not a git repo"); });
87
+ currentBranchOr.mockReturnValue(undefined);
86
88
  expect(gatherGitInfo("/test")).toBeUndefined();
87
89
  });
88
90
 
89
- it("returns info without PR when gh fails", () => {
90
- execSyncMock
91
- .mockReturnValueOnce("main\n")
92
- .mockReturnValueOnce("git@github.com:user/repo.git\n")
93
- .mockImplementationOnce(() => { throw new Error("gh not found"); });
91
+ it("returns GitInfo for a repo with branch + remote + PR", () => {
92
+ currentBranchOr.mockReturnValue("feature/x");
93
+ remoteUrlOr.mockReturnValue("git@github.com:org/repo.git");
94
+ prNumberOr.mockReturnValue(123);
94
95
 
95
96
  const info = gatherGitInfo("/test");
96
- expect(info?.gitBranch).toBe("main");
97
- expect(info?.gitPrNumber).toBeUndefined();
98
- expect(info?.gitPrUrl).toBeUndefined();
97
+ expect(info?.gitBranch).toBe("feature/x");
98
+ expect(info?.gitPrNumber).toBe(123);
99
+ // Branch URLs URL-encode slashes (feature/x → feature%2Fx) in some builders
100
+ expect(info?.gitBranchUrl).toMatch(/feature(\/|%2F)x/);
101
+ expect(info?.gitPrUrl).toContain("123");
99
102
  });
100
103
 
101
- it("returns info without links when no remote", () => {
102
- execSyncMock
103
- .mockReturnValueOnce("main\n")
104
- .mockImplementationOnce(() => { throw new Error("no remote"); })
105
- .mockImplementationOnce(() => { throw new Error("gh not found"); });
104
+ it("returns GitInfo without links when there's no remote", () => {
105
+ currentBranchOr.mockReturnValue("main");
106
+ remoteUrlOr.mockReturnValue(undefined);
107
+ prNumberOr.mockReturnValue(undefined);
106
108
 
107
109
  const info = gatherGitInfo("/test");
108
110
  expect(info?.gitBranch).toBe("main");
109
111
  expect(info?.gitBranchUrl).toBeUndefined();
110
112
  });
113
+
114
+ it("handles detached HEAD with short SHA", () => {
115
+ currentBranchOr.mockReturnValue("HEAD");
116
+ headShaOr.mockReturnValue("abc1234");
117
+ remoteUrlOr.mockReturnValue(undefined);
118
+ prNumberOr.mockReturnValue(undefined);
119
+
120
+ const info = gatherGitInfo("/test");
121
+ expect(info?.gitBranch).toBe("abc1234");
122
+ });
111
123
  });
112
124
  });
@@ -1,119 +1,124 @@
1
+ /**
2
+ * Tests for openspec-poller.ts — the higher-level aggregator that combines
3
+ * `openspec list` + per-change `openspec status` into the dashboard's
4
+ * `OpenSpecData` shape.
5
+ *
6
+ * The file now delegates to `platform/openspec.ts` for the subprocess work.
7
+ * We mock that module so the tests focus on the aggregation logic
8
+ * (empty results, artifact mapping, per-change status failures) without
9
+ * spawning openspec.
10
+ *
11
+ * See change: platform-command-executor.
12
+ */
1
13
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import type { SpawnSyncReturns } from "node:child_process";
3
-
4
- // We need to mock spawnSync before importing the module under test
5
- const mockSpawnSync = vi.fn<(...args: any[]) => any>();
6
- const mockExecFile = vi.fn<(...args: any[]) => any>();
7
- vi.mock("node:child_process", () => ({
8
- spawnSync: mockSpawnSync,
9
- execFile: mockExecFile,
10
- // re-export defaults that node:child_process has
11
- default: { spawnSync: mockSpawnSync, execFile: mockExecFile },
14
+
15
+ const { listOr, statusOr } = vi.hoisted(() => ({
16
+ listOr: vi.fn(),
17
+ statusOr: vi.fn(),
12
18
  }));
13
19
 
14
- // Import after mock is set up
15
- const { pollOpenSpec } = await import("@blackbelt-technology/pi-dashboard-shared/openspec-poller.js");
16
-
17
- function ok(data: unknown): Partial<SpawnSyncReturns<string>> {
18
- return { status: 0, stdout: JSON.stringify(data), stderr: "" };
19
- }
20
-
21
- function fail(): Partial<SpawnSyncReturns<string>> {
22
- return { status: 1, stdout: "", stderr: "error" };
23
- }
24
-
25
- function mockCli(responses: Map<string, unknown>) {
26
- mockSpawnSync.mockImplementation((_cmd: any, args: any) => {
27
- const a = args as string[];
28
- if (a.includes("list")) {
29
- const d = responses.get("list");
30
- return d ? ok(d) : fail();
31
- }
32
- if (a.includes("status")) {
33
- const idx = a.indexOf("--change");
34
- const name = idx >= 0 ? a[idx + 1] : "";
35
- const d = responses.get(`status:${name}`);
36
- return d ? ok(d) : fail();
37
- }
38
- return fail();
39
- });
40
- }
20
+ vi.mock("@blackbelt-technology/pi-dashboard-shared/platform/openspec.js", () => ({
21
+ listOr,
22
+ statusOr,
23
+ }));
41
24
 
42
- beforeEach(() => {
43
- mockSpawnSync.mockReset();
44
- });
25
+ import { pollOpenSpec } from "@blackbelt-technology/pi-dashboard-shared/openspec-poller.js";
45
26
 
46
27
  describe("pollOpenSpec", () => {
47
- it("returns initialized=false when CLI throws", () => {
48
- mockSpawnSync.mockImplementation(() => { throw new Error("ENOENT"); });
49
- const result = pollOpenSpec("/project");
50
- expect(result).toEqual({ initialized: false, changes: [] });
28
+ beforeEach(() => {
29
+ listOr.mockReset();
30
+ statusOr.mockReset();
51
31
  });
52
32
 
53
- it("returns initialized=false when list returns non-zero exit", () => {
54
- mockSpawnSync.mockReturnValue(fail());
55
- const result = pollOpenSpec("/project");
56
- expect(result).toEqual({ initialized: false, changes: [] });
33
+ it("returns initialized=false when list fails", () => {
34
+ listOr.mockReturnValue(null);
35
+ expect(pollOpenSpec("/test")).toEqual({ initialized: false, changes: [] });
36
+ });
37
+
38
+ it("returns initialized=false when list returns non-array changes", () => {
39
+ listOr.mockReturnValue({ changes: "not an array" });
40
+ expect(pollOpenSpec("/test")).toEqual({ initialized: false, changes: [] });
57
41
  });
58
42
 
59
43
  it("returns initialized=true with changes on success", () => {
60
- mockCli(new Map([
61
- ["list", {
62
- changes: [
63
- { name: "feat-a", status: "in-progress", completedTasks: 3, totalTasks: 5 },
64
- { name: "feat-b", status: "complete", completedTasks: 4, totalTasks: 4 },
65
- ],
66
- }],
67
- ["status:feat-a", {
68
- artifacts: [
69
- { id: "proposal", status: "done" },
70
- { id: "design", status: "ready" },
71
- { id: "tasks", status: "blocked" },
72
- ],
73
- }],
74
- ["status:feat-b", {
75
- artifacts: [
76
- { id: "proposal", status: "done" },
77
- { id: "tasks", status: "done" },
78
- ],
79
- }],
80
- ]));
81
-
82
- const result = pollOpenSpec("/project");
83
- expect(result.initialized).toBe(true);
84
- expect(result.changes).toHaveLength(2);
85
- expect(result.changes[0]).toEqual({
86
- name: "feat-a",
87
- status: "in-progress",
88
- completedTasks: 3,
89
- totalTasks: 5,
44
+ listOr.mockReturnValue({
45
+ changes: [
46
+ { name: "add-auth", status: "in-progress", completedTasks: 3, totalTasks: 10 },
47
+ ],
48
+ });
49
+ statusOr.mockReturnValue({
90
50
  artifacts: [
91
51
  { id: "proposal", status: "done" },
92
- { id: "design", status: "ready" },
93
- { id: "tasks", status: "blocked" },
52
+ { id: "tasks", status: "ready" },
94
53
  ],
95
54
  });
96
- expect(result.changes[1].status).toBe("complete");
97
- });
98
55
 
99
- it("handles invalid JSON gracefully", () => {
100
- mockSpawnSync.mockReturnValue({ status: 0, stdout: "not json {{{", stderr: "" });
101
- const result = pollOpenSpec("/project");
102
- expect(result).toEqual({ initialized: false, changes: [] });
56
+ const result = pollOpenSpec("/test");
57
+ expect(result.initialized).toBe(true);
58
+ expect(result.changes).toHaveLength(1);
59
+ expect(result.changes[0]).toMatchObject({
60
+ name: "add-auth",
61
+ status: "in-progress",
62
+ completedTasks: 3,
63
+ totalTasks: 10,
64
+ });
65
+ expect(result.changes[0].artifacts).toEqual([
66
+ { id: "proposal", status: "done" },
67
+ { id: "tasks", status: "ready" },
68
+ ]);
103
69
  });
104
70
 
105
71
  it("handles status call failure gracefully (empty artifacts)", () => {
106
- mockCli(new Map([
107
- ["list", {
108
- changes: [
109
- { name: "feat-a", status: "no-tasks", completedTasks: 0, totalTasks: 0 },
110
- ],
111
- }],
112
- // No status:feat-a — status call fails
113
- ]));
114
-
115
- const result = pollOpenSpec("/project");
72
+ listOr.mockReturnValue({
73
+ changes: [
74
+ { name: "x", status: "complete", completedTasks: 5, totalTasks: 5 },
75
+ ],
76
+ });
77
+ statusOr.mockReturnValue(null); // status failed
78
+
79
+ const result = pollOpenSpec("/test");
116
80
  expect(result.initialized).toBe(true);
117
81
  expect(result.changes[0].artifacts).toEqual([]);
118
82
  });
83
+
84
+ it("normalizes unknown status values to 'no-tasks'", () => {
85
+ listOr.mockReturnValue({
86
+ changes: [
87
+ { name: "x", status: "weird-future-status", completedTasks: 0, totalTasks: 0 },
88
+ ],
89
+ });
90
+ statusOr.mockReturnValue(null);
91
+
92
+ const result = pollOpenSpec("/test");
93
+ expect(result.changes[0].status).toBe("no-tasks");
94
+ });
95
+
96
+ it("normalizes unknown artifact statuses to 'blocked'", () => {
97
+ listOr.mockReturnValue({
98
+ changes: [
99
+ { name: "x", status: "in-progress", completedTasks: 0, totalTasks: 1 },
100
+ ],
101
+ });
102
+ statusOr.mockReturnValue({
103
+ artifacts: [{ id: "proposal", status: "some-new-state" }],
104
+ });
105
+
106
+ const result = pollOpenSpec("/test");
107
+ expect(result.changes[0].artifacts[0].status).toBe("blocked");
108
+ });
109
+
110
+ it("calls statusOr once per change, with the change name", () => {
111
+ listOr.mockReturnValue({
112
+ changes: [
113
+ { name: "a", status: "in-progress", completedTasks: 0, totalTasks: 1 },
114
+ { name: "b", status: "complete", completedTasks: 2, totalTasks: 2 },
115
+ ],
116
+ });
117
+ statusOr.mockReturnValue({ artifacts: [] });
118
+
119
+ pollOpenSpec("/test");
120
+ expect(statusOr).toHaveBeenCalledTimes(2);
121
+ expect(statusOr).toHaveBeenNthCalledWith(1, { cwd: "/test", change: "a" });
122
+ expect(statusOr).toHaveBeenNthCalledWith(2, { cwd: "/test", change: "b" });
123
+ });
119
124
  });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Tests that `killProcessByPgid` routes through the platform's
3
+ * `killPidWithGroup` helper (not raw `process.kill`).
4
+ *
5
+ * See change: route-kill-paths-through-platform.
6
+ */
7
+ import { describe, it, expect, vi, beforeEach } from "vitest";
8
+
9
+ const killPidWithGroupSpy = vi.fn((_pid: number, _sig: any, _opts?: any) => undefined);
10
+
11
+ vi.mock("@blackbelt-technology/pi-dashboard-shared/platform/process.js", async () => {
12
+ const actual = await vi.importActual<typeof import("@blackbelt-technology/pi-dashboard-shared/platform/process.js")>(
13
+ "@blackbelt-technology/pi-dashboard-shared/platform/process.js",
14
+ );
15
+ return {
16
+ ...actual,
17
+ killPidWithGroup: (pid: number, sig: any, opts?: any) => killPidWithGroupSpy(pid, sig, opts),
18
+ };
19
+ });
20
+
21
+ const { killProcessByPgid } = await import("../process-scanner.js");
22
+
23
+ describe("killProcessByPgid platform routing", () => {
24
+ beforeEach(() => {
25
+ killPidWithGroupSpy.mockClear();
26
+ });
27
+
28
+ it("invokes killPidWithGroup with the resolved platform on Unix", () => {
29
+ const ok = killProcessByPgid(4242, { _platform: "linux" } as any);
30
+ expect(ok).toBe(true);
31
+ expect(killPidWithGroupSpy).toHaveBeenCalledTimes(1);
32
+ const [pid, sig, opts] = killPidWithGroupSpy.mock.calls[0];
33
+ expect(pid).toBe(4242);
34
+ expect(sig).toBe("SIGTERM");
35
+ expect(opts?.platform).toBe("linux");
36
+ });
37
+
38
+ it("invokes killPidWithGroup with platform=darwin for macOS pgids", () => {
39
+ killProcessByPgid(9999, { _platform: "darwin" } as any);
40
+ const [pid, , opts] = killPidWithGroupSpy.mock.calls[0];
41
+ expect(pid).toBe(9999);
42
+ expect(opts?.platform).toBe("darwin");
43
+ });
44
+
45
+ it("does NOT call process.kill directly on Unix", () => {
46
+ const processKillSpy = vi.spyOn(process, "kill");
47
+ try {
48
+ killProcessByPgid(1234, { _platform: "linux" } as any);
49
+ } catch { /* ignore */ }
50
+ expect(processKillSpy).not.toHaveBeenCalled();
51
+ processKillSpy.mockRestore();
52
+ });
53
+
54
+ it("reports failure if killPidWithGroup throws", () => {
55
+ killPidWithGroupSpy.mockImplementationOnce(() => {
56
+ throw new Error("ESRCH");
57
+ });
58
+ const ok = killProcessByPgid(4242, { _platform: "linux" } as any);
59
+ expect(ok).toBe(false);
60
+ });
61
+ });