@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
|
@@ -20,7 +20,7 @@ import type { PiCorePackage } from "@blackbelt-technology/pi-dashboard-shared/re
|
|
|
20
20
|
|
|
21
21
|
function makePkg(overrides: Partial<PiCorePackage> = {}): PiCorePackage {
|
|
22
22
|
return {
|
|
23
|
-
name: "@
|
|
23
|
+
name: "@earendil-works/pi-coding-agent",
|
|
24
24
|
displayName: "pi",
|
|
25
25
|
currentVersion: "0.1.0",
|
|
26
26
|
latestVersion: "0.2.0",
|
|
@@ -90,8 +90,11 @@ describe("defaultRunNpmUpdate — registry resolution + managed PATH", () => {
|
|
|
90
90
|
expect(capturedCmd).toBe("C:\\node\\node.exe");
|
|
91
91
|
expect(capturedArgs.slice(0, 2)).toEqual([
|
|
92
92
|
"C:\\node\\node_modules\\npm\\bin\\npm-cli.js",
|
|
93
|
-
"
|
|
93
|
+
"install",
|
|
94
94
|
]);
|
|
95
|
+
// Anchor the @latest suffix — the regression guard for
|
|
96
|
+
// fix-pi-core-update-cross-minor.
|
|
97
|
+
expect(capturedArgs).toContain("@earendil-works/pi-coding-agent@latest");
|
|
95
98
|
});
|
|
96
99
|
|
|
97
100
|
it("rejects with a clear 'npm' error when registry can't resolve", async () => {
|
|
@@ -152,7 +155,63 @@ describe("defaultRunNpmUpdate — registry resolution + managed PATH", () => {
|
|
|
152
155
|
_envBuilder: () => ({}),
|
|
153
156
|
},
|
|
154
157
|
),
|
|
155
|
-
).rejects.toThrow(/sudo npm
|
|
158
|
+
).rejects.toThrow(/sudo npm install -g @example\/pkg@latest/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("spawns npm install with @latest suffix for managed install (regression guard)", async () => {
|
|
162
|
+
// fix-pi-core-update-cross-minor: managed updates must not run
|
|
163
|
+
// `npm update` (which respects the consuming package.json range)
|
|
164
|
+
// — they must run `npm install <pkg>@latest`.
|
|
165
|
+
let capturedArgs: readonly string[] = [];
|
|
166
|
+
const spawnFn = makeFakeSpawn({
|
|
167
|
+
exitCode: 0,
|
|
168
|
+
captureSpawn: (_c, args) => {
|
|
169
|
+
capturedArgs = args;
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
// Pre-create the managed dir so the existence check passes.
|
|
173
|
+
const managedDir = path.join(os.homedir(), ".pi-dashboard");
|
|
174
|
+
fs.mkdirSync(managedDir, { recursive: true });
|
|
175
|
+
|
|
176
|
+
await defaultRunNpmUpdate(
|
|
177
|
+
makePkg({ name: "@mariozechner/pi-coding-agent", installSource: "managed" }),
|
|
178
|
+
() => {},
|
|
179
|
+
{
|
|
180
|
+
_resolveNpm: () => ({ ok: true, argv: ["/usr/bin/npm"] }),
|
|
181
|
+
_spawn: spawnFn,
|
|
182
|
+
_envBuilder: () => ({}),
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(capturedArgs[0]).toBe("install");
|
|
187
|
+
// NOT "-g" for managed installs.
|
|
188
|
+
expect(capturedArgs).not.toContain("-g");
|
|
189
|
+
// The hot bit: @latest suffix.
|
|
190
|
+
expect(capturedArgs.some((a) => a === "@mariozechner/pi-coding-agent@latest")).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("spawns npm install -g with @latest suffix for global install (regression guard)", async () => {
|
|
194
|
+
let capturedArgs: readonly string[] = [];
|
|
195
|
+
const spawnFn = makeFakeSpawn({
|
|
196
|
+
exitCode: 0,
|
|
197
|
+
captureSpawn: (_c, args) => {
|
|
198
|
+
capturedArgs = args;
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await defaultRunNpmUpdate(
|
|
203
|
+
makePkg({ name: "@mariozechner/pi-coding-agent", installSource: "global" }),
|
|
204
|
+
() => {},
|
|
205
|
+
{
|
|
206
|
+
_resolveNpm: () => ({ ok: true, argv: ["/usr/bin/npm"] }),
|
|
207
|
+
_spawn: spawnFn,
|
|
208
|
+
_envBuilder: () => ({}),
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(capturedArgs[0]).toBe("install");
|
|
213
|
+
expect(capturedArgs).toContain("-g");
|
|
214
|
+
expect(capturedArgs.some((a) => a.endsWith("@latest"))).toBe(true);
|
|
156
215
|
});
|
|
157
216
|
|
|
158
217
|
it("rejects up front when managed install dir does not exist", async () => {
|
|
@@ -8,7 +8,7 @@ import type { PiCorePackage } from "@blackbelt-technology/pi-dashboard-shared/re
|
|
|
8
8
|
|
|
9
9
|
// Pi PM is mocked in other tests via vi.mock; we don't need it here because
|
|
10
10
|
// we never call the install/remove/update methods — only runExclusive().
|
|
11
|
-
vi.mock("@
|
|
11
|
+
vi.mock("@earendil-works/pi-coding-agent", () => ({
|
|
12
12
|
DefaultPackageManager: function () {
|
|
13
13
|
return {};
|
|
14
14
|
},
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `packages/server/bin/pi-dashboard.mjs` — the published CLI
|
|
3
|
+
* bin entry. Spawns the wrapper as a child process to exercise the
|
|
4
|
+
* real jiti-resolution + re-exec behaviour.
|
|
5
|
+
*
|
|
6
|
+
* See change: replace-tsx-with-jiti.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import url from "node:url";
|
|
14
|
+
|
|
15
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
16
|
+
const wrapperPath = path.resolve(here, "..", "..", "bin", "pi-dashboard.mjs");
|
|
17
|
+
const repoNodeModules = path.resolve(here, "..", "..", "..", "..", "node_modules");
|
|
18
|
+
const repoJitiRegister = path.join(repoNodeModules, "jiti", "lib", "jiti-register.mjs");
|
|
19
|
+
|
|
20
|
+
describe("bin/pi-dashboard.mjs wrapper", () => {
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
if (!existsSync(wrapperPath)) {
|
|
23
|
+
throw new Error(`Wrapper missing at ${wrapperPath}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("exits 1 with install-hint when jiti cannot be resolved", () => {
|
|
28
|
+
// Build an isolated anchor with NO node_modules tree — createRequire on
|
|
29
|
+
// it will fail to resolve `jiti/package.json`, triggering the miss path.
|
|
30
|
+
const tmp = mkdtempSync(path.join(tmpdir(), "pi-dashboard-bin-test-"));
|
|
31
|
+
try {
|
|
32
|
+
const fakeAnchor = path.join(tmp, "fake-anchor.js");
|
|
33
|
+
writeFileSync(fakeAnchor, "// no-op anchor with no node_modules\n");
|
|
34
|
+
|
|
35
|
+
// Spawn the wrapper. We override process.argv[1] indirectly by
|
|
36
|
+
// invoking node with `<wrapper>` then forcing argv[1] to the fake
|
|
37
|
+
// anchor via a tiny preamble — but the wrapper reads its OWN
|
|
38
|
+
// process.argv[1] which is the wrapper path itself when invoked
|
|
39
|
+
// directly. Strategy: copy the wrapper into the isolated tmp dir so
|
|
40
|
+
// its argv[1] resolves there with no jiti adjacency.
|
|
41
|
+
const isolatedWrapper = path.join(tmp, "pi-dashboard.mjs");
|
|
42
|
+
const wrapperSrc = require("node:fs").readFileSync(wrapperPath, "utf-8");
|
|
43
|
+
writeFileSync(isolatedWrapper, wrapperSrc);
|
|
44
|
+
|
|
45
|
+
const result = spawnSync(process.execPath, [isolatedWrapper, "--version"], {
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
env: { ...process.env, NODE_PATH: "" },
|
|
48
|
+
timeout: 10_000,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.status).toBe(1);
|
|
52
|
+
expect(result.stderr).toContain("pi-dashboard: cannot find jiti");
|
|
53
|
+
expect(result.stderr).toContain("npm install -g @earendil-works/pi-coding-agent");
|
|
54
|
+
// No tsx mention — proposal mandates no-fallback wrapper.
|
|
55
|
+
expect(result.stderr).not.toMatch(/tsx/i);
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("resolves jiti from process.argv[1] anchor and re-execs cli.ts", () => {
|
|
62
|
+
// Repo root has jiti at node_modules/jiti — wrapper invoked with its
|
|
63
|
+
// real path SHOULD walk createRequire(realpath(argv[1])) up into the
|
|
64
|
+
// repo's node_modules and find jiti.
|
|
65
|
+
if (!existsSync(repoJitiRegister)) {
|
|
66
|
+
// CI / fresh clone without `npm install` — skip rather than fail.
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Use `status` — it doesn't bind ports and exits quickly regardless
|
|
71
|
+
// of whether a server is running. We don't care about exit code (0 if
|
|
72
|
+
// a dashboard is up, 1 if not — both are valid outcomes that prove
|
|
73
|
+
// the wrapper successfully resolved jiti and re-execed cli.ts). What
|
|
74
|
+
// we DO care about: (a) no jiti-miss error on stderr, (b) cli.ts
|
|
75
|
+
// produced its own "Dashboard server" output (running OR not running).
|
|
76
|
+
const result = spawnSync(process.execPath, [wrapperPath, "status"], {
|
|
77
|
+
encoding: "utf-8",
|
|
78
|
+
timeout: 30_000,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(result.stderr).not.toContain("pi-dashboard: cannot find jiti");
|
|
82
|
+
expect(result.stdout).toMatch(/Dashboard server/i);
|
|
83
|
+
}, 60_000);
|
|
84
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `pi-dev-version-check.ts`.
|
|
3
|
+
*
|
|
4
|
+
* See change: improve-pi-update-detection.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
parsePackageVersion,
|
|
9
|
+
comparePackageVersions,
|
|
10
|
+
isNewerPackageVersion,
|
|
11
|
+
getPiUserAgent,
|
|
12
|
+
getLatestPiRelease,
|
|
13
|
+
} from "../pi-dev-version-check.js";
|
|
14
|
+
|
|
15
|
+
describe("parsePackageVersion", () => {
|
|
16
|
+
it("parses plain semver", () => {
|
|
17
|
+
expect(parsePackageVersion("0.70.6")).toEqual({
|
|
18
|
+
major: 0,
|
|
19
|
+
minor: 70,
|
|
20
|
+
patch: 6,
|
|
21
|
+
prerelease: undefined,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("parses semver with v prefix", () => {
|
|
26
|
+
expect(parsePackageVersion("v1.2.3")).toEqual({
|
|
27
|
+
major: 1,
|
|
28
|
+
minor: 2,
|
|
29
|
+
patch: 3,
|
|
30
|
+
prerelease: undefined,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("parses semver with prerelease", () => {
|
|
35
|
+
expect(parsePackageVersion("0.71.0-rc.1")).toEqual({
|
|
36
|
+
major: 0,
|
|
37
|
+
minor: 71,
|
|
38
|
+
patch: 0,
|
|
39
|
+
prerelease: "rc.1",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns undefined for unparseable", () => {
|
|
44
|
+
expect(parsePackageVersion("not-a-version")).toBeUndefined();
|
|
45
|
+
expect(parsePackageVersion("")).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("comparePackageVersions", () => {
|
|
50
|
+
it("orders by major then minor then patch", () => {
|
|
51
|
+
expect(comparePackageVersions("0.70.6", "0.74.0")).toBeLessThan(0);
|
|
52
|
+
expect(comparePackageVersions("1.0.0", "0.99.99")).toBeGreaterThan(0);
|
|
53
|
+
expect(comparePackageVersions("0.70.6", "0.70.6")).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("treats prerelease as less than full release", () => {
|
|
57
|
+
expect(comparePackageVersions("0.71.0-rc.1", "0.71.0")).toBeLessThan(0);
|
|
58
|
+
expect(comparePackageVersions("0.71.0", "0.71.0-rc.1")).toBeGreaterThan(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns undefined for unparseable", () => {
|
|
62
|
+
expect(comparePackageVersions("nope", "0.0.1")).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("isNewerPackageVersion", () => {
|
|
67
|
+
it("true when candidate is strictly newer", () => {
|
|
68
|
+
expect(isNewerPackageVersion("0.74.0", "0.70.6")).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("false when candidate is same or older", () => {
|
|
72
|
+
expect(isNewerPackageVersion("0.70.6", "0.70.6")).toBe(false);
|
|
73
|
+
expect(isNewerPackageVersion("0.70.5", "0.70.6")).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("falls back to string-inequality when unparseable", () => {
|
|
77
|
+
expect(isNewerPackageVersion("nightly-abc", "nightly-abc")).toBe(false);
|
|
78
|
+
expect(isNewerPackageVersion("nightly-xyz", "nightly-abc")).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("getPiUserAgent", () => {
|
|
83
|
+
it("formats as pi/<version> (<platform>; <runtime>; <arch>)", () => {
|
|
84
|
+
const ua = getPiUserAgent("0.70.6", "node/v22.20.0");
|
|
85
|
+
expect(ua).toMatch(/^pi\/0\.70\.6 \([a-z0-9]+; node\/v22\.20\.0; [a-z0-9]+\)$/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("auto-detects current Node runtime when not provided", () => {
|
|
89
|
+
const ua = getPiUserAgent("0.70.6");
|
|
90
|
+
expect(ua).toContain("pi/0.70.6");
|
|
91
|
+
// process.version is something like "v22.x.y"
|
|
92
|
+
expect(ua).toContain(`node/${process.version}`);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("getLatestPiRelease", () => {
|
|
97
|
+
let originalSkip: string | undefined;
|
|
98
|
+
let originalOffline: string | undefined;
|
|
99
|
+
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
originalSkip = process.env.PI_SKIP_VERSION_CHECK;
|
|
102
|
+
originalOffline = process.env.PI_OFFLINE;
|
|
103
|
+
delete process.env.PI_SKIP_VERSION_CHECK;
|
|
104
|
+
delete process.env.PI_OFFLINE;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
if (originalSkip !== undefined) process.env.PI_SKIP_VERSION_CHECK = originalSkip;
|
|
109
|
+
else delete process.env.PI_SKIP_VERSION_CHECK;
|
|
110
|
+
if (originalOffline !== undefined) process.env.PI_OFFLINE = originalOffline;
|
|
111
|
+
else delete process.env.PI_OFFLINE;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("returns parsed { version, packageName } on success", async () => {
|
|
115
|
+
const fetchImpl = vi.fn().mockResolvedValue({
|
|
116
|
+
ok: true,
|
|
117
|
+
json: async () => ({ version: "0.74.0", packageName: "@earendil-works/pi-coding-agent" }),
|
|
118
|
+
});
|
|
119
|
+
const out = await getLatestPiRelease("0.70.6", { fetchImpl });
|
|
120
|
+
expect(out).toEqual({ version: "0.74.0", packageName: "@earendil-works/pi-coding-agent" });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns undefined when packageName is absent", async () => {
|
|
124
|
+
const fetchImpl = vi.fn().mockResolvedValue({
|
|
125
|
+
ok: true,
|
|
126
|
+
json: async () => ({ version: "0.70.6" }),
|
|
127
|
+
});
|
|
128
|
+
const out = await getLatestPiRelease("0.70.6", { fetchImpl });
|
|
129
|
+
expect(out).toEqual({ version: "0.70.6", packageName: undefined });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("sends User-Agent matching pi's format", async () => {
|
|
133
|
+
let capturedUA: string | undefined;
|
|
134
|
+
const fetchImpl = vi.fn().mockImplementation((_url: string, opts: any) => {
|
|
135
|
+
capturedUA = opts.headers?.["User-Agent"];
|
|
136
|
+
return Promise.resolve({
|
|
137
|
+
ok: true,
|
|
138
|
+
json: async () => ({ version: "0.74.0" }),
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
await getLatestPiRelease("0.70.6", { fetchImpl });
|
|
142
|
+
expect(capturedUA).toMatch(/^pi\/0\.70\.6 \([a-z0-9]+; node\/v[\d.]+; [a-z0-9]+\)$/);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("returns undefined on non-2xx", async () => {
|
|
146
|
+
const fetchImpl = vi.fn().mockResolvedValue({ ok: false, status: 503, json: async () => ({}) });
|
|
147
|
+
expect(await getLatestPiRelease("0.70.6", { fetchImpl })).toBeUndefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("returns undefined on network error", async () => {
|
|
151
|
+
const fetchImpl = vi.fn().mockRejectedValue(new Error("ECONNRESET"));
|
|
152
|
+
expect(await getLatestPiRelease("0.70.6", { fetchImpl })).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("returns undefined on missing version field", async () => {
|
|
156
|
+
const fetchImpl = vi.fn().mockResolvedValue({
|
|
157
|
+
ok: true,
|
|
158
|
+
json: async () => ({ packageName: "@x/y" }),
|
|
159
|
+
});
|
|
160
|
+
expect(await getLatestPiRelease("0.70.6", { fetchImpl })).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("returns undefined on empty version field", async () => {
|
|
164
|
+
const fetchImpl = vi.fn().mockResolvedValue({
|
|
165
|
+
ok: true,
|
|
166
|
+
json: async () => ({ version: " " }),
|
|
167
|
+
});
|
|
168
|
+
expect(await getLatestPiRelease("0.70.6", { fetchImpl })).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("skips request when PI_OFFLINE is set", async () => {
|
|
172
|
+
process.env.PI_OFFLINE = "1";
|
|
173
|
+
const fetchImpl = vi.fn();
|
|
174
|
+
expect(await getLatestPiRelease("0.70.6", { fetchImpl })).toBeUndefined();
|
|
175
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("skips request when PI_SKIP_VERSION_CHECK is set", async () => {
|
|
179
|
+
process.env.PI_SKIP_VERSION_CHECK = "1";
|
|
180
|
+
const fetchImpl = vi.fn();
|
|
181
|
+
expect(await getLatestPiRelease("0.70.6", { fetchImpl })).toBeUndefined();
|
|
182
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -187,17 +187,17 @@ describe("pi-version-skew", () => {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
it("npm-global symlinked bin launcher resolves to the real package.json", () => {
|
|
190
|
-
// Simulate ~/.nvm/.../bin/pi → ../lib/node_modules/@
|
|
190
|
+
// Simulate ~/.nvm/.../bin/pi → ../lib/node_modules/@earendil-works/pi-coding-agent/dist/cli.js
|
|
191
191
|
const nodeRoot = path.join(tmpDir, "node-install");
|
|
192
192
|
const binDir = path.join(nodeRoot, "bin");
|
|
193
|
-
const pkgDir = path.join(nodeRoot, "lib", "node_modules", "@
|
|
193
|
+
const pkgDir = path.join(nodeRoot, "lib", "node_modules", "@earendil-works", "pi-coding-agent");
|
|
194
194
|
const distDir = path.join(pkgDir, "dist");
|
|
195
195
|
fs.mkdirSync(binDir, { recursive: true });
|
|
196
196
|
fs.mkdirSync(distDir, { recursive: true });
|
|
197
197
|
fs.writeFileSync(path.join(distDir, "cli.js"), "// stub");
|
|
198
198
|
fs.writeFileSync(
|
|
199
199
|
path.join(pkgDir, "package.json"),
|
|
200
|
-
JSON.stringify({ name: "@
|
|
200
|
+
JSON.stringify({ name: "@earendil-works/pi-coding-agent", version: "0.74.0" }),
|
|
201
201
|
);
|
|
202
202
|
// The bad path (what old code computed) must NOT exist.
|
|
203
203
|
// That is: nodeRoot/package.json. We leave it absent.
|
|
@@ -210,7 +210,7 @@ describe("pi-version-skew", () => {
|
|
|
210
210
|
);
|
|
211
211
|
|
|
212
212
|
const registry = stubRegistry(binLink);
|
|
213
|
-
expect(readCurrentPiVersion(registry)).toBe("0.
|
|
213
|
+
expect(readCurrentPiVersion(registry)).toBe("0.74.0");
|
|
214
214
|
});
|
|
215
215
|
|
|
216
216
|
it("non-symlinked path is a no-op under realpath", () => {
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `useRpcKeeper: true` branch in `spawnHeadless` (Phase 5).
|
|
3
|
+
*
|
|
4
|
+
* Drives `spawnPiSession({strategy: "headless"})` with the keeper-flag
|
|
5
|
+
* override on, an injected fake KeeperManager, and verifies:
|
|
6
|
+
* - keeper branch fires (KeeperManager.spawnKeeperFor called, NOT pi resolved)
|
|
7
|
+
* - returned SpawnResult.pid is the keeper PID
|
|
8
|
+
* - env passed to the keeper includes `PI_DASHBOARD_SPAWN_TOKEN`
|
|
9
|
+
* - keeper failure surfaces as `PI_CRASHED` or `SPAWN_ERRNO`
|
|
10
|
+
* - flag OFF (default) → keeper is NOT used
|
|
11
|
+
*/
|
|
12
|
+
import { EventEmitter } from "node:events";
|
|
13
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
|
+
import type {
|
|
17
|
+
KeeperManager,
|
|
18
|
+
KeeperSpawnResult,
|
|
19
|
+
} from "../rpc-keeper/keeper-manager.js";
|
|
20
|
+
import {
|
|
21
|
+
setKeeperManager,
|
|
22
|
+
_setUseRpcKeeperOverrideForTests,
|
|
23
|
+
spawnPiSession,
|
|
24
|
+
} from "../process-manager.js";
|
|
25
|
+
|
|
26
|
+
class FakeKeeperChild extends EventEmitter {
|
|
27
|
+
pid: number;
|
|
28
|
+
unref = vi.fn();
|
|
29
|
+
kill = vi.fn();
|
|
30
|
+
// Never emits "exit" → waitForNoCrash window completes cleanly.
|
|
31
|
+
constructor(pid: number) { super(); this.pid = pid; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface FakeKeeperManagerState {
|
|
35
|
+
spawnCalls: Array<{ sessionId: string; cwd: string; env: NodeJS.ProcessEnv; piArgs?: string[] }>;
|
|
36
|
+
writeCalls: Array<{ sessionId: string; line: string }>;
|
|
37
|
+
killCalls: string[];
|
|
38
|
+
spawnResult: KeeperSpawnResult;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeFakeKeeperManager(
|
|
42
|
+
state: Partial<FakeKeeperManagerState> & { spawnResult: KeeperSpawnResult },
|
|
43
|
+
): { km: KeeperManager; state: FakeKeeperManagerState } {
|
|
44
|
+
const full: FakeKeeperManagerState = {
|
|
45
|
+
spawnCalls: state.spawnCalls ?? [],
|
|
46
|
+
writeCalls: state.writeCalls ?? [],
|
|
47
|
+
killCalls: state.killCalls ?? [],
|
|
48
|
+
spawnResult: state.spawnResult,
|
|
49
|
+
};
|
|
50
|
+
const km: KeeperManager = {
|
|
51
|
+
sessionsDir: "/fake/sessions",
|
|
52
|
+
spawnKeeperFor: async (sessionId, cwd, env, piArgs) => {
|
|
53
|
+
full.spawnCalls.push({ sessionId, cwd, env, piArgs });
|
|
54
|
+
return full.spawnResult;
|
|
55
|
+
},
|
|
56
|
+
writeRpc: async (sessionId, line) => {
|
|
57
|
+
full.writeCalls.push({ sessionId, line });
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
writeRpcToSockPath: async (_sockPath, _line) => true,
|
|
61
|
+
killKeeper: (sessionId) => {
|
|
62
|
+
full.killCalls.push(sessionId);
|
|
63
|
+
return true;
|
|
64
|
+
},
|
|
65
|
+
discoverExistingKeepers: async () => [],
|
|
66
|
+
};
|
|
67
|
+
return { km, state: full };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let tmpCwd: string;
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
tmpCwd = mkdtempSync(path.join("/tmp", "km-cwd-"));
|
|
74
|
+
});
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
setKeeperManager(null);
|
|
77
|
+
_setUseRpcKeeperOverrideForTests(null);
|
|
78
|
+
rmSync(tmpCwd, { recursive: true, force: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("spawnHeadless (useRpcKeeper: true)", () => {
|
|
82
|
+
it("routes through KeeperManager when flag is on", async () => {
|
|
83
|
+
const fakeChild = new FakeKeeperChild(11111);
|
|
84
|
+
const { km, state } = makeFakeKeeperManager({
|
|
85
|
+
spawnResult: {
|
|
86
|
+
success: true,
|
|
87
|
+
pid: 11111,
|
|
88
|
+
sockPath: "/fake/sessions/sid.rpc.sock",
|
|
89
|
+
process: fakeChild as unknown as import("node:child_process").ChildProcess,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
setKeeperManager(km);
|
|
93
|
+
_setUseRpcKeeperOverrideForTests(true);
|
|
94
|
+
|
|
95
|
+
const result = await spawnPiSession(tmpCwd, { strategy: "headless" });
|
|
96
|
+
|
|
97
|
+
expect(result.success).toBe(true);
|
|
98
|
+
expect(result.pid).toBe(11111);
|
|
99
|
+
expect(state.spawnCalls).toHaveLength(1);
|
|
100
|
+
expect(state.spawnCalls[0].cwd).toBe(tmpCwd);
|
|
101
|
+
|
|
102
|
+
// spawnToken contract (task 5.3): the env passed to the keeper carries
|
|
103
|
+
// PI_DASHBOARD_SPAWN_TOKEN, which the keeper forwards to pi via
|
|
104
|
+
// process.env inheritance.
|
|
105
|
+
expect(state.spawnCalls[0].env.PI_DASHBOARD_SPAWN_TOKEN).toBeDefined();
|
|
106
|
+
expect(typeof state.spawnCalls[0].env.PI_DASHBOARD_SPAWN_TOKEN).toBe("string");
|
|
107
|
+
expect(state.spawnCalls[0].env.PI_DASHBOARD_SPAWN_TOKEN!.length).toBeGreaterThan(0);
|
|
108
|
+
|
|
109
|
+
// The returned spawnToken matches what was injected into env.
|
|
110
|
+
expect(result.spawnToken).toBe(state.spawnCalls[0].env.PI_DASHBOARD_SPAWN_TOKEN);
|
|
111
|
+
|
|
112
|
+
// Bare-spawn piArgs are at least `--mode rpc`.
|
|
113
|
+
expect(state.spawnCalls[0].piArgs).toBeDefined();
|
|
114
|
+
expect(state.spawnCalls[0].piArgs).toContain("--mode");
|
|
115
|
+
expect(state.spawnCalls[0].piArgs).toContain("rpc");
|
|
116
|
+
|
|
117
|
+
// SpawnResult.keeperSockPath populated so callers can pass it to
|
|
118
|
+
// `headlessPidRegistry.register(..., {keeperPid, keeperSockPath})`
|
|
119
|
+
// (Phase 6 contract). See change: add-rpc-stdin-dispatch-with-keeper-sidecar.
|
|
120
|
+
expect(result.keeperSockPath).toBe("/fake/sessions/sid.rpc.sock");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("forwards resume flags (sessionFile / mode) to the keeper as piArgs", async () => {
|
|
124
|
+
const fakeChild = new FakeKeeperChild(33333);
|
|
125
|
+
const { km, state } = makeFakeKeeperManager({
|
|
126
|
+
spawnResult: {
|
|
127
|
+
success: true,
|
|
128
|
+
pid: 33333,
|
|
129
|
+
sockPath: "/fake/x.sock",
|
|
130
|
+
process: fakeChild as unknown as import("node:child_process").ChildProcess,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
setKeeperManager(km);
|
|
134
|
+
_setUseRpcKeeperOverrideForTests(true);
|
|
135
|
+
|
|
136
|
+
const sessionFile = "/tmp/sess-resume.jsonl";
|
|
137
|
+
const result = await spawnPiSession(tmpCwd, {
|
|
138
|
+
strategy: "headless",
|
|
139
|
+
sessionFile,
|
|
140
|
+
mode: "continue",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(result.success).toBe(true);
|
|
144
|
+
expect(state.spawnCalls).toHaveLength(1);
|
|
145
|
+
const piArgs = state.spawnCalls[0].piArgs ?? [];
|
|
146
|
+
// piArgs MUST carry the session-file flag so resume actually resumes
|
|
147
|
+
// (regression guard: in the first Phase-5 cut the keeper hardcoded
|
|
148
|
+
// ["--mode","rpc"] and resume created a fresh session instead).
|
|
149
|
+
expect(piArgs).toContain("--mode");
|
|
150
|
+
expect(piArgs).toContain("rpc");
|
|
151
|
+
// sessionFlagsToArgv emits the session-file path; the exact flag name
|
|
152
|
+
// (`--session-file`) is verified in spawn-mechanism unit tests; here
|
|
153
|
+
// we only assert the path token is present so we don't double-bind to
|
|
154
|
+
// upstream argv shape.
|
|
155
|
+
expect(piArgs).toContain(sessionFile);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns SPAWN_ERRNO when KeeperManager.spawnKeeperFor reports !success", async () => {
|
|
159
|
+
const { km } = makeFakeKeeperManager({
|
|
160
|
+
spawnResult: { success: false, error: "EACCES on socket bind" },
|
|
161
|
+
});
|
|
162
|
+
setKeeperManager(km);
|
|
163
|
+
_setUseRpcKeeperOverrideForTests(true);
|
|
164
|
+
|
|
165
|
+
const result = await spawnPiSession(tmpCwd, { strategy: "headless" });
|
|
166
|
+
expect(result.success).toBe(false);
|
|
167
|
+
expect(result.code).toBe("SPAWN_ERRNO");
|
|
168
|
+
expect(result.message).toMatch(/RPC keeper/);
|
|
169
|
+
expect(result.message).toMatch(/EACCES/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("returns PI_CRASHED when keeper exits within the crash window", async () => {
|
|
173
|
+
// A child that emits "exit" inside 300 ms triggers the waitForNoCrash gate.
|
|
174
|
+
const fakeChild = new FakeKeeperChild(22222);
|
|
175
|
+
setTimeout(() => fakeChild.emit("exit", 1, null), 20);
|
|
176
|
+
|
|
177
|
+
const { km } = makeFakeKeeperManager({
|
|
178
|
+
spawnResult: {
|
|
179
|
+
success: true,
|
|
180
|
+
pid: 22222,
|
|
181
|
+
sockPath: "/fake/sessions/sid.rpc.sock",
|
|
182
|
+
process: fakeChild as unknown as import("node:child_process").ChildProcess,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
setKeeperManager(km);
|
|
186
|
+
_setUseRpcKeeperOverrideForTests(true);
|
|
187
|
+
|
|
188
|
+
const result = await spawnPiSession(tmpCwd, { strategy: "headless" });
|
|
189
|
+
expect(result.success).toBe(false);
|
|
190
|
+
expect(result.code).toBe("PI_CRASHED");
|
|
191
|
+
expect(result.message).toMatch(/crash window/);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("does NOT route through KeeperManager when flag is off (default)", async () => {
|
|
195
|
+
const { km, state } = makeFakeKeeperManager({
|
|
196
|
+
spawnResult: { success: true, pid: 99999, sockPath: "/fake/x.sock" },
|
|
197
|
+
});
|
|
198
|
+
setKeeperManager(km);
|
|
199
|
+
_setUseRpcKeeperOverrideForTests(false);
|
|
200
|
+
|
|
201
|
+
// We don't care about the actual headless spawn result here — only that
|
|
202
|
+
// it does NOT call the fake KeeperManager.
|
|
203
|
+
await spawnPiSession(tmpCwd, { strategy: "headless" });
|
|
204
|
+
expect(state.spawnCalls).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -114,7 +114,14 @@ describe("provider-auth-routes", () => {
|
|
|
114
114
|
|
|
115
115
|
// /exchange endpoint removed — token exchange happens in the callback server's onCode
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
// notifyBridges semantics changed: it now ONLY broadcasts
|
|
118
|
+
// `credentials_updated` to bridges. The previous `models_refreshed`
|
|
119
|
+
// broadcast to browsers was removed because the per-session
|
|
120
|
+
// `models_list` channel is self-healing: each bridge pushes a fresh
|
|
121
|
+
// models_list for its session on credentials_updated, and browsers
|
|
122
|
+
// update modelsMap[sid] incrementally without a global wipe. See
|
|
123
|
+
// change: simplify-model-selection-channels.
|
|
124
|
+
it("PUT /api/provider-auth/api-key saves and broadcasts credentials_updated to bridges", async () => {
|
|
118
125
|
const { writeCredential } = await import("../provider-auth-storage.js");
|
|
119
126
|
const res = await app.inject({
|
|
120
127
|
method: "PUT",
|
|
@@ -125,10 +132,11 @@ describe("provider-auth-routes", () => {
|
|
|
125
132
|
expect(JSON.parse(res.payload).ok).toBe(true);
|
|
126
133
|
expect(writeCredential).toHaveBeenCalledWith("openai", { type: "api_key", key: "sk-test" });
|
|
127
134
|
expect(piGateway.broadcast).toHaveBeenCalledWith({ type: "credentials_updated" });
|
|
128
|
-
|
|
135
|
+
// No models_refreshed broadcast — see simplify-model-selection-channels.
|
|
136
|
+
expect(browserGateway.broadcastToAll).not.toHaveBeenCalledWith({ type: "models_refreshed" });
|
|
129
137
|
});
|
|
130
138
|
|
|
131
|
-
it("DELETE /api/provider-auth/:provider removes and
|
|
139
|
+
it("DELETE /api/provider-auth/:provider removes and broadcasts credentials_updated", async () => {
|
|
132
140
|
const { removeCredential } = await import("../provider-auth-storage.js");
|
|
133
141
|
const res = await app.inject({
|
|
134
142
|
method: "DELETE",
|
|
@@ -137,7 +145,7 @@ describe("provider-auth-routes", () => {
|
|
|
137
145
|
expect(res.statusCode).toBe(200);
|
|
138
146
|
expect(removeCredential).toHaveBeenCalledWith("anthropic");
|
|
139
147
|
expect(piGateway.broadcast).toHaveBeenCalledWith({ type: "credentials_updated" });
|
|
140
|
-
expect(browserGateway.broadcastToAll).toHaveBeenCalledWith({ type: "models_refreshed" });
|
|
148
|
+
expect(browserGateway.broadcastToAll).not.toHaveBeenCalledWith({ type: "models_refreshed" });
|
|
141
149
|
});
|
|
142
150
|
|
|
143
151
|
// /callback/:provider route removed — temp callback server handles this directly
|