@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.1

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 (216) hide show
  1. package/AGENTS.md +87 -114
  2. package/README.md +408 -430
  3. package/docs/architecture.md +465 -12
  4. package/package.json +10 -5
  5. package/packages/extension/package.json +14 -4
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +40 -8
  7. package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
  8. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
  9. package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
  10. package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
  11. package/packages/extension/src/__tests__/git-info.test.ts +67 -55
  12. package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
  13. package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
  14. package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
  15. package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
  16. package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
  17. package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
  18. package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
  19. package/packages/extension/src/ask-user-tool.ts +5 -4
  20. package/packages/extension/src/bridge.ts +171 -17
  21. package/packages/extension/src/dev-build.ts +1 -1
  22. package/packages/extension/src/git-info.ts +9 -19
  23. package/packages/extension/src/multiselect-list.ts +146 -0
  24. package/packages/extension/src/multiselect-polyfill.ts +43 -0
  25. package/packages/extension/src/pi-env.d.ts +1 -0
  26. package/packages/extension/src/process-scanner.ts +72 -38
  27. package/packages/extension/src/provider-register.ts +304 -16
  28. package/packages/extension/src/server-auto-start.ts +27 -1
  29. package/packages/extension/src/server-launcher.ts +83 -27
  30. package/packages/server/package.json +16 -2
  31. package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
  32. package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
  33. package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
  34. package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
  35. package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
  36. package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
  37. package/packages/server/src/__tests__/config-api.test.ts +68 -0
  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-registry.test.ts +28 -15
  41. package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
  42. package/packages/server/src/__tests__/extension-register.test.ts +3 -1
  43. package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
  44. package/packages/server/src/__tests__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
  45. package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
  46. package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
  47. package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
  48. package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
  49. package/packages/server/src/__tests__/home-lock.test.ts +308 -0
  50. package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
  51. package/packages/server/src/__tests__/node-guard.test.ts +85 -0
  52. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
  53. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
  54. package/packages/server/src/__tests__/pi-version-skew.test.ts +237 -0
  55. package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
  56. package/packages/server/src/__tests__/process-manager.test.ts +45 -18
  57. package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
  58. package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
  59. package/packages/server/src/__tests__/restart-helper.test.ts +111 -0
  60. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
  61. package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
  62. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
  63. package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
  64. package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
  65. package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
  66. package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
  67. package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
  68. package/packages/server/src/__tests__/tunnel.test.ts +13 -7
  69. package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
  70. package/packages/server/src/bootstrap-queue.ts +130 -0
  71. package/packages/server/src/bootstrap-state.ts +131 -0
  72. package/packages/server/src/browse.ts +8 -3
  73. package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
  74. package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
  75. package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
  76. package/packages/server/src/cli.ts +310 -39
  77. package/packages/server/src/config-api.ts +16 -0
  78. package/packages/server/src/directory-service.ts +270 -39
  79. package/packages/server/src/editor-detection.ts +12 -9
  80. package/packages/server/src/editor-manager.ts +19 -4
  81. package/packages/server/src/editor-pid-registry.ts +9 -8
  82. package/packages/server/src/editor-registry.ts +22 -25
  83. package/packages/server/src/git-operations.ts +1 -1
  84. package/packages/server/src/headless-pid-registry.ts +7 -20
  85. package/packages/server/src/home-lock-release.ts +72 -0
  86. package/packages/server/src/home-lock.ts +389 -0
  87. package/packages/server/src/node-guard.ts +52 -0
  88. package/packages/server/src/package-manager-wrapper.ts +207 -47
  89. package/packages/server/src/pi-core-checker.ts +1 -1
  90. package/packages/server/src/pi-core-updater.ts +7 -1
  91. package/packages/server/src/pi-resource-scanner.ts +5 -8
  92. package/packages/server/src/pi-version-skew.ts +207 -0
  93. package/packages/server/src/preferences-store.ts +17 -3
  94. package/packages/server/src/process-manager.ts +403 -222
  95. package/packages/server/src/provider-probe.ts +234 -0
  96. package/packages/server/src/restart-helper.ts +141 -0
  97. package/packages/server/src/routes/bootstrap-routes.ts +88 -0
  98. package/packages/server/src/routes/openspec-routes.ts +25 -1
  99. package/packages/server/src/routes/pi-core-routes.ts +24 -1
  100. package/packages/server/src/routes/provider-auth-routes.ts +8 -8
  101. package/packages/server/src/routes/provider-routes.ts +43 -0
  102. package/packages/server/src/routes/recommended-routes.ts +10 -12
  103. package/packages/server/src/routes/system-routes.ts +20 -33
  104. package/packages/server/src/routes/tool-routes.ts +153 -0
  105. package/packages/server/src/server-pid.ts +5 -9
  106. package/packages/server/src/server.ts +211 -10
  107. package/packages/server/src/session-api.ts +77 -8
  108. package/packages/server/src/session-bootstrap.ts +17 -3
  109. package/packages/server/src/session-diff.ts +21 -21
  110. package/packages/server/src/terminal-manager.ts +61 -20
  111. package/packages/server/src/tunnel.ts +42 -28
  112. package/packages/shared/package.json +10 -3
  113. package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
  114. package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
  115. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
  116. package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
  117. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
  118. package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
  119. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
  120. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
  121. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
  122. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
  123. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
  124. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
  125. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
  126. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
  127. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
  128. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
  129. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
  130. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
  131. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
  132. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
  133. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
  134. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
  135. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
  136. package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
  137. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
  138. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
  139. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
  140. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
  141. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
  142. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
  143. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
  144. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
  145. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
  146. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
  147. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
  148. package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
  149. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
  150. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
  151. package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
  152. package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
  153. package/packages/shared/src/__tests__/config.test.ts +56 -0
  154. package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
  155. package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
  156. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
  157. package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
  158. package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
  159. package/packages/shared/src/__tests__/no-hardcoded-node-modules-paths.test.ts +176 -0
  160. package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
  161. package/packages/shared/src/__tests__/node-spawn.test.ts +210 -0
  162. package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
  163. package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
  164. package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
  165. package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
  166. package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
  167. package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
  168. package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
  169. package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
  170. package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
  171. package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
  172. package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
  173. package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
  174. package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
  175. package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
  176. package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
  177. package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
  178. package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
  179. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
  180. package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
  181. package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
  182. package/packages/shared/src/bootstrap-install.ts +212 -0
  183. package/packages/shared/src/bridge-register.ts +87 -20
  184. package/packages/shared/src/browser-protocol.ts +71 -1
  185. package/packages/shared/src/config.ts +87 -15
  186. package/packages/shared/src/managed-paths.ts +31 -4
  187. package/packages/shared/src/openspec-poller.ts +63 -46
  188. package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
  189. package/packages/shared/src/platform/commands.ts +100 -0
  190. package/packages/shared/src/platform/detached-spawn.ts +305 -0
  191. package/packages/shared/src/platform/exec.ts +220 -0
  192. package/packages/shared/src/platform/git.ts +155 -0
  193. package/packages/shared/src/platform/index.ts +16 -0
  194. package/packages/shared/src/platform/node-spawn.ts +154 -0
  195. package/packages/shared/src/platform/npm.ts +162 -0
  196. package/packages/shared/src/platform/openspec.ts +91 -0
  197. package/packages/shared/src/platform/paths.ts +276 -0
  198. package/packages/shared/src/platform/process-identify.ts +126 -0
  199. package/packages/shared/src/platform/process-scan.ts +94 -0
  200. package/packages/shared/src/platform/process.ts +168 -0
  201. package/packages/shared/src/platform/runner.ts +369 -0
  202. package/packages/shared/src/platform/shell.ts +44 -0
  203. package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
  204. package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
  205. package/packages/shared/src/protocol.ts +23 -0
  206. package/packages/shared/src/recommended-extensions.ts +18 -2
  207. package/packages/shared/src/resolve-jiti.ts +62 -3
  208. package/packages/shared/src/rest-api.ts +26 -0
  209. package/packages/shared/src/semaphore.ts +83 -0
  210. package/packages/shared/src/state-replay.ts +9 -0
  211. package/packages/shared/src/tool-registry/definitions.ts +434 -0
  212. package/packages/shared/src/tool-registry/index.ts +56 -0
  213. package/packages/shared/src/tool-registry/overrides.ts +118 -0
  214. package/packages/shared/src/tool-registry/registry.ts +262 -0
  215. package/packages/shared/src/tool-registry/strategies.ts +198 -0
  216. package/packages/shared/src/tool-registry/types.ts +180 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Unit tests for the pi version-skew detection module.
