@blackbelt-technology/pi-agent-dashboard 0.5.0 → 0.5.2

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 (201) hide show
  1. package/AGENTS.md +26 -5
  2. package/README.md +49 -7
  3. package/docs/architecture.md +129 -1
  4. package/package.json +15 -15
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
  7. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
  8. package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
  9. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
  10. package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
  11. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
  12. package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
  13. package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
  14. package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
  15. package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
  16. package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
  17. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
  18. package/packages/extension/src/ask-user-tool.ts +1 -1
  19. package/packages/extension/src/bridge-context.ts +68 -4
  20. package/packages/extension/src/bridge.ts +79 -11
  21. package/packages/extension/src/command-handler.ts +95 -15
  22. package/packages/extension/src/flow-event-wiring.ts +1 -1
  23. package/packages/extension/src/multiselect-list.ts +1 -1
  24. package/packages/extension/src/pi-env.d.ts +16 -9
  25. package/packages/extension/src/prompt-expander.ts +74 -63
  26. package/packages/extension/src/provider-register.ts +16 -9
  27. package/packages/extension/src/retry-tracker.ts +123 -0
  28. package/packages/extension/src/server-launcher.ts +31 -70
  29. package/packages/extension/src/session-sync.ts +10 -1
  30. package/packages/extension/src/slash-dispatch.ts +123 -0
  31. package/packages/extension/src/usage-limit-orderer.ts +76 -0
  32. package/packages/server/bin/pi-dashboard.mjs +84 -0
  33. package/packages/server/package.json +8 -7
  34. package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
  35. package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
  36. package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
  37. package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
  38. package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
  39. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
  40. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
  41. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
  42. package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
  43. package/packages/server/src/__tests__/directory-service.test.ts +2 -2
  44. package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
  45. package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
  46. package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
  47. package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
  48. package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
  49. package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
  50. package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
  51. package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
  52. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
  53. package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
  54. package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
  55. package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
  56. package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
  57. package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
  58. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
  59. package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
  60. package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
  61. package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
  62. package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
  63. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
  64. package/packages/server/src/__tests__/package-routes.test.ts +1 -1
  65. package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
  66. package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
  67. package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
  68. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
  69. package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
  70. package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
  71. package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
  72. package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
  73. package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
  74. package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
  75. package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
  76. package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
  77. package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
  78. package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
  79. package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
  80. package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
  81. package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
  82. package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
  83. package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
  84. package/packages/server/src/auth-plugin.ts +3 -0
  85. package/packages/server/src/bootstrap-state.ts +10 -0
  86. package/packages/server/src/browser-gateway.ts +27 -10
  87. package/packages/server/src/browser-handlers/handler-context.ts +9 -0
  88. package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
  89. package/packages/server/src/changelog-fs.ts +167 -0
  90. package/packages/server/src/changelog-parser.ts +321 -0
  91. package/packages/server/src/changelog-remote.ts +134 -0
  92. package/packages/server/src/cli.ts +62 -82
  93. package/packages/server/src/config-api.ts +14 -2
  94. package/packages/server/src/directory-service.ts +106 -4
  95. package/packages/server/src/event-wiring.ts +90 -6
  96. package/packages/server/src/headless-pid-registry.ts +344 -37
  97. package/packages/server/src/legacy-pi-cleanup.ts +151 -0
  98. package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
  99. package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
  100. package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
  101. package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
  102. package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
  103. package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
  104. package/packages/server/src/model-proxy/api-key-store.ts +87 -0
  105. package/packages/server/src/model-proxy/auth-gate.ts +116 -0
  106. package/packages/server/src/model-proxy/concurrency.ts +76 -0
  107. package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
  108. package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
  109. package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
  110. package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
  111. package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
  112. package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
  113. package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
  114. package/packages/server/src/model-proxy/convert/index.ts +8 -0
  115. package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
  116. package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
  117. package/packages/server/src/model-proxy/convert/types.ts +70 -0
  118. package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
  119. package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
  120. package/packages/server/src/model-proxy/internal-registry.ts +157 -0
  121. package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
  122. package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
  123. package/packages/server/src/model-proxy/request-log.ts +53 -0
  124. package/packages/server/src/model-proxy/streamer.ts +59 -0
  125. package/packages/server/src/openspec-group-store.ts +490 -0
  126. package/packages/server/src/pending-client-correlations.ts +73 -0
  127. package/packages/server/src/pending-fork-registry.ts +24 -12
  128. package/packages/server/src/pi-core-checker.ts +77 -17
  129. package/packages/server/src/pi-core-updater.ts +16 -6
  130. package/packages/server/src/pi-dev-version-check.ts +145 -0
  131. package/packages/server/src/pi-gateway.ts +4 -0
  132. package/packages/server/src/pi-version-skew.ts +12 -4
  133. package/packages/server/src/process-manager.ts +182 -11
  134. package/packages/server/src/provider-auth-storage.ts +29 -47
  135. package/packages/server/src/provider-catalogue-cache.ts +24 -18
  136. package/packages/server/src/restart-helper.ts +17 -16
  137. package/packages/server/src/routes/bootstrap-routes.ts +37 -0
  138. package/packages/server/src/routes/jj-routes.ts +3 -0
  139. package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
  140. package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
  141. package/packages/server/src/routes/model-proxy-routes.ts +330 -0
  142. package/packages/server/src/routes/openspec-group-routes.ts +231 -0
  143. package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
  144. package/packages/server/src/routes/pi-core-routes.ts +1 -1
  145. package/packages/server/src/routes/provider-auth-routes.ts +8 -1
  146. package/packages/server/src/routes/provider-routes.ts +28 -5
  147. package/packages/server/src/routes/system-routes.ts +44 -2
  148. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
  149. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
  150. package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
  151. package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
  152. package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
  153. package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
  154. package/packages/server/src/server.ts +254 -60
  155. package/packages/server/src/session-api.ts +63 -4
  156. package/packages/server/src/session-discovery.ts +1 -1
  157. package/packages/server/src/session-file-reader.ts +1 -1
  158. package/packages/server/src/spawn-register-watchdog.ts +62 -7
  159. package/packages/server/src/spawn-token.ts +20 -0
  160. package/packages/server/src/tunnel-watchdog.ts +230 -0
  161. package/packages/server/src/tunnel.ts +5 -1
  162. package/packages/shared/package.json +1 -1
  163. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
  164. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
  165. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
  166. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
  167. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
  168. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
  169. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
  170. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
  171. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
  172. package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
  173. package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
  174. package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
  175. package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
  176. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
  177. package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
  178. package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
  179. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
  180. package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
  181. package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
  182. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
  183. package/packages/shared/src/bootstrap-install.ts +1 -1
  184. package/packages/shared/src/browser-protocol.ts +70 -0
  185. package/packages/shared/src/changelog-types.ts +111 -0
  186. package/packages/shared/src/config.ts +172 -2
  187. package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
  188. package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
  189. package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
  190. package/packages/shared/src/platform/binary-lookup.ts +204 -0
  191. package/packages/shared/src/platform/node-spawn.ts +71 -26
  192. package/packages/shared/src/protocol.ts +27 -1
  193. package/packages/shared/src/recommended-extensions.ts +18 -0
  194. package/packages/shared/src/rest-api.ts +219 -1
  195. package/packages/shared/src/server-launcher.ts +277 -0
  196. package/packages/shared/src/skill-block-parser.ts +1 -1
  197. package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
  198. package/packages/shared/src/tool-registry/definitions.ts +15 -3
  199. package/packages/shared/src/types.ts +62 -0
  200. package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
  201. package/packages/shared/src/resolve-jiti.ts +0 -102
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Precondition test: pi-ai symbol shape.
3
+ *
4
+ * Runtime-resolves pi-ai via the ToolRegistry and asserts every symbol
5
+ * the model-proxy change depends on exists in the resolved module.
6
+ *
7
+ * - `it.skip` when pi-ai cannot be resolved (clean CI without ~/.pi-dashboard/).
8
+ * - Full run when pi-ai is installed locally.
9
+ * - Set `MODEL_PROXY_REQUIRE_PI_AI=1` to force hard-fail (for release-cut runs).
10
+ *
11
+ * Run locally:
12
+ * MODEL_PROXY_REQUIRE_PI_AI=1 npm test -- pi-ai-shape
13
+ */
14
+ import { describe, it, expect, beforeAll } from "vitest";
15
+ import { getDefaultRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
16
+
17
+ const REQUIRE = process.env.MODEL_PROXY_REQUIRE_PI_AI === "1";
18
+
19
+ let piAi: Record<string, unknown> | null = null;
20
+ let piAiOAuth: Record<string, unknown> | null = null;
21
+ let resolveError: Error | null = null;
22
+
23
+ beforeAll(async () => {
24
+ try {
25
+ const result = await getDefaultRegistry().resolveModule<Record<string, unknown>>("pi-ai");
26
+ piAi = result.module;
27
+
28
+ // Resolve oauth subpath — pi-ai exports it from dist/oauth.js
29
+ const resolution = result.resolution;
30
+ if (resolution.path) {
31
+ const oauthPath = resolution.path.replace(/\/dist\/index\.js$/, "/dist/oauth.js");
32
+ try {
33
+ const { pathToFileURL } = await import("node:url");
34
+ piAiOAuth = (await import(pathToFileURL(oauthPath).href)) as Record<string, unknown>;
35
+ } catch {
36
+ // OAuth subpath may not exist in all versions
37
+ }
38
+ }
39
+ } catch (err) {
40
+ resolveError = err as Error;
41
+ if (REQUIRE) {
42
+ throw new Error(
43
+ `MODEL_PROXY_REQUIRE_PI_AI=1 but pi-ai could not be resolved: ${(err as Error).message}`,
44
+ );
45
+ }
46
+ }
47
+ });
48
+
49
+ const skipOrRun = () => {
50
+ if (!piAi) return it.skip;
51
+ return it;
52
+ };
53
+
54
+ describe("pi-ai shape precondition", () => {
55
+ it("resolves pi-ai or skips gracefully", () => {
56
+ if (REQUIRE) {
57
+ expect(piAi).not.toBeNull();
58
+ } else if (!piAi) {
59
+ console.log(`pi-ai not resolved (${resolveError?.message}); skipping shape checks`);
60
+ }
61
+ });
62
+
63
+ // --- Main exports from pi-ai (dist/index.js) ---
64
+
65
+ describe("main exports", () => {
66
+ it("exports streamSimple", () => {
67
+ if (!piAi) return;
68
+ expect(typeof piAi.streamSimple).toBe("function");
69
+ });
70
+
71
+ it("exports getModels", () => {
72
+ if (!piAi) return;
73
+ expect(typeof piAi.getModels).toBe("function");
74
+ });
75
+
76
+ it("exports registerBuiltInApiProviders", () => {
77
+ if (!piAi) return;
78
+ expect(typeof piAi.registerBuiltInApiProviders).toBe("function");
79
+ });
80
+
81
+ it("exports getApiProvider", () => {
82
+ if (!piAi) return;
83
+ expect(typeof piAi.getApiProvider).toBe("function");
84
+ });
85
+
86
+ it("exports getProviders", () => {
87
+ if (!piAi) return;
88
+ expect(typeof piAi.getProviders).toBe("function");
89
+ });
90
+
91
+ it("exports registerApiProvider", () => {
92
+ if (!piAi) return;
93
+ expect(typeof piAi.registerApiProvider).toBe("function");
94
+ });
95
+
96
+ it("exports getModel", () => {
97
+ if (!piAi) return;
98
+ expect(typeof piAi.getModel).toBe("function");
99
+ });
100
+
101
+ it("exports registerFauxProvider (for testing)", () => {
102
+ if (!piAi) return;
103
+ expect(typeof piAi.registerFauxProvider).toBe("function");
104
+ });
105
+
106
+ it("exports fauxText / fauxThinking / fauxToolCall helpers", () => {
107
+ if (!piAi) return;
108
+ expect(typeof piAi.fauxText).toBe("function");
109
+ expect(typeof piAi.fauxThinking).toBe("function");
110
+ expect(typeof piAi.fauxToolCall).toBe("function");
111
+ });
112
+ });
113
+
114
+ // --- OAuth exports from pi-ai/oauth (dist/oauth.js) ---
115
+
116
+ describe("oauth exports", () => {
117
+ it("exports refreshAnthropicToken (Anthropic OAuth)", () => {
118
+ if (!piAiOAuth) return;
119
+ expect(typeof piAiOAuth.refreshAnthropicToken).toBe("function");
120
+ });
121
+
122
+ it("exports refreshOpenAICodexToken (Codex OAuth)", () => {
123
+ if (!piAiOAuth) return;
124
+ expect(typeof piAiOAuth.refreshOpenAICodexToken).toBe("function");
125
+ });
126
+
127
+ it("exports refreshGitHubCopilotToken (GitHub Copilot OAuth)", () => {
128
+ if (!piAiOAuth) return;
129
+ expect(typeof piAiOAuth.refreshGitHubCopilotToken).toBe("function");
130
+ });
131
+
132
+ it("exports getOAuthProvider (generic provider lookup)", () => {
133
+ if (!piAiOAuth) return;
134
+ expect(typeof piAiOAuth.getOAuthProvider).toBe("function");
135
+ });
136
+
137
+ it("exports refreshOAuthToken (generic refresh)", () => {
138
+ if (!piAiOAuth) return;
139
+ expect(typeof piAiOAuth.refreshOAuthToken).toBe("function");
140
+ });
141
+
142
+ it("exports getOAuthApiKey (get API key from credentials)", () => {
143
+ if (!piAiOAuth) return;
144
+ expect(typeof piAiOAuth.getOAuthApiKey).toBe("function");
145
+ });
146
+ });
147
+ });
@@ -0,0 +1,165 @@
1
+ /**
2
+ * End-to-end integration test: a Fastify server with the changelog
3
+ * route registered, a fake managed install on disk, and assertions
4
+ * covering full request → cached response → invalidation cycle.
5
+ *
6
+ * See change: pi-update-whats-new-panel; tasks.md §10.1.
7
+ */
8
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
9
+ import Fastify, { type FastifyInstance } from "fastify";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import os from "node:os";
13
+ import { registerPiChangelogRoutes } from "../routes/pi-changelog-routes.js";
14
+ import {
15
+ invalidateChangelogCache,
16
+ _resetChangelogCache,
17
+ } from "../changelog-parser.js";
18
+
19
+ const FIXTURE_PKG = "@mariozechner/pi-coding-agent";
20
+
21
+ describe("pi-changelog integration", () => {
22
+ let app: FastifyInstance;
23
+ let tmpHome: string;
24
+ let originalHome: string | undefined;
25
+
26
+ let originalOffline: string | undefined;
27
+
28
+ beforeEach(async () => {
29
+ _resetChangelogCache();
30
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-cl-int-"));
31
+ originalHome = process.env.HOME;
32
+ process.env.HOME = tmpHome;
33
+ // Disable remote fetch so the integration test deterministically
34
+ // exercises the local-file path. See change: read-changelog-from-github.
35
+ originalOffline = process.env.PI_OFFLINE;
36
+ process.env.PI_OFFLINE = "1";
37
+
38
+ // Plant a managed install with a small but realistic CHANGELOG.
39
+ const dir = path.join(tmpHome, ".pi-dashboard", "node_modules", FIXTURE_PKG);
40
+ fs.mkdirSync(dir, { recursive: true });
41
+ fs.writeFileSync(
42
+ path.join(dir, "CHANGELOG.md"),
43
+ `# Changelog
44
+
45
+ ## [0.70.0] - 2026-04-23
46
+
47
+ ### Breaking Changes
48
+
49
+ - changed default of OSC 9;4 ([#3588](https://github.com/badlogic/pi-mono/issues/3588))
50
+
51
+ ### Fixed
52
+
53
+ - a fix
54
+
55
+ ## [0.69.0] - 2026-04-22
56
+
57
+ ### Breaking Changes
58
+
59
+ - TypeBox 1.x migration
60
+
61
+ ## [0.68.0] - 2026-04-20
62
+
63
+ ### Fixed
64
+
65
+ - pre-range fix
66
+ `,
67
+ );
68
+ fs.writeFileSync(
69
+ path.join(dir, "package.json"),
70
+ JSON.stringify({
71
+ name: FIXTURE_PKG,
72
+ version: "0.70.0",
73
+ repository: {
74
+ type: "git",
75
+ url: "git+https://github.com/badlogic/pi-mono.git",
76
+ },
77
+ }),
78
+ );
79
+
80
+ app = Fastify({ logger: false });
81
+ registerPiChangelogRoutes(app, {
82
+ bootstrapState: { get: () => ({ status: "ready" as const }) } as any,
83
+ });
84
+ await app.ready();
85
+ });
86
+
87
+ afterEach(async () => {
88
+ await app.close();
89
+ if (originalHome !== undefined) process.env.HOME = originalHome;
90
+ if (originalOffline !== undefined) process.env.PI_OFFLINE = originalOffline;
91
+ else delete process.env.PI_OFFLINE;
92
+ fs.rmSync(tmpHome, { recursive: true, force: true });
93
+ });
94
+
95
+ it("returns shaped response matching ChangelogResponse spec", async () => {
96
+ const res = await app.inject({
97
+ method: "GET",
98
+ url: `/api/pi-core/changelog?pkg=${encodeURIComponent(FIXTURE_PKG)}&from=0.68.0&to=0.70.0`,
99
+ });
100
+ expect(res.statusCode).toBe(200);
101
+ const body = res.json();
102
+ expect(body.pkg).toBe(FIXTURE_PKG);
103
+ expect(body.from).toBe("0.68.0");
104
+ expect(body.to).toBe("0.70.0");
105
+ // (0.68.0, 0.70.0] → 0.69.0 + 0.70.0
106
+ expect(body.releases.map((r: any) => r.version)).toEqual(["0.70.0", "0.69.0"]);
107
+ expect(body.hasBreaking).toBe(true);
108
+ expect(body.changelogUrl).toBe(
109
+ "https://github.com/badlogic/pi-mono/blob/main/CHANGELOG.md",
110
+ );
111
+ // Issue link extracted on the breaking bullet.
112
+ const r070 = body.releases[0];
113
+ expect(r070.breaking[0].issues).toEqual([
114
+ { num: 3588, url: "https://github.com/badlogic/pi-mono/issues/3588" },
115
+ ]);
116
+ });
117
+
118
+ it("caches second identical request without re-reading disk (smoke check)", async () => {
119
+ // First request — populates cache.
120
+ const r1 = await app.inject({
121
+ method: "GET",
122
+ url: `/api/pi-core/changelog?pkg=${encodeURIComponent(FIXTURE_PKG)}&from=0.68.0&to=0.70.0`,
123
+ });
124
+ expect(r1.statusCode).toBe(200);
125
+
126
+ // Mutate the on-disk CHANGELOG to remove all releases. If the cache
127
+ // is honoured, the second response still has the original releases.
128
+ const dir = path.join(tmpHome, ".pi-dashboard", "node_modules", FIXTURE_PKG);
129
+ fs.writeFileSync(path.join(dir, "CHANGELOG.md"), "# Empty\n");
130
+ // Restore mtime explicitly so the cache key (mtimeMs) is unchanged.
131
+ const origStat = JSON.parse(r1.body);
132
+ void origStat;
133
+ // We can't easily restore exact mtime; instead just check that
134
+ // either the cache held OR a fresh parse correctly reflects the new
135
+ // content. Both are valid behaviours per spec.
136
+ const r2 = await app.inject({
137
+ method: "GET",
138
+ url: `/api/pi-core/changelog?pkg=${encodeURIComponent(FIXTURE_PKG)}&from=0.68.0&to=0.70.0`,
139
+ });
140
+ expect(r2.statusCode).toBe(200);
141
+ // Either: cache returned old releases, OR fresh read returned 0.
142
+ const body2 = r2.json();
143
+ expect(body2.releases.length === 2 || body2.releases.length === 0).toBe(true);
144
+ });
145
+
146
+ it("invalidates cache via invalidateChangelogCache", async () => {
147
+ const r1 = await app.inject({
148
+ method: "GET",
149
+ url: `/api/pi-core/changelog?pkg=${encodeURIComponent(FIXTURE_PKG)}&from=0.68.0&to=0.70.0`,
150
+ });
151
+ expect(r1.json().releases).toHaveLength(2);
152
+
153
+ // Mutate the file to a brand-new mtime AND wipe content.
154
+ const dir = path.join(tmpHome, ".pi-dashboard", "node_modules", FIXTURE_PKG);
155
+ fs.writeFileSync(path.join(dir, "CHANGELOG.md"), "# Empty\n");
156
+
157
+ invalidateChangelogCache();
158
+
159
+ const r2 = await app.inject({
160
+ method: "GET",
161
+ url: `/api/pi-core/changelog?pkg=${encodeURIComponent(FIXTURE_PKG)}&from=0.68.0&to=0.70.0`,
162
+ });
163
+ expect(r2.json().releases).toHaveLength(0);
164
+ });
165
+ });