@blackbelt-technology/pi-agent-dashboard 0.2.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +64 -8
- package/README.md +308 -101
- package/docs/architecture.md +515 -16
- package/package.json +14 -7
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/ask-user-tool.ts +289 -20
- package/packages/extension/src/bridge.ts +107 -6
- package/packages/extension/src/command-handler.ts +34 -39
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/prompt-expander.ts +25 -4
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +71 -27
- package/packages/server/package.json +17 -2
- package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
- package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +246 -10
- package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/cors.test.ts +34 -2
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-manager-pid-registry.test.ts +168 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
- package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +29 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/fix-pty-permissions.test.ts +59 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/git-operations.test.ts +9 -7
- package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +178 -0
- package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
- package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +13 -3
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +389 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +10 -10
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
- package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +103 -6
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +108 -9
- package/packages/server/src/browser-gateway.ts +16 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +256 -32
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +39 -5
- package/packages/server/src/editor-pid-registry.ts +199 -0
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/fix-pty-permissions.ts +44 -0
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +16 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/npm-search-proxy.ts +71 -0
- package/packages/server/src/openspec-tasks.ts +158 -0
- package/packages/server/src/package-manager-wrapper.ts +225 -34
- package/packages/server/src/pi-core-checker.ts +290 -0
- package/packages/server/src/pi-core-updater.ts +172 -0
- package/packages/server/src/pi-gateway.ts +7 -0
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +196 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +130 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/file-routes.ts +30 -3
- package/packages/server/src/routes/openspec-routes.ts +107 -1
- package/packages/server/src/routes/pi-core-routes.ts +140 -0
- package/packages/server/src/routes/provider-auth-routes.ts +12 -10
- package/packages/server/src/routes/provider-routes.ts +55 -2
- package/packages/server/src/routes/recommended-routes.ts +225 -0
- package/packages/server/src/routes/system-routes.ts +30 -34
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +363 -26
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +65 -20
- package/packages/server/src/test-env-guard.ts +26 -0
- package/packages/server/src/test-support/test-server.ts +63 -0
- package/packages/server/src/tunnel.ts +172 -34
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +59 -3
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/openspec-poller.test.ts +44 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +156 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +93 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +71 -49
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +15 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/recommended-extensions.ts +196 -0
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +97 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/source-matching.ts +126 -0
- package/packages/shared/src/test-support/setup-home.ts +74 -0
- package/packages/shared/src/tool-registry/definitions.ts +342 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
- package/packages/shared/src/types.ts +7 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the pi version-skew detection module.
|
|
3
|
+
*
|
|
4
|
+
* See change: unified-bootstrap-install \u00a79.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
parseVersion,
|
|
12
|
+
compareVersions,
|
|
13
|
+
isBelow,
|
|
14
|
+
isAbove,
|
|
15
|
+
readPiCompatibility,
|
|
16
|
+
computeCompatibility,
|
|
17
|
+
_resetVersionSkewCache,
|
|
18
|
+
} from "../pi-version-skew.js";
|
|
19
|
+
|
|
20
|
+
describe("pi-version-skew", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
_resetVersionSkewCache();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("parseVersion", () => {
|
|
26
|
+
it("parses simple x.y.z", () => {
|
|
27
|
+
expect(parseVersion("1.2.3")).toEqual([1, 2, 3]);
|
|
28
|
+
});
|
|
29
|
+
it("parses with v prefix", () => {
|
|
30
|
+
expect(parseVersion("v0.6.7")).toEqual([0, 6, 7]);
|
|
31
|
+
});
|
|
32
|
+
it("ignores pre-release suffix", () => {
|
|
33
|
+
expect(parseVersion("0.6.7-beta.1")).toEqual([0, 6, 7]);
|
|
34
|
+
});
|
|
35
|
+
it("ignores build metadata", () => {
|
|
36
|
+
expect(parseVersion("0.6.7+abc")).toEqual([0, 6, 7]);
|
|
37
|
+
});
|
|
38
|
+
it("returns null for non-numeric", () => {
|
|
39
|
+
expect(parseVersion("latest")).toBeNull();
|
|
40
|
+
expect(parseVersion("")).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("compareVersions", () => {
|
|
45
|
+
it("equal versions", () => {
|
|
46
|
+
expect(compareVersions("0.6.7", "0.6.7")).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
it("lower major", () => {
|
|
49
|
+
expect(compareVersions("0.9.9", "1.0.0")).toBe(-1);
|
|
50
|
+
});
|
|
51
|
+
it("higher major", () => {
|
|
52
|
+
expect(compareVersions("2.0.0", "1.9.9")).toBe(1);
|
|
53
|
+
});
|
|
54
|
+
it("lower minor", () => {
|
|
55
|
+
expect(compareVersions("0.5.7", "0.6.0")).toBe(-1);
|
|
56
|
+
});
|
|
57
|
+
it("lower patch", () => {
|
|
58
|
+
expect(compareVersions("0.6.6", "0.6.7")).toBe(-1);
|
|
59
|
+
});
|
|
60
|
+
it("unparseable sorts as equal (conservative)", () => {
|
|
61
|
+
expect(compareVersions("latest", "0.6.7")).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("isBelow / isAbove", () => {
|
|
66
|
+
it("isBelow", () => {
|
|
67
|
+
expect(isBelow("0.5.0", "0.6.7")).toBe(true);
|
|
68
|
+
expect(isBelow("0.6.7", "0.6.7")).toBe(false);
|
|
69
|
+
expect(isBelow("0.7.0", "0.6.7")).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
it("isAbove with .x wildcard", () => {
|
|
72
|
+
expect(isAbove("0.10.0", "0.9.x")).toBe(true);
|
|
73
|
+
expect(isAbove("0.9.5", "0.9.x")).toBe(false);
|
|
74
|
+
expect(isAbove("0.9.99998", "0.9.x")).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
it("isAbove with concrete version", () => {
|
|
77
|
+
expect(isAbove("1.0.1", "1.0.0")).toBe(true);
|
|
78
|
+
expect(isAbove("1.0.0", "1.0.0")).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("readPiCompatibility", () => {
|
|
83
|
+
let tmpDir: string;
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-skew-"));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("reads the field from a well-formed package.json", () => {
|
|
90
|
+
const pkg = path.join(tmpDir, "package.json");
|
|
91
|
+
fs.writeFileSync(
|
|
92
|
+
pkg,
|
|
93
|
+
JSON.stringify({ piCompatibility: { minimum: "1.0.0", recommended: "1.2.0", maximum: "2.x" } }),
|
|
94
|
+
);
|
|
95
|
+
expect(readPiCompatibility(pkg)).toEqual({
|
|
96
|
+
minimum: "1.0.0",
|
|
97
|
+
recommended: "1.2.0",
|
|
98
|
+
maximum: "2.x",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("tolerates null maximum", () => {
|
|
103
|
+
const pkg = path.join(tmpDir, "package.json");
|
|
104
|
+
fs.writeFileSync(
|
|
105
|
+
pkg,
|
|
106
|
+
JSON.stringify({ piCompatibility: { minimum: "1.0.0", recommended: "1.2.0", maximum: null } }),
|
|
107
|
+
);
|
|
108
|
+
expect(readPiCompatibility(pkg).maximum).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("falls back to defaults when field is missing", () => {
|
|
112
|
+
const pkg = path.join(tmpDir, "package.json");
|
|
113
|
+
fs.writeFileSync(pkg, JSON.stringify({ name: "something" }));
|
|
114
|
+
expect(readPiCompatibility(pkg)).toEqual({
|
|
115
|
+
minimum: "0.6.7",
|
|
116
|
+
recommended: "0.6.7",
|
|
117
|
+
maximum: null,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("falls back to defaults when file is unreadable", () => {
|
|
122
|
+
expect(readPiCompatibility("/does/not/exist")).toEqual({
|
|
123
|
+
minimum: "0.6.7",
|
|
124
|
+
recommended: "0.6.7",
|
|
125
|
+
maximum: null,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("computeCompatibility", () => {
|
|
131
|
+
const range = { minimum: "0.6.7", recommended: "0.6.7", maximum: null };
|
|
132
|
+
|
|
133
|
+
it("returns range unchanged when pi is not yet installed", () => {
|
|
134
|
+
expect(computeCompatibility(range, undefined)).toEqual({ ...range, current: undefined });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("flags upgradeRecommended when below minimum", () => {
|
|
138
|
+
const out = computeCompatibility(range, "0.5.0");
|
|
139
|
+
expect(out.current).toBe("0.5.0");
|
|
140
|
+
expect(out.upgradeRecommended).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("flags upgradeRecommended when below recommended (but >= minimum)", () => {
|
|
144
|
+
const out = computeCompatibility(
|
|
145
|
+
{ minimum: "0.5.0", recommended: "0.6.7", maximum: null },
|
|
146
|
+
"0.6.0",
|
|
147
|
+
);
|
|
148
|
+
expect(out.upgradeRecommended).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("no upgrade flag when at or above recommended", () => {
|
|
152
|
+
const out = computeCompatibility(range, "0.6.7");
|
|
153
|
+
expect(out.upgradeRecommended).toBeUndefined();
|
|
154
|
+
expect(out.upgradeDashboard).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("flags upgradeDashboard when above maximum", () => {
|
|
158
|
+
const out = computeCompatibility(
|
|
159
|
+
{ minimum: "0.6.7", recommended: "0.6.7", maximum: "0.9.x" },
|
|
160
|
+
"0.10.0",
|
|
161
|
+
);
|
|
162
|
+
expect(out.upgradeDashboard).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -9,6 +9,14 @@ vi.mock("../resolve-path.js", () => ({
|
|
|
9
9
|
safeRealpathSync: (p: string) => p,
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
+
// Canonical host-platform absolute paths. Using raw POSIX strings like
|
|
13
|
+
// `/a` would normalize to `B:\a` on Windows (path.win32.resolve prepends
|
|
14
|
+
// the current drive), breaking assertions. These constants produce paths
|
|
15
|
+
// that survive `normalizePath` unchanged on their host platform.
|
|
16
|
+
const A_PATH = path.resolve(os.tmpdir(), "pref-a");
|
|
17
|
+
const B_PATH = path.resolve(os.tmpdir(), "pref-b");
|
|
18
|
+
const X_PATH = path.resolve(os.tmpdir(), "pref-x");
|
|
19
|
+
|
|
12
20
|
describe("preferences-store", () => {
|
|
13
21
|
let tmpDir: string;
|
|
14
22
|
let filePath: string;
|
|
@@ -33,12 +41,12 @@ describe("preferences-store", () => {
|
|
|
33
41
|
|
|
34
42
|
it("should load existing preferences", () => {
|
|
35
43
|
fs.writeFileSync(filePath, JSON.stringify({
|
|
36
|
-
pinnedDirectories: [
|
|
37
|
-
sessionOrder: {
|
|
44
|
+
pinnedDirectories: [A_PATH, B_PATH],
|
|
45
|
+
sessionOrder: { [A_PATH]: ["s1", "s2"] },
|
|
38
46
|
}));
|
|
39
47
|
const store = createPreferencesStore(filePath);
|
|
40
|
-
expect(store.getPinnedDirectories()).toEqual([
|
|
41
|
-
expect(store.getSessionOrder()).toEqual({
|
|
48
|
+
expect(store.getPinnedDirectories()).toEqual([A_PATH, B_PATH]);
|
|
49
|
+
expect(store.getSessionOrder()).toEqual({ [A_PATH]: ["s1", "s2"] });
|
|
42
50
|
store.dispose();
|
|
43
51
|
});
|
|
44
52
|
|
|
@@ -97,6 +105,67 @@ describe("preferences-store", () => {
|
|
|
97
105
|
store.dispose();
|
|
98
106
|
});
|
|
99
107
|
|
|
108
|
+
// ── Normalize-on-load migration (platform-path-normalization) ───────────
|
|
109
|
+
|
|
110
|
+
it("normalizes drifty pinned paths on load", () => {
|
|
111
|
+
// Seed a file with the kinds of drift that existed pre-normalization:
|
|
112
|
+
// trailing separators, `.` / `..` segments, duplicate separators. The
|
|
113
|
+
// store should collapse them to canonical form on first read.
|
|
114
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
115
|
+
pinnedDirectories: [
|
|
116
|
+
process.platform === "win32"
|
|
117
|
+
? "C:\\Users\\me\\Dev\\" // trailing separator
|
|
118
|
+
: "/Users/me/Dev/",
|
|
119
|
+
process.platform === "win32"
|
|
120
|
+
? "C:\\Users\\me\\Dev\\.\\BB" // `.` segment
|
|
121
|
+
: "/Users/me/Dev/./BB",
|
|
122
|
+
],
|
|
123
|
+
sessionOrder: {},
|
|
124
|
+
}));
|
|
125
|
+
const store = createPreferencesStore(filePath);
|
|
126
|
+
const pinned = store.getPinnedDirectories();
|
|
127
|
+
expect(pinned).toHaveLength(2);
|
|
128
|
+
// Expect canonical forms (trailing separator stripped, `.` resolved).
|
|
129
|
+
if (process.platform === "win32") {
|
|
130
|
+
expect(pinned[0]).toBe("C:\\Users\\me\\Dev");
|
|
131
|
+
expect(pinned[1]).toBe("C:\\Users\\me\\Dev\\BB");
|
|
132
|
+
} else {
|
|
133
|
+
expect(pinned[0]).toBe("/Users/me/Dev");
|
|
134
|
+
expect(pinned[1]).toBe("/Users/me/Dev/BB");
|
|
135
|
+
}
|
|
136
|
+
store.dispose();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("deduplicates entries that collapse to the same canonical form", () => {
|
|
140
|
+
// Two different-looking entries that normalize to the same path must
|
|
141
|
+
// become one stored entry.
|
|
142
|
+
const entries = process.platform === "win32"
|
|
143
|
+
? ["C:\\Users\\me", "C:\\Users\\me\\", "C:/Users/me"]
|
|
144
|
+
: ["/Users/me", "/Users/me/", "/Users/./me"];
|
|
145
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
146
|
+
pinnedDirectories: entries,
|
|
147
|
+
sessionOrder: {},
|
|
148
|
+
}));
|
|
149
|
+
const store = createPreferencesStore(filePath);
|
|
150
|
+
expect(store.getPinnedDirectories()).toHaveLength(1);
|
|
151
|
+
store.dispose();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("persists the normalized form back to disk on first debounce", () => {
|
|
155
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
156
|
+
pinnedDirectories: [
|
|
157
|
+
process.platform === "win32" ? "C:\\Users\\me\\" : "/Users/me/",
|
|
158
|
+
],
|
|
159
|
+
sessionOrder: {},
|
|
160
|
+
}));
|
|
161
|
+
const store = createPreferencesStore(filePath);
|
|
162
|
+
vi.advanceTimersByTime(1000);
|
|
163
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
164
|
+
const expected = process.platform === "win32" ? "C:\\Users\\me" : "/Users/me";
|
|
165
|
+
expect(data.pinnedDirectories).toEqual([expected]);
|
|
166
|
+
store.dispose();
|
|
167
|
+
});
|
|
168
|
+
|
|
100
169
|
it("should not contain hiddenSessions in output", () => {
|
|
101
170
|
const store = createPreferencesStore(filePath);
|
|
102
171
|
store.pinDirectory("/a");
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { buildTmuxCommand, buildHeadlessArgs, shellEscape, spawnPiSession, buildSpawnEnv, type SessionOptions } from "../process-manager.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
expect(result.strategy).toBe("tmux");
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("should detect Linux", () => {
|
|
12
|
-
const result = detectPlatform("linux");
|
|
13
|
-
expect(result.strategy).toBe("tmux");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("should detect Windows with WSL fallback", () => {
|
|
17
|
-
const result = detectPlatform("win32");
|
|
18
|
-
expect(result.strategy).toBe("wsl");
|
|
19
|
-
});
|
|
20
|
-
});
|
|
4
|
+
// Note: platform-dispatch tests live in packages/shared/src/__tests__/
|
|
5
|
+
// spawn-mechanism.test.ts. `detectPlatform` was removed in change:
|
|
6
|
+
// consolidate-windows-spawn-and-platform-handlers — its job is now
|
|
7
|
+
// owned by platform/spawn-mechanism.ts `selectMechanism`.
|
|
21
8
|
|
|
9
|
+
describe("Process Manager", () => {
|
|
22
10
|
describe("buildTmuxCommand", () => {
|
|
23
11
|
it("should create new session when no pi-dashboard session exists", () => {
|
|
24
12
|
const cmd = buildTmuxCommand("/home/user/project", false);
|
|
@@ -184,4 +172,43 @@ describe("Process Manager", () => {
|
|
|
184
172
|
expect(result.message).toContain("does not exist");
|
|
185
173
|
});
|
|
186
174
|
});
|
|
175
|
+
|
|
176
|
+
// ── Fork/continue option forwarding ──────────────────────────────────────
|
|
177
|
+
// Regression guard for B1/B2: Windows WSL/cmd fallback used to drop
|
|
178
|
+
// sessionFile + mode silently. buildTmuxCommand and buildHeadlessArgs
|
|
179
|
+
// both go through `sessionFlagsToArgv`; make sure neither drops.
|
|
180
|
+
describe("session-flag forwarding", () => {
|
|
181
|
+
it("buildHeadlessArgs includes --fork for fork mode", () => {
|
|
182
|
+
const args = buildHeadlessArgs({ sessionFile: "C:\\x\\session.jsonl", mode: "fork" });
|
|
183
|
+
expect(args).toEqual(["--mode", "rpc", "--fork", "C:\\x\\session.jsonl"]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("buildHeadlessArgs includes --session for continue mode", () => {
|
|
187
|
+
const args = buildHeadlessArgs({ sessionFile: "/s/abc.jsonl", mode: "continue" });
|
|
188
|
+
expect(args).toEqual(["--mode", "rpc", "--session", "/s/abc.jsonl"]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("buildHeadlessArgs omits session flags when absent", () => {
|
|
192
|
+
const args = buildHeadlessArgs({});
|
|
193
|
+
expect(args).toEqual(["--mode", "rpc"]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("buildTmuxCommand includes --fork in the pi command", () => {
|
|
197
|
+
const cmd = buildTmuxCommand("/project", false, { sessionFile: "/s/abc.jsonl", mode: "fork" });
|
|
198
|
+
expect(cmd).toContain("pi --fork /s/abc.jsonl");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("buildTmuxCommand includes --session in the pi command", () => {
|
|
202
|
+
const cmd = buildTmuxCommand("/project", false, { sessionFile: "/s/abc.jsonl", mode: "continue" });
|
|
203
|
+
expect(cmd).toContain("pi --session /s/abc.jsonl");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("buildTmuxCommand with special-character sessionFile still shell-escapes", () => {
|
|
207
|
+
const cmd = buildTmuxCommand("/project", false, {
|
|
208
|
+
sessionFile: "/s/with space.jsonl",
|
|
209
|
+
mode: "fork",
|
|
210
|
+
});
|
|
211
|
+
expect(cmd).toContain("--fork '/s/with space.jsonl'");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
187
214
|
});
|
|
@@ -44,14 +44,22 @@ function createMockPiGateway() {
|
|
|
44
44
|
} as any;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function createMockBrowserGateway() {
|
|
48
|
+
return {
|
|
49
|
+
broadcastToAll: vi.fn(),
|
|
50
|
+
} as any;
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
describe("provider-auth-routes", () => {
|
|
48
54
|
let app: ReturnType<typeof Fastify>;
|
|
49
55
|
let piGateway: ReturnType<typeof createMockPiGateway>;
|
|
56
|
+
let browserGateway: ReturnType<typeof createMockBrowserGateway>;
|
|
50
57
|
|
|
51
58
|
beforeEach(async () => {
|
|
52
59
|
app = Fastify();
|
|
53
60
|
piGateway = createMockPiGateway();
|
|
54
|
-
|
|
61
|
+
browserGateway = createMockBrowserGateway();
|
|
62
|
+
registerProviderAuthRoutes(app, { piGateway, browserGateway });
|
|
55
63
|
await app.ready();
|
|
56
64
|
});
|
|
57
65
|
|
|
@@ -106,7 +114,7 @@ describe("provider-auth-routes", () => {
|
|
|
106
114
|
|
|
107
115
|
// /exchange endpoint removed — token exchange happens in the callback server's onCode
|
|
108
116
|
|
|
109
|
-
it("PUT /api/provider-auth/api-key saves and notifies", async () => {
|
|
117
|
+
it("PUT /api/provider-auth/api-key saves and notifies bridges and browsers", async () => {
|
|
110
118
|
const { writeCredential } = await import("../provider-auth-storage.js");
|
|
111
119
|
const res = await app.inject({
|
|
112
120
|
method: "PUT",
|
|
@@ -117,9 +125,10 @@ describe("provider-auth-routes", () => {
|
|
|
117
125
|
expect(JSON.parse(res.payload).ok).toBe(true);
|
|
118
126
|
expect(writeCredential).toHaveBeenCalledWith("openai", { type: "api_key", key: "sk-test" });
|
|
119
127
|
expect(piGateway.broadcast).toHaveBeenCalledWith({ type: "credentials_updated" });
|
|
128
|
+
expect(browserGateway.broadcastToAll).toHaveBeenCalledWith({ type: "models_refreshed" });
|
|
120
129
|
});
|
|
121
130
|
|
|
122
|
-
it("DELETE /api/provider-auth/:provider removes and notifies", async () => {
|
|
131
|
+
it("DELETE /api/provider-auth/:provider removes and notifies bridges and browsers", async () => {
|
|
123
132
|
const { removeCredential } = await import("../provider-auth-storage.js");
|
|
124
133
|
const res = await app.inject({
|
|
125
134
|
method: "DELETE",
|
|
@@ -128,6 +137,7 @@ describe("provider-auth-routes", () => {
|
|
|
128
137
|
expect(res.statusCode).toBe(200);
|
|
129
138
|
expect(removeCredential).toHaveBeenCalledWith("anthropic");
|
|
130
139
|
expect(piGateway.broadcast).toHaveBeenCalledWith({ type: "credentials_updated" });
|
|
140
|
+
expect(browserGateway.broadcastToAll).toHaveBeenCalledWith({ type: "models_refreshed" });
|
|
131
141
|
});
|
|
132
142
|
|
|
133
143
|
// /callback/:provider route removed — temp callback server handles this directly
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildProbeRequest,
|
|
4
|
+
resolveProbeApiKey,
|
|
5
|
+
probeProvider,
|
|
6
|
+
type ProbeInput,
|
|
7
|
+
} from "../provider-probe.js";
|
|
8
|
+
|
|
9
|
+
describe("buildProbeRequest", () => {
|
|
10
|
+
it("openai-completions: GET {baseUrl}/models with Authorization: Bearer", () => {
|
|
11
|
+
const req = buildProbeRequest({
|
|
12
|
+
baseUrl: "https://api.example.com/v1",
|
|
13
|
+
apiKey: "sk-abc",
|
|
14
|
+
api: "openai-completions",
|
|
15
|
+
});
|
|
16
|
+
expect(req.url).toBe("https://api.example.com/v1/models");
|
|
17
|
+
expect(req.headers.Authorization).toBe("Bearer sk-abc");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("openai-completions: handles trailing slash on baseUrl", () => {
|
|
21
|
+
const req = buildProbeRequest({
|
|
22
|
+
baseUrl: "https://api.example.com/v1/",
|
|
23
|
+
apiKey: "sk-abc",
|
|
24
|
+
api: "openai-completions",
|
|
25
|
+
});
|
|
26
|
+
expect(req.url).toBe("https://api.example.com/v1/models");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("openai-responses: same shape as openai-completions", () => {
|
|
30
|
+
const req = buildProbeRequest({
|
|
31
|
+
baseUrl: "https://api.example.com/v1",
|
|
32
|
+
apiKey: "sk-abc",
|
|
33
|
+
api: "openai-responses",
|
|
34
|
+
});
|
|
35
|
+
expect(req.url).toBe("https://api.example.com/v1/models");
|
|
36
|
+
expect(req.headers.Authorization).toBe("Bearer sk-abc");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("anthropic-messages: x-api-key + anthropic-version, no Authorization", () => {
|
|
40
|
+
const req = buildProbeRequest({
|
|
41
|
+
baseUrl: "https://api.anthropic.com",
|
|
42
|
+
apiKey: "sk-ant-123",
|
|
43
|
+
api: "anthropic-messages",
|
|
44
|
+
});
|
|
45
|
+
expect(req.url).toBe("https://api.anthropic.com/v1/models");
|
|
46
|
+
expect(req.headers["x-api-key"]).toBe("sk-ant-123");
|
|
47
|
+
expect(req.headers["anthropic-version"]).toBe("2023-06-01");
|
|
48
|
+
expect(req.headers.Authorization).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("google-generative-ai: key query param, no Authorization", () => {
|
|
52
|
+
const req = buildProbeRequest({
|
|
53
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
54
|
+
apiKey: "AIza abc+def",
|
|
55
|
+
api: "google-generative-ai",
|
|
56
|
+
});
|
|
57
|
+
// apiKey must be URL-encoded
|
|
58
|
+
expect(req.url).toBe(
|
|
59
|
+
"https://generativelanguage.googleapis.com/v1beta/models?key=AIza%20abc%2Bdef",
|
|
60
|
+
);
|
|
61
|
+
expect(req.headers.Authorization).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("throws on unknown api type", () => {
|
|
65
|
+
expect(() =>
|
|
66
|
+
buildProbeRequest({
|
|
67
|
+
baseUrl: "https://x",
|
|
68
|
+
apiKey: "k",
|
|
69
|
+
api: "unknown-api" as any,
|
|
70
|
+
}),
|
|
71
|
+
).toThrow(/unsupported api/i);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("resolveProbeApiKey", () => {
|
|
76
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
process.env = { ...ORIGINAL_ENV };
|
|
79
|
+
});
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
process.env = { ...ORIGINAL_ENV };
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("literal key passes through", () => {
|
|
85
|
+
const result = resolveProbeApiKey({ apiKey: "sk-abc", readProviders: () => ({}) });
|
|
86
|
+
expect(result).toEqual({ ok: true, key: "sk-abc" });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("$ENV_VAR: reads from process.env when set", () => {
|
|
90
|
+
process.env.MY_LLM_KEY = "resolved-value";
|
|
91
|
+
const result = resolveProbeApiKey({ apiKey: "$MY_LLM_KEY", readProviders: () => ({}) });
|
|
92
|
+
expect(result).toEqual({ ok: true, key: "resolved-value" });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("$ENV_VAR: returns error when env var is missing", () => {
|
|
96
|
+
delete process.env.NONEXISTENT_VAR;
|
|
97
|
+
const result = resolveProbeApiKey({ apiKey: "$NONEXISTENT_VAR", readProviders: () => ({}) });
|
|
98
|
+
expect(result.ok).toBe(false);
|
|
99
|
+
if (!result.ok) expect(result.error).toMatch(/NONEXISTENT_VAR/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("REDACTED (***): resolves via provider name from readProviders", () => {
|
|
103
|
+
const result = resolveProbeApiKey({
|
|
104
|
+
apiKey: "***",
|
|
105
|
+
name: "my-provider",
|
|
106
|
+
readProviders: () => ({
|
|
107
|
+
"my-provider": { baseUrl: "u", apiKey: "stored-key", api: "openai-completions" },
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
expect(result).toEqual({ ok: true, key: "stored-key" });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("REDACTED + stored key is $ENV_VAR: follows env-var resolution", () => {
|
|
114
|
+
process.env.STORED_ENV = "env-value";
|
|
115
|
+
const result = resolveProbeApiKey({
|
|
116
|
+
apiKey: "***",
|
|
117
|
+
name: "my-provider",
|
|
118
|
+
readProviders: () => ({
|
|
119
|
+
"my-provider": { baseUrl: "u", apiKey: "$STORED_ENV", api: "openai-completions" },
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
expect(result).toEqual({ ok: true, key: "env-value" });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("REDACTED without name: returns error", () => {
|
|
126
|
+
const result = resolveProbeApiKey({ apiKey: "***", readProviders: () => ({}) });
|
|
127
|
+
expect(result.ok).toBe(false);
|
|
128
|
+
if (!result.ok) expect(result.error).toMatch(/no.*provider/i);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("REDACTED with unknown name: returns error", () => {
|
|
132
|
+
const result = resolveProbeApiKey({
|
|
133
|
+
apiKey: "***",
|
|
134
|
+
name: "missing",
|
|
135
|
+
readProviders: () => ({}),
|
|
136
|
+
});
|
|
137
|
+
expect(result.ok).toBe(false);
|
|
138
|
+
if (!result.ok) expect(result.error).toMatch(/missing/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("empty key: returns error", () => {
|
|
142
|
+
const result = resolveProbeApiKey({ apiKey: "", readProviders: () => ({}) });
|
|
143
|
+
expect(result.ok).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("probeProvider", () => {
|
|
148
|
+
const baseInput: ProbeInput = {
|
|
149
|
+
baseUrl: "https://api.example.com/v1",
|
|
150
|
+
apiKey: "sk-abc",
|
|
151
|
+
api: "openai-completions",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let originalFetch: typeof globalThis.fetch;
|
|
155
|
+
beforeEach(() => {
|
|
156
|
+
originalFetch = globalThis.fetch;
|
|
157
|
+
});
|
|
158
|
+
afterEach(() => {
|
|
159
|
+
globalThis.fetch = originalFetch;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
function mockFetch(impl: (url: string, init: RequestInit) => Promise<Response>) {
|
|
163
|
+
globalThis.fetch = vi.fn(impl) as any;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
it("2xx with data array returns ok + modelCount + sample (capped at 5)", async () => {
|
|
167
|
+
const ids = ["m1", "m2", "m3", "m4", "m5", "m6", "m7"];
|
|
168
|
+
mockFetch(async () =>
|
|
169
|
+
new Response(JSON.stringify({ data: ids.map((id) => ({ id })) }), { status: 200 }),
|
|
170
|
+
);
|
|
171
|
+
const result = await probeProvider(baseInput);
|
|
172
|
+
expect(result.ok).toBe(true);
|
|
173
|
+
if (result.ok) {
|
|
174
|
+
expect(result.status).toBe(200);
|
|
175
|
+
expect(result.modelCount).toBe(7);
|
|
176
|
+
expect(result.sample).toEqual(["m1", "m2", "m3", "m4", "m5"]);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("2xx with unexpected body shape: ok with modelCount=0", async () => {
|
|
181
|
+
mockFetch(async () => new Response(JSON.stringify({ weird: true }), { status: 200 }));
|
|
182
|
+
const result = await probeProvider(baseInput);
|
|
183
|
+
expect(result.ok).toBe(true);
|
|
184
|
+
if (result.ok) {
|
|
185
|
+
expect(result.modelCount).toBe(0);
|
|
186
|
+
expect(result.sample).toEqual([]);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("401 returns ok=false with status + error", async () => {
|
|
191
|
+
mockFetch(async () =>
|
|
192
|
+
new Response("Invalid API key", { status: 401, statusText: "Unauthorized" }),
|
|
193
|
+
);
|
|
194
|
+
const result = await probeProvider(baseInput);
|
|
195
|
+
expect(result.ok).toBe(false);
|
|
196
|
+
if (!result.ok) {
|
|
197
|
+
expect(result.status).toBe(401);
|
|
198
|
+
expect(result.error).toMatch(/Invalid API key|Unauthorized|401/);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("500 returns ok=false with status", async () => {
|
|
203
|
+
mockFetch(async () => new Response("boom", { status: 500 }));
|
|
204
|
+
const result = await probeProvider(baseInput);
|
|
205
|
+
expect(result.ok).toBe(false);
|
|
206
|
+
if (!result.ok) expect(result.status).toBe(500);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("body excerpt is truncated to 500 chars", async () => {
|
|
210
|
+
const long = "x".repeat(2000);
|
|
211
|
+
mockFetch(async () => new Response(long, { status: 400 }));
|
|
212
|
+
const result = await probeProvider(baseInput);
|
|
213
|
+
expect(result.ok).toBe(false);
|
|
214
|
+
if (!result.ok && result.error) expect(result.error.length).toBeLessThanOrEqual(500);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("network error: ok=false with error, no status", async () => {
|
|
218
|
+
mockFetch(async () => {
|
|
219
|
+
throw new Error("ECONNREFUSED");
|
|
220
|
+
});
|
|
221
|
+
const result = await probeProvider(baseInput);
|
|
222
|
+
expect(result.ok).toBe(false);
|
|
223
|
+
if (!result.ok) {
|
|
224
|
+
expect(result.error).toMatch(/ECONNREFUSED/);
|
|
225
|
+
expect(result.status).toBeUndefined();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("timeout aborts the request and returns error", async () => {
|
|
230
|
+
mockFetch(
|
|
231
|
+
(_url, init) =>
|
|
232
|
+
new Promise((_resolve, reject) => {
|
|
233
|
+
const signal = init.signal as AbortSignal;
|
|
234
|
+
signal.addEventListener("abort", () => reject(new Error("aborted")));
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
const result = await probeProvider({ ...baseInput, timeoutMs: 20 });
|
|
238
|
+
expect(result.ok).toBe(false);
|
|
239
|
+
if (!result.ok) expect(result.error).toBeDefined();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("response never echoes the apiKey", async () => {
|
|
243
|
+
mockFetch(async () =>
|
|
244
|
+
new Response(`echoed sk-abc in body`, { status: 401 }),
|
|
245
|
+
);
|
|
246
|
+
const result = await probeProvider({ ...baseInput, apiKey: "sk-abc" });
|
|
247
|
+
// even though upstream echoes the key, our result error should not leak it
|
|
248
|
+
if (!result.ok && result.error) {
|
|
249
|
+
expect(result.error).not.toContain("sk-abc");
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("anthropic-messages uses x-api-key header (not Authorization)", async () => {
|
|
254
|
+
let capturedInit: RequestInit | undefined;
|
|
255
|
+
mockFetch(async (_url, init) => {
|
|
256
|
+
capturedInit = init;
|
|
257
|
+
return new Response(JSON.stringify({ data: [] }), { status: 200 });
|
|
258
|
+
});
|
|
259
|
+
await probeProvider({
|
|
260
|
+
baseUrl: "https://api.anthropic.com",
|
|
261
|
+
apiKey: "sk-ant-x",
|
|
262
|
+
api: "anthropic-messages",
|
|
263
|
+
});
|
|
264
|
+
const headers = capturedInit!.headers as Record<string, string>;
|
|
265
|
+
expect(headers["x-api-key"]).toBe("sk-ant-x");
|
|
266
|
+
expect(headers["anthropic-version"]).toBe("2023-06-01");
|
|
267
|
+
expect(headers.Authorization).toBeUndefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("google-generative-ai uses ?key= query param (no Authorization)", async () => {
|
|
271
|
+
let capturedUrl: string | undefined;
|
|
272
|
+
let capturedInit: RequestInit | undefined;
|
|
273
|
+
mockFetch(async (url, init) => {
|
|
274
|
+
capturedUrl = url;
|
|
275
|
+
capturedInit = init;
|
|
276
|
+
return new Response(JSON.stringify({ data: [] }), { status: 200 });
|
|
277
|
+
});
|
|
278
|
+
await probeProvider({
|
|
279
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
280
|
+
apiKey: "AIzaTest",
|
|
281
|
+
api: "google-generative-ai",
|
|
282
|
+
});
|
|
283
|
+
expect(capturedUrl).toContain("?key=AIzaTest");
|
|
284
|
+
const headers = (capturedInit!.headers ?? {}) as Record<string, string>;
|
|
285
|
+
expect(headers.Authorization).toBeUndefined();
|
|
286
|
+
});
|
|
287
|
+
});
|