3
+ *
4
+ * See change: unified-bootstrap-install \u00a79.
5
+ */
6
+ import { describe, it, expect, beforeEach } from "vitest";
7
+ import fs from "node:fs";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import {
11
+ parseVersion,
12
+ compareVersions,
13
+ isBelow,
14
+ isAbove,
15
+ readPiCompatibility,
16
+ readCurrentPiVersion,
17
+ computeCompatibility,
18
+ _resetVersionSkewCache,
19
+ } from "../pi-version-skew.js";
20
+ import type { ToolRegistry, Resolution } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
21
+
22
+ describe("pi-version-skew", () => {
23
+ beforeEach(() => {
24
+ _resetVersionSkewCache();
25
+ });
26
+
27
+ describe("parseVersion", () => {
28
+ it("parses simple x.y.z", () => {
29
+ expect(parseVersion("1.2.3")).toEqual([1, 2, 3]);
30
+ });
31
+ it("parses with v prefix", () => {
32
+ expect(parseVersion("v0.6.7")).toEqual([0, 6, 7]);
33
+ });
34
+ it("ignores pre-release suffix", () => {
35
+ expect(parseVersion("0.6.7-beta.1")).toEqual([0, 6, 7]);
36
+ });
37
+ it("ignores build metadata", () => {
38
+ expect(parseVersion("0.6.7+abc")).toEqual([0, 6, 7]);
39
+ });
40
+ it("returns null for non-numeric", () => {
41
+ expect(parseVersion("latest")).toBeNull();
42
+ expect(parseVersion("")).toBeNull();
43
+ });
44
+ });
45
+
46
+ describe("compareVersions", () => {
47
+ it("equal versions", () => {
48
+ expect(compareVersions("0.6.7", "0.6.7")).toBe(0);
49
+ });
50
+ it("lower major", () => {
51
+ expect(compareVersions("0.9.9", "1.0.0")).toBe(-1);
52
+ });
53
+ it("higher major", () => {
54
+ expect(compareVersions("2.0.0", "1.9.9")).toBe(1);
55
+ });
56
+ it("lower minor", () => {
57
+ expect(compareVersions("0.5.7", "0.6.0")).toBe(-1);
58
+ });
59
+ it("lower patch", () => {
60
+ expect(compareVersions("0.6.6", "0.6.7")).toBe(-1);
61
+ });
62
+ it("unparseable sorts as equal (conservative)", () => {
63
+ expect(compareVersions("latest", "0.6.7")).toBe(0);
64
+ });
65
+ });
66
+
67
+ describe("isBelow / isAbove", () => {
68
+ it("isBelow", () => {
69
+ expect(isBelow("0.5.0", "0.6.7")).toBe(true);
70
+ expect(isBelow("0.6.7", "0.6.7")).toBe(false);
71
+ expect(isBelow("0.7.0", "0.6.7")).toBe(false);
72
+ });
73
+ it("isAbove with .x wildcard", () => {
74
+ expect(isAbove("0.10.0", "0.9.x")).toBe(true);
75
+ expect(isAbove("0.9.5", "0.9.x")).toBe(false);
76
+ expect(isAbove("0.9.99998", "0.9.x")).toBe(false);
77
+ });
78
+ it("isAbove with concrete version", () => {
79
+ expect(isAbove("1.0.1", "1.0.0")).toBe(true);
80
+ expect(isAbove("1.0.0", "1.0.0")).toBe(false);
81
+ });
82
+ });
83
+
84
+ describe("readPiCompatibility", () => {
85
+ let tmpDir: string;
86
+
87
+ beforeEach(() => {
88
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-skew-"));
89
+ });
90
+
91
+ it("reads the field from a well-formed package.json", () => {
92
+ const pkg = path.join(tmpDir, "package.json");
93
+ fs.writeFileSync(
94
+ pkg,
95
+ JSON.stringify({ piCompatibility: { minimum: "1.0.0", recommended: "1.2.0", maximum: "2.x" } }),
96
+ );
97
+ expect(readPiCompatibility(pkg)).toEqual({
98
+ minimum: "1.0.0",
99
+ recommended: "1.2.0",
100
+ maximum: "2.x",
101
+ });
102
+ });
103
+
104
+ it("tolerates null maximum", () => {
105
+ const pkg = path.join(tmpDir, "package.json");
106
+ fs.writeFileSync(
107
+ pkg,
108
+ JSON.stringify({ piCompatibility: { minimum: "1.0.0", recommended: "1.2.0", maximum: null } }),
109
+ );
110
+ expect(readPiCompatibility(pkg).maximum).toBeNull();
111
+ });
112
+
113
+ it("falls back to defaults when field is missing", () => {
114
+ const pkg = path.join(tmpDir, "package.json");
115
+ fs.writeFileSync(pkg, JSON.stringify({ name: "something" }));
116
+ expect(readPiCompatibility(pkg)).toEqual({
117
+ minimum: "0.6.7",
118
+ recommended: "0.6.7",
119
+ maximum: null,
120
+ });
121
+ });
122
+
123
+ it("falls back to defaults when file is unreadable", () => {
124
+ expect(readPiCompatibility("/does/not/exist")).toEqual({
125
+ minimum: "0.6.7",
126
+ recommended: "0.6.7",
127
+ maximum: null,
128
+ });
129
+ });
130
+ });
131
+
132
+ describe("computeCompatibility", () => {
133
+ const range = { minimum: "0.6.7", recommended: "0.6.7", maximum: null };
134
+
135
+ it("returns range unchanged when pi is not yet installed", () => {
136
+ expect(computeCompatibility(range, undefined)).toEqual({ ...range, current: undefined });
137
+ });
138
+
139
+ it("flags upgradeRecommended when below minimum", () => {
140
+ const out = computeCompatibility(range, "0.5.0");
141
+ expect(out.current).toBe("0.5.0");
142
+ expect(out.upgradeRecommended).toBe(true);
143
+ });
144
+
145
+ it("flags upgradeRecommended when below recommended (but >= minimum)", () => {
146
+ const out = computeCompatibility(
147
+ { minimum: "0.5.0", recommended: "0.6.7", maximum: null },
148
+ "0.6.0",
149
+ );
150
+ expect(out.upgradeRecommended).toBe(true);
151
+ });
152
+
153
+ it("no upgrade flag when at or above recommended", () => {
154
+ const out = computeCompatibility(range, "0.6.7");
155
+ expect(out.upgradeRecommended).toBeUndefined();
156
+ expect(out.upgradeDashboard).toBeUndefined();
157
+ });
158
+
159
+ it("flags upgradeDashboard when above maximum", () => {
160
+ const out = computeCompatibility(
161
+ { minimum: "0.6.7", recommended: "0.6.7", maximum: "0.9.x" },
162
+ "0.10.0",
163
+ );
164
+ expect(out.upgradeDashboard).toBe(true);
165
+ });
166
+ });
167
+
168
+ // See change: warn-pi-version-skew-in-cli.
169
+ describe("readCurrentPiVersion (realpath symlinks)", () => {
170
+ let tmpDir: string;
171
+
172
+ beforeEach(() => {
173
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-skew-realpath-"));
174
+ });
175
+
176
+ function stubRegistry(resolvedPath: string): ToolRegistry {
177
+ return {
178
+ resolve: (name: string): Resolution => ({
179
+ ok: true,
180
+ name,
181
+ path: resolvedPath,
182
+ source: "system",
183
+ tried: [],
184
+ resolvedAt: Date.now(),
185
+ }),
186
+ } as unknown as ToolRegistry;
187
+ }
188
+
189
+ it("npm-global symlinked bin launcher resolves to the real package.json", () => {
190
+ // Simulate ~/.nvm/.../bin/pi → ../lib/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
191
+ const nodeRoot = path.join(tmpDir, "node-install");
192
+ const binDir = path.join(nodeRoot, "bin");
193
+ const pkgDir = path.join(nodeRoot, "lib", "node_modules", "@mariozechner", "pi-coding-agent");
194
+ const distDir = path.join(pkgDir, "dist");
195
+ fs.mkdirSync(binDir, { recursive: true });
196
+ fs.mkdirSync(distDir, { recursive: true });
197
+ fs.writeFileSync(path.join(distDir, "cli.js"), "// stub");
198
+ fs.writeFileSync(
199
+ path.join(pkgDir, "package.json"),
200
+ JSON.stringify({ name: "@mariozechner/pi-coding-agent", version: "0.70.0" }),
201
+ );
202
+ // The bad path (what old code computed) must NOT exist.
203
+ // That is: nodeRoot/package.json. We leave it absent.
204
+
205
+ const binLink = path.join(binDir, "pi");
206
+ // relative symlink matches npm's install layout.
207
+ fs.symlinkSync(
208
+ path.relative(binDir, path.join(distDir, "cli.js")),
209
+ binLink,
210
+ );
211
+
212
+ const registry = stubRegistry(binLink);
213
+ expect(readCurrentPiVersion(registry)).toBe("0.70.0");
214
+ });
215
+
216
+ it("non-symlinked path is a no-op under realpath", () => {
217
+ const pkgDir = path.join(tmpDir, "pkg");
218
+ const distDir = path.join(pkgDir, "dist");
219
+ fs.mkdirSync(distDir, { recursive: true });
220
+ const cli = path.join(distDir, "cli.js");
221
+ fs.writeFileSync(cli, "// stub");
222
+ fs.writeFileSync(
223
+ path.join(pkgDir, "package.json"),
224
+ JSON.stringify({ name: "@mariozechner/pi-coding-agent", version: "0.69.0" }),
225
+ );
226
+ const registry = stubRegistry(cli);
227
+ expect(readCurrentPiVersion(registry)).toBe("0.69.0");
228
+ });
229
+
230
+ it("dangling symlink returns undefined", () => {
231
+ const link = path.join(tmpDir, "dangling-pi");
232
+ fs.symlinkSync(path.join(tmpDir, "does-not-exist", "cli.js"), link);
233
+ const registry = stubRegistry(link);
234
+ expect(readCurrentPiVersion(registry)).toBeUndefined();
235
+ });
236
+ });
237
+ });
@@ -9,6 +9,14 @@ vi.mock("../resolve-path.js", () => ({
9
9
  safeRealpathSync: (p: string) => p,
10
10
  }));
