@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,149 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import Fastify from "fastify";
3
+ import { registerProviderRoutes } from "../routes/provider-routes.js";
4
+
5
+ // Pass-through network guard — we're testing the endpoint contract, not auth
6
+ const allowGuard = async () => {};
7
+
8
+ function makeApp() {
9
+ const app = Fastify();
10
+ registerProviderRoutes(app, { networkGuard: allowGuard as any });
11
+ return app;
12
+ }
13
+
14
+ describe("POST /api/providers/test", () => {
15
+ let originalFetch: typeof globalThis.fetch;
16
+ beforeEach(() => {
17
+ originalFetch = globalThis.fetch;
18
+ });
19
+ afterEach(() => {
20
+ globalThis.fetch = originalFetch;
21
+ });
22
+
23
+ function mockFetch(impl: (url: string, init: RequestInit) => Promise<Response>) {
24
+ globalThis.fetch = vi.fn(impl) as any;
25
+ }
26
+
27
+ it("400 when body is invalid", async () => {
28
+ const app = makeApp();
29
+ await app.ready();
30
+ const res = await app.inject({
31
+ method: "POST",
32
+ url: "/api/providers/test",
33
+ payload: "not-json",
34
+ headers: { "content-type": "application/json" },
35
+ });
36
+ expect(res.statusCode).toBe(400);
37
+ await app.close();
38
+ });
39
+
40
+ it("400 when baseUrl missing", async () => {
41
+ const app = makeApp();
42
+ await app.ready();
43
+ const res = await app.inject({
44
+ method: "POST",
45
+ url: "/api/providers/test",
46
+ payload: { apiKey: "sk-abc", api: "openai-completions" },
47
+ });
48
+ expect(res.statusCode).toBe(400);
49
+ const body = JSON.parse(res.payload);
50
+ expect(body.error).toMatch(/baseUrl/);
51
+ await app.close();
52
+ });
53
+
54
+ it("400 when api missing", async () => {
55
+ const app = makeApp();
56
+ await app.ready();
57
+ const res = await app.inject({
58
+ method: "POST",
59
+ url: "/api/providers/test",
60
+ payload: { baseUrl: "https://x", apiKey: "sk" },
61
+ });
62
+ expect(res.statusCode).toBe(400);
63
+ await app.close();
64
+ });
65
+
66
+ it("happy path: probe returns ok with modelCount", async () => {
67
+ mockFetch(async () =>
68
+ new Response(JSON.stringify({ data: [{ id: "m1" }, { id: "m2" }] }), { status: 200 }),
69
+ );
70
+ const app = makeApp();
71
+ await app.ready();
72
+ const res = await app.inject({
73
+ method: "POST",
74
+ url: "/api/providers/test",
75
+ payload: {
76
+ baseUrl: "https://api.example.com/v1",
77
+ apiKey: "sk-abc",
78
+ api: "openai-completions",
79
+ },
80
+ });
81
+ expect(res.statusCode).toBe(200);
82
+ const body = JSON.parse(res.payload);
83
+ expect(body.ok).toBe(true);
84
+ expect(body.modelCount).toBe(2);
85
+ expect(body.sample).toEqual(["m1", "m2"]);
86
+ await app.close();
87
+ });
88
+
89
+ it("401 upstream: returns ok=false with status", async () => {
90
+ mockFetch(async () => new Response("bad key", { status: 401 }));
91
+ const app = makeApp();
92
+ await app.ready();
93
+ const res = await app.inject({
94
+ method: "POST",
95
+ url: "/api/providers/test",
96
+ payload: {
97
+ baseUrl: "https://api.example.com/v1",
98
+ apiKey: "sk-bad",
99
+ api: "openai-completions",
100
+ },
101
+ });
102
+ // Our endpoint always returns 200 with the structured result
103
+ expect(res.statusCode).toBe(200);
104
+ const body = JSON.parse(res.payload);
105
+ expect(body.ok).toBe(false);
106
+ expect(body.status).toBe(401);
107
+ await app.close();
108
+ });
109
+
110
+ it("missing $ENV_VAR: returns ok=false without hitting upstream", async () => {
111
+ delete process.env.PROBE_ROUTE_MISSING;
112
+ const fetchSpy = vi.fn(async () => new Response("{}", { status: 200 }));
113
+ globalThis.fetch = fetchSpy as any;
114
+ const app = makeApp();
115
+ await app.ready();
116
+ const res = await app.inject({
117
+ method: "POST",
118
+ url: "/api/providers/test",
119
+ payload: {
120
+ baseUrl: "https://x",
121
+ apiKey: "$PROBE_ROUTE_MISSING",
122
+ api: "openai-completions",
123
+ },
124
+ });
125
+ expect(res.statusCode).toBe(200);
126
+ const body = JSON.parse(res.payload);
127
+ expect(body.ok).toBe(false);
128
+ expect(body.error).toMatch(/PROBE_ROUTE_MISSING/);
129
+ expect(fetchSpy).not.toHaveBeenCalled();
130
+ await app.close();
131
+ });
132
+
133
+ it("response never echoes the apiKey", async () => {
134
+ mockFetch(async () => new Response("bad key xyz-secret-abc", { status: 401 }));
135
+ const app = makeApp();
136
+ await app.ready();
137
+ const res = await app.inject({
138
+ method: "POST",
139
+ url: "/api/providers/test",
140
+ payload: {
141
+ baseUrl: "https://api.example.com/v1",
142
+ apiKey: "xyz-secret-abc",
143
+ api: "openai-completions",
144
+ },
145
+ });
146
+ expect(res.payload).not.toContain("xyz-secret-abc");
147
+ await app.close();
148
+ });
149
+ });
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Tests for the GET /api/packages/recommended route and its helpers.
3
+ */
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
5
+ import Fastify, { type FastifyInstance } from "fastify";
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+
10
+ // Mock pi dependency (pulled transitively by package-manager-wrapper)
11
+ vi.mock("@mariozechner/pi-coding-agent", () => ({
12
+ DefaultPackageManager: function () {
13
+ return {};
14
+ },
15
+ SettingsManager: { create: () => ({}) },
16
+ }));
17
+
18
+ // Mock the npm-search-proxy so we can assert enrichment + failure paths.
19
+ vi.mock("../npm-search-proxy.js", () => ({
20
+ fetchPackageMeta: vi.fn(),
21
+ fetchGithubPackageJson: vi.fn(),
22
+ }));
23
+
24
+ import { fetchPackageMeta, fetchGithubPackageJson } from "../npm-search-proxy.js";
25
+ import {
26
+ registerRecommendedRoutes,
27
+ invalidateRecommendedCache,
28
+ parseSourceKey,
29
+ sourcesMatch,
30
+ } from "../routes/recommended-routes.js";
31
+
32
+ function makeWrapper(installed: {
33
+ global?: Array<{ source: string; installedPath?: string }>;
34
+ local?: Array<{ source: string; installedPath?: string }>;
35
+ }): any {
36
+ return {
37
+ listInstalled: vi.fn(async (scope: string) =>
38
+ scope === "global" ? installed.global ?? [] : installed.local ?? [],
39
+ ),
40
+ };
41
+ }
42
+
43
+ describe("parseSourceKey", () => {
44
+ it("parses npm: sources", () => {
45
+ expect(parseSourceKey("npm:pi-web-access")).toEqual({
46
+ kind: "npm",
47
+ name: "pi-web-access",
48
+ });
49
+ });
50
+
51
+ it("parses scoped npm: sources", () => {
52
+ expect(parseSourceKey("npm:@tintinweb/pi-subagents")).toEqual({
53
+ kind: "npm",
54
+ name: "@tintinweb/pi-subagents",
55
+ });
56
+ });
57
+
58
+ it("strips version from npm: sources", () => {
59
+ expect(parseSourceKey("npm:pi-web-access@1.2.3")).toEqual({
60
+ kind: "npm",
61
+ name: "pi-web-access",
62
+ });
63
+ expect(parseSourceKey("npm:@scope/pkg@1.0.0")).toEqual({
64
+ kind: "npm",
65
+ name: "@scope/pkg",
66
+ });
67
+ });
68
+
69
+ it("parses git@ SSH URLs", () => {
70
+ expect(parseSourceKey("git@github.com:BlackBeltTechnology/pi-flows.git")).toEqual({
71
+ kind: "git",
72
+ host: "github.com",
73
+ owner: "BlackBeltTechnology",
74
+ repo: "pi-flows",
75
+ });
76
+ });
77
+
78
+ it("parses https git URLs", () => {
79
+ expect(
80
+ parseSourceKey("https://github.com/BlackBeltTechnology/pi-flows.git"),
81
+ ).toEqual({
82
+ kind: "git",
83
+ host: "github.com",
84
+ owner: "BlackBeltTechnology",
85
+ repo: "pi-flows",
86
+ });
87
+ });
88
+
89
+ it("falls back to raw for unknown forms", () => {
90
+ expect(parseSourceKey("/local/path")).toEqual({
91
+ kind: "raw",
92
+ source: "/local/path",
93
+ });
94
+ });
95
+ });
96
+
97
+ describe("sourcesMatch", () => {
98
+ it("matches npm sources with and without version", () => {
99
+ expect(sourcesMatch("npm:pi-web-access", "npm:pi-web-access@1.0.0")).toBe(true);
100
+ });
101
+
102
+ it("matches git SSH and HTTPS forms of the same repo", () => {
103
+ expect(
104
+ sourcesMatch(
105
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
106
+ "https://github.com/BlackBeltTechnology/pi-flows.git",
107
+ ),
108
+ ).toBe(true);
109
+ });
110
+
111
+ it("is case-insensitive on the git host/owner/repo", () => {
112
+ expect(
113
+ sourcesMatch(
114
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
115
+ "git@github.com:blackbelttechnology/pi-flows.git",
116
+ ),
117
+ ).toBe(true);
118
+ });
119
+
120
+ it("distinguishes different repos", () => {
121
+ expect(
122
+ sourcesMatch(
123
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
124
+ "git@github.com:BlackBeltTechnology/pi-anthropic-messages.git",
125
+ ),
126
+ ).toBe(false);
127
+ });
128
+
129
+ it("matches a git URL against a local path whose basename equals the repo name", () => {
130
+ expect(
131
+ sourcesMatch(
132
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
133
+ "../pi-flows",
134
+ ),
135
+ ).toBe(true);
136
+ expect(
137
+ sourcesMatch(
138
+ "../pi-anthropic-messages",
139
+ "git@github.com:BlackBeltTechnology/pi-anthropic-messages.git",
140
+ ),
141
+ ).toBe(true);
142
+ expect(
143
+ sourcesMatch(
144
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
145
+ "/home/user/src/pi-flows/",
146
+ ),
147
+ ).toBe(true);
148
+ });
149
+
150
+ it("does not cross-match a git URL against an unrelated local path", () => {
151
+ expect(
152
+ sourcesMatch(
153
+ "git@github.com:BlackBeltTechnology/pi-flows.git",
154
+ "../pi-web-access",
155
+ ),
156
+ ).toBe(false);
157
+ });
158
+ });
159
+
160
+ describe("GET /api/packages/recommended", () => {
161
+ let fastify: FastifyInstance;
162
+ let tmpHome: string;
163
+ let origCwd: string;
164
+
165
+ beforeEach(() => {
166
+ invalidateRecommendedCache();
167
+ vi.mocked(fetchPackageMeta).mockReset();
168
+ vi.mocked(fetchGithubPackageJson).mockReset();
169
+
170
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-rec-"));
171
+ process.env.HOME = tmpHome;
172
+
173
+ // chdir to a clean subdirectory so the route's CWD-based local
174
+ // settings read doesn't pick up the host repo's .pi/settings.json.
175
+ origCwd = process.cwd();
176
+ const scratchCwd = path.join(tmpHome, "scratch");
177
+ fs.mkdirSync(scratchCwd, { recursive: true });
178
+ process.chdir(scratchCwd);
179
+ });
180
+
181
+ afterEach(async () => {
182
+ if (fastify) await fastify.close();
183
+ process.chdir(origCwd);
184
+ if (fs.existsSync(tmpHome)) fs.rmSync(tmpHome, { recursive: true, force: true });
185
+ });
186
+
187
+ async function setupRoute(installed: {
188
+ global?: Array<{ source: string; installedPath?: string }>;
189
+ local?: Array<{ source: string; installedPath?: string }>;
190
+ } = {}): Promise<FastifyInstance> {
191
+ fastify = Fastify();
192
+ const wrapper = makeWrapper(installed);
193
+ registerRecommendedRoutes(fastify, { packageManagerWrapper: wrapper });
194
+ await fastify.ready();
195
+ return fastify;
196
+ }
197
+
198
+ it("returns the 5 manifest entries with default (offline) descriptions", async () => {
199
+ vi.mocked(fetchPackageMeta).mockResolvedValue(null);
200
+ vi.mocked(fetchGithubPackageJson).mockResolvedValue(null);
201
+ await setupRoute();
202
+
203
+ const res = await fastify.inject({
204
+ method: "GET",
205
+ url: "/api/packages/recommended",
206
+ });
207
+ expect(res.statusCode).toBe(200);
208
+ const body = JSON.parse(res.payload);
209
+ expect(body.success).toBe(true);
210
+ const entries = body.data.recommended;
211
+ expect(entries).toHaveLength(5);
212
+ // Every entry falls back to fallbackDescription and has no version.
213
+ for (const e of entries) {
214
+ expect(typeof e.description).toBe("string");
215
+ expect(e.description.length).toBeGreaterThan(10);
216
+ expect(e.version).toBeUndefined();
217
+ expect(e.installed.scope).toBeNull();
218
+ expect(e.activeInPi).toBe(false);
219
+ expect(e.updateAvailable).toBe(false);
220
+ }
221
+ });
222
+
223
+ it("uses npm metadata when registry is reachable", async () => {
224
+ vi.mocked(fetchPackageMeta).mockImplementation(async (name: string) => {
225
+ if (name === "pi-web-access") {
226
+ return { description: "LIVE npm desc", version: "9.9.9" };
227
+ }
228
+ return null;
229
+ });
230
+ vi.mocked(fetchGithubPackageJson).mockResolvedValue(null);
231
+ await setupRoute();
232
+
233
+ const res = await fastify.inject({ method: "GET", url: "/api/packages/recommended" });
234
+ const body = JSON.parse(res.payload);
235
+ const pwa = body.data.recommended.find((e: any) => e.id === "pi-web-access");
236
+ expect(pwa.description).toBe("LIVE npm desc");
237
+ expect(pwa.version).toBe("9.9.9");
238
+ });
239
+
240
+ it("uses GitHub metadata for git-sourced entries", async () => {
241
+ vi.mocked(fetchPackageMeta).mockResolvedValue(null);
242
+ vi.mocked(fetchGithubPackageJson).mockImplementation(async (owner, repo) => {
243
+ if (owner === "BlackBeltTechnology" && repo === "pi-flows") {
244
+ return { description: "LIVE github desc", version: "0.1.0" };
245
+ }
246
+ return null;
247
+ });
248
+ await setupRoute();
249
+
250
+ const res = await fastify.inject({ method: "GET", url: "/api/packages/recommended" });
251
+ const body = JSON.parse(res.payload);
252
+ const flows = body.data.recommended.find((e: any) => e.id === "pi-flows");
253
+ expect(flows.description).toBe("LIVE github desc");
254
+ expect(flows.version).toBe("0.1.0");
255
+ });
256
+
257
+ it("reports installed + activeInPi correctly when settings.json lists the source", async () => {
258
+ vi.mocked(fetchPackageMeta).mockResolvedValue(null);
259
+ vi.mocked(fetchGithubPackageJson).mockResolvedValue(null);
260
+
261
+ // Write settings.json with pi-web-access as an active package
262
+ const settingsDir = path.join(tmpHome, ".pi", "agent");
263
+ fs.mkdirSync(settingsDir, { recursive: true });
264
+ fs.writeFileSync(
265
+ path.join(settingsDir, "settings.json"),
266
+ JSON.stringify({ packages: ["npm:pi-web-access"] }),
267
+ );
268
+
269
+ await setupRoute({
270
+ global: [{ source: "npm:pi-web-access", installedPath: "/fake" }],
271
+ });
272
+
273
+ const res = await fastify.inject({ method: "GET", url: "/api/packages/recommended" });
274
+ const body = JSON.parse(res.payload);
275
+ const pwa = body.data.recommended.find((e: any) => e.id === "pi-web-access");
276
+ expect(pwa.installed.scope).toBe("global");
277
+ expect(pwa.activeInPi).toBe(true);
278
+
279
+ // Entries not in settings.json remain inactive
280
+ const browser = body.data.recommended.find((e: any) => e.id === "pi-agent-browser");
281
+ expect(browser.installed.scope).toBeNull();
282
+ expect(browser.activeInPi).toBe(false);
283
+ });
284
+
285
+ it("matches git SSH source against git HTTPS active source", async () => {
286
+ vi.mocked(fetchPackageMeta).mockResolvedValue(null);
287
+ vi.mocked(fetchGithubPackageJson).mockResolvedValue(null);
288
+
289
+ const settingsDir = path.join(tmpHome, ".pi", "agent");
290
+ fs.mkdirSync(settingsDir, { recursive: true });
291
+ // User wrote HTTPS in settings; manifest has SSH. They should match.
292
+ fs.writeFileSync(
293
+ path.join(settingsDir, "settings.json"),
294
+ JSON.stringify({
295
+ packages: ["https://github.com/BlackBeltTechnology/pi-flows.git"],
296
+ }),
297
+ );
298
+
299
+ await setupRoute();
300
+ const res = await fastify.inject({ method: "GET", url: "/api/packages/recommended" });
301
+ const body = JSON.parse(res.payload);
302
+ const flows = body.data.recommended.find((e: any) => e.id === "pi-flows");
303
+ expect(flows.activeInPi).toBe(true);
304
+ });
305
+
306
+ it("matches git manifest source against a local-path active source (basename heuristic)", async () => {
307
+ vi.mocked(fetchPackageMeta).mockResolvedValue(null);
308
+ vi.mocked(fetchGithubPackageJson).mockResolvedValue(null);
309
+
310
+ // User has pi-flows checked out locally and registered via `pi install -l`
311
+ // which records the local path in .pi/settings.json. The manifest has the
312
+ // git SSH URL. The two should still match via basename.
313
+ const projectDir = path.join(tmpHome, "workspace");
314
+ fs.mkdirSync(path.join(projectDir, ".pi"), { recursive: true });
315
+ fs.writeFileSync(
316
+ path.join(projectDir, ".pi", "settings.json"),
317
+ JSON.stringify({ packages: ["../pi-flows", "../pi-anthropic-messages"] }),
318
+ );
319
+
320
+ const origCwd = process.cwd();
321
+ process.chdir(projectDir);
322
+ try {
323
+ await setupRoute();
324
+ const res = await fastify.inject({ method: "GET", url: "/api/packages/recommended" });
325
+ const body = JSON.parse(res.payload);
326
+ const flows = body.data.recommended.find((e: any) => e.id === "pi-flows");
327
+ const msg = body.data.recommended.find(
328
+ (e: any) => e.id === "pi-anthropic-messages",
329
+ );
330
+ expect(flows.activeInPi).toBe(true);
331
+ expect(msg.activeInPi).toBe(true);
332
+ } finally {
333
+ process.chdir(origCwd);
334
+ }
335
+ });
336
+
337
+ it("considers project-local .pi/settings.json for activeInPi", async () => {
338
+ vi.mocked(fetchPackageMeta).mockResolvedValue(null);
339
+ vi.mocked(fetchGithubPackageJson).mockResolvedValue(null);
340
+
341
+ const projectDir = path.join(tmpHome, "workspace");
342
+ fs.mkdirSync(path.join(projectDir, ".pi"), { recursive: true });
343
+ fs.writeFileSync(
344
+ path.join(projectDir, ".pi", "settings.json"),
345
+ JSON.stringify({ packages: ["npm:pi-web-access"] }),
346
+ );
347
+
348
+ const origCwd = process.cwd();
349
+ process.chdir(projectDir);
350
+ try {
351
+ await setupRoute();
352
+ const res = await fastify.inject({ method: "GET", url: "/api/packages/recommended" });
353
+ const body = JSON.parse(res.payload);
354
+ const pwa = body.data.recommended.find((e: any) => e.id === "pi-web-access");
355
+ expect(pwa.activeInPi).toBe(true);
356
+ } finally {
357
+ process.chdir(origCwd);
358
+ }
359
+ });
360
+
361
+ it("serves cached data on the second call within 60s", async () => {
362
+ vi.mocked(fetchPackageMeta).mockResolvedValue({
363
+ description: "cached",
364
+ version: "1.0.0",
365
+ });
366
+ vi.mocked(fetchGithubPackageJson).mockResolvedValue(null);
367
+ await setupRoute();
368
+
369
+ await fastify.inject({ method: "GET", url: "/api/packages/recommended" });
370
+ const callsAfterFirst = vi.mocked(fetchPackageMeta).mock.calls.length;
371
+ await fastify.inject({ method: "GET", url: "/api/packages/recommended" });
372
+ expect(vi.mocked(fetchPackageMeta).mock.calls.length).toBe(callsAfterFirst);
373
+ });
374
+
375
+ it("refetches after invalidateRecommendedCache()", async () => {
376
+ vi.mocked(fetchPackageMeta).mockResolvedValue({
377
+ description: "refresh",
378
+ version: "1.0.0",
379
+ });
380
+ vi.mocked(fetchGithubPackageJson).mockResolvedValue(null);
381
+ await setupRoute();
382
+
383
+ await fastify.inject({ method: "GET", url: "/api/packages/recommended" });
384
+ const before = vi.mocked(fetchPackageMeta).mock.calls.length;
385
+ invalidateRecommendedCache();
386
+ await fastify.inject({ method: "GET", url: "/api/packages/recommended" });
387
+ expect(vi.mocked(fetchPackageMeta).mock.calls.length).toBeGreaterThan(before);
388
+ });
389
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Tests for the cross-platform restart orchestrator.
3
+ * See change: fix-windows-server-parity.
4
+ */
5
+ import { describe, it, expect } from "vitest";
6
+ import { buildOrchestratorScript } from "../restart-helper.js";
7
+
8
+ describe("buildOrchestratorScript", () => {
9
+ const baseParams = {
10
+ cliPath: "/tmp/cli.ts",
11
+ loader: "file:///tmp/jiti-register.mjs",
12
+ port: 8000,
13
+ extraArgs: [] as string[],
14
+ execPath: "/usr/bin/node",
15
+ };
16
+
17
+ it("produces a self-contained Node script (no shell/lsof/curl)", () => {
18
+ const script = buildOrchestratorScript(baseParams);
19
+ expect(script).not.toMatch(/\blsof\b/);
20
+ expect(script).not.toMatch(/\bcurl\b/);
21
+ expect(script).not.toMatch(/\bsh\s+-c\b/);
22
+ // Uses Node built-ins
23
+ expect(script).toMatch(/require\("node:net"\)/);
24
+ expect(script).toMatch(/require\("node:http"\)/);
25
+ expect(script).toMatch(/require\("node:child_process"\)/);
26
+ });
27
+
28
+ it("embeds the port as a number literal", () => {
29
+ const script = buildOrchestratorScript({ ...baseParams, port: 12345 });
30
+ expect(script).toMatch(/const PORT = 12345/);
31
+ });
32
+
33
+ it("embeds the loader as a --import arg when provided", () => {
34
+ const script = buildOrchestratorScript(baseParams);
35
+ // ARGS should be a JSON array containing --import and the loader
36
+ expect(script).toMatch(/const ARGS = \[.*"--import".*"file:\/\/\/tmp\/jiti-register\.mjs"/);
37
+ expect(script).toMatch(/"\/tmp\/cli\.ts"/);
38
+ expect(script).toMatch(/"start"/);
39
+ });
40
+
41
+ it("omits --import when loader is empty", () => {
42
+ const script = buildOrchestratorScript({ ...baseParams, loader: "" });
43
+ expect(script).not.toMatch(/"--import"/);
44
+ expect(script).toMatch(/"\/tmp\/cli\.ts"/);
45
+ expect(script).toMatch(/"start"/);
46
+ });
47
+
48
+ it("appends extra args (e.g. --dev) after 'start'", () => {
49
+ const script = buildOrchestratorScript({ ...baseParams, extraArgs: ["--dev"] });
50
+ // ARGS array should have "start" immediately followed by "--dev"
51
+ expect(script).toMatch(/"start","--dev"/);
52
+ });
53
+
54
+ it("safely embeds Windows paths with backslashes and drive letters", () => {
55
+ const winParams = {
56
+ ...baseParams,
57
+ cliPath: "B:\\Dev\\BB\\pi-agent-dashboard\\packages\\server\\src\\cli.ts",
58
+ loader: "file:///B:/Dev/Nodejs/global/node_modules/@mariozechner/jiti/lib/jiti-register.mjs",
59
+ execPath: "C:\\Program Files\\nodejs\\node.exe",
60
+ };
61
+ const script = buildOrchestratorScript(winParams);
62
+ // Must be embedded via JSON.stringify (backslashes escaped, quotes preserved)
63
+ expect(script).toContain(JSON.stringify(winParams.execPath));
64
+ expect(script).toContain(JSON.stringify(winParams.cliPath));
65
+ expect(script).toContain(JSON.stringify(winParams.loader));
66
+ // Should not contain raw unescaped backslashes that would break the JS
67
+ // (we embed via JSON.stringify which escapes them to \\)
68
+ expect(script).toMatch(/B:\\\\Dev\\\\BB/);
69
+ });
70
+
71
+ it("references ~/.pi/dashboard/restart.log for failure logging", () => {
72
+ const script = buildOrchestratorScript(baseParams);
73
+ expect(script).toMatch(/restart\.log/);
74
+ expect(script).toMatch(/fs\.appendFileSync/);
75
+ });
76
+
77
+ it("health-check target is /api/health on the configured port", () => {
78
+ const script = buildOrchestratorScript({ ...baseParams, port: 8765 });
79
+ expect(script).toMatch(/\/api\/health/);
80
+ expect(script).toMatch(/const PORT = 8765/);
81
+ expect(script).toMatch(/port: PORT/);
82
+ });
83
+ });