@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.1
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 +87 -114
- package/README.md +408 -430
- package/docs/architecture.md +465 -12
- package/package.json +10 -5
- package/packages/extension/package.json +14 -4
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +40 -8
- package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
- 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 +5 -4
- package/packages/extension/src/bridge.ts +171 -17
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +43 -0
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- 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 +83 -27
- package/packages/server/package.json +16 -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 +17 -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__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-registry.test.ts +28 -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__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
- 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__/package-manager-wrapper-resolve.test.ts +5 -1
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-version-skew.test.ts +237 -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-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +111 -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__/terminal-manager.test.ts +41 -1
- 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 +13 -7
- 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 +8 -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 +310 -39
- 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 +19 -4
- package/packages/server/src/editor-pid-registry.ts +9 -8
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +7 -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/package-manager-wrapper.ts +207 -47
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-core-updater.ts +7 -1
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +207 -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 +141 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/openspec-routes.ts +25 -1
- package/packages/server/src/routes/pi-core-routes.ts +24 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -8
- package/packages/server/src/routes/provider-routes.ts +43 -0
- package/packages/server/src/routes/recommended-routes.ts +10 -12
- package/packages/server/src/routes/system-routes.ts +20 -33
- 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 +211 -10
- 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 +61 -20
- package/packages/server/src/tunnel.ts +42 -28
- 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 +56 -0
- 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__/no-hardcoded-node-modules-paths.test.ts +176 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
- package/packages/shared/src/__tests__/node-spawn.test.ts +210 -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 +40 -7
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -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 +71 -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 +63 -46
- 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 +16 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -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/protocol.ts +23 -0
- package/packages/shared/src/recommended-extensions.ts +18 -2
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +26 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/state-replay.ts +9 -0
- package/packages/shared/src/tool-registry/definitions.ts +434 -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
|
@@ -1,119 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for openspec-poller.ts — the higher-level aggregator that combines
|
|
3
|
+
* `openspec list` + per-change `openspec status` into the dashboard's
|
|
4
|
+
* `OpenSpecData` shape.
|
|
5
|
+
*
|
|
6
|
+
* The file now delegates to `platform/openspec.ts` for the subprocess work.
|
|
7
|
+
* We mock that module so the tests focus on the aggregation logic
|
|
8
|
+
* (empty results, artifact mapping, per-change status failures) without
|
|
9
|
+
* spawning openspec.
|
|
10
|
+
*
|
|
11
|
+
* See change: platform-command-executor.
|
|
12
|
+
*/
|
|
1
13
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const mockExecFile = vi.fn<(...args: any[]) => any>();
|
|
7
|
-
vi.mock("node:child_process", () => ({
|
|
8
|
-
spawnSync: mockSpawnSync,
|
|
9
|
-
execFile: mockExecFile,
|
|
10
|
-
// re-export defaults that node:child_process has
|
|
11
|
-
default: { spawnSync: mockSpawnSync, execFile: mockExecFile },
|
|
14
|
+
|
|
15
|
+
const { listOr, statusOr } = vi.hoisted(() => ({
|
|
16
|
+
listOr: vi.fn(),
|
|
17
|
+
statusOr: vi.fn(),
|
|
12
18
|
}));
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return { status: 0, stdout: JSON.stringify(data), stderr: "" };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function fail(): Partial<SpawnSyncReturns<string>> {
|
|
22
|
-
return { status: 1, stdout: "", stderr: "error" };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function mockCli(responses: Map<string, unknown>) {
|
|
26
|
-
mockSpawnSync.mockImplementation((_cmd: any, args: any) => {
|
|
27
|
-
const a = args as string[];
|
|
28
|
-
if (a.includes("list")) {
|
|
29
|
-
const d = responses.get("list");
|
|
30
|
-
return d ? ok(d) : fail();
|
|
31
|
-
}
|
|
32
|
-
if (a.includes("status")) {
|
|
33
|
-
const idx = a.indexOf("--change");
|
|
34
|
-
const name = idx >= 0 ? a[idx + 1] : "";
|
|
35
|
-
const d = responses.get(`status:${name}`);
|
|
36
|
-
return d ? ok(d) : fail();
|
|
37
|
-
}
|
|
38
|
-
return fail();
|
|
39
|
-
});
|
|
40
|
-
}
|
|
20
|
+
vi.mock("@blackbelt-technology/pi-dashboard-shared/platform/openspec.js", () => ({
|
|
21
|
+
listOr,
|
|
22
|
+
statusOr,
|
|
23
|
+
}));
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
mockSpawnSync.mockReset();
|
|
44
|
-
});
|
|
25
|
+
import { pollOpenSpec } from "@blackbelt-technology/pi-dashboard-shared/openspec-poller.js";
|
|
45
26
|
|
|
46
27
|
describe("pollOpenSpec", () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
expect(result).toEqual({ initialized: false, changes: [] });
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
listOr.mockReset();
|
|
30
|
+
statusOr.mockReset();
|
|
51
31
|
});
|
|
52
32
|
|
|
53
|
-
it("returns initialized=false when list
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
33
|
+
it("returns initialized=false when list fails", () => {
|
|
34
|
+
listOr.mockReturnValue(null);
|
|
35
|
+
expect(pollOpenSpec("/test")).toEqual({ initialized: false, changes: [] });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns initialized=false when list returns non-array changes", () => {
|
|
39
|
+
listOr.mockReturnValue({ changes: "not an array" });
|
|
40
|
+
expect(pollOpenSpec("/test")).toEqual({ initialized: false, changes: [] });
|
|
57
41
|
});
|
|
58
42
|
|
|
59
43
|
it("returns initialized=true with changes on success", () => {
|
|
60
|
-
|
|
61
|
-
[
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}],
|
|
67
|
-
["status:feat-a", {
|
|
68
|
-
artifacts: [
|
|
69
|
-
{ id: "proposal", status: "done" },
|
|
70
|
-
{ id: "design", status: "ready" },
|
|
71
|
-
{ id: "tasks", status: "blocked" },
|
|
72
|
-
],
|
|
73
|
-
}],
|
|
74
|
-
["status:feat-b", {
|
|
75
|
-
artifacts: [
|
|
76
|
-
{ id: "proposal", status: "done" },
|
|
77
|
-
{ id: "tasks", status: "done" },
|
|
78
|
-
],
|
|
79
|
-
}],
|
|
80
|
-
]));
|
|
81
|
-
|
|
82
|
-
const result = pollOpenSpec("/project");
|
|
83
|
-
expect(result.initialized).toBe(true);
|
|
84
|
-
expect(result.changes).toHaveLength(2);
|
|
85
|
-
expect(result.changes[0]).toEqual({
|
|
86
|
-
name: "feat-a",
|
|
87
|
-
status: "in-progress",
|
|
88
|
-
completedTasks: 3,
|
|
89
|
-
totalTasks: 5,
|
|
44
|
+
listOr.mockReturnValue({
|
|
45
|
+
changes: [
|
|
46
|
+
{ name: "add-auth", status: "in-progress", completedTasks: 3, totalTasks: 10 },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
statusOr.mockReturnValue({
|
|
90
50
|
artifacts: [
|
|
91
51
|
{ id: "proposal", status: "done" },
|
|
92
|
-
{ id: "
|
|
93
|
-
{ id: "tasks", status: "blocked" },
|
|
52
|
+
{ id: "tasks", status: "ready" },
|
|
94
53
|
],
|
|
95
54
|
});
|
|
96
|
-
expect(result.changes[1].status).toBe("complete");
|
|
97
|
-
});
|
|
98
55
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
expect(result
|
|
56
|
+
const result = pollOpenSpec("/test");
|
|
57
|
+
expect(result.initialized).toBe(true);
|
|
58
|
+
expect(result.changes).toHaveLength(1);
|
|
59
|
+
expect(result.changes[0]).toMatchObject({
|
|
60
|
+
name: "add-auth",
|
|
61
|
+
status: "in-progress",
|
|
62
|
+
completedTasks: 3,
|
|
63
|
+
totalTasks: 10,
|
|
64
|
+
});
|
|
65
|
+
expect(result.changes[0].artifacts).toEqual([
|
|
66
|
+
{ id: "proposal", status: "done" },
|
|
67
|
+
{ id: "tasks", status: "ready" },
|
|
68
|
+
]);
|
|
103
69
|
});
|
|
104
70
|
|
|
105
71
|
it("handles status call failure gracefully (empty artifacts)", () => {
|
|
106
|
-
|
|
107
|
-
[
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const result = pollOpenSpec("/project");
|
|
72
|
+
listOr.mockReturnValue({
|
|
73
|
+
changes: [
|
|
74
|
+
{ name: "x", status: "complete", completedTasks: 5, totalTasks: 5 },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
statusOr.mockReturnValue(null); // status failed
|
|
78
|
+
|
|
79
|
+
const result = pollOpenSpec("/test");
|
|
116
80
|
expect(result.initialized).toBe(true);
|
|
117
81
|
expect(result.changes[0].artifacts).toEqual([]);
|
|
118
82
|
});
|
|
83
|
+
|
|
84
|
+
it("normalizes unknown status values to 'no-tasks'", () => {
|
|
85
|
+
listOr.mockReturnValue({
|
|
86
|
+
changes: [
|
|
87
|
+
{ name: "x", status: "weird-future-status", completedTasks: 0, totalTasks: 0 },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
statusOr.mockReturnValue(null);
|
|
91
|
+
|
|
92
|
+
const result = pollOpenSpec("/test");
|
|
93
|
+
expect(result.changes[0].status).toBe("no-tasks");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("normalizes unknown artifact statuses to 'blocked'", () => {
|
|
97
|
+
listOr.mockReturnValue({
|
|
98
|
+
changes: [
|
|
99
|
+
{ name: "x", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
statusOr.mockReturnValue({
|
|
103
|
+
artifacts: [{ id: "proposal", status: "some-new-state" }],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = pollOpenSpec("/test");
|
|
107
|
+
expect(result.changes[0].artifacts[0].status).toBe("blocked");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("calls statusOr once per change, with the change name", () => {
|
|
111
|
+
listOr.mockReturnValue({
|
|
112
|
+
changes: [
|
|
113
|
+
{ name: "a", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
114
|
+
{ name: "b", status: "complete", completedTasks: 2, totalTasks: 2 },
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
statusOr.mockReturnValue({ artifacts: [] });
|
|
118
|
+
|
|
119
|
+
pollOpenSpec("/test");
|
|
120
|
+
expect(statusOr).toHaveBeenCalledTimes(2);
|
|
121
|
+
expect(statusOr).toHaveBeenNthCalledWith(1, { cwd: "/test", change: "a" });
|
|
122
|
+
expect(statusOr).toHaveBeenNthCalledWith(2, { cwd: "/test", change: "b" });
|
|
123
|
+
});
|
|
119
124
|
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests that `killProcessByPgid` routes through the platform's
|
|
3
|
+
* `killPidWithGroup` helper (not raw `process.kill`).
|
|
4
|
+
*
|
|
5
|
+
* See change: route-kill-paths-through-platform.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
8
|
+
|
|
9
|
+
const killPidWithGroupSpy = vi.fn((_pid: number, _sig: any, _opts?: any) => undefined);
|
|
10
|
+
|
|
11
|
+
vi.mock("@blackbelt-technology/pi-dashboard-shared/platform/process.js", async () => {
|
|
12
|
+
const actual = await vi.importActual<typeof import("@blackbelt-technology/pi-dashboard-shared/platform/process.js")>(
|
|
13
|
+
"@blackbelt-technology/pi-dashboard-shared/platform/process.js",
|
|
14
|
+
);
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
killPidWithGroup: (pid: number, sig: any, opts?: any) => killPidWithGroupSpy(pid, sig, opts),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const { killProcessByPgid } = await import("../process-scanner.js");
|
|
22
|
+
|
|
23
|
+
describe("killProcessByPgid platform routing", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
killPidWithGroupSpy.mockClear();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("invokes killPidWithGroup with the resolved platform on Unix", () => {
|
|
29
|
+
const ok = killProcessByPgid(4242, { _platform: "linux" } as any);
|
|
30
|
+
expect(ok).toBe(true);
|
|
31
|
+
expect(killPidWithGroupSpy).toHaveBeenCalledTimes(1);
|
|
32
|
+
const [pid, sig, opts] = killPidWithGroupSpy.mock.calls[0];
|
|
33
|
+
expect(pid).toBe(4242);
|
|
34
|
+
expect(sig).toBe("SIGTERM");
|
|
35
|
+
expect(opts?.platform).toBe("linux");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("invokes killPidWithGroup with platform=darwin for macOS pgids", () => {
|
|
39
|
+
killProcessByPgid(9999, { _platform: "darwin" } as any);
|
|
40
|
+
const [pid, , opts] = killPidWithGroupSpy.mock.calls[0];
|
|
41
|
+
expect(pid).toBe(9999);
|
|
42
|
+
expect(opts?.platform).toBe("darwin");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does NOT call process.kill directly on Unix", () => {
|
|
46
|
+
const processKillSpy = vi.spyOn(process, "kill");
|
|
47
|
+
try {
|
|
48
|
+
killProcessByPgid(1234, { _platform: "linux" } as any);
|
|
49
|
+
} catch { /* ignore */ }
|
|
50
|
+
expect(processKillSpy).not.toHaveBeenCalled();
|
|
51
|
+
processKillSpy.mockRestore();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("reports failure if killPidWithGroup throws", () => {
|
|
55
|
+
killPidWithGroupSpy.mockImplementationOnce(() => {
|
|
56
|
+
throw new Error("ESRCH");
|
|
57
|
+
});
|
|
58
|
+
const ok = killProcessByPgid(4242, { _platform: "linux" } as any);
|
|
59
|
+
expect(ok).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the provider-register hot-reload path.
|
|
3
|
+
*
|
|
4
|
+
* `reloadProviders(pi)` diffs the current providers.json against a
|
|
5
|
+
* last-registered snapshot and calls pi.registerProvider / pi.unregisterProvider
|
|
6
|
+
* as needed. Async model discovery is stubbed via a fetch mock.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
9
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
// We re-import the module fresh in each test so module-level `lastRegistered`
|
|
14
|
+
// state starts empty.
|
|
15
|
+
async function importFresh() {
|
|
16
|
+
vi.resetModules();
|
|
17
|
+
return (await import("../provider-register.js")) as typeof import("../provider-register.js");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeMockPi() {
|
|
21
|
+
const registerProvider = vi.fn();
|
|
22
|
+
const unregisterProvider = vi.fn();
|
|
23
|
+
const pi = {
|
|
24
|
+
registerProvider,
|
|
25
|
+
unregisterProvider,
|
|
26
|
+
events: { on: vi.fn(), emit: vi.fn() },
|
|
27
|
+
on: vi.fn(),
|
|
28
|
+
} as any;
|
|
29
|
+
return { pi, registerProvider, unregisterProvider };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeProvidersJson(home: string, providers: Record<string, any>) {
|
|
33
|
+
const dir = join(home, ".pi", "agent");
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
writeFileSync(
|
|
36
|
+
join(dir, "providers.json"),
|
|
37
|
+
JSON.stringify({ providers }, null, 2),
|
|
38
|
+
"utf-8",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("reloadProviders", () => {
|
|
43
|
+
let tmpHome: string;
|
|
44
|
+
const originalHome = process.env.HOME;
|
|
45
|
+
const originalFetch = globalThis.fetch;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
tmpHome = mkdtempSync(join(tmpdir(), "provider-reload-"));
|
|
49
|
+
process.env.HOME = tmpHome;
|
|
50
|
+
// Stub fetch to return 2 models so discovery succeeds cheaply
|
|
51
|
+
globalThis.fetch = vi.fn(async () =>
|
|
52
|
+
new Response(JSON.stringify({ data: [{ id: "m1" }, { id: "m2" }] }), { status: 200 }),
|
|
53
|
+
) as any;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
globalThis.fetch = originalFetch;
|
|
58
|
+
process.env.HOME = originalHome;
|
|
59
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("adds a new provider when providers.json gains an entry (snapshot was empty)", async () => {
|
|
63
|
+
const mod = await importFresh();
|
|
64
|
+
const { pi, registerProvider, unregisterProvider } = makeMockPi();
|
|
65
|
+
|
|
66
|
+
writeProvidersJson(tmpHome, {
|
|
67
|
+
"my-llm": {
|
|
68
|
+
baseUrl: "https://api.example.com/v1",
|
|
69
|
+
apiKey: "sk-abc",
|
|
70
|
+
api: "openai-completions",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const diff = await mod.reloadProviders(pi);
|
|
75
|
+
expect(diff.added).toEqual(["my-llm"]);
|
|
76
|
+
expect(diff.removed).toEqual([]);
|
|
77
|
+
expect(diff.changed).toEqual([]);
|
|
78
|
+
expect(registerProvider).toHaveBeenCalledTimes(1);
|
|
79
|
+
expect(registerProvider).toHaveBeenCalledWith("my-llm", expect.objectContaining({
|
|
80
|
+
baseUrl: "https://api.example.com/v1",
|
|
81
|
+
api: "openai-completions",
|
|
82
|
+
}));
|
|
83
|
+
expect(unregisterProvider).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("removes a provider when its entry disappears from providers.json", async () => {
|
|
87
|
+
const mod = await importFresh();
|
|
88
|
+
const { pi, registerProvider, unregisterProvider } = makeMockPi();
|
|
89
|
+
|
|
90
|
+
writeProvidersJson(tmpHome, {
|
|
91
|
+
"my-llm": { baseUrl: "https://x", apiKey: "k", api: "openai-completions" },
|
|
92
|
+
});
|
|
93
|
+
await mod.reloadProviders(pi);
|
|
94
|
+
expect(registerProvider).toHaveBeenCalledTimes(1);
|
|
95
|
+
|
|
96
|
+
writeProvidersJson(tmpHome, {});
|
|
97
|
+
const diff = await mod.reloadProviders(pi);
|
|
98
|
+
|
|
99
|
+
expect(diff.removed).toEqual(["my-llm"]);
|
|
100
|
+
expect(diff.added).toEqual([]);
|
|
101
|
+
expect(unregisterProvider).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(unregisterProvider).toHaveBeenCalledWith("my-llm");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("re-registers (unregister then register) when baseUrl changes", async () => {
|
|
106
|
+
const mod = await importFresh();
|
|
107
|
+
const { pi, registerProvider, unregisterProvider } = makeMockPi();
|
|
108
|
+
|
|
109
|
+
writeProvidersJson(tmpHome, {
|
|
110
|
+
"my-llm": { baseUrl: "https://old.example.com/v1", apiKey: "k", api: "openai-completions" },
|
|
111
|
+
});
|
|
112
|
+
await mod.reloadProviders(pi);
|
|
113
|
+
expect(registerProvider).toHaveBeenCalledTimes(1);
|
|
114
|
+
|
|
115
|
+
writeProvidersJson(tmpHome, {
|
|
116
|
+
"my-llm": { baseUrl: "https://new.example.com/v1", apiKey: "k", api: "openai-completions" },
|
|
117
|
+
});
|
|
118
|
+
const diff = await mod.reloadProviders(pi);
|
|
119
|
+
|
|
120
|
+
expect(diff.changed).toEqual(["my-llm"]);
|
|
121
|
+
// unregister must be called before the second register
|
|
122
|
+
const unregOrder = unregisterProvider.mock.invocationCallOrder[0];
|
|
123
|
+
const reg2Order = registerProvider.mock.invocationCallOrder[1];
|
|
124
|
+
expect(unregOrder).toBeLessThan(reg2Order);
|
|
125
|
+
expect(registerProvider).toHaveBeenLastCalledWith(
|
|
126
|
+
"my-llm",
|
|
127
|
+
expect.objectContaining({ baseUrl: "https://new.example.com/v1" }),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("unchanged providers.json produces no register/unregister calls on second reload", async () => {
|
|
132
|
+
const mod = await importFresh();
|
|
133
|
+
const { pi, registerProvider, unregisterProvider } = makeMockPi();
|
|
134
|
+
|
|
135
|
+
writeProvidersJson(tmpHome, {
|
|
136
|
+
"my-llm": { baseUrl: "https://x", apiKey: "k", api: "openai-completions" },
|
|
137
|
+
});
|
|
138
|
+
await mod.reloadProviders(pi);
|
|
139
|
+
registerProvider.mockClear();
|
|
140
|
+
unregisterProvider.mockClear();
|
|
141
|
+
|
|
142
|
+
const diff = await mod.reloadProviders(pi);
|
|
143
|
+
expect(diff.added).toEqual([]);
|
|
144
|
+
expect(diff.removed).toEqual([]);
|
|
145
|
+
expect(diff.changed).toEqual([]);
|
|
146
|
+
expect(registerProvider).not.toHaveBeenCalled();
|
|
147
|
+
expect(unregisterProvider).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("malformed providers.json returns empty diff and does not throw", async () => {
|
|
151
|
+
const mod = await importFresh();
|
|
152
|
+
const { pi } = makeMockPi();
|
|
153
|
+
|
|
154
|
+
const dir = join(tmpHome, ".pi", "agent");
|
|
155
|
+
mkdirSync(dir, { recursive: true });
|
|
156
|
+
writeFileSync(join(dir, "providers.json"), "not valid json {", "utf-8");
|
|
157
|
+
|
|
158
|
+
await expect(mod.reloadProviders(pi)).resolves.toEqual({
|
|
159
|
+
added: [],
|
|
160
|
+
removed: [],
|
|
161
|
+
changed: [],
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("treats apiKey change as 'changed'", async () => {
|
|
166
|
+
const mod = await importFresh();
|
|
167
|
+
const { pi, registerProvider, unregisterProvider } = makeMockPi();
|
|
168
|
+
|
|
169
|
+
writeProvidersJson(tmpHome, {
|
|
170
|
+
"my-llm": { baseUrl: "https://x", apiKey: "old", api: "openai-completions" },
|
|
171
|
+
});
|
|
172
|
+
await mod.reloadProviders(pi);
|
|
173
|
+
|
|
174
|
+
writeProvidersJson(tmpHome, {
|
|
175
|
+
"my-llm": { baseUrl: "https://x", apiKey: "new", api: "openai-completions" },
|
|
176
|
+
});
|
|
177
|
+
const diff = await mod.reloadProviders(pi);
|
|
178
|
+
expect(diff.changed).toEqual(["my-llm"]);
|
|
179
|
+
expect(unregisterProvider).toHaveBeenCalledWith("my-llm");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("treats api type change as 'changed'", async () => {
|
|
183
|
+
const mod = await importFresh();
|
|
184
|
+
const { pi } = makeMockPi();
|
|
185
|
+
|
|
186
|
+
writeProvidersJson(tmpHome, {
|
|
187
|
+
"my-llm": { baseUrl: "https://x", apiKey: "k", api: "openai-completions" },
|
|
188
|
+
});
|
|
189
|
+
await mod.reloadProviders(pi);
|
|
190
|
+
|
|
191
|
+
writeProvidersJson(tmpHome, {
|
|
192
|
+
"my-llm": { baseUrl: "https://x", apiKey: "k", api: "openai-responses" },
|
|
193
|
+
});
|
|
194
|
+
const diff = await mod.reloadProviders(pi);
|
|
195
|
+
expect(diff.changed).toEqual(["my-llm"]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ── input capability default (see change: enable-image-input-custom-providers) ─────
|
|
199
|
+
// Every discovered model must advertise `input: ["text", "image"]` so pi-ai does
|
|
200
|
+
// not strip pasted images via `downgradeUnsupportedImages` before the request
|
|
201
|
+
// leaves the bridge. This guards both the initial-registration path and the
|
|
202
|
+
// reloadProviders() re-register path.
|
|
203
|
+
|
|
204
|
+
it("discovered models default to input: [\"text\", \"image\"] on fresh register", async () => {
|
|
205
|
+
const mod = await importFresh();
|
|
206
|
+
const { pi, registerProvider } = makeMockPi();
|
|
207
|
+
|
|
208
|
+
writeProvidersJson(tmpHome, {
|
|
209
|
+
"my-llm": {
|
|
210
|
+
baseUrl: "https://api.example.com/v1",
|
|
211
|
+
apiKey: "sk-abc",
|
|
212
|
+
api: "openai-completions",
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await mod.reloadProviders(pi);
|
|
217
|
+
|
|
218
|
+
expect(registerProvider).toHaveBeenCalledTimes(1);
|
|
219
|
+
const [, config] = registerProvider.mock.calls[0];
|
|
220
|
+
expect(Array.isArray(config.models)).toBe(true);
|
|
221
|
+
expect(config.models.length).toBe(2); // two models from the fetch stub
|
|
222
|
+
for (const m of config.models) {
|
|
223
|
+
expect(m.input).toEqual(["text", "image"]);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("re-registered models (after reload diff) retain input: [\"text\", \"image\"]", async () => {
|
|
228
|
+
const mod = await importFresh();
|
|
229
|
+
const { pi, registerProvider } = makeMockPi();
|
|
230
|
+
|
|
231
|
+
// Initial register
|
|
232
|
+
writeProvidersJson(tmpHome, {
|
|
233
|
+
"my-llm": { baseUrl: "https://old.example.com/v1", apiKey: "k", api: "openai-completions" },
|
|
234
|
+
});
|
|
235
|
+
await mod.reloadProviders(pi);
|
|
236
|
+
|
|
237
|
+
// Change baseUrl → triggers unregister + re-register
|
|
238
|
+
writeProvidersJson(tmpHome, {
|
|
239
|
+
"my-llm": { baseUrl: "https://new.example.com/v1", apiKey: "k", api: "openai-completions" },
|
|
240
|
+
});
|
|
241
|
+
const diff = await mod.reloadProviders(pi);
|
|
242
|
+
expect(diff.changed).toEqual(["my-llm"]);
|
|
243
|
+
|
|
244
|
+
// Second registerProvider call is the re-register; must carry same input default
|
|
245
|
+
expect(registerProvider).toHaveBeenCalledTimes(2);
|
|
246
|
+
const [, secondConfig] = registerProvider.mock.calls[1];
|
|
247
|
+
for (const m of secondConfig.models) {
|
|
248
|
+
expect(m.input).toEqual(["text", "image"]);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── model metadata enrichment (see change: enrich-custom-provider-model-metadata) ──
|
|
253
|
+
// registerEntry() resolves per-model metadata via enrichModelMetadata() which
|
|
254
|
+
// consults pi-ai's bundled catalog. These tests verify the end-to-end path:
|
|
255
|
+
// discovered id → catalog match → registerProvider receives accurate fields.
|
|
256
|
+
|
|
257
|
+
it("captures ctx.modelRegistry from session_start and re-registers providers with enriched metadata", async () => {
|
|
258
|
+
const mod = await importFresh();
|
|
259
|
+
const { pi, registerProvider } = makeMockPi();
|
|
260
|
+
|
|
261
|
+
// Capture the session_start handler registered by activate().
|
|
262
|
+
const handlers = new Map<string, (event: any, ctx: any) => Promise<void> | void>();
|
|
263
|
+
pi.on = vi.fn((event: string, handler: any) => {
|
|
264
|
+
handlers.set(event, handler);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Fake pi's ModelRegistry. `find(provider, id)` returns Opus 4.7's real
|
|
268
|
+
// metadata for the anthropic entry, simulating what pi passes via
|
|
269
|
+
// ctx.modelRegistry at session_start.
|
|
270
|
+
const fakeRegistry = {
|
|
271
|
+
find: vi.fn((provider: string, id: string) => {
|
|
272
|
+
if (provider === "anthropic" && id === "claude-opus-4-7") {
|
|
273
|
+
return {
|
|
274
|
+
contextWindow: 1_000_000,
|
|
275
|
+
maxTokens: 128_000,
|
|
276
|
+
reasoning: true,
|
|
277
|
+
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
278
|
+
input: ["text", "image"],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Fetch mock advertises the real model id.
|
|
286
|
+
globalThis.fetch = vi.fn(async () =>
|
|
287
|
+
new Response(
|
|
288
|
+
JSON.stringify({ data: [{ id: "cc/claude-opus-4-7" }] }),
|
|
289
|
+
{ status: 200 },
|
|
290
|
+
),
|
|
291
|
+
) as any;
|
|
292
|
+
|
|
293
|
+
writeProvidersJson(tmpHome, {
|
|
294
|
+
proxy: {
|
|
295
|
+
baseUrl: "https://llmproxy.example.com/v1",
|
|
296
|
+
apiKey: "sk-test",
|
|
297
|
+
api: "anthropic-messages",
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// 1) activate() registers the provider BEFORE ctx.modelRegistry is
|
|
302
|
+
// available — first registration hits the fallback path.
|
|
303
|
+
mod.activate(pi);
|
|
304
|
+
// Wait a microtask for the fire-and-forget registerEntry inside activate.
|
|
305
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
306
|
+
|
|
307
|
+
expect(registerProvider).toHaveBeenCalledTimes(1);
|
|
308
|
+
const firstCall = registerProvider.mock.calls[0];
|
|
309
|
+
expect(firstCall[1].models[0].contextWindow).toBe(200_000); // fallback
|
|
310
|
+
|
|
311
|
+
// 2) Fire session_start with ctx.modelRegistry — the handler captures it
|
|
312
|
+
// and re-registers all known providers with the enriched metadata.
|
|
313
|
+
const sessionStartHandler = handlers.get("session_start");
|
|
314
|
+
expect(sessionStartHandler).toBeDefined();
|
|
315
|
+
await sessionStartHandler!({ type: "session_start" }, {
|
|
316
|
+
ui: { notify: vi.fn() },
|
|
317
|
+
modelRegistry: fakeRegistry,
|
|
318
|
+
model: undefined,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// 3) The re-registration should have issued a second registerProvider
|
|
322
|
+
// call, this time with the catalog-enriched metadata.
|
|
323
|
+
expect(registerProvider).toHaveBeenCalledTimes(2);
|
|
324
|
+
const secondCall = registerProvider.mock.calls[1];
|
|
325
|
+
const [name, config] = secondCall;
|
|
326
|
+
expect(name).toBe("proxy");
|
|
327
|
+
const [opus] = config.models;
|
|
328
|
+
expect(opus.id).toBe("cc/claude-opus-4-7");
|
|
329
|
+
expect(opus.contextWindow).toBe(1_000_000);
|
|
330
|
+
expect(opus.maxTokens).toBe(128_000);
|
|
331
|
+
expect(opus.reasoning).toBe(true);
|
|
332
|
+
expect(opus.cost.input).toBe(5);
|
|
333
|
+
expect(opus.cost.output).toBe(25);
|
|
334
|
+
// Probed with prefix-stripped bare id under `anthropic`.
|
|
335
|
+
expect(fakeRegistry.find).toHaveBeenCalledWith("anthropic", "claude-opus-4-7");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("without modelRegistry (never captured), discovered models fall back to api-appropriate defaults", async () => {
|
|
339
|
+
const mod = await importFresh();
|
|
340
|
+
const { pi, registerProvider } = makeMockPi();
|
|
341
|
+
|
|
342
|
+
// No session_start fires — modelRegistryRef stays null and the probe is
|
|
343
|
+
// null, so every discovered id hits the fallback table.
|
|
344
|
+
globalThis.fetch = vi.fn(async () =>
|
|
345
|
+
new Response(
|
|
346
|
+
JSON.stringify({ data: [{ id: "cc/claude-opus-4-7" }] }),
|
|
347
|
+
{ status: 200 },
|
|
348
|
+
),
|
|
349
|
+
) as any;
|
|
350
|
+
|
|
351
|
+
writeProvidersJson(tmpHome, {
|
|
352
|
+
proxy: {
|
|
353
|
+
baseUrl: "https://llmproxy.example.com/v1",
|
|
354
|
+
apiKey: "sk-test",
|
|
355
|
+
api: "anthropic-messages",
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await mod.reloadProviders(pi);
|
|
360
|
+
|
|
361
|
+
const [, config] = registerProvider.mock.calls[0];
|
|
362
|
+
const [opus] = config.models;
|
|
363
|
+
// anthropic-messages fallback: 200k / 64k / no reasoning / zero cost / text+image.
|
|
364
|
+
expect(opus.contextWindow).toBe(200_000);
|
|
365
|
+
expect(opus.maxTokens).toBe(64_000);
|
|
366
|
+
expect(opus.reasoning).toBe(false);
|
|
367
|
+
expect(opus.input).toEqual(["text", "image"]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("discovered unknown model falls back to api-appropriate defaults (openai-completions → 128k)", async () => {
|
|
371
|
+
const mod = await importFresh();
|
|
372
|
+
const { pi, registerProvider } = makeMockPi();
|
|
373
|
+
|
|
374
|
+
// Default beforeEach fetch stub returns `m1` and `m2` — neither exists in catalog.
|
|
375
|
+
writeProvidersJson(tmpHome, {
|
|
376
|
+
"my-llm": {
|
|
377
|
+
baseUrl: "https://api.example.com/v1",
|
|
378
|
+
apiKey: "sk-abc",
|
|
379
|
+
api: "openai-completions",
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await mod.reloadProviders(pi);
|
|
384
|
+
|
|
385
|
+
const [, config] = registerProvider.mock.calls[0];
|
|
386
|
+
for (const m of config.models) {
|
|
387
|
+
expect(m.contextWindow).toBe(128_000);
|
|
388
|
+
expect(m.maxTokens).toBe(16_384);
|
|
389
|
+
expect(m.reasoning).toBe(false);
|
|
390
|
+
expect(m.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
391
|
+
expect(m.input).toEqual(["text", "image"]);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|