11
11
 
12
+ // Canonical host-platform absolute paths. Using raw POSIX strings like
13
+ // `/a` would normalize to `B:\a` on Windows (path.win32.resolve prepends
14
+ // the current drive), breaking assertions. These constants produce paths
15
+ // that survive `normalizePath` unchanged on their host platform.
16
+ const A_PATH = path.resolve(os.tmpdir(), "pref-a");
17
+ const B_PATH = path.resolve(os.tmpdir(), "pref-b");
18
+ const X_PATH = path.resolve(os.tmpdir(), "pref-x");
19
+
12
20
  describe("preferences-store", () => {
13
21
  let tmpDir: string;
14
22
  let filePath: string;
@@ -33,12 +41,12 @@ describe("preferences-store", () => {
33
41
 
34
42
  it("should load existing preferences", () => {
35
43
  fs.writeFileSync(filePath, JSON.stringify({
36
- pinnedDirectories: ["/a", "/b"],
37
- sessionOrder: { "/a": ["s1", "s2"] },
44
+ pinnedDirectories: [A_PATH, B_PATH],
45
+ sessionOrder: { [A_PATH]: ["s1", "s2"] },
38
46
  }));
39
47
  const store = createPreferencesStore(filePath);
40
- expect(store.getPinnedDirectories()).toEqual(["/a", "/b"]);
41
- expect(store.getSessionOrder()).toEqual({ "/a": ["s1", "s2"] });
48
+ expect(store.getPinnedDirectories()).toEqual([A_PATH, B_PATH]);
49
+ expect(store.getSessionOrder()).toEqual({ [A_PATH]: ["s1", "s2"] });
42
50
  store.dispose();
43
51
  });
44
52
 
@@ -97,6 +105,67 @@ describe("preferences-store", () => {
97
105
  store.dispose();
98
106
  });
99
107
 
108
+ // ── Normalize-on-load migration (platform-path-normalization) ───────────
109
+
110
+ it("normalizes drifty pinned paths on load", () => {
111
+ // Seed a file with the kinds of drift that existed pre-normalization:
112
+ // trailing separators, `.` / `..` segments, duplicate separators. The
113
+ // store should collapse them to canonical form on first read.
114
+ fs.writeFileSync(filePath, JSON.stringify({
115
+ pinnedDirectories: [
116
+ process.platform === "win32"
117
+ ? "C:\\Users\\me\\Dev\\" // trailing separator
118
+ : "/Users/me/Dev/",
119
+ process.platform === "win32"
120
+ ? "C:\\Users\\me\\Dev\\.\\BB" // `.` segment
121
+ : "/Users/me/Dev/./BB",
122
+ ],
123
+ sessionOrder: {},
124
+ }));
125
+ const store = createPreferencesStore(filePath);
126
+ const pinned = store.getPinnedDirectories();
127
+ expect(pinned).toHaveLength(2);
128
+ // Expect canonical forms (trailing separator stripped, `.` resolved).
129
+ if (process.platform === "win32") {
130
+ expect(pinned[0]).toBe("C:\\Users\\me\\Dev");
131
+ expect(pinned[1]).toBe("C:\\Users\\me\\Dev\\BB");
132
+ } else {
133
+ expect(pinned[0]).toBe("/Users/me/Dev");
134
+ expect(pinned[1]).toBe("/Users/me/Dev/BB");
135
+ }
136
+ store.dispose();
137
+ });
138
+
139
+ it("deduplicates entries that collapse to the same canonical form", () => {
140
+ // Two different-looking entries that normalize to the same path must
141
+ // become one stored entry.
142
+ const entries = process.platform === "win32"
143
+ ? ["C:\\Users\\me", "C:\\Users\\me\\", "C:/Users/me"]
144
+ : ["/Users/me", "/Users/me/", "/Users/./me"];
145
+ fs.writeFileSync(filePath, JSON.stringify({
146
+ pinnedDirectories: entries,
147
+ sessionOrder: {},
148
+ }));
149
+ const store = createPreferencesStore(filePath);
150
+ expect(store.getPinnedDirectories()).toHaveLength(1);
151
+ store.dispose();
152
+ });
153
+
154
+ it("persists the normalized form back to disk on first debounce", () => {
155
+ fs.writeFileSync(filePath, JSON.stringify({
156
+ pinnedDirectories: [
157
+ process.platform === "win32" ? "C:\\Users\\me\\" : "/Users/me/",
158
+ ],
159
+ sessionOrder: {},
160
+ }));
161
+ const store = createPreferencesStore(filePath);
162
+ vi.advanceTimersByTime(1000);
163
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
164
+ const expected = process.platform === "win32" ? "C:\\Users\\me" : "/Users/me";
165
+ expect(data.pinnedDirectories).toEqual([expected]);
166
+ store.dispose();
167
+ });
168
+
100
169
  it("should not contain hiddenSessions in output", () => {
101
170
  const store = createPreferencesStore(filePath);
102
171
  store.pinDirectory("/a");
@@ -1,24 +1,12 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
- import { detectPlatform, buildTmuxCommand, buildHeadlessArgs, shellEscape, spawnPiSession, buildSpawnEnv, type SessionOptions } from "../process-manager.js";
2
+ import { buildTmuxCommand, buildHeadlessArgs, shellEscape, spawnPiSession, buildSpawnEnv, type SessionOptions } from "../process-manager.js";
3
3
 
4
- describe("Process Manager", () => {
5
- describe("detectPlatform", () => {
6
- it("should detect macOS", () => {
7
- const result = detectPlatform("darwin");
8
- expect(result.strategy).toBe("tmux");
9
- });
10
-
11
- it("should detect Linux", () => {
12
- const result = detectPlatform("linux");
13
- expect(result.strategy).toBe("tmux");
14
- });
15
-
16
- it("should detect Windows with WSL fallback", () => {
17
- const result = detectPlatform("win32");
18
- expect(result.strategy).toBe("wsl");
19
- });
20
- });
4
+ // Note: platform-dispatch tests live in packages/shared/src/__tests__/
5
+ // spawn-mechanism.test.ts. `detectPlatform` was removed in change:
6
+ // consolidate-windows-spawn-and-platform-handlers its job is now
7
+ // owned by platform/spawn-mechanism.ts `selectMechanism`.
21
8
 
9
+ describe("Process Manager", () => {
22
10
  describe("buildTmuxCommand", () => {
23
11
  it("should create new session when no pi-dashboard session exists", () => {
24
12
  const cmd = buildTmuxCommand("/home/user/project", false);
@@ -184,4 +172,43 @@ describe("Process Manager", () => {
184
172
  expect(result.message).toContain("does not exist");
185
173
  });
186
174
  });
175
+
176
+ // ── Fork/continue option forwarding ──────────────────────────────────────
177
+ // Regression guard for B1/B2: Windows WSL/cmd fallback used to drop
178
+ // sessionFile + mode silently. buildTmuxCommand and buildHeadlessArgs
179
+ // both go through `sessionFlagsToArgv`; make sure neither drops.
180
+ describe("session-flag forwarding", () => {
181
+ it("buildHeadlessArgs includes --fork for fork mode", () => {
182
+ const args = buildHeadlessArgs({ sessionFile: "C:\\x\\session.jsonl", mode: "fork" });
183
+ expect(args).toEqual(["--mode", "rpc", "--fork", "C:\\x\\session.jsonl"]);
184
+ });
185
+
186
+ it("buildHeadlessArgs includes --session for continue mode", () => {
187
+ const args = buildHeadlessArgs({ sessionFile: "/s/abc.jsonl", mode: "continue" });
188
+ expect(args).toEqual(["--mode", "rpc", "--session", "/s/abc.jsonl"]);
189
+ });
190
+
191
+ it("buildHeadlessArgs omits session flags when absent", () => {
192
+ const args = buildHeadlessArgs({});
193
+ expect(args).toEqual(["--mode", "rpc"]);
194
+ });
195
+
196
+ it("buildTmuxCommand includes --fork in the pi command", () => {
197
+ const cmd = buildTmuxCommand("/project", false, { sessionFile: "/s/abc.jsonl", mode: "fork" });
198
+ expect(cmd).toContain("pi --fork /s/abc.jsonl");
199
+ });
200
+
201
+ it("buildTmuxCommand includes --session in the pi command", () => {
202
+ const cmd = buildTmuxCommand("/project", false, { sessionFile: "/s/abc.jsonl", mode: "continue" });
203
+ expect(cmd).toContain("pi --session /s/abc.jsonl");
204
+ });
205
+
206
+ it("buildTmuxCommand with special-character sessionFile still shell-escapes", () => {
207
+ const cmd = buildTmuxCommand("/project", false, {
208
+ sessionFile: "/s/with space.jsonl",
209
+ mode: "fork",
210
+ });
211
+ expect(cmd).toContain("--fork '/s/with space.jsonl'");
212
+ });
213
+ });
187
214
  });