@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,165 @@
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
+ computeCompatibility,
17
+ _resetVersionSkewCache,
18
+ } from "../pi-version-skew.js";
19
+
20
+ describe("pi-version-skew", () => {
21
+ beforeEach(() => {
22
+ _resetVersionSkewCache();
23
+ });
24
+
25
+ describe("parseVersion", () => {
26
+ it("parses simple x.y.z", () => {
27
+ expect(parseVersion("1.2.3")).toEqual([1, 2, 3]);
28
+ });
29
+ it("parses with v prefix", () => {
30
+ expect(parseVersion("v0.6.7")).toEqual([0, 6, 7]);
31
+ });
32
+ it("ignores pre-release suffix", () => {
33
+ expect(parseVersion("0.6.7-beta.1")).toEqual([0, 6, 7]);
34
+ });
35
+ it("ignores build metadata", () => {
36
+ expect(parseVersion("0.6.7+abc")).toEqual([0, 6, 7]);
37
+ });
38
+ it("returns null for non-numeric", () => {
39
+ expect(parseVersion("latest")).toBeNull();
40
+ expect(parseVersion("")).toBeNull();
41
+ });
42
+ });
43
+
44
+ describe("compareVersions", () => {
45
+ it("equal versions", () => {
46
+ expect(compareVersions("0.6.7", "0.6.7")).toBe(0);
47
+ });
48
+ it("lower major", () => {
49
+ expect(compareVersions("0.9.9", "1.0.0")).toBe(-1);
50
+ });
51
+ it("higher major", () => {
52
+ expect(compareVersions("2.0.0", "1.9.9")).toBe(1);
53
+ });
54
+ it("lower minor", () => {
55
+ expect(compareVersions("0.5.7", "0.6.0")).toBe(-1);
56
+ });
57
+ it("lower patch", () => {
58
+ expect(compareVersions("0.6.6", "0.6.7")).toBe(-1);
59
+ });
60
+ it("unparseable sorts as equal (conservative)", () => {
61
+ expect(compareVersions("latest", "0.6.7")).toBe(0);
62
+ });
63
+ });
64
+
65
+ describe("isBelow / isAbove", () => {
66
+ it("isBelow", () => {
67
+ expect(isBelow("0.5.0", "0.6.7")).toBe(true);
68
+ expect(isBelow("0.6.7", "0.6.7")).toBe(false);
69
+ expect(isBelow("0.7.0", "0.6.7")).toBe(false);
70
+ });
71
+ it("isAbove with .x wildcard", () => {
72
+ expect(isAbove("0.10.0", "0.9.x")).toBe(true);
73
+ expect(isAbove("0.9.5", "0.9.x")).toBe(false);
74
+ expect(isAbove("0.9.99998", "0.9.x")).toBe(false);
75
+ });
76
+ it("isAbove with concrete version", () => {
77
+ expect(isAbove("1.0.1", "1.0.0")).toBe(true);
78
+ expect(isAbove("1.0.0", "1.0.0")).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe("readPiCompatibility", () => {
83
+ let tmpDir: string;
84
+
85
+ beforeEach(() => {
86
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-skew-"));
87
+ });
88
+
89
+ it("reads the field from a well-formed package.json", () => {
90
+ const pkg = path.join(tmpDir, "package.json");
91
+ fs.writeFileSync(
92
+ pkg,
93
+ JSON.stringify({ piCompatibility: { minimum: "1.0.0", recommended: "1.2.0", maximum: "2.x" } }),
94
+ );
95
+ expect(readPiCompatibility(pkg)).toEqual({
96
+ minimum: "1.0.0",
97
+ recommended: "1.2.0",
98
+ maximum: "2.x",
99
+ });
100
+ });
101
+
102
+ it("tolerates null maximum", () => {
103
+ const pkg = path.join(tmpDir, "package.json");
104
+ fs.writeFileSync(
105
+ pkg,
106
+ JSON.stringify({ piCompatibility: { minimum: "1.0.0", recommended: "1.2.0", maximum: null } }),
107
+ );
108
+ expect(readPiCompatibility(pkg).maximum).toBeNull();
109
+ });
110
+
111
+ it("falls back to defaults when field is missing", () => {
112
+ const pkg = path.join(tmpDir, "package.json");
113
+ fs.writeFileSync(pkg, JSON.stringify({ name: "something" }));
114
+ expect(readPiCompatibility(pkg)).toEqual({
115
+ minimum: "0.6.7",
116
+ recommended: "0.6.7",
117
+ maximum: null,
118
+ });
119
+ });
120
+
121
+ it("falls back to defaults when file is unreadable", () => {
122
+ expect(readPiCompatibility("/does/not/exist")).toEqual({
123
+ minimum: "0.6.7",
124
+ recommended: "0.6.7",
125
+ maximum: null,
126
+ });
127
+ });
128
+ });
129
+
130
+ describe("computeCompatibility", () => {
131
+ const range = { minimum: "0.6.7", recommended: "0.6.7", maximum: null };
132
+
133
+ it("returns range unchanged when pi is not yet installed", () => {
134
+ expect(computeCompatibility(range, undefined)).toEqual({ ...range, current: undefined });
135
+ });
136
+
137
+ it("flags upgradeRecommended when below minimum", () => {
138
+ const out = computeCompatibility(range, "0.5.0");
139
+ expect(out.current).toBe("0.5.0");
140
+ expect(out.upgradeRecommended).toBe(true);
141
+ });
142
+
143
+ it("flags upgradeRecommended when below recommended (but >= minimum)", () => {
144
+ const out = computeCompatibility(
145
+ { minimum: "0.5.0", recommended: "0.6.7", maximum: null },
146
+ "0.6.0",
147
+ );
148
+ expect(out.upgradeRecommended).toBe(true);
149
+ });
150
+
151
+ it("no upgrade flag when at or above recommended", () => {
152
+ const out = computeCompatibility(range, "0.6.7");
153
+ expect(out.upgradeRecommended).toBeUndefined();
154
+ expect(out.upgradeDashboard).toBeUndefined();
155
+ });
156
+
157
+ it("flags upgradeDashboard when above maximum", () => {
158
+ const out = computeCompatibility(
159
+ { minimum: "0.6.7", recommended: "0.6.7", maximum: "0.9.x" },
160
+ "0.10.0",
161
+ );
162
+ expect(out.upgradeDashboard).toBe(true);
163
+ });
164
+ });
165
+ });
@@ -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
  });
@@ -44,14 +44,22 @@ function createMockPiGateway() {
44
44
  } as any;
45
45
  }
46
46
 
47
+ function createMockBrowserGateway() {
48
+ return {
49
+ broadcastToAll: vi.fn(),
50
+ } as any;
51
+ }
52
+
47
53
  describe("provider-auth-routes", () => {
48
54
  let app: ReturnType<typeof Fastify>;
49
55
  let piGateway: ReturnType<typeof createMockPiGateway>;
56
+ let browserGateway: ReturnType<typeof createMockBrowserGateway>;
50
57
 
51
58
  beforeEach(async () => {
52
59
  app = Fastify();
53
60
  piGateway = createMockPiGateway();
54
- registerProviderAuthRoutes(app, { piGateway });
61
+ browserGateway = createMockBrowserGateway();
62
+ registerProviderAuthRoutes(app, { piGateway, browserGateway });
55
63
  await app.ready();
56
64
  });
57
65
 
@@ -106,7 +114,7 @@ describe("provider-auth-routes", () => {
106
114
 
107
115
  // /exchange endpoint removed — token exchange happens in the callback server's onCode
108
116
 
109
- it("PUT /api/provider-auth/api-key saves and notifies", async () => {
117
+ it("PUT /api/provider-auth/api-key saves and notifies bridges and browsers", async () => {
110
118
  const { writeCredential } = await import("../provider-auth-storage.js");
111
119
  const res = await app.inject({
112
120
  method: "PUT",
@@ -117,9 +125,10 @@ describe("provider-auth-routes", () => {
117
125
  expect(JSON.parse(res.payload).ok).toBe(true);
118
126
  expect(writeCredential).toHaveBeenCalledWith("openai", { type: "api_key", key: "sk-test" });
119
127
  expect(piGateway.broadcast).toHaveBeenCalledWith({ type: "credentials_updated" });
128
+ expect(browserGateway.broadcastToAll).toHaveBeenCalledWith({ type: "models_refreshed" });
120
129
  });
121
130
 
122
- it("DELETE /api/provider-auth/:provider removes and notifies", async () => {
131
+ it("DELETE /api/provider-auth/:provider removes and notifies bridges and browsers", async () => {
123
132
  const { removeCredential } = await import("../provider-auth-storage.js");
124
133
  const res = await app.inject({
125
134
  method: "DELETE",
@@ -128,6 +137,7 @@ describe("provider-auth-routes", () => {
128
137
  expect(res.statusCode).toBe(200);
129
138
  expect(removeCredential).toHaveBeenCalledWith("anthropic");
130
139
  expect(piGateway.broadcast).toHaveBeenCalledWith({ type: "credentials_updated" });
140
+ expect(browserGateway.broadcastToAll).toHaveBeenCalledWith({ type: "models_refreshed" });
131
141
  });
132
142
 
133
143
  // /callback/:provider route removed — temp callback server handles this directly
@@ -0,0 +1,287 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import {
3
+ buildProbeRequest,
4
+ resolveProbeApiKey,
5
+ probeProvider,
6
+ type ProbeInput,
7
+ } from "../provider-probe.js";
8
+
9
+ describe("buildProbeRequest", () => {
10
+ it("openai-completions: GET {baseUrl}/models with Authorization: Bearer", () => {
11
+ const req = buildProbeRequest({
12
+ baseUrl: "https://api.example.com/v1",
13
+ apiKey: "sk-abc",
14
+ api: "openai-completions",
15
+ });
16
+ expect(req.url).toBe("https://api.example.com/v1/models");
17
+ expect(req.headers.Authorization).toBe("Bearer sk-abc");
18
+ });
19
+
20
+ it("openai-completions: handles trailing slash on baseUrl", () => {
21
+ const req = buildProbeRequest({
22
+ baseUrl: "https://api.example.com/v1/",
23
+ apiKey: "sk-abc",
24
+ api: "openai-completions",
25
+ });
26
+ expect(req.url).toBe("https://api.example.com/v1/models");
27
+ });
28
+
29
+ it("openai-responses: same shape as openai-completions", () => {
30
+ const req = buildProbeRequest({
31
+ baseUrl: "https://api.example.com/v1",
32
+ apiKey: "sk-abc",
33
+ api: "openai-responses",
34
+ });
35
+ expect(req.url).toBe("https://api.example.com/v1/models");
36
+ expect(req.headers.Authorization).toBe("Bearer sk-abc");
37
+ });
38
+
39
+ it("anthropic-messages: x-api-key + anthropic-version, no Authorization", () => {
40
+ const req = buildProbeRequest({
41
+ baseUrl: "https://api.anthropic.com",
42
+ apiKey: "sk-ant-123",
43
+ api: "anthropic-messages",
44
+ });
45
+ expect(req.url).toBe("https://api.anthropic.com/v1/models");
46
+ expect(req.headers["x-api-key"]).toBe("sk-ant-123");
47
+ expect(req.headers["anthropic-version"]).toBe("2023-06-01");
48
+ expect(req.headers.Authorization).toBeUndefined();
49
+ });
50
+
51
+ it("google-generative-ai: key query param, no Authorization", () => {
52
+ const req = buildProbeRequest({
53
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta",
54
+ apiKey: "AIza abc+def",
55
+ api: "google-generative-ai",
56
+ });
57
+ // apiKey must be URL-encoded
58
+ expect(req.url).toBe(
59
+ "https://generativelanguage.googleapis.com/v1beta/models?key=AIza%20abc%2Bdef",
60
+ );
61
+ expect(req.headers.Authorization).toBeUndefined();
62
+ });
63
+
64
+ it("throws on unknown api type", () => {
65
+ expect(() =>
66
+ buildProbeRequest({
67
+ baseUrl: "https://x",
68
+ apiKey: "k",
69
+ api: "unknown-api" as any,
70
+ }),
71
+ ).toThrow(/unsupported api/i);
72
+ });
73
+ });
74
+
75
+ describe("resolveProbeApiKey", () => {
76
+ const ORIGINAL_ENV = { ...process.env };
77
+ beforeEach(() => {
78
+ process.env = { ...ORIGINAL_ENV };
79
+ });
80
+ afterEach(() => {
81
+ process.env = { ...ORIGINAL_ENV };
82
+ });
83
+
84
+ it("literal key passes through", () => {
85
+ const result = resolveProbeApiKey({ apiKey: "sk-abc", readProviders: () => ({}) });
86
+ expect(result).toEqual({ ok: true, key: "sk-abc" });
87
+ });
88
+
89
+ it("$ENV_VAR: reads from process.env when set", () => {
90
+ process.env.MY_LLM_KEY = "resolved-value";
91
+ const result = resolveProbeApiKey({ apiKey: "$MY_LLM_KEY", readProviders: () => ({}) });
92
+ expect(result).toEqual({ ok: true, key: "resolved-value" });
93
+ });
94
+
95
+ it("$ENV_VAR: returns error when env var is missing", () => {
96
+ delete process.env.NONEXISTENT_VAR;
97
+ const result = resolveProbeApiKey({ apiKey: "$NONEXISTENT_VAR", readProviders: () => ({}) });
98
+ expect(result.ok).toBe(false);
99
+ if (!result.ok) expect(result.error).toMatch(/NONEXISTENT_VAR/);
100
+ });
101
+
102
+ it("REDACTED (***): resolves via provider name from readProviders", () => {
103
+ const result = resolveProbeApiKey({
104
+ apiKey: "***",
105
+ name: "my-provider",
106
+ readProviders: () => ({
107
+ "my-provider": { baseUrl: "u", apiKey: "stored-key", api: "openai-completions" },
108
+ }),
109
+ });
110
+ expect(result).toEqual({ ok: true, key: "stored-key" });
111
+ });
112
+
113
+ it("REDACTED + stored key is $ENV_VAR: follows env-var resolution", () => {
114
+ process.env.STORED_ENV = "env-value";
115
+ const result = resolveProbeApiKey({
116
+ apiKey: "***",
117
+ name: "my-provider",
118
+ readProviders: () => ({
119
+ "my-provider": { baseUrl: "u", apiKey: "$STORED_ENV", api: "openai-completions" },
120
+ }),
121
+ });
122
+ expect(result).toEqual({ ok: true, key: "env-value" });
123
+ });
124
+
125
+ it("REDACTED without name: returns error", () => {
126
+ const result = resolveProbeApiKey({ apiKey: "***", readProviders: () => ({}) });
127
+ expect(result.ok).toBe(false);
128
+ if (!result.ok) expect(result.error).toMatch(/no.*provider/i);
129
+ });
130
+
131
+ it("REDACTED with unknown name: returns error", () => {
132
+ const result = resolveProbeApiKey({
133
+ apiKey: "***",
134
+ name: "missing",
135
+ readProviders: () => ({}),
136
+ });
137
+ expect(result.ok).toBe(false);
138
+ if (!result.ok) expect(result.error).toMatch(/missing/);
139
+ });
140
+
141
+ it("empty key: returns error", () => {
142
+ const result = resolveProbeApiKey({ apiKey: "", readProviders: () => ({}) });
143
+ expect(result.ok).toBe(false);
144
+ });
145
+ });
146
+
147
+ describe("probeProvider", () => {
148
+ const baseInput: ProbeInput = {
149
+ baseUrl: "https://api.example.com/v1",
150
+ apiKey: "sk-abc",
151
+ api: "openai-completions",
152
+ };
153
+
154
+ let originalFetch: typeof globalThis.fetch;
155
+ beforeEach(() => {
156
+ originalFetch = globalThis.fetch;
157
+ });
158
+ afterEach(() => {
159
+ globalThis.fetch = originalFetch;
160
+ });
161
+
162
+ function mockFetch(impl: (url: string, init: RequestInit) => Promise<Response>) {
163
+ globalThis.fetch = vi.fn(impl) as any;
164
+ }
165
+
166
+ it("2xx with data array returns ok + modelCount + sample (capped at 5)", async () => {
167
+ const ids = ["m1", "m2", "m3", "m4", "m5", "m6", "m7"];
168
+ mockFetch(async () =>
169
+ new Response(JSON.stringify({ data: ids.map((id) => ({ id })) }), { status: 200 }),
170
+ );
171
+ const result = await probeProvider(baseInput);
172
+ expect(result.ok).toBe(true);
173
+ if (result.ok) {
174
+ expect(result.status).toBe(200);
175
+ expect(result.modelCount).toBe(7);
176
+ expect(result.sample).toEqual(["m1", "m2", "m3", "m4", "m5"]);
177
+ }
178
+ });
179
+
180
+ it("2xx with unexpected body shape: ok with modelCount=0", async () => {
181
+ mockFetch(async () => new Response(JSON.stringify({ weird: true }), { status: 200 }));
182
+ const result = await probeProvider(baseInput);
183
+ expect(result.ok).toBe(true);
184
+ if (result.ok) {
185
+ expect(result.modelCount).toBe(0);
186
+ expect(result.sample).toEqual([]);
187
+ }
188
+ });
189
+
190
+ it("401 returns ok=false with status + error", async () => {
191
+ mockFetch(async () =>
192
+ new Response("Invalid API key", { status: 401, statusText: "Unauthorized" }),
193
+ );
194
+ const result = await probeProvider(baseInput);
195
+ expect(result.ok).toBe(false);
196
+ if (!result.ok) {
197
+ expect(result.status).toBe(401);
198
+ expect(result.error).toMatch(/Invalid API key|Unauthorized|401/);
199
+ }
200
+ });
201
+
202
+ it("500 returns ok=false with status", async () => {
203
+ mockFetch(async () => new Response("boom", { status: 500 }));
204
+ const result = await probeProvider(baseInput);
205
+ expect(result.ok).toBe(false);
206
+ if (!result.ok) expect(result.status).toBe(500);
207
+ });
208
+
209
+ it("body excerpt is truncated to 500 chars", async () => {
210
+ const long = "x".repeat(2000);
211
+ mockFetch(async () => new Response(long, { status: 400 }));
212
+ const result = await probeProvider(baseInput);
213
+ expect(result.ok).toBe(false);
214
+ if (!result.ok && result.error) expect(result.error.length).toBeLessThanOrEqual(500);
215
+ });
216
+
217
+ it("network error: ok=false with error, no status", async () => {
218
+ mockFetch(async () => {
219
+ throw new Error("ECONNREFUSED");
220
+ });
221
+ const result = await probeProvider(baseInput);
222
+ expect(result.ok).toBe(false);
223
+ if (!result.ok) {
224
+ expect(result.error).toMatch(/ECONNREFUSED/);
225
+ expect(result.status).toBeUndefined();
226
+ }
227
+ });
228
+
229
+ it("timeout aborts the request and returns error", async () => {
230
+ mockFetch(
231
+ (_url, init) =>
232
+ new Promise((_resolve, reject) => {
233
+ const signal = init.signal as AbortSignal;
234
+ signal.addEventListener("abort", () => reject(new Error("aborted")));
235
+ }),
236
+ );
237
+ const result = await probeProvider({ ...baseInput, timeoutMs: 20 });
238
+ expect(result.ok).toBe(false);
239
+ if (!result.ok) expect(result.error).toBeDefined();
240
+ });
241
+
242
+ it("response never echoes the apiKey", async () => {
243
+ mockFetch(async () =>
244
+ new Response(`echoed sk-abc in body`, { status: 401 }),
245
+ );
246
+ const result = await probeProvider({ ...baseInput, apiKey: "sk-abc" });
247
+ // even though upstream echoes the key, our result error should not leak it
248
+ if (!result.ok && result.error) {
249
+ expect(result.error).not.toContain("sk-abc");
250
+ }
251
+ });
252
+
253
+ it("anthropic-messages uses x-api-key header (not Authorization)", async () => {
254
+ let capturedInit: RequestInit | undefined;
255
+ mockFetch(async (_url, init) => {
256
+ capturedInit = init;
257
+ return new Response(JSON.stringify({ data: [] }), { status: 200 });
258
+ });
259
+ await probeProvider({
260
+ baseUrl: "https://api.anthropic.com",
261
+ apiKey: "sk-ant-x",
262
+ api: "anthropic-messages",
263
+ });
264
+ const headers = capturedInit!.headers as Record<string, string>;
265
+ expect(headers["x-api-key"]).toBe("sk-ant-x");
266
+ expect(headers["anthropic-version"]).toBe("2023-06-01");
267
+ expect(headers.Authorization).toBeUndefined();
268
+ });
269
+
270
+ it("google-generative-ai uses ?key= query param (no Authorization)", async () => {
271
+ let capturedUrl: string | undefined;
272
+ let capturedInit: RequestInit | undefined;
273
+ mockFetch(async (url, init) => {
274
+ capturedUrl = url;
275
+ capturedInit = init;
276
+ return new Response(JSON.stringify({ data: [] }), { status: 200 });
277
+ });
278
+ await probeProvider({
279
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta",
280
+ apiKey: "AIzaTest",
281
+ api: "google-generative-ai",
282
+ });
283
+ expect(capturedUrl).toContain("?key=AIzaTest");
284
+ const headers = (capturedInit!.headers ?? {}) as Record<string, string>;
285
+ expect(headers.Authorization).toBeUndefined();
286
+ });
287
+ });