@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackbelt-technology/pi-agent-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Web dashboard for monitoring and interacting with pi agent sessions",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
"build": "npm run build --workspace=@blackbelt-technology/pi-dashboard-web",
|
|
52
52
|
"test": "HOME=$(mktemp -d -t pi-test-XXXXXX) vitest run",
|
|
53
53
|
"test:watch": "HOME=$(mktemp -d -t pi-test-XXXXXX) vitest",
|
|
54
|
+
"test:bootstrap": "HOME=$(mktemp -d -t pi-test-XXXXXX) vitest run packages/shared/src/__tests__/bootstrap",
|
|
55
|
+
"test:bootstrap:watch": "HOME=$(mktemp -d -t pi-test-XXXXXX) vitest packages/shared/src/__tests__/bootstrap",
|
|
54
56
|
"lint": "tsc --noEmit",
|
|
55
57
|
"reload": "./scripts/reload-all.sh",
|
|
56
58
|
"reload:check": "./scripts/reload-all.sh --check",
|
|
@@ -64,9 +66,9 @@
|
|
|
64
66
|
"screenshots": "npm --prefix site run screenshots"
|
|
65
67
|
},
|
|
66
68
|
"dependencies": {
|
|
67
|
-
"@blackbelt-technology/pi-dashboard-extension": "
|
|
68
|
-
"@blackbelt-technology/pi-dashboard-server": "
|
|
69
|
-
"@blackbelt-technology/pi-dashboard-web": "
|
|
69
|
+
"@blackbelt-technology/pi-dashboard-extension": "^0.4.1",
|
|
70
|
+
"@blackbelt-technology/pi-dashboard-server": "^0.4.1",
|
|
71
|
+
"@blackbelt-technology/pi-dashboard-web": "^0.4.1"
|
|
70
72
|
},
|
|
71
73
|
"devDependencies": {
|
|
72
74
|
"jsdom": "^29.0.2",
|
|
@@ -81,7 +83,7 @@
|
|
|
81
83
|
"@oh-my-pi/pi-ai": "*",
|
|
82
84
|
"@oh-my-pi/pi-coding-agent": "*",
|
|
83
85
|
"@oh-my-pi/pi-tui": "*",
|
|
84
|
-
"
|
|
86
|
+
"typebox": "*"
|
|
85
87
|
},
|
|
86
88
|
"peerDependenciesMeta": {
|
|
87
89
|
"@mariozechner/pi-coding-agent": {
|
|
@@ -101,6 +103,9 @@
|
|
|
101
103
|
},
|
|
102
104
|
"@oh-my-pi/pi-tui": {
|
|
103
105
|
"optional": true
|
|
106
|
+
},
|
|
107
|
+
"typebox": {
|
|
108
|
+
"optional": true
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackbelt-technology/pi-dashboard-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Pi bridge extension for pi-dashboard",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
6
9
|
"pi": {
|
|
7
10
|
"extensions": [
|
|
8
11
|
"src/bridge.ts"
|
|
@@ -16,18 +19,25 @@
|
|
|
16
19
|
".pi/skills/pi-dashboard/"
|
|
17
20
|
],
|
|
18
21
|
"dependencies": {
|
|
19
|
-
"@blackbelt-technology/pi-dashboard-shared": "
|
|
22
|
+
"@blackbelt-technology/pi-dashboard-shared": "^0.4.1",
|
|
20
23
|
"ws": "^8.18.0"
|
|
21
24
|
},
|
|
22
25
|
"peerDependencies": {
|
|
23
|
-
"@mariozechner/pi-coding-agent": "*"
|
|
26
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
27
|
+
"@mariozechner/pi-tui": "*",
|
|
28
|
+
"typebox": "*"
|
|
24
29
|
},
|
|
25
30
|
"peerDependenciesMeta": {
|
|
26
31
|
"@mariozechner/pi-coding-agent": {
|
|
27
32
|
"optional": true
|
|
33
|
+
},
|
|
34
|
+
"@mariozechner/pi-tui": {
|
|
35
|
+
"optional": true
|
|
28
36
|
}
|
|
29
37
|
},
|
|
30
38
|
"devDependencies": {
|
|
31
|
-
"@
|
|
39
|
+
"@mariozechner/pi-tui": "*",
|
|
40
|
+
"@types/ws": "^8.18.1",
|
|
41
|
+
"typebox": "^1.1.33"
|
|
32
42
|
}
|
|
33
43
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
// Mock modules before importing
|
|
4
|
-
vi.mock("
|
|
4
|
+
vi.mock("typebox", () => ({
|
|
5
5
|
Type: {
|
|
6
6
|
Object: vi.fn(() => ({})),
|
|
7
7
|
String: vi.fn(() => ({})),
|
|
@@ -44,20 +44,37 @@ describe("registerAskUserTool", () => {
|
|
|
44
44
|
expect(tool.promptGuidelines.length).toBeGreaterThan(0);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
it("description instructs agents not to add a Select all option", () => {
|
|
48
|
+
const pi = createMockPi();
|
|
49
|
+
registerAskUserTool(pi as any);
|
|
50
|
+
const tool = pi.registerTool.mock.calls[0][0];
|
|
51
|
+
expect(tool.description).toMatch(/UI provides a Select all/i);
|
|
52
|
+
});
|
|
53
|
+
|
|
47
54
|
describe("message passthrough", () => {
|
|
48
55
|
function getToolAndMockCtx() {
|
|
49
56
|
const pi = createMockPi();
|
|
50
57
|
registerAskUserTool(pi as any);
|
|
51
58
|
const tool = pi.registerTool.mock.calls[0][0];
|
|
59
|
+
// `custom` stands in for the multiselect polyfill: it invokes the factory
|
|
60
|
+
// with a `done` callback; the factory-returned component exposes
|
|
61
|
+
// onConfirm/onCancel. We auto-confirm with ["A"] to preserve the legacy
|
|
62
|
+
// mock return value that the multiselect assertions expected.
|
|
63
|
+
const custom = vi.fn().mockImplementation(async (factory: any) => {
|
|
64
|
+
return await new Promise<unknown>((resolve) => {
|
|
65
|
+
const component: any = factory({}, {}, {}, (r: unknown) => resolve(r));
|
|
66
|
+
component?.onConfirm?.(["A"]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
52
69
|
const ctx = {
|
|
53
70
|
ui: {
|
|
54
71
|
confirm: vi.fn().mockResolvedValue(true),
|
|
55
72
|
select: vi.fn().mockResolvedValue("A"),
|
|
56
73
|
input: vi.fn().mockResolvedValue("hello"),
|
|
57
|
-
|
|
74
|
+
custom,
|
|
58
75
|
},
|
|
59
76
|
};
|
|
60
|
-
return { tool, ctx };
|
|
77
|
+
return { tool, ctx, custom };
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
it("passes message through opts for input", async () => {
|
|
@@ -72,10 +89,19 @@ describe("registerAskUserTool", () => {
|
|
|
72
89
|
expect(ctx.ui.select).toHaveBeenCalledWith("Pick", ["A", "B"], { message: "Context" });
|
|
73
90
|
});
|
|
74
91
|
|
|
75
|
-
it("
|
|
92
|
+
it("dispatches multiselect through the polyfill via ctx.ui.custom", async () => {
|
|
76
93
|
const { tool, ctx } = getToolAndMockCtx();
|
|
77
|
-
await tool.execute(
|
|
78
|
-
|
|
94
|
+
const result = await tool.execute(
|
|
95
|
+
"id",
|
|
96
|
+
{ method: "multiselect", title: "Multi", message: "Info", options: ["A"] },
|
|
97
|
+
undefined,
|
|
98
|
+
undefined,
|
|
99
|
+
ctx,
|
|
100
|
+
);
|
|
101
|
+
// Polyfill routes via custom(factory); multiselect is not called directly.
|
|
102
|
+
expect(ctx.ui.custom).toHaveBeenCalledTimes(1);
|
|
103
|
+
expect(result.details.method).toBe("multiselect");
|
|
104
|
+
expect(result.details.result).toEqual(["A"]);
|
|
79
105
|
});
|
|
80
106
|
|
|
81
107
|
it("does not pass opts when message is undefined", async () => {
|
|
@@ -123,7 +149,7 @@ describe("registerAskUserTool", () => {
|
|
|
123
149
|
await expect(
|
|
124
150
|
tool.execute("id", { method: "multiselect", title: "Pick" }, undefined, undefined, ctx),
|
|
125
151
|
).rejects.toThrow(/options/i);
|
|
126
|
-
expect(ctx.ui.
|
|
152
|
+
expect(ctx.ui.custom).not.toHaveBeenCalled();
|
|
127
153
|
});
|
|
128
154
|
});
|
|
129
155
|
|
|
@@ -311,12 +337,18 @@ describe("registerAskUserTool", () => {
|
|
|
311
337
|
const pi = createMockPi();
|
|
312
338
|
registerAskUserTool(pi as any);
|
|
313
339
|
const tool = pi.registerTool.mock.calls[0][0];
|
|
340
|
+
const custom = vi.fn().mockImplementation(async (factory: any) => {
|
|
341
|
+
return await new Promise<unknown>((resolve) => {
|
|
342
|
+
const component: any = factory({}, {}, {}, (r: unknown) => resolve(r));
|
|
343
|
+
component?.onConfirm?.(["A"]);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
314
346
|
const ctx = {
|
|
315
347
|
ui: {
|
|
316
348
|
confirm: vi.fn().mockResolvedValue(true),
|
|
317
349
|
select: vi.fn().mockResolvedValue("A"),
|
|
318
350
|
input: vi.fn().mockResolvedValue("hello"),
|
|
319
|
-
|
|
351
|
+
custom,
|
|
320
352
|
},
|
|
321
353
|
};
|
|
322
354
|
return { tool, ctx };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the bridge entryId stamping under pi 0.70.x's emit-then-await-then-append
|
|
3
|
+
* ordering. Pi 0.70.x's _processAgentEvent does (paraphrased):
|
|
4
|
+
*
|
|
5
|
+
* await this._emitExtensionEvent(event); // <-- bridge runs here, awaited
|
|
6
|
+
* this._emit(event); // sync legacy listeners
|
|
7
|
+
* if (event.type === "message_end") {
|
|
8
|
+
* sessionManager.appendMessage(event.message); // <-- entry id GENERATED HERE
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* The bridge's old `queueMicrotask` deferral resolves INSIDE the awaited dispatcher,
|
|
12
|
+
* before appendMessage runs — so getLeafId() still returns the previous leaf. The fix
|
|
13
|
+
* is `setTimeout(0)` (macrotask) so the entire await chain unwinds and appendMessage
|
|
14
|
+
* runs first; OR reading `event.message.id` after pi mutates it in-place.
|
|
15
|
+
*
|
|
16
|
+
* This test simulates that ordering and asserts the correct mechanisms.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, it, expect } from "vitest";
|
|
19
|
+
|
|
20
|
+
interface SimMessage {
|
|
21
|
+
role: string;
|
|
22
|
+
content: string;
|
|
23
|
+
id?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Simulate pi 0.70.x's _processAgentEvent ordering. Returns a promise that
|
|
28
|
+
* resolves when the entire event has been processed (including appendMessage).
|
|
29
|
+
*
|
|
30
|
+
* The `bridgeHandler` is registered as an "extension handler" — runs awaited
|
|
31
|
+
* inside _emitExtensionEvent. It receives the event and a pseudo-ctx with
|
|
32
|
+
* sessionManager.getLeafId().
|
|
33
|
+
*/
|
|
34
|
+
async function simulatePi070Emit(opts: {
|
|
35
|
+
event: { type: string; message: SimMessage };
|
|
36
|
+
state: { leafId: string; nextId: string };
|
|
37
|
+
appendMessage: (msg: SimMessage) => string; // returns the new id
|
|
38
|
+
bridgeHandler: (event: any, ctx: any) => Promise<void> | void;
|
|
39
|
+
}): Promise<void> {
|
|
40
|
+
const ctx = {
|
|
41
|
+
sessionManager: { getLeafId: () => opts.state.leafId },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Step 1: await _emitExtensionEvent — runs the bridge handler awaited.
|
|
45
|
+
await opts.bridgeHandler(opts.event, ctx);
|
|
46
|
+
|
|
47
|
+
// Step 2: _emit (sync legacy listeners) — no-op in this simulation.
|
|
48
|
+
|
|
49
|
+
// Step 3: persistence on message_end.
|
|
50
|
+
if (opts.event.type === "message_end") {
|
|
51
|
+
const id = opts.appendMessage(opts.event.message);
|
|
52
|
+
opts.state.leafId = id;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("pi 0.70 emit/append ordering", () => {
|
|
57
|
+
it("queueMicrotask deferral DOES NOT capture the post-persist id (the bug)", async () => {
|
|
58
|
+
const state = { leafId: "prev", nextId: "new-id-42" };
|
|
59
|
+
let captured: string | undefined;
|
|
60
|
+
|
|
61
|
+
const buggyBridge = async (event: any, ctx: any) => {
|
|
62
|
+
// What the OLD bridge does today:
|
|
63
|
+
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
|
64
|
+
captured = ctx.sessionManager.getLeafId();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
await simulatePi070Emit({
|
|
68
|
+
event: { type: "message_end", message: { role: "assistant", content: "hi" } },
|
|
69
|
+
state,
|
|
70
|
+
appendMessage: (m) => {
|
|
71
|
+
m.id = state.nextId;
|
|
72
|
+
return state.nextId;
|
|
73
|
+
},
|
|
74
|
+
bridgeHandler: buggyBridge,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Bug: captured is the previous leaf, NOT the just-appended id.
|
|
78
|
+
expect(captured).toBe("prev");
|
|
79
|
+
expect(captured).not.toBe("new-id-42");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("setTimeout(0) deferral DOES capture the post-persist id (the fix)", async () => {
|
|
83
|
+
const state = { leafId: "prev", nextId: "new-id-42" };
|
|
84
|
+
let capturedFromGetLeafId: string | undefined;
|
|
85
|
+
let capturedFromMessageId: string | undefined;
|
|
86
|
+
let sendDone!: () => void;
|
|
87
|
+
const sendCompleted = new Promise<void>((r) => { sendDone = r; });
|
|
88
|
+
|
|
89
|
+
// The bridge schedules and returns synchronously — the only way the
|
|
90
|
+
// awaited dispatcher can unwind so appendMessage runs before the timeout.
|
|
91
|
+
const fixedBridge = (event: any, ctx: any) => {
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
capturedFromMessageId = (event.message as any).id;
|
|
94
|
+
capturedFromGetLeafId = ctx.sessionManager.getLeafId();
|
|
95
|
+
sendDone();
|
|
96
|
+
}, 0);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await simulatePi070Emit({
|
|
100
|
+
event: { type: "message_end", message: { role: "assistant", content: "hi" } },
|
|
101
|
+
state,
|
|
102
|
+
appendMessage: (m) => {
|
|
103
|
+
m.id = state.nextId;
|
|
104
|
+
return state.nextId;
|
|
105
|
+
},
|
|
106
|
+
bridgeHandler: fixedBridge,
|
|
107
|
+
});
|
|
108
|
+
await sendCompleted;
|
|
109
|
+
|
|
110
|
+
// Both signals should now point at the just-persisted entry.
|
|
111
|
+
expect(capturedFromMessageId).toBe("new-id-42");
|
|
112
|
+
expect(capturedFromGetLeafId).toBe("new-id-42");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("WeakMap-on-appendMessage captures the id even before the macrotask", async () => {
|
|
116
|
+
const state = { leafId: "prev", nextId: "new-id-77" };
|
|
117
|
+
const idByMessage = new WeakMap<object, string>();
|
|
118
|
+
const wrappedAppend = (m: SimMessage): string => {
|
|
119
|
+
m.id = state.nextId;
|
|
120
|
+
idByMessage.set(m as object, m.id);
|
|
121
|
+
return m.id;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let viaWeakMap: string | undefined;
|
|
125
|
+
let viaMutation: string | undefined;
|
|
126
|
+
let sendDone!: () => void;
|
|
127
|
+
const sentP = new Promise<void>((r) => { sendDone = r; });
|
|
128
|
+
|
|
129
|
+
// CRITICAL: bridge SCHEDULES the send and RETURNS IMMEDIATELY.
|
|
130
|
+
// It does NOT await its own setTimeout — that would keep the
|
|
131
|
+
// outer dispatcher awaiting and we'd be back to the queueMicrotask
|
|
132
|
+
// bug (timeout fires before appendMessage).
|
|
133
|
+
const fixedBridge = (event: any) => {
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
viaMutation = (event.message as any).id;
|
|
136
|
+
viaWeakMap = idByMessage.get(event.message as object);
|
|
137
|
+
sendDone();
|
|
138
|
+
}, 0);
|
|
139
|
+
// Return synchronously — let the awaited dispatcher unwind.
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
await simulatePi070Emit({
|
|
143
|
+
event: { type: "message_end", message: { role: "assistant", content: "hi" } },
|
|
144
|
+
state,
|
|
145
|
+
appendMessage: wrappedAppend,
|
|
146
|
+
bridgeHandler: fixedBridge,
|
|
147
|
+
});
|
|
148
|
+
await sentP;
|
|
149
|
+
|
|
150
|
+
expect(viaMutation).toBe("new-id-77");
|
|
151
|
+
expect(viaWeakMap).toBe("new-id-77");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("user message_start has NO id (pi defers user persistence to message_end)", async () => {
|
|
155
|
+
const state = { leafId: "prev-assistant", nextId: "new-user-id" };
|
|
156
|
+
let captured: string | undefined;
|
|
157
|
+
|
|
158
|
+
const fixedBridge = async (event: any) => {
|
|
159
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
160
|
+
captured = (event.message as any).id; // still undefined for message_start
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
await simulatePi070Emit({
|
|
164
|
+
event: { type: "message_start", message: { role: "user", content: "hello" } },
|
|
165
|
+
state,
|
|
166
|
+
appendMessage: () => state.nextId, // not called for message_start
|
|
167
|
+
bridgeHandler: fixedBridge,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// No id available at message_start time — must rely on entry_persisted
|
|
171
|
+
// back-fill (delivered when the message_end of the SAME message fires later).
|
|
172
|
+
expect(captured).toBeUndefined();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for enrichModelMetadata — the pure helper that resolves a discovered
|
|
3
|
+
* custom-provider model id against a catalog probe (typically backed by pi's
|
|
4
|
+
* `modelRegistry.find(provider, id)`) and falls back to api-appropriate
|
|
5
|
+
* defaults when the probe has no match.
|
|
6
|
+
*
|
|
7
|
+
* The helper takes an optional `probe` parameter so unit tests can supply a
|
|
8
|
+
* fake catalog without needing `@mariozechner/pi-ai` installed — in
|
|
9
|
+
* production, registerEntry() injects `modelRegistry.find` as the probe.
|
|
10
|
+
*
|
|
11
|
+
* Spec: openspec/changes/enrich-custom-provider-model-metadata/specs/provider-auth-bridge/spec.md
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from "vitest";
|
|
15
|
+
import { enrichModelMetadata, type CatalogProbe } from "../provider-register.js";
|
|
16
|
+
|
|
17
|
+
// Minimal fake catalog mirroring a subset of pi-ai's real MODELS export.
|
|
18
|
+
// Keys are `${provider}|${id}` so our probe is a single Map lookup.
|
|
19
|
+
const FAKE_CATALOG = new Map<string, any>([
|
|
20
|
+
[
|
|
21
|
+
"anthropic|claude-opus-4-7",
|
|
22
|
+
{
|
|
23
|
+
contextWindow: 1_000_000,
|
|
24
|
+
maxTokens: 128_000,
|
|
25
|
+
reasoning: true,
|
|
26
|
+
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
27
|
+
input: ["text", "image"],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
"anthropic|claude-sonnet-4-6",
|
|
32
|
+
{
|
|
33
|
+
contextWindow: 1_000_000,
|
|
34
|
+
maxTokens: 64_000,
|
|
35
|
+
reasoning: true,
|
|
36
|
+
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
37
|
+
input: ["text", "image"],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"anthropic|claude-haiku-4-5",
|
|
42
|
+
{
|
|
43
|
+
contextWindow: 200_000,
|
|
44
|
+
maxTokens: 64_000,
|
|
45
|
+
reasoning: true,
|
|
46
|
+
cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
47
|
+
input: ["text", "image"],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"openai|gpt-5",
|
|
52
|
+
{
|
|
53
|
+
contextWindow: 400_000,
|
|
54
|
+
maxTokens: 128_000,
|
|
55
|
+
reasoning: true,
|
|
56
|
+
cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
|
|
57
|
+
input: ["text", "image"],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"opencode|claude-opus-4-7",
|
|
62
|
+
{
|
|
63
|
+
// Deliberately different values so tests can prove the candidate-
|
|
64
|
+
// provider probe order is deterministic (anthropic checked first).
|
|
65
|
+
contextWindow: 999,
|
|
66
|
+
maxTokens: 1,
|
|
67
|
+
reasoning: true,
|
|
68
|
+
cost: { input: 9, output: 9, cacheRead: 9, cacheWrite: 9 },
|
|
69
|
+
input: ["text"],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
const fakeProbe: CatalogProbe = (provider, id) =>
|
|
75
|
+
FAKE_CATALOG.get(`${provider}|${id}`) ?? null;
|
|
76
|
+
|
|
77
|
+
describe("enrichModelMetadata — catalog matches via probe", () => {
|
|
78
|
+
it("resolves `cc/claude-opus-4-7` + anthropic-messages to Opus 4.7 catalog entry (1M ctx)", () => {
|
|
79
|
+
const result = enrichModelMetadata("cc/claude-opus-4-7", "anthropic-messages", fakeProbe);
|
|
80
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
81
|
+
expect(result.maxTokens).toBe(128_000);
|
|
82
|
+
expect(result.reasoning).toBe(true);
|
|
83
|
+
expect(result.cost).toEqual({
|
|
84
|
+
input: 5,
|
|
85
|
+
output: 25,
|
|
86
|
+
cacheRead: 0.5,
|
|
87
|
+
cacheWrite: 6.25,
|
|
88
|
+
});
|
|
89
|
+
expect(result.input).toEqual(["text", "image"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("resolves bare `claude-sonnet-4-6` + anthropic-messages to Sonnet 4.6 (1M ctx)", () => {
|
|
93
|
+
const result = enrichModelMetadata("claude-sonnet-4-6", "anthropic-messages", fakeProbe);
|
|
94
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
95
|
+
expect(result.reasoning).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("resolves `anthropic/claude-opus-4-7` prefix to Opus 4.7 (1M ctx)", () => {
|
|
99
|
+
const result = enrichModelMetadata("anthropic/claude-opus-4-7", "anthropic-messages", fakeProbe);
|
|
100
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
101
|
+
expect(result.maxTokens).toBe(128_000);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("resolves `claude-haiku-4-5` + anthropic-messages to Haiku 4.5 (200k ctx — verifies we don't over-claim)", () => {
|
|
105
|
+
const result = enrichModelMetadata("claude-haiku-4-5", "anthropic-messages", fakeProbe);
|
|
106
|
+
expect(result.contextWindow).toBe(200_000);
|
|
107
|
+
expect(result.reasoning).toBe(true);
|
|
108
|
+
expect(result.cost.input).toBe(1);
|
|
109
|
+
expect(result.cost.output).toBe(5);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("resolves `openrouter/openai/gpt-5` prefix to gpt-5 via openai-completions candidates", () => {
|
|
113
|
+
const result = enrichModelMetadata("openrouter/openai/gpt-5", "openai-completions", fakeProbe);
|
|
114
|
+
expect(result.contextWindow).toBe(400_000);
|
|
115
|
+
expect(result.maxTokens).toBe(128_000);
|
|
116
|
+
expect(result.reasoning).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("enrichModelMetadata — fallback defaults (no probe or no match)", () => {
|
|
121
|
+
it("no probe supplied → falls back to api-appropriate default", () => {
|
|
122
|
+
const result = enrichModelMetadata("cc/claude-opus-4-7", "anthropic-messages");
|
|
123
|
+
expect(result.contextWindow).toBe(200_000);
|
|
124
|
+
expect(result.maxTokens).toBe(64_000);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("unknown id + anthropic-messages returns 200k / 64k / no reasoning / zero cost / text+image", () => {
|
|
128
|
+
const result = enrichModelMetadata("some-unknown-anthropic-model", "anthropic-messages", fakeProbe);
|
|
129
|
+
expect(result.contextWindow).toBe(200_000);
|
|
130
|
+
expect(result.maxTokens).toBe(64_000);
|
|
131
|
+
expect(result.reasoning).toBe(false);
|
|
132
|
+
expect(result.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
133
|
+
expect(result.input).toEqual(["text", "image"]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("unknown id + openai-completions returns 128k / 16k / zero cost / text+image", () => {
|
|
137
|
+
const result = enrichModelMetadata("some-unknown-openai-model", "openai-completions", fakeProbe);
|
|
138
|
+
expect(result.contextWindow).toBe(128_000);
|
|
139
|
+
expect(result.maxTokens).toBe(16_384);
|
|
140
|
+
expect(result.reasoning).toBe(false);
|
|
141
|
+
expect(result.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
142
|
+
expect(result.input).toEqual(["text", "image"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("unknown id + google-generative-ai returns 1M / 65k / zero cost / text+image", () => {
|
|
146
|
+
const result = enrichModelMetadata("some-unknown-gemini-model", "google-generative-ai", fakeProbe);
|
|
147
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
148
|
+
expect(result.maxTokens).toBe(65_536);
|
|
149
|
+
expect(result.reasoning).toBe(false);
|
|
150
|
+
expect(result.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
151
|
+
expect(result.input).toEqual(["text", "image"]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("prefixed unknown + openai-completions falls back (both prefixed and bare miss)", () => {
|
|
155
|
+
const result = enrichModelMetadata("minimax/custom-private-model", "openai-completions", fakeProbe);
|
|
156
|
+
expect(result.contextWindow).toBe(128_000);
|
|
157
|
+
expect(result.maxTokens).toBe(16_384);
|
|
158
|
+
expect(result.cost.input).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("no api argument + no probe defaults to openai-completions fallback", () => {
|
|
162
|
+
const result = enrichModelMetadata("some-unknown-model");
|
|
163
|
+
expect(result.contextWindow).toBe(128_000);
|
|
164
|
+
expect(result.maxTokens).toBe(16_384);
|
|
165
|
+
expect(result.input).toEqual(["text", "image"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("unknown api string + probe still probes openai-completions candidates", () => {
|
|
169
|
+
const result = enrichModelMetadata("some-unknown-model", "some-weird-api", fakeProbe);
|
|
170
|
+
expect(result.contextWindow).toBe(128_000);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("probe that throws is tolerated (falls through to fallback)", () => {
|
|
174
|
+
const throwingProbe: CatalogProbe = () => {
|
|
175
|
+
throw new Error("registry not ready");
|
|
176
|
+
};
|
|
177
|
+
const result = enrichModelMetadata("claude-opus-4-7", "anthropic-messages", throwingProbe);
|
|
178
|
+
expect(result.contextWindow).toBe(200_000); // anthropic-messages fallback
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("enrichModelMetadata — probe order determinism", () => {
|
|
183
|
+
it("prefers `anthropic` over `opencode` for anthropic-messages (first in candidate list wins)", () => {
|
|
184
|
+
// The fake catalog has `claude-opus-4-7` under BOTH `anthropic` (1M) and
|
|
185
|
+
// `opencode` (999). With api=anthropic-messages, candidates = ["anthropic", "opencode"]
|
|
186
|
+
// so the anthropic entry must win.
|
|
187
|
+
const result = enrichModelMetadata("claude-opus-4-7", "anthropic-messages", fakeProbe);
|
|
188
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
189
|
+
expect(result.maxTokens).toBe(128_000);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("multi-segment prefix `openrouter/anthropic/claude-opus-4-7` resolves via bare-id lookup", () => {
|
|
193
|
+
// After stripping the last segment, `claude-opus-4-7` matches `anthropic` (1M).
|
|
194
|
+
const result = enrichModelMetadata(
|
|
195
|
+
"openrouter/anthropic/claude-opus-4-7",
|
|
196
|
+
"anthropic-messages",
|
|
197
|
+
fakeProbe,
|
|
198
|
+
);
|
|
199
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -74,6 +74,36 @@ describe("mapEventToProtocol", () => {
|
|
|
74
74
|
expect(result.event.data).toEqual(piEvent);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
it("should map an entry_persisted event (per fix-per-message-fork)", () => {
|
|
78
|
+
const piEvent = {
|
|
79
|
+
type: "entry_persisted",
|
|
80
|
+
entryId: "abc-123",
|
|
81
|
+
nonce: "n-7",
|
|
82
|
+
};
|
|
83
|
+
const result = mapEventToProtocol(sessionId, piEvent);
|
|
84
|
+
expect(result.type).toBe("event_forward");
|
|
85
|
+
expect(result.sessionId).toBe(sessionId);
|
|
86
|
+
expect(result.event.eventType).toBe("entry_persisted");
|
|
87
|
+
expect(result.event.data).toMatchObject({
|
|
88
|
+
type: "entry_persisted",
|
|
89
|
+
entryId: "abc-123",
|
|
90
|
+
nonce: "n-7",
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should map a message_end event with nonce (per fix-per-message-fork)", () => {
|
|
95
|
+
const piEvent = {
|
|
96
|
+
type: "message_end",
|
|
97
|
+
message: { role: "assistant", content: "hi", id: "asst-9" },
|
|
98
|
+
entryId: "asst-9",
|
|
99
|
+
nonce: "n-7",
|
|
100
|
+
};
|
|
101
|
+
const result = mapEventToProtocol(sessionId, piEvent);
|
|
102
|
+
expect(result.event.eventType).toBe("message_end");
|
|
103
|
+
expect((result.event.data as any).nonce).toBe("n-7");
|
|
104
|
+
expect((result.event.data as any).entryId).toBe("asst-9");
|
|
105
|
+
});
|
|
106
|
+
|
|
77
107
|
it("should strip non-serializable fields", () => {
|
|
78
108
|
const piEvent = {
|
|
79
109
|
type: "test_event",
|