@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.
- package/AGENTS.md +26 -5
- package/README.md +49 -7
- package/docs/architecture.md +129 -1
- package/package.json +15 -15
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
- package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
- package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
- package/packages/extension/src/ask-user-tool.ts +1 -1
- package/packages/extension/src/bridge-context.ts +68 -4
- package/packages/extension/src/bridge.ts +79 -11
- package/packages/extension/src/command-handler.ts +95 -15
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/multiselect-list.ts +1 -1
- package/packages/extension/src/pi-env.d.ts +16 -9
- package/packages/extension/src/prompt-expander.ts +74 -63
- package/packages/extension/src/provider-register.ts +16 -9
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +31 -70
- package/packages/extension/src/session-sync.ts +10 -1
- package/packages/extension/src/slash-dispatch.ts +123 -0
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/bin/pi-dashboard.mjs +84 -0
- package/packages/server/package.json +8 -7
- package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
- package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
- package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
- package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service.test.ts +2 -2
- package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
- package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
- package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
- package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
- package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
- package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
- package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
- package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
- package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
- package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
- package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
- package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
- package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
- package/packages/server/src/__tests__/package-routes.test.ts +1 -1
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
- package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
- package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
- package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
- package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
- package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
- package/packages/server/src/auth-plugin.ts +3 -0
- package/packages/server/src/bootstrap-state.ts +10 -0
- package/packages/server/src/browser-gateway.ts +27 -10
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
- package/packages/server/src/changelog-fs.ts +167 -0
- package/packages/server/src/changelog-parser.ts +321 -0
- package/packages/server/src/changelog-remote.ts +134 -0
- package/packages/server/src/cli.ts +62 -82
- package/packages/server/src/config-api.ts +14 -2
- package/packages/server/src/directory-service.ts +106 -4
- package/packages/server/src/event-wiring.ts +90 -6
- package/packages/server/src/headless-pid-registry.ts +344 -37
- package/packages/server/src/legacy-pi-cleanup.ts +151 -0
- package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
- package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
- package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
- package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
- package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
- package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
- package/packages/server/src/model-proxy/api-key-store.ts +87 -0
- package/packages/server/src/model-proxy/auth-gate.ts +116 -0
- package/packages/server/src/model-proxy/concurrency.ts +76 -0
- package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
- package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
- package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
- package/packages/server/src/model-proxy/convert/index.ts +8 -0
- package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
- package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
- package/packages/server/src/model-proxy/convert/types.ts +70 -0
- package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
- package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
- package/packages/server/src/model-proxy/internal-registry.ts +157 -0
- package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
- package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
- package/packages/server/src/model-proxy/request-log.ts +53 -0
- package/packages/server/src/model-proxy/streamer.ts +59 -0
- package/packages/server/src/openspec-group-store.ts +490 -0
- package/packages/server/src/pending-client-correlations.ts +73 -0
- package/packages/server/src/pending-fork-registry.ts +24 -12
- package/packages/server/src/pi-core-checker.ts +77 -17
- package/packages/server/src/pi-core-updater.ts +16 -6
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +4 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +182 -11
- package/packages/server/src/provider-auth-storage.ts +29 -47
- package/packages/server/src/provider-catalogue-cache.ts +24 -18
- package/packages/server/src/restart-helper.ts +17 -16
- package/packages/server/src/routes/bootstrap-routes.ts +37 -0
- package/packages/server/src/routes/jj-routes.ts +3 -0
- package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
- package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
- package/packages/server/src/routes/model-proxy-routes.ts +330 -0
- package/packages/server/src/routes/openspec-group-routes.ts +231 -0
- package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
- package/packages/server/src/routes/pi-core-routes.ts +1 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -1
- package/packages/server/src/routes/provider-routes.ts +28 -5
- package/packages/server/src/routes/system-routes.ts +44 -2
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
- package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
- package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
- package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
- package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
- package/packages/server/src/server.ts +254 -60
- package/packages/server/src/session-api.ts +63 -4
- package/packages/server/src/session-discovery.ts +1 -1
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/spawn-register-watchdog.ts +62 -7
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/tunnel-watchdog.ts +230 -0
- package/packages/server/src/tunnel.ts +5 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
- package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
- package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
- package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +1 -1
- package/packages/shared/src/browser-protocol.ts +70 -0
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +172 -2
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
- package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
- package/packages/shared/src/platform/binary-lookup.ts +204 -0
- package/packages/shared/src/platform/node-spawn.ts +71 -26
- package/packages/shared/src/protocol.ts +27 -1
- package/packages/shared/src/recommended-extensions.ts +18 -0
- package/packages/shared/src/rest-api.ts +219 -1
- package/packages/shared/src/server-launcher.ts +277 -0
- package/packages/shared/src/skill-block-parser.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
- package/packages/shared/src/tool-registry/definitions.ts +15 -3
- package/packages/shared/src/types.ts +62 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
- package/packages/shared/src/resolve-jiti.ts +0 -102
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fastify integration tests for `GET /api/pi-core/changelog`.
|
|
3
|
+
*
|
|
4
|
+
* Covers every scenario in spec
|
|
5
|
+
* `pi-changelog-display#Requirement: Changelog REST endpoint`.
|
|
6
|
+
*
|
|
7
|
+
* See change: pi-update-whats-new-panel.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
10
|
+
import Fastify, { type FastifyInstance } from "fastify";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import { registerPiChangelogRoutes } from "../routes/pi-changelog-routes.js";
|
|
15
|
+
import { _resetChangelogCache } from "../changelog-parser.js";
|
|
16
|
+
|
|
17
|
+
// We can't easily patch findChangelogPath at import-time (vitest module
|
|
18
|
+
// mocks vary), so we set up a fake managed install at HOME/.pi-dashboard
|
|
19
|
+
// and rely on findChangelogPath's defaultManagedDir() reading $HOME.
|
|
20
|
+
let tmpHome: string;
|
|
21
|
+
let originalHome: string | undefined;
|
|
22
|
+
|
|
23
|
+
function makeManagedPkg(pkg: string, files: Record<string, string>): string {
|
|
24
|
+
const dir = path.join(tmpHome, ".pi-dashboard", "node_modules", pkg);
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
for (const [name, content] of Object.entries(files)) {
|
|
27
|
+
fs.writeFileSync(path.join(dir, name), content);
|
|
28
|
+
}
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SAMPLE_CHANGELOG = `# Changelog
|
|
33
|
+
|
|
34
|
+
## [0.70.0] - 2026-04-23
|
|
35
|
+
|
|
36
|
+
### Breaking Changes
|
|
37
|
+
|
|
38
|
+
- broke X
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- a fix
|
|
43
|
+
|
|
44
|
+
## [0.69.0] - 2026-04-22
|
|
45
|
+
|
|
46
|
+
### Breaking Changes
|
|
47
|
+
|
|
48
|
+
- broke Y
|
|
49
|
+
|
|
50
|
+
## [0.68.0] - 2026-04-20
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- another fix
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
describe("pi-changelog-routes", () => {
|
|
58
|
+
let app: FastifyInstance;
|
|
59
|
+
let bootstrapState: any;
|
|
60
|
+
let originalOffline: string | undefined;
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
_resetChangelogCache();
|
|
64
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-cl-route-"));
|
|
65
|
+
originalHome = process.env.HOME;
|
|
66
|
+
process.env.HOME = tmpHome;
|
|
67
|
+
// Disable remote-CHANGELOG fetch so existing tests deterministically
|
|
68
|
+
// exercise the local-file path. The new remote-source tests below
|
|
69
|
+
// toggle PI_OFFLINE off and stub fetch explicitly. See change:
|
|
70
|
+
// read-changelog-from-github.
|
|
71
|
+
originalOffline = process.env.PI_OFFLINE;
|
|
72
|
+
process.env.PI_OFFLINE = "1";
|
|
73
|
+
bootstrapState = {
|
|
74
|
+
get: () => ({ status: "ready" as const }),
|
|
75
|
+
};
|
|
76
|
+
app = Fastify({ logger: false });
|
|
77
|
+
registerPiChangelogRoutes(app, { bootstrapState });
|
|
78
|
+
await app.ready();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(async () => {
|
|
82
|
+
await app.close();
|
|
83
|
+
if (originalHome !== undefined) process.env.HOME = originalHome;
|
|
84
|
+
if (originalOffline !== undefined) process.env.PI_OFFLINE = originalOffline;
|
|
85
|
+
else delete process.env.PI_OFFLINE;
|
|
86
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns 200 with filtered releases for a valid range", async () => {
|
|
90
|
+
makeManagedPkg("@mariozechner/pi-coding-agent", {
|
|
91
|
+
"CHANGELOG.md": SAMPLE_CHANGELOG,
|
|
92
|
+
"package.json": JSON.stringify({
|
|
93
|
+
name: "@mariozechner/pi-coding-agent",
|
|
94
|
+
version: "0.70.0",
|
|
95
|
+
repository: "https://github.com/badlogic/pi-mono.git",
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
const res = await app.inject({
|
|
99
|
+
method: "GET",
|
|
100
|
+
url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.68.0&to=0.70.0",
|
|
101
|
+
});
|
|
102
|
+
expect(res.statusCode).toBe(200);
|
|
103
|
+
const body = res.json();
|
|
104
|
+
expect(body.pkg).toBe("@mariozechner/pi-coding-agent");
|
|
105
|
+
expect(body.from).toBe("0.68.0");
|
|
106
|
+
expect(body.to).toBe("0.70.0");
|
|
107
|
+
// (0.68.0, 0.70.0] → 0.69.0 + 0.70.0
|
|
108
|
+
expect(body.releases.map((r: any) => r.version)).toEqual(["0.70.0", "0.69.0"]);
|
|
109
|
+
expect(body.hasBreaking).toBe(true);
|
|
110
|
+
expect(body.changelogUrl).toBe(
|
|
111
|
+
"https://github.com/badlogic/pi-mono/blob/main/CHANGELOG.md",
|
|
112
|
+
);
|
|
113
|
+
expect(typeof body.parsedAt).toBe("string");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("hasBreaking is false when no release in range has breaking changes", async () => {
|
|
117
|
+
makeManagedPkg("@mariozechner/pi-coding-agent", {
|
|
118
|
+
"CHANGELOG.md": SAMPLE_CHANGELOG,
|
|
119
|
+
"package.json": JSON.stringify({
|
|
120
|
+
name: "@mariozechner/pi-coding-agent",
|
|
121
|
+
version: "0.68.0",
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
const res = await app.inject({
|
|
125
|
+
method: "GET",
|
|
126
|
+
url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.67.0&to=0.68.0",
|
|
127
|
+
});
|
|
128
|
+
const body = res.json();
|
|
129
|
+
expect(body.releases.map((r: any) => r.version)).toEqual(["0.68.0"]);
|
|
130
|
+
expect(body.hasBreaking).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("rejects pkg outside the core whitelist with 400", async () => {
|
|
134
|
+
const res = await app.inject({
|
|
135
|
+
method: "GET",
|
|
136
|
+
url: "/api/pi-core/changelog?pkg=evil-pkg&from=0.0.1&to=0.0.2",
|
|
137
|
+
});
|
|
138
|
+
expect(res.statusCode).toBe(400);
|
|
139
|
+
expect(res.json().error).toMatch(/pkg must be one of/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns 200 with empty releases when CHANGELOG missing", async () => {
|
|
143
|
+
// Whitelisted package but nothing on disk.
|
|
144
|
+
const res = await app.inject({
|
|
145
|
+
method: "GET",
|
|
146
|
+
url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.68.0&to=0.70.0",
|
|
147
|
+
});
|
|
148
|
+
expect(res.statusCode).toBe(200);
|
|
149
|
+
const body = res.json();
|
|
150
|
+
expect(body.releases).toEqual([]);
|
|
151
|
+
expect(body.hasBreaking).toBe(false);
|
|
152
|
+
expect(body.changelogUrl).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("rejects missing from/to with 400", async () => {
|
|
156
|
+
const res = await app.inject({
|
|
157
|
+
method: "GET",
|
|
158
|
+
url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.68.0",
|
|
159
|
+
});
|
|
160
|
+
expect(res.statusCode).toBe(400);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("rejects unparseable versions with 400", async () => {
|
|
164
|
+
const res = await app.inject({
|
|
165
|
+
method: "GET",
|
|
166
|
+
url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=junk&to=0.70.0",
|
|
167
|
+
});
|
|
168
|
+
expect(res.statusCode).toBe(400);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("returns 503 when bootstrap is not ready", async () => {
|
|
172
|
+
await app.close();
|
|
173
|
+
bootstrapState.get = () => ({ status: "installing" as const });
|
|
174
|
+
app = Fastify({ logger: false });
|
|
175
|
+
registerPiChangelogRoutes(app, { bootstrapState });
|
|
176
|
+
await app.ready();
|
|
177
|
+
const res = await app.inject({
|
|
178
|
+
method: "GET",
|
|
179
|
+
url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.68.0&to=0.70.0",
|
|
180
|
+
});
|
|
181
|
+
expect(res.statusCode).toBe(503);
|
|
182
|
+
expect(res.json().bootstrap).toBe("installing");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("returns no releases when from === to", async () => {
|
|
186
|
+
makeManagedPkg("@mariozechner/pi-coding-agent", {
|
|
187
|
+
"CHANGELOG.md": SAMPLE_CHANGELOG,
|
|
188
|
+
"package.json": "{}",
|
|
189
|
+
});
|
|
190
|
+
const res = await app.inject({
|
|
191
|
+
method: "GET",
|
|
192
|
+
url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.70.0&to=0.70.0",
|
|
193
|
+
});
|
|
194
|
+
const body = res.json();
|
|
195
|
+
// Half-open (from, to] — equal endpoints means range is empty.
|
|
196
|
+
expect(body.releases).toEqual([]);
|
|
197
|
+
expect(body.hasBreaking).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("derives null changelogUrl when repository is missing or non-GitHub", async () => {
|
|
201
|
+
makeManagedPkg("@mariozechner/pi-coding-agent", {
|
|
202
|
+
"CHANGELOG.md": SAMPLE_CHANGELOG,
|
|
203
|
+
"package.json": JSON.stringify({ name: "x", version: "0.70.0" }),
|
|
204
|
+
});
|
|
205
|
+
const res = await app.inject({
|
|
206
|
+
method: "GET",
|
|
207
|
+
url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.68.0&to=0.70.0",
|
|
208
|
+
});
|
|
209
|
+
expect(res.json().changelogUrl).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ── Remote-CHANGELOG path ─────────────────────────────────────────────────────────
|
|
214
|
+
// See change: read-changelog-from-github.
|
|
215
|
+
|
|
216
|
+
import { vi } from "vitest";
|
|
217
|
+
|
|
218
|
+
describe("pi-changelog-routes remote source", () => {
|
|
219
|
+
let app: FastifyInstance;
|
|
220
|
+
let tmpHomeRemote: string;
|
|
221
|
+
let originalHomeRemote: string | undefined;
|
|
222
|
+
let originalOfflineRemote: string | undefined;
|
|
223
|
+
let originalFetch: typeof globalThis.fetch;
|
|
224
|
+
|
|
225
|
+
// SAMPLE_CHANGELOG_REMOTE simulates an upstream CHANGELOG that
|
|
226
|
+
// contains a release entry NEWER than what the local install knows.
|
|
227
|
+
const SAMPLE_CHANGELOG_REMOTE = `# Changelog
|
|
228
|
+
|
|
229
|
+
## [0.99.0] - 2026-12-01
|
|
230
|
+
|
|
231
|
+
### Breaking Changes
|
|
232
|
+
|
|
233
|
+
- only-on-remote breaking change
|
|
234
|
+
|
|
235
|
+
### Fixed
|
|
236
|
+
|
|
237
|
+
- only-on-remote fix
|
|
238
|
+
|
|
239
|
+
## [0.70.0] - 2026-04-23
|
|
240
|
+
|
|
241
|
+
### Fixed
|
|
242
|
+
|
|
243
|
+
- old fix
|
|
244
|
+
`;
|
|
245
|
+
|
|
246
|
+
// SAMPLE_CHANGELOG_LOCAL simulates the locally-installed older copy.
|
|
247
|
+
const SAMPLE_CHANGELOG_LOCAL = `# Changelog
|
|
248
|
+
|
|
249
|
+
## [0.70.0] - 2026-04-23
|
|
250
|
+
|
|
251
|
+
### Fixed
|
|
252
|
+
|
|
253
|
+
- old fix
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
function makePkg(home: string, pkg: string, files: Record<string, string>): string {
|
|
257
|
+
const dir = path.join(home, ".pi-dashboard", "node_modules", pkg);
|
|
258
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
259
|
+
for (const [name, content] of Object.entries(files)) {
|
|
260
|
+
fs.writeFileSync(path.join(dir, name), content);
|
|
261
|
+
}
|
|
262
|
+
return dir;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
beforeEach(async () => {
|
|
266
|
+
_resetChangelogCache();
|
|
267
|
+
tmpHomeRemote = fs.mkdtempSync(path.join(os.tmpdir(), "pi-cl-remote-"));
|
|
268
|
+
originalHomeRemote = process.env.HOME;
|
|
269
|
+
process.env.HOME = tmpHomeRemote;
|
|
270
|
+
// Remote path needs PI_OFFLINE OFF.
|
|
271
|
+
originalOfflineRemote = process.env.PI_OFFLINE;
|
|
272
|
+
delete process.env.PI_OFFLINE;
|
|
273
|
+
originalFetch = globalThis.fetch;
|
|
274
|
+
app = Fastify({ logger: false });
|
|
275
|
+
registerPiChangelogRoutes(app, {
|
|
276
|
+
bootstrapState: { get: () => ({ status: "ready" as const }) } as any,
|
|
277
|
+
});
|
|
278
|
+
await app.ready();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
afterEach(async () => {
|
|
282
|
+
await app.close();
|
|
283
|
+
if (originalHomeRemote !== undefined) process.env.HOME = originalHomeRemote;
|
|
284
|
+
if (originalOfflineRemote !== undefined) process.env.PI_OFFLINE = originalOfflineRemote;
|
|
285
|
+
else delete process.env.PI_OFFLINE;
|
|
286
|
+
globalThis.fetch = originalFetch;
|
|
287
|
+
fs.rmSync(tmpHomeRemote, { recursive: true, force: true });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("prefers remote CHANGELOG over local when both are available", async () => {
|
|
291
|
+
makePkg(tmpHomeRemote, "@mariozechner/pi-coding-agent", {
|
|
292
|
+
"CHANGELOG.md": SAMPLE_CHANGELOG_LOCAL,
|
|
293
|
+
"package.json": JSON.stringify({
|
|
294
|
+
name: "@mariozechner/pi-coding-agent",
|
|
295
|
+
version: "0.70.0",
|
|
296
|
+
repository: { type: "git", url: "git+https://github.com/badlogic/pi-mono.git", directory: "packages/coding-agent" },
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
300
|
+
ok: true,
|
|
301
|
+
status: 200,
|
|
302
|
+
headers: new Headers({ etag: '"v1"' }),
|
|
303
|
+
text: async () => SAMPLE_CHANGELOG_REMOTE,
|
|
304
|
+
}) as any;
|
|
305
|
+
|
|
306
|
+
const res = await app.inject({
|
|
307
|
+
method: "GET",
|
|
308
|
+
url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.70.0&to=0.99.0",
|
|
309
|
+
});
|
|
310
|
+
expect(res.statusCode).toBe(200);
|
|
311
|
+
const body = res.json();
|
|
312
|
+
// Range (0.70.0, 0.99.0] — should include 0.99.0 from REMOTE,
|
|
313
|
+
// which the local file does not contain.
|
|
314
|
+
expect(body.releases.map((r: any) => r.version)).toEqual(["0.99.0"]);
|
|
315
|
+
expect(body.hasBreaking).toBe(true);
|
|
316
|
+
expect(body.releases[0].breaking[0].text).toBe("only-on-remote breaking change");
|
|
317
|
+
// changelogUrl is the human URL (the /blob/main/ form), not raw.
|
|
318
|
+
expect(body.changelogUrl).toBe(
|
|
319
|
+
"https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md",
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("falls back to local CHANGELOG when remote fails", async () => {
|
|
324
|
+
makePkg(tmpHomeRemote, "@mariozechner/pi-coding-agent", {
|
|
325
|
+
"CHANGELOG.md": SAMPLE_CHANGELOG_LOCAL,
|
|
326
|
+
"package.json": JSON.stringify({
|
|
327
|
+
name: "@mariozechner/pi-coding-agent",
|
|
328
|
+
version: "0.70.0",
|
|
329
|
+
repository: { type: "git", url: "git+https://github.com/badlogic/pi-mono.git" },
|
|
330
|
+
}),
|
|
331
|
+
});
|
|
332
|
+
// Remote returns 503 → fetchRemoteChangelog returns null → route falls
|
|
333
|
+
// back to local.
|
|
334
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
335
|
+
ok: false,
|
|
336
|
+
status: 503,
|
|
337
|
+
headers: new Headers(),
|
|
338
|
+
text: async () => "",
|
|
339
|
+
}) as any;
|
|
340
|
+
|
|
341
|
+
const res = await app.inject({
|
|
342
|
+
method: "GET",
|
|
343
|
+
url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.69.0&to=0.70.0",
|
|
344
|
+
});
|
|
345
|
+
expect(res.statusCode).toBe(200);
|
|
346
|
+
const body = res.json();
|
|
347
|
+
// Range (0.69.0, 0.70.0] — local has only 0.70.0; remote returned 503.
|
|
348
|
+
expect(body.releases.map((r: any) => r.version)).toEqual(["0.70.0"]);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("PI_OFFLINE=1 skips remote and reads local directly", async () => {
|
|
352
|
+
process.env.PI_OFFLINE = "1";
|
|
353
|
+
makePkg(tmpHomeRemote, "@mariozechner/pi-coding-agent", {
|
|
354
|
+
"CHANGELOG.md": SAMPLE_CHANGELOG_LOCAL,
|
|
355
|
+
"package.json": JSON.stringify({
|
|
356
|
+
name: "@mariozechner/pi-coding-agent",
|
|
357
|
+
version: "0.70.0",
|
|
358
|
+
repository: "https://github.com/badlogic/pi-mono.git",
|
|
359
|
+
}),
|
|
360
|
+
});
|
|
361
|
+
let fetchCalled = false;
|
|
362
|
+
globalThis.fetch = vi.fn().mockImplementation(() => {
|
|
363
|
+
fetchCalled = true;
|
|
364
|
+
return Promise.resolve({
|
|
365
|
+
ok: true,
|
|
366
|
+
status: 200,
|
|
367
|
+
headers: new Headers(),
|
|
368
|
+
text: async () => SAMPLE_CHANGELOG_REMOTE,
|
|
369
|
+
});
|
|
370
|
+
}) as any;
|
|
371
|
+
|
|
372
|
+
const res = await app.inject({
|
|
373
|
+
method: "GET",
|
|
374
|
+
url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.69.0&to=0.99.0",
|
|
375
|
+
});
|
|
376
|
+
expect(res.statusCode).toBe(200);
|
|
377
|
+
expect(fetchCalled).toBe(false);
|
|
378
|
+
// 0.99.0 only exists in remote — with PI_OFFLINE we don't see it.
|
|
379
|
+
const body = res.json();
|
|
380
|
+
expect(body.releases.map((r: any) => r.version)).toEqual(["0.70.0"]);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("second request within TTL serves cached remote without re-fetching", async () => {
|
|
384
|
+
makePkg(tmpHomeRemote, "@mariozechner/pi-coding-agent", {
|
|
385
|
+
"CHANGELOG.md": SAMPLE_CHANGELOG_LOCAL,
|
|
386
|
+
"package.json": JSON.stringify({
|
|
387
|
+
name: "@mariozechner/pi-coding-agent",
|
|
388
|
+
version: "0.70.0",
|
|
389
|
+
repository: "https://github.com/badlogic/pi-mono.git",
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
let fetchCalls = 0;
|
|
393
|
+
globalThis.fetch = vi.fn().mockImplementation(() => {
|
|
394
|
+
fetchCalls++;
|
|
395
|
+
return Promise.resolve({
|
|
396
|
+
ok: true,
|
|
397
|
+
status: 200,
|
|
398
|
+
headers: new Headers({ etag: '"v1"' }),
|
|
399
|
+
text: async () => SAMPLE_CHANGELOG_REMOTE,
|
|
400
|
+
});
|
|
401
|
+
}) as any;
|
|
402
|
+
|
|
403
|
+
const url =
|
|
404
|
+
"/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.70.0&to=0.99.0";
|
|
405
|
+
await app.inject({ method: "GET", url });
|
|
406
|
+
await app.inject({ method: "GET", url });
|
|
407
|
+
expect(fetchCalls).toBe(1);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs from "node:fs";
|
|
@@ -34,9 +34,22 @@ describe("PiCoreChecker._internal.looksLikePiEcosystem", () => {
|
|
|
34
34
|
|
|
35
35
|
describe("PiCoreChecker.getStatus", () => {
|
|
36
36
|
let tmpManagedDir: string;
|
|
37
|
+
let originalOffline: string | undefined;
|
|
37
38
|
|
|
38
39
|
beforeEach(() => {
|
|
39
40
|
tmpManagedDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-core-test-"));
|
|
41
|
+
// Disable the default pi.dev fetcher in this block so existing
|
|
42
|
+
// tests (which mock fetchLatest only) aren't surprised by live
|
|
43
|
+
// network calls. The pi.dev integration tests in the next describe
|
|
44
|
+
// block inject `fetchPiDevRelease` explicitly and therefore bypass
|
|
45
|
+
// this env. See change: improve-pi-update-detection.
|
|
46
|
+
originalOffline = process.env.PI_OFFLINE;
|
|
47
|
+
process.env.PI_OFFLINE = "1";
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
if (originalOffline !== undefined) process.env.PI_OFFLINE = originalOffline;
|
|
52
|
+
else delete process.env.PI_OFFLINE;
|
|
40
53
|
});
|
|
41
54
|
|
|
42
55
|
function writeManagedPackage(managedDir: string, name: string, version: string) {
|
|
@@ -50,14 +63,14 @@ describe("PiCoreChecker.getStatus", () => {
|
|
|
50
63
|
npmList: async () =>
|
|
51
64
|
JSON.stringify({
|
|
52
65
|
dependencies: {
|
|
53
|
-
"@
|
|
66
|
+
"@earendil-works/pi-coding-agent": { version: "0.67.1" },
|
|
54
67
|
"@blackbelt-technology/pi-agent-dashboard": { version: "0.4.0" },
|
|
55
68
|
"pi-web-access": { version: "0.10.6" }, // NOT in whitelist → ignored
|
|
56
69
|
react: { version: "19.0.0" }, // ignored
|
|
57
70
|
},
|
|
58
71
|
}),
|
|
59
72
|
fetchLatest: async (name) => {
|
|
60
|
-
if (name === "@
|
|
73
|
+
if (name === "@earendil-works/pi-coding-agent") return "0.67.6";
|
|
61
74
|
if (name === "@blackbelt-technology/pi-agent-dashboard") return "0.4.1";
|
|
62
75
|
return null;
|
|
63
76
|
},
|
|
@@ -69,7 +82,7 @@ describe("PiCoreChecker.getStatus", () => {
|
|
|
69
82
|
expect(status.packages.length).toBe(2);
|
|
70
83
|
expect(status.packages.find((p) => p.name === "pi-web-access")).toBeUndefined();
|
|
71
84
|
|
|
72
|
-
const pi = status.packages.find((p) => p.name === "@
|
|
85
|
+
const pi = status.packages.find((p) => p.name === "@earendil-works/pi-coding-agent")!;
|
|
73
86
|
expect(pi.displayName).toBe("pi (core agent)");
|
|
74
87
|
expect(pi.currentVersion).toBe("0.67.1");
|
|
75
88
|
expect(pi.latestVersion).toBe("0.67.6");
|
|
@@ -103,13 +116,13 @@ describe("PiCoreChecker.getStatus", () => {
|
|
|
103
116
|
});
|
|
104
117
|
|
|
105
118
|
it("discovers managed packages and prefers them over global duplicates", async () => {
|
|
106
|
-
writeManagedPackage(tmpManagedDir, "@
|
|
119
|
+
writeManagedPackage(tmpManagedDir, "@earendil-works/pi-coding-agent", "0.67.5");
|
|
107
120
|
|
|
108
121
|
const checker = new PiCoreChecker({
|
|
109
122
|
npmList: async () =>
|
|
110
123
|
JSON.stringify({
|
|
111
124
|
dependencies: {
|
|
112
|
-
"@
|
|
125
|
+
"@earendil-works/pi-coding-agent": { version: "0.67.1" },
|
|
113
126
|
},
|
|
114
127
|
}),
|
|
115
128
|
fetchLatest: async () => "0.67.6",
|
|
@@ -155,7 +168,7 @@ describe("PiCoreChecker.getStatus", () => {
|
|
|
155
168
|
const err = new Error("npm warn") as Error & { stdout: string };
|
|
156
169
|
err.stdout = JSON.stringify({
|
|
157
170
|
dependencies: {
|
|
158
|
-
"@
|
|
171
|
+
"@earendil-works/pi-coding-agent": { version: "0.67.1" },
|
|
159
172
|
},
|
|
160
173
|
});
|
|
161
174
|
throw err;
|
|
@@ -165,7 +178,7 @@ describe("PiCoreChecker.getStatus", () => {
|
|
|
165
178
|
});
|
|
166
179
|
const status = await checker.getStatus();
|
|
167
180
|
expect(status.packages.length).toBe(1);
|
|
168
|
-
expect(status.packages[0].name).toBe("@
|
|
181
|
+
expect(status.packages[0].name).toBe("@earendil-works/pi-coding-agent");
|
|
169
182
|
});
|
|
170
183
|
|
|
171
184
|
it("caches results within 5 minutes", async () => {
|
|
@@ -174,7 +187,7 @@ describe("PiCoreChecker.getStatus", () => {
|
|
|
174
187
|
npmList: async () => {
|
|
175
188
|
calls++;
|
|
176
189
|
return JSON.stringify({
|
|
177
|
-
dependencies: { "@
|
|
190
|
+
dependencies: { "@earendil-works/pi-coding-agent": { version: "0.67.1" } },
|
|
178
191
|
});
|
|
179
192
|
},
|
|
180
193
|
fetchLatest: async () => "0.67.6",
|
|
@@ -191,7 +204,7 @@ describe("PiCoreChecker.getStatus", () => {
|
|
|
191
204
|
npmList: async () => {
|
|
192
205
|
calls++;
|
|
193
206
|
return JSON.stringify({
|
|
194
|
-
dependencies: { "@
|
|
207
|
+
dependencies: { "@earendil-works/pi-coding-agent": { version: "0.67.1" } },
|
|
195
208
|
});
|
|
196
209
|
},
|
|
197
210
|
fetchLatest: async () => "0.67.6",
|
|
@@ -206,7 +219,7 @@ describe("PiCoreChecker.getStatus", () => {
|
|
|
206
219
|
const checker = new PiCoreChecker({
|
|
207
220
|
npmList: async () =>
|
|
208
221
|
JSON.stringify({
|
|
209
|
-
dependencies: { "@
|
|
222
|
+
dependencies: { "@earendil-works/pi-coding-agent": { version: "0.67.1" } },
|
|
210
223
|
}),
|
|
211
224
|
fetchLatest: async () => {
|
|
212
225
|
throw new Error("network down");
|
|
@@ -225,14 +238,143 @@ describe("PiCoreChecker.getStatus", () => {
|
|
|
225
238
|
JSON.stringify({
|
|
226
239
|
dependencies: {
|
|
227
240
|
"@blackbelt-technology/pi-agent-dashboard": { version: "0.4.0" },
|
|
228
|
-
"@
|
|
241
|
+
"@earendil-works/pi-coding-agent": { version: "0.67.1" },
|
|
229
242
|
},
|
|
230
243
|
}),
|
|
231
244
|
fetchLatest: async () => null,
|
|
232
245
|
managedDir: path.join(tmpManagedDir, "nope"),
|
|
233
246
|
});
|
|
234
247
|
const status = await checker.getStatus();
|
|
235
|
-
expect(status.packages[0].name).toBe("@
|
|
248
|
+
expect(status.packages[0].name).toBe("@earendil-works/pi-coding-agent");
|
|
236
249
|
expect(status.packages[1].name).toBe("@blackbelt-technology/pi-agent-dashboard");
|
|
237
250
|
});
|
|
238
251
|
});
|
|
252
|
+
|
|
253
|
+
describe("PiCoreChecker pi.dev integration", () => {
|
|
254
|
+
let tmpManagedDir: string;
|
|
255
|
+
|
|
256
|
+
beforeEach(async () => {
|
|
257
|
+
tmpManagedDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-core-pidev-"));
|
|
258
|
+
const { _resetDynamicPiAliases } = await import("../pi-core-checker.js");
|
|
259
|
+
_resetDynamicPiAliases();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("prefers pi.dev for @mariozechner/pi-coding-agent latestVersion", async () => {
|
|
263
|
+
let npmCalled = false;
|
|
264
|
+
const checker = new PiCoreChecker({
|
|
265
|
+
npmList: async () =>
|
|
266
|
+
JSON.stringify({
|
|
267
|
+
dependencies: {
|
|
268
|
+
"@mariozechner/pi-coding-agent": { version: "0.70.6" },
|
|
269
|
+
},
|
|
270
|
+
}),
|
|
271
|
+
fetchLatest: async () => {
|
|
272
|
+
npmCalled = true;
|
|
273
|
+
return "0.73.1"; // npm registry says 0.73.1 …
|
|
274
|
+
},
|
|
275
|
+
fetchPiDevRelease: async () => ({ version: "0.74.0" }), // … but pi.dev says 0.74.0
|
|
276
|
+
managedDir: tmpManagedDir,
|
|
277
|
+
});
|
|
278
|
+
const status = await checker.getStatus();
|
|
279
|
+
const pi = status.packages.find((p) => p.name === "@mariozechner/pi-coding-agent")!;
|
|
280
|
+
expect(pi.latestVersion).toBe("0.74.0");
|
|
281
|
+
expect(pi.updateAvailable).toBe(true);
|
|
282
|
+
expect(npmCalled).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("falls back to npm registry when pi.dev returns undefined", async () => {
|
|
286
|
+
let npmCalled = false;
|
|
287
|
+
const checker = new PiCoreChecker({
|
|
288
|
+
npmList: async () =>
|
|
289
|
+
JSON.stringify({
|
|
290
|
+
dependencies: {
|
|
291
|
+
"@mariozechner/pi-coding-agent": { version: "0.70.6" },
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
fetchLatest: async () => {
|
|
295
|
+
npmCalled = true;
|
|
296
|
+
return "0.73.1";
|
|
297
|
+
},
|
|
298
|
+
fetchPiDevRelease: async () => undefined, // pi.dev unreachable / skipped
|
|
299
|
+
managedDir: tmpManagedDir,
|
|
300
|
+
});
|
|
301
|
+
const status = await checker.getStatus();
|
|
302
|
+
const pi = status.packages.find((p) => p.name === "@mariozechner/pi-coding-agent")!;
|
|
303
|
+
expect(pi.latestVersion).toBe("0.73.1");
|
|
304
|
+
expect(npmCalled).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("falls back to npm registry when pi.dev throws", async () => {
|
|
308
|
+
const checker = new PiCoreChecker({
|
|
309
|
+
npmList: async () =>
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
dependencies: {
|
|
312
|
+
"@mariozechner/pi-coding-agent": { version: "0.70.6" },
|
|
313
|
+
},
|
|
314
|
+
}),
|
|
315
|
+
fetchLatest: async () => "0.73.1",
|
|
316
|
+
fetchPiDevRelease: async () => {
|
|
317
|
+
throw new Error("network down");
|
|
318
|
+
},
|
|
319
|
+
managedDir: tmpManagedDir,
|
|
320
|
+
});
|
|
321
|
+
const status = await checker.getStatus();
|
|
322
|
+
const pi = status.packages.find((p) => p.name === "@mariozechner/pi-coding-agent")!;
|
|
323
|
+
expect(pi.latestVersion).toBe("0.73.1");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("does NOT call pi.dev for non-pi packages", async () => {
|
|
327
|
+
let piDevCalled = false;
|
|
328
|
+
const checker = new PiCoreChecker({
|
|
329
|
+
npmList: async () =>
|
|
330
|
+
JSON.stringify({
|
|
331
|
+
dependencies: {
|
|
332
|
+
"@blackbelt-technology/pi-agent-dashboard": { version: "0.4.0" },
|
|
333
|
+
},
|
|
334
|
+
}),
|
|
335
|
+
fetchLatest: async () => "0.5.0",
|
|
336
|
+
fetchPiDevRelease: async () => {
|
|
337
|
+
piDevCalled = true;
|
|
338
|
+
return { version: "99.99.99" };
|
|
339
|
+
},
|
|
340
|
+
managedDir: tmpManagedDir,
|
|
341
|
+
});
|
|
342
|
+
const status = await checker.getStatus();
|
|
343
|
+
const dash = status.packages.find((p) => p.name === "@blackbelt-technology/pi-agent-dashboard")!;
|
|
344
|
+
expect(dash.latestVersion).toBe("0.5.0");
|
|
345
|
+
expect(piDevCalled).toBe(false);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("records pi.dev's packageName as a trusted alias", async () => {
|
|
349
|
+
const checker = new PiCoreChecker({
|
|
350
|
+
npmList: async () =>
|
|
351
|
+
JSON.stringify({
|
|
352
|
+
dependencies: {
|
|
353
|
+
"@mariozechner/pi-coding-agent": { version: "0.70.6" },
|
|
354
|
+
},
|
|
355
|
+
}),
|
|
356
|
+
fetchLatest: async () => "0.70.6",
|
|
357
|
+
fetchPiDevRelease: async () => ({
|
|
358
|
+
version: "0.74.0",
|
|
359
|
+
packageName: "@earendil-works/pi-coding-agent",
|
|
360
|
+
}),
|
|
361
|
+
managedDir: tmpManagedDir,
|
|
362
|
+
});
|
|
363
|
+
await checker.getStatus();
|
|
364
|
+
|
|
365
|
+
// After the alias is recorded, a second discovery that finds the
|
|
366
|
+
// renamed package should accept it through the whitelist gate.
|
|
367
|
+
writeManagedPackage(tmpManagedDir, "@earendil-works/pi-coding-agent", "0.74.0");
|
|
368
|
+
checker.invalidate();
|
|
369
|
+
const status2 = await checker.getStatus();
|
|
370
|
+
const aliased = status2.packages.find((p) => p.name === "@earendil-works/pi-coding-agent");
|
|
371
|
+
expect(aliased).toBeDefined();
|
|
372
|
+
expect(aliased!.currentVersion).toBe("0.74.0");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
function writeManagedPackage(managedDir: string, name: string, version: string) {
|
|
376
|
+
const dir = path.join(managedDir, "node_modules", name);
|
|
377
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
378
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name, version }));
|
|
379
|
+
}
|
|
380
|
+
});
|