@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7
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/CHANGELOG.md +54 -0
- package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
- package/dist/cli.js +3342 -3274
- package/dist/types/capability/index.d.ts +1 -1
- package/dist/types/capability/types.d.ts +23 -1
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
- package/dist/types/internal-urls/parse.d.ts +12 -0
- package/dist/types/internal-urls/router.d.ts +6 -0
- package/dist/types/internal-urls/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +5 -0
- package/dist/types/mcp/tool-bridge.d.ts +13 -0
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +8 -6
- package/dist/types/session/agent-session.d.ts +28 -1
- package/dist/types/session/model-controls.d.ts +4 -1
- package/dist/types/session/session-advisors.d.ts +7 -1
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -4
- package/dist/types/session/turn-recovery.d.ts +5 -3
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +3 -11
- package/dist/types/thinking.d.ts +21 -2
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/dist/types/utils/title-generator.d.ts +17 -16
- package/package.json +12 -12
- package/src/capability/index.ts +43 -12
- package/src/capability/mcp.ts +21 -0
- package/src/capability/types.ts +20 -1
- package/src/cli/read-cli.ts +44 -2
- package/src/config/settings-schema.ts +42 -2
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/eval/py/runner.py +16 -2
- package/src/extensibility/extensions/runner.ts +117 -5
- package/src/extensibility/extensions/types.ts +0 -1
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/hooks/types.ts +0 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/internal-urls/mcp-protocol.ts +17 -3
- package/src/internal-urls/parse.ts +31 -0
- package/src/internal-urls/router.ts +24 -4
- package/src/internal-urls/types.ts +6 -0
- package/src/live/transport.ts +2 -2
- package/src/lsp/client.ts +2 -2
- package/src/lsp/config.ts +4 -0
- package/src/lsp/types.ts +2 -0
- package/src/mcp/config.ts +26 -14
- package/src/mcp/manager.ts +26 -9
- package/src/mcp/tool-bridge.ts +52 -1
- package/src/memories/index.ts +25 -6
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/command-controller.ts +10 -10
- package/src/modes/controllers/extension-ui-controller.ts +7 -7
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +60 -47
- package/src/prompts/steering/user-interjection.md +2 -5
- package/src/prompts/tools/task.md +4 -2
- package/src/sdk.ts +64 -58
- package/src/session/agent-session-types.ts +8 -5
- package/src/session/agent-session.ts +144 -11
- package/src/session/model-controls.ts +48 -12
- package/src/session/session-advisors.ts +30 -14
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +162 -54
- package/src/session/streaming-output.ts +18 -6
- package/src/session/tool-choice-queue.ts +19 -4
- package/src/session/turn-recovery.ts +25 -6
- package/src/slash-commands/builtin-registry.ts +69 -0
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +43 -32
- package/src/task/types.ts +12 -17
- package/src/thinking.ts +68 -5
- package/src/tools/bash.ts +16 -9
- package/src/tools/index.ts +36 -16
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +89 -15
- package/src/tools/write.ts +16 -7
- package/src/tools/xdev.ts +198 -210
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/title-generator.ts +88 -34
- package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
- package/dist/types/cli/update-cli.test.d.ts +0 -1
- package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
- package/dist/types/hindsight/client.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
- package/dist/types/launch/broker-list-order.test.d.ts +0 -1
- package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
- package/dist/types/launch/protocol.test.d.ts +0 -1
- package/dist/types/launch/spawn-options.test.d.ts +0 -1
- package/dist/types/launch/terminal-output.test.d.ts +0 -1
- package/dist/types/live/protocol.test.d.ts +0 -1
- package/dist/types/mcp/config-writer.test.d.ts +0 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
- package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
- package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
- package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
- package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
- package/dist/types/modes/print-mode.test.d.ts +0 -1
- package/dist/types/modes/session-teardown.test.d.ts +0 -1
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
- package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
- package/dist/types/modes/warp-events.test.d.ts +0 -1
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
- package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
- package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
- package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
- package/dist/types/session/blob-store.test.d.ts +0 -1
- package/dist/types/session/messages.test.d.ts +0 -1
- package/dist/types/session/session-context.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
- package/dist/types/system-prompt.test.d.ts +0 -1
- package/dist/types/task/render.test.d.ts +0 -1
- package/dist/types/task/spawn-policy.test.d.ts +0 -1
- package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
- package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
- package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
- package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
- package/src/advisor/__tests__/advisor.test.ts +0 -4889
- package/src/advisor/__tests__/config.test.ts +0 -349
- package/src/advisor/__tests__/emission-guard.test.ts +0 -147
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
- package/src/cli/update-cli.test.ts +0 -28
- package/src/config/__tests__/model-registry.test.ts +0 -182
- package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
- package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
- package/src/eval/__tests__/budget-bridge.test.ts +0 -80
- package/src/eval/__tests__/completion-bridge.test.ts +0 -412
- package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
- package/src/eval/__tests__/idle-timeout.test.ts +0 -80
- package/src/eval/__tests__/js-context-manager.test.ts +0 -456
- package/src/eval/__tests__/julia-prelude.test.ts +0 -66
- package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
- package/src/eval/__tests__/prelude-agent.test.ts +0 -156
- package/src/eval/__tests__/process-entry-import.test.ts +0 -137
- package/src/eval/py/__tests__/prelude.test.ts +0 -104
- package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
- package/src/hindsight/client.test.ts +0 -75
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
- package/src/launch/broker-list-order.test.ts +0 -89
- package/src/launch/broker-output-snapshot.test.ts +0 -126
- package/src/launch/protocol.test.ts +0 -59
- package/src/launch/spawn-options.test.ts +0 -31
- package/src/launch/terminal-output.test.ts +0 -107
- package/src/live/protocol.test.ts +0 -140
- package/src/mcp/config-writer.test.ts +0 -43
- package/src/mcp/smithery-auth.test.ts +0 -29
- package/src/mcp/smithery-registry.test.ts +0 -51
- package/src/mcp/transports/stdio.test.ts +0 -427
- package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
- package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
- package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
- package/src/modes/components/__tests__/skill-message.test.ts +0 -94
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
- package/src/modes/components/custom-editor.test.ts +0 -510
- package/src/modes/components/login-dialog.test.ts +0 -56
- package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
- package/src/modes/components/status-line/component.test.ts +0 -84
- package/src/modes/components/tool-execution.test.ts +0 -162
- package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
- package/src/modes/noninteractive-dispose.test.ts +0 -73
- package/src/modes/print-mode.test.ts +0 -71
- package/src/modes/session-teardown.test.ts +0 -219
- package/src/modes/theme/mermaid-rendering.test.ts +0 -53
- package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
- package/src/modes/warp-events.test.ts +0 -794
- package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
- package/src/plan-mode/model-transition.test.ts +0 -60
- package/src/plan-mode/reentry-prompt.test.ts +0 -41
- package/src/session/agent-session-error-log.test.ts +0 -59
- package/src/session/blob-store.test.ts +0 -56
- package/src/session/messages.test.ts +0 -282
- package/src/session/session-context.test.ts +0 -384
- package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
- package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
- package/src/system-prompt.test.ts +0 -236
- package/src/task/render.test.ts +0 -290
- package/src/task/spawn-policy.test.ts +0 -62
- package/src/tools/__tests__/eval-description.test.ts +0 -18
- package/src/tools/__tests__/glob.test.ts +0 -37
- package/src/tools/__tests__/json-tree.test.ts +0 -35
- package/src/tools/__tests__/vibe-render.test.ts +0 -210
- package/src/tools/hub/launch-compat.test.ts +0 -40
- package/src/vibe/__tests__/token-rate.test.ts +0 -96
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import * as fsp from "node:fs/promises";
|
|
3
|
-
import * as os from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
advisorConfigFilePath,
|
|
7
|
-
discoverAdvisorConfigs,
|
|
8
|
-
getOrCreateAdvisorProviderSessionId,
|
|
9
|
-
loadWatchdogConfigFile,
|
|
10
|
-
resolveAdvisorConfigEditPath,
|
|
11
|
-
saveWatchdogConfigFile,
|
|
12
|
-
serializeWatchdogConfig,
|
|
13
|
-
slugifyAdvisorName,
|
|
14
|
-
type WatchdogConfigDoc,
|
|
15
|
-
} from "../config";
|
|
16
|
-
|
|
17
|
-
describe("discoverAdvisorConfigs", () => {
|
|
18
|
-
let tmp: string;
|
|
19
|
-
let agentDir: string;
|
|
20
|
-
|
|
21
|
-
beforeEach(async () => {
|
|
22
|
-
tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-advisor-config-"));
|
|
23
|
-
// Empty agent dir so the user-level search path can't pick up a real ~/.omp/WATCHDOG.yml.
|
|
24
|
-
agentDir = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-advisor-agentdir-"));
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(async () => {
|
|
28
|
-
await fsp.rm(tmp, { recursive: true, force: true });
|
|
29
|
-
await fsp.rm(agentDir, { recursive: true, force: true });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("parses advisors, the model thinking suffix, tool filtering, and shared instructions", async () => {
|
|
33
|
-
const yaml = [
|
|
34
|
-
"instructions: Shared baseline for all advisors.",
|
|
35
|
-
"advisors:",
|
|
36
|
-
" - name: Architecture",
|
|
37
|
-
" model: x-ai/grok-code-fast:high",
|
|
38
|
-
" instructions: Watch module boundaries.",
|
|
39
|
-
" - name: Security Reviewer",
|
|
40
|
-
" tools: [read, definitely-not-a-tool]",
|
|
41
|
-
].join("\n");
|
|
42
|
-
await Bun.write(path.join(tmp, "WATCHDOG.yml"), yaml);
|
|
43
|
-
|
|
44
|
-
const { advisors, sharedInstructions } = await discoverAdvisorConfigs(tmp, agentDir);
|
|
45
|
-
expect(advisors).toHaveLength(2);
|
|
46
|
-
const [arch, sec] = advisors;
|
|
47
|
-
expect(arch.name).toBe("Architecture");
|
|
48
|
-
// The model selector (incl. the `:high` thinking suffix) is stored verbatim;
|
|
49
|
-
// resolution happens later in the session, not here.
|
|
50
|
-
expect(arch.model).toBe("x-ai/grok-code-fast:high");
|
|
51
|
-
expect(arch.instructions).toBe("Watch module boundaries.");
|
|
52
|
-
expect(sec.name).toBe("Security Reviewer");
|
|
53
|
-
expect(sec.model).toBeUndefined();
|
|
54
|
-
// The unknown/non-read-only tool is dropped; only `read` survives.
|
|
55
|
-
expect(sec.tools).toEqual(["read"]);
|
|
56
|
-
expect(sharedInstructions).toBe("Shared baseline for all advisors.");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("distinguishes omitted tools, explicit no-tools, and invalid-only lists", async () => {
|
|
60
|
-
const yaml = [
|
|
61
|
-
"advisors:",
|
|
62
|
-
" - name: No Tools",
|
|
63
|
-
" tools: []",
|
|
64
|
-
" - name: Default Tools",
|
|
65
|
-
" - name: Invalid Only",
|
|
66
|
-
" tools: [reed]",
|
|
67
|
-
].join("\n");
|
|
68
|
-
await Bun.write(path.join(tmp, "WATCHDOG.yml"), yaml);
|
|
69
|
-
|
|
70
|
-
const { advisors } = await discoverAdvisorConfigs(tmp, agentDir);
|
|
71
|
-
const noTools = advisors.find(a => a.name === "No Tools");
|
|
72
|
-
const defaultTools = advisors.find(a => a.name === "Default Tools");
|
|
73
|
-
const invalidOnly = advisors.find(a => a.name === "Invalid Only");
|
|
74
|
-
|
|
75
|
-
expect(noTools?.tools).toEqual([]);
|
|
76
|
-
expect(defaultTools?.tools).toBeUndefined();
|
|
77
|
-
expect(invalidOnly?.tools).toBeUndefined();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("ignores a malformed YAML file without throwing", async () => {
|
|
81
|
-
await Bun.write(path.join(tmp, "WATCHDOG.yml"), "advisors: [unclosed bracket");
|
|
82
|
-
const result = await discoverAdvisorConfigs(tmp, agentDir);
|
|
83
|
-
expect(result.advisors).toEqual([]);
|
|
84
|
-
expect(result.sharedInstructions).toBeUndefined();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("skips a file whose shape fails the schema (advisors must be a list)", async () => {
|
|
88
|
-
await Bun.write(path.join(tmp, "WATCHDOG.yml"), "advisors: not-an-array");
|
|
89
|
-
const result = await discoverAdvisorConfigs(tmp, agentDir);
|
|
90
|
-
expect(result.advisors).toEqual([]);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("returns an empty roster when no config file exists", async () => {
|
|
94
|
-
const result = await discoverAdvisorConfigs(tmp, agentDir);
|
|
95
|
-
expect(result.advisors).toEqual([]);
|
|
96
|
-
expect(result.sharedInstructions).toBeUndefined();
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe("slugifyAdvisorName", () => {
|
|
101
|
-
it("lowercases and collapses non-alphanumeric runs to single hyphens", () => {
|
|
102
|
-
expect(slugifyAdvisorName("Security Reviewer")).toBe("security-reviewer");
|
|
103
|
-
expect(slugifyAdvisorName(" Arch/Boundaries! ")).toBe("arch-boundaries");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("falls back to 'advisor' when nothing alphanumeric survives", () => {
|
|
107
|
-
expect(slugifyAdvisorName("!!!")).toBe("advisor");
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe("getOrCreateAdvisorProviderSessionId", () => {
|
|
112
|
-
const primarySessionA = "018f8f5d-75b0-7cc6-8a6f-2f1c0b8e4c9d";
|
|
113
|
-
const primarySessionB = "018f8f5d-75b1-7cc6-8a6f-2f1c0b8e4c9d";
|
|
114
|
-
|
|
115
|
-
it("returns the generated UUIDv7 instead of a local advisor label", () => {
|
|
116
|
-
const generated = "0193c8f2-7b1a-7c4d-9e2f-123456789abc";
|
|
117
|
-
|
|
118
|
-
const providerSessionId = getOrCreateAdvisorProviderSessionId(
|
|
119
|
-
new Map<string, string>(),
|
|
120
|
-
primarySessionA,
|
|
121
|
-
"security-advisor",
|
|
122
|
-
() => generated,
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
expect(providerSessionId).toBe(generated);
|
|
126
|
-
expect(providerSessionId).not.toContain("-advisor");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("reuses the same generated UUIDv7 for repeated calls with the same primary session and slug", () => {
|
|
130
|
-
const generatedIds = ["0193c8f2-7b1a-7c4d-9e2f-123456789abc", "0193c8f2-7b1b-7c4d-9e2f-123456789abc"];
|
|
131
|
-
let nextGeneratedIdIndex = 0;
|
|
132
|
-
const ids = new Map<string, string>();
|
|
133
|
-
|
|
134
|
-
const first = getOrCreateAdvisorProviderSessionId(ids, primarySessionA, "architecture", () => {
|
|
135
|
-
const generated = generatedIds[nextGeneratedIdIndex];
|
|
136
|
-
if (!generated) throw new Error("unexpected generator call");
|
|
137
|
-
nextGeneratedIdIndex += 1;
|
|
138
|
-
return generated;
|
|
139
|
-
});
|
|
140
|
-
const second = getOrCreateAdvisorProviderSessionId(ids, primarySessionA, "architecture", () => {
|
|
141
|
-
const generated = generatedIds[nextGeneratedIdIndex];
|
|
142
|
-
if (!generated) throw new Error("unexpected generator call");
|
|
143
|
-
nextGeneratedIdIndex += 1;
|
|
144
|
-
return generated;
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
expect(first).toBe(generatedIds[0]);
|
|
148
|
-
expect(second).toBe(generatedIds[0]);
|
|
149
|
-
expect(nextGeneratedIdIndex).toBe(1);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("creates distinct UUIDv7 values for different advisor slugs or primary sessions", () => {
|
|
153
|
-
const generatedIds = [
|
|
154
|
-
"0193c8f2-7b1a-7c4d-9e2f-123456789abc",
|
|
155
|
-
"0193c8f2-7b1b-7c4d-9e2f-123456789abc",
|
|
156
|
-
"0193c8f2-7b1c-7c4d-9e2f-123456789abc",
|
|
157
|
-
];
|
|
158
|
-
let nextGeneratedIdIndex = 0;
|
|
159
|
-
const ids = new Map<string, string>();
|
|
160
|
-
const nextGeneratedId = () => {
|
|
161
|
-
const generated = generatedIds[nextGeneratedIdIndex];
|
|
162
|
-
if (!generated) throw new Error("unexpected generator call");
|
|
163
|
-
nextGeneratedIdIndex += 1;
|
|
164
|
-
return generated;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const architecture = getOrCreateAdvisorProviderSessionId(ids, primarySessionA, "architecture", nextGeneratedId);
|
|
168
|
-
const security = getOrCreateAdvisorProviderSessionId(ids, primarySessionA, "security", nextGeneratedId);
|
|
169
|
-
const architectureForOtherSession = getOrCreateAdvisorProviderSessionId(
|
|
170
|
-
ids,
|
|
171
|
-
primarySessionB,
|
|
172
|
-
"architecture",
|
|
173
|
-
nextGeneratedId,
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
expect(architecture).toBe(generatedIds[0]);
|
|
177
|
-
expect(security).toBe(generatedIds[1]);
|
|
178
|
-
expect(architectureForOtherSession).toBe(generatedIds[2]);
|
|
179
|
-
expect(new Set([architecture, security, architectureForOtherSession]).size).toBe(3);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("rejects generated values that are not UUIDv7", () => {
|
|
183
|
-
expect(() =>
|
|
184
|
-
getOrCreateAdvisorProviderSessionId(
|
|
185
|
-
new Map<string, string>(),
|
|
186
|
-
primarySessionA,
|
|
187
|
-
"architecture",
|
|
188
|
-
() => "550e8400-e29b-41d4-a716-446655440000",
|
|
189
|
-
),
|
|
190
|
-
).toThrow("non-UUIDv7");
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
describe("WATCHDOG.yml file round-trip", () => {
|
|
195
|
-
let tmp: string;
|
|
196
|
-
beforeEach(async () => {
|
|
197
|
-
tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-advisor-file-"));
|
|
198
|
-
});
|
|
199
|
-
afterEach(async () => {
|
|
200
|
-
await fsp.rm(tmp, { recursive: true, force: true });
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
const doc: WatchdogConfigDoc = {
|
|
204
|
-
instructions: 'Shared baseline.\n\nSecond line with: a colon and "quotes".',
|
|
205
|
-
advisors: [
|
|
206
|
-
{
|
|
207
|
-
name: "Architecture",
|
|
208
|
-
model: "x-ai/grok-code-fast:high",
|
|
209
|
-
instructions: "Watch module boundaries.\nReport coupling.",
|
|
210
|
-
},
|
|
211
|
-
{ name: "Security", tools: ["read", "grep"] },
|
|
212
|
-
],
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
it("saves and reloads a doc byte-equivalently (incl. multiline and special chars)", async () => {
|
|
216
|
-
const file = path.join(tmp, "WATCHDOG.yml");
|
|
217
|
-
await saveWatchdogConfigFile(file, doc);
|
|
218
|
-
const loaded = await loadWatchdogConfigFile(file);
|
|
219
|
-
expect(loaded).toEqual(doc);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("serializes block-style YAML that the discovery path also parses", async () => {
|
|
223
|
-
const file = path.join(tmp, "WATCHDOG.yml");
|
|
224
|
-
await saveWatchdogConfigFile(file, doc);
|
|
225
|
-
const text = await Bun.file(file).text();
|
|
226
|
-
// Block style (not the flow `{...}` form), so it stays hand-editable.
|
|
227
|
-
expect(text).toContain("advisors:");
|
|
228
|
-
expect(text).not.toMatch(/^\{/);
|
|
229
|
-
expect(text).toContain('instructions: |2-\n Shared baseline.\n \n Second line with: a colon and "quotes".');
|
|
230
|
-
expect(text).toContain(" instructions: |2-\n Watch module boundaries.\n Report coupling.");
|
|
231
|
-
expect(text).not.toContain("\\n");
|
|
232
|
-
const { advisors, sharedInstructions } = await discoverAdvisorConfigs(tmp, tmp);
|
|
233
|
-
expect(advisors.map(a => a.name)).toEqual(["Architecture", "Security"]);
|
|
234
|
-
expect(sharedInstructions).toContain("Shared baseline.");
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("preserves significant leading whitespace and trailing newlines in block scalars", async () => {
|
|
238
|
-
const file = path.join(tmp, "WATCHDOG.yml");
|
|
239
|
-
const whitespaceDoc: WatchdogConfigDoc = {
|
|
240
|
-
instructions: " indented first line\nplain second line\n\n",
|
|
241
|
-
advisors: [{ name: "Whitespace", instructions: "\n indented after blank\nplain" }],
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
await saveWatchdogConfigFile(file, whitespaceDoc);
|
|
245
|
-
expect(await loadWatchdogConfigFile(file)).toEqual(whitespaceDoc);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("round-trips an explicit empty tools list without collapsing it into the default", async () => {
|
|
249
|
-
const file = path.join(tmp, "WATCHDOG.yml");
|
|
250
|
-
const explicitNoToolsDoc: WatchdogConfigDoc = {
|
|
251
|
-
advisors: [{ name: "No Tools", tools: [] }, { name: "Default Tools" }],
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
await saveWatchdogConfigFile(file, explicitNoToolsDoc);
|
|
255
|
-
const serializedDoc = await loadWatchdogConfigFile(file);
|
|
256
|
-
expect(serializedDoc).toEqual(explicitNoToolsDoc);
|
|
257
|
-
|
|
258
|
-
const { advisors } = await discoverAdvisorConfigs(tmp, tmp);
|
|
259
|
-
expect(advisors.find(a => a.name === "No Tools")?.tools).toEqual([]);
|
|
260
|
-
expect(advisors.find(a => a.name === "Default Tools")?.tools).toBeUndefined();
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("removes the file when the doc is empty so legacy discovery resumes", async () => {
|
|
264
|
-
const file = path.join(tmp, "WATCHDOG.yml");
|
|
265
|
-
await saveWatchdogConfigFile(file, doc);
|
|
266
|
-
await saveWatchdogConfigFile(file, { advisors: [] });
|
|
267
|
-
expect(await Bun.file(file).exists()).toBe(false);
|
|
268
|
-
// Loading a missing file yields an empty doc, never throws.
|
|
269
|
-
expect(await loadWatchdogConfigFile(file)).toEqual({ advisors: [] });
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("returns an empty serialization for an empty doc", () => {
|
|
273
|
-
expect(serializeWatchdogConfig({ advisors: [] })).toBe("");
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("resolves project and user scope paths", () => {
|
|
277
|
-
expect(advisorConfigFilePath("project", { projectDir: "/repo", agentDir: "/home/.omp" })).toBe(
|
|
278
|
-
path.join("/repo", "WATCHDOG.yml"),
|
|
279
|
-
);
|
|
280
|
-
expect(advisorConfigFilePath("user", { projectDir: "/repo", agentDir: "/home/.omp" })).toBe(
|
|
281
|
-
path.join("/home/.omp", "WATCHDOG.yml"),
|
|
282
|
-
);
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
describe("resolveAdvisorConfigEditPath", () => {
|
|
287
|
-
let tmp: string;
|
|
288
|
-
beforeEach(async () => {
|
|
289
|
-
tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-advisor-resolve-"));
|
|
290
|
-
});
|
|
291
|
-
afterEach(async () => {
|
|
292
|
-
await fsp.rm(tmp, { recursive: true, force: true });
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
const dirs = (d: string) => ({ projectDir: d, agentDir: d });
|
|
296
|
-
|
|
297
|
-
it("defaults to .yml when neither file exists", async () => {
|
|
298
|
-
expect(await resolveAdvisorConfigEditPath("project", dirs(tmp))).toBe(path.join(tmp, "WATCHDOG.yml"));
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it("edits an existing .yaml in place when only it exists", async () => {
|
|
302
|
-
await Bun.write(path.join(tmp, "WATCHDOG.yaml"), "advisors: []\n");
|
|
303
|
-
expect(await resolveAdvisorConfigEditPath("project", dirs(tmp))).toBe(path.join(tmp, "WATCHDOG.yaml"));
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it("prefers the canonical .yml when both exist", async () => {
|
|
307
|
-
await Bun.write(path.join(tmp, "WATCHDOG.yml"), "advisors: []\n");
|
|
308
|
-
await Bun.write(path.join(tmp, "WATCHDOG.yaml"), "advisors: []\n");
|
|
309
|
-
expect(await resolveAdvisorConfigEditPath("project", dirs(tmp))).toBe(path.join(tmp, "WATCHDOG.yml"));
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
describe("per-advisor enabled field", () => {
|
|
314
|
-
it("preserves explicit true, explicit false, and absence through save and discovery", async () => {
|
|
315
|
-
const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-advisor-enabled-"));
|
|
316
|
-
try {
|
|
317
|
-
const doc: WatchdogConfigDoc = {
|
|
318
|
-
advisors: [
|
|
319
|
-
{ name: "Explicit On", model: "test/model-a", enabled: true },
|
|
320
|
-
{ name: "Explicit Off", model: "test/model-b", enabled: false },
|
|
321
|
-
{ name: "Default", model: "test/model-c" },
|
|
322
|
-
],
|
|
323
|
-
};
|
|
324
|
-
const file = path.join(tmp, "WATCHDOG.yml");
|
|
325
|
-
await saveWatchdogConfigFile(file, doc);
|
|
326
|
-
|
|
327
|
-
const loaded = await loadWatchdogConfigFile(file);
|
|
328
|
-
expect(loaded.advisors.map(advisor => advisor.enabled)).toEqual([true, false, undefined]);
|
|
329
|
-
|
|
330
|
-
const { advisors } = await discoverAdvisorConfigs(tmp, tmp);
|
|
331
|
-
expect(advisors.map(advisor => advisor.enabled)).toEqual([true, false, undefined]);
|
|
332
|
-
} finally {
|
|
333
|
-
await fsp.rm(tmp, { recursive: true, force: true });
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it("emits explicit boolean values but omits an absent enabled field", () => {
|
|
338
|
-
const text = serializeWatchdogConfig({
|
|
339
|
-
advisors: [
|
|
340
|
-
{ name: "Explicit On", enabled: true },
|
|
341
|
-
{ name: "Explicit Off", enabled: false },
|
|
342
|
-
{ name: "Default" },
|
|
343
|
-
],
|
|
344
|
-
});
|
|
345
|
-
expect(text).toContain("enabled: true");
|
|
346
|
-
expect(text).toContain("enabled: false");
|
|
347
|
-
expect(text.match(/enabled:/g)).toHaveLength(2);
|
|
348
|
-
});
|
|
349
|
-
});
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { AdvisorEmissionGuard, normalizeAdvisorNote } from "../emission-guard";
|
|
3
|
-
|
|
4
|
-
describe("normalizeAdvisorNote", () => {
|
|
5
|
-
it("collapses punctuation, casing, and surrounding whitespace into one canonical key", () => {
|
|
6
|
-
// The reporter's three top duplicates all key to the same canonical form
|
|
7
|
-
// regardless of trailing punctuation or casing — that's what makes the
|
|
8
|
-
// dedupe + suppression checks single-membership.
|
|
9
|
-
expect(normalizeAdvisorNote("Stop.")).toBe("stop");
|
|
10
|
-
expect(normalizeAdvisorNote(" STOP! ")).toBe("stop");
|
|
11
|
-
expect(normalizeAdvisorNote("*Stop*")).toBe("stop");
|
|
12
|
-
expect(normalizeAdvisorNote("Done.")).toBe("done");
|
|
13
|
-
expect(normalizeAdvisorNote("No issue; continue.")).toBe("no issue continue");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("returns empty string for whitespace-only input so callers can short-circuit", () => {
|
|
17
|
-
expect(normalizeAdvisorNote("")).toBe("");
|
|
18
|
-
expect(normalizeAdvisorNote(" ")).toBe("");
|
|
19
|
-
expect(normalizeAdvisorNote("...")).toBe("");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("preserves internal letters/digits but folds non-alphanumeric runs to one space", () => {
|
|
23
|
-
expect(normalizeAdvisorNote("Refactor `auth-flow.ts`: drop legacy branch.")).toBe(
|
|
24
|
-
"refactor auth flow ts drop legacy branch",
|
|
25
|
-
);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("AdvisorEmissionGuard", () => {
|
|
30
|
-
it("drops the exact content-free filler the reporter observed flooding the chat", () => {
|
|
31
|
-
// Issue #3520: 114× "Stop.", 52× "No issue; continue.", 41× "Done." —
|
|
32
|
-
// none of these carry a concrete reason and they cannot be acted on, so
|
|
33
|
-
// the guard suppresses them regardless of severity.
|
|
34
|
-
const guard = new AdvisorEmissionGuard();
|
|
35
|
-
expect(guard.accept("Stop.")).toBe(false);
|
|
36
|
-
expect(guard.accept("Done.")).toBe(false);
|
|
37
|
-
expect(guard.accept("No issue; continue.")).toBe(false);
|
|
38
|
-
expect(guard.accept("LGTM")).toBe(false);
|
|
39
|
-
expect(guard.accept("No further watcher input needed.")).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("dedupes by normalized text across the session, ignoring casing and trailing punctuation", () => {
|
|
43
|
-
const guard = new AdvisorEmissionGuard();
|
|
44
|
-
expect(guard.accept("Move retries into the queue, not the request path.")).toBe(true);
|
|
45
|
-
// Same advice with different casing and trailing punctuation must NOT
|
|
46
|
-
// land twice in the primary transcript.
|
|
47
|
-
expect(guard.accept("move retries into the queue, not the request path")).toBe(false);
|
|
48
|
-
expect(guard.accept("Move retries into the queue, not the request path!")).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("rate-limits to one accepted advise per advisor update cycle", () => {
|
|
52
|
-
// The advisor system prompt says "at most one `advise` per update". Real
|
|
53
|
-
// models violate this; the guard enforces it at the boundary so the
|
|
54
|
-
// primary transcript never receives two advisories from one model cycle.
|
|
55
|
-
const guard = new AdvisorEmissionGuard();
|
|
56
|
-
expect(guard.accept("First concern: missing await in #handleRetry.")).toBe(true);
|
|
57
|
-
expect(guard.accept("Second concern: wrong env var name.")).toBe(false);
|
|
58
|
-
guard.beginUpdate();
|
|
59
|
-
// New cycle: budget reset.
|
|
60
|
-
expect(guard.accept("Second concern: wrong env var name.")).toBe(true);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("does not let a suppressed call consume the per-update budget", () => {
|
|
64
|
-
// A noise call like "Stop." must never displace a real concern that
|
|
65
|
-
// follows in the same advisor model cycle.
|
|
66
|
-
const guard = new AdvisorEmissionGuard();
|
|
67
|
-
expect(guard.accept("Stop.")).toBe(false);
|
|
68
|
-
expect(guard.accept("Concrete: read race in #handleRetry.")).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("does not let a deduped call consume the per-update budget", () => {
|
|
72
|
-
// A repeat of a prior session note is dropped, but the model can still
|
|
73
|
-
// follow it with a fresh concrete concern in the same cycle.
|
|
74
|
-
const guard = new AdvisorEmissionGuard();
|
|
75
|
-
expect(guard.accept("Concrete: read race in #handleRetry.")).toBe(true);
|
|
76
|
-
guard.beginUpdate();
|
|
77
|
-
expect(guard.accept("Concrete: read race in #handleRetry.")).toBe(false);
|
|
78
|
-
expect(guard.accept("New concern: cache eviction never fires.")).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("reset clears dedupe and the per-update gate so a re-primed advisor can re-raise old issues", () => {
|
|
82
|
-
// Compaction / session-switch rewrites the primary transcript. The
|
|
83
|
-
// advisor is re-primed from scratch and may legitimately re-raise the
|
|
84
|
-
// same concerns — they're new context for a freshly-primed reviewer.
|
|
85
|
-
const guard = new AdvisorEmissionGuard();
|
|
86
|
-
expect(guard.accept("Race in #handleRetry.")).toBe(true);
|
|
87
|
-
expect(guard.accept("Race in #handleRetry.")).toBe(false);
|
|
88
|
-
guard.reset();
|
|
89
|
-
expect(guard.accept("Race in #handleRetry.")).toBe(true);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("evicts oldest entries when dedupe history exceeds capacity", () => {
|
|
93
|
-
// Bounded so very long sessions cannot grow the dedupe state without
|
|
94
|
-
// bound. Pre-eviction unique notes are remembered; post-eviction the
|
|
95
|
-
// oldest one is forgotten and can resurface.
|
|
96
|
-
const guard = new AdvisorEmissionGuard({ capacity: 3 });
|
|
97
|
-
expect(guard.accept("first")).toBe(true);
|
|
98
|
-
guard.beginUpdate();
|
|
99
|
-
expect(guard.accept("second")).toBe(true);
|
|
100
|
-
guard.beginUpdate();
|
|
101
|
-
expect(guard.accept("third")).toBe(true);
|
|
102
|
-
guard.beginUpdate();
|
|
103
|
-
// "first" still in history.
|
|
104
|
-
expect(guard.accept("first")).toBe(false);
|
|
105
|
-
guard.beginUpdate();
|
|
106
|
-
// Fourth unique entry evicts "first".
|
|
107
|
-
expect(guard.accept("fourth")).toBe(true);
|
|
108
|
-
guard.beginUpdate();
|
|
109
|
-
expect(guard.accept("first")).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("rejects empty / whitespace-only notes without consuming the budget", () => {
|
|
113
|
-
const guard = new AdvisorEmissionGuard();
|
|
114
|
-
expect(guard.accept("")).toBe(false);
|
|
115
|
-
expect(guard.accept(" ")).toBe(false);
|
|
116
|
-
expect(guard.accept("Concrete advice.")).toBe(true);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("end-to-end: the reporter's 309-call spam log produces ≤1 accepted note across many updates", () => {
|
|
120
|
-
// Mimic the issue's distribution: 114× "Stop.", 52× "No issue; continue.",
|
|
121
|
-
// 41× "Done.", plus 102 copies of one concrete-but-repeated nit. Spread
|
|
122
|
-
// the calls across 50 advisor update cycles. Each cycle is allowed at
|
|
123
|
-
// most one accepted note, and identical-text repeats never escape the
|
|
124
|
-
// guard. After all calls, exactly the concrete nit has been accepted
|
|
125
|
-
// — and only once.
|
|
126
|
-
const guard = new AdvisorEmissionGuard();
|
|
127
|
-
const accepted: string[] = [];
|
|
128
|
-
const stream: string[] = [
|
|
129
|
-
...Array(114).fill("Stop."),
|
|
130
|
-
...Array(52).fill("No issue; continue."),
|
|
131
|
-
...Array(41).fill("Done."),
|
|
132
|
-
...Array(102).fill("Concrete-but-repeated nit: x"),
|
|
133
|
-
];
|
|
134
|
-
// Interleave across 50 update cycles.
|
|
135
|
-
const cycles = 50;
|
|
136
|
-
const perCycle = Math.ceil(stream.length / cycles);
|
|
137
|
-
for (let c = 0; c < cycles; c++) {
|
|
138
|
-
guard.beginUpdate();
|
|
139
|
-
for (let i = 0; i < perCycle; i++) {
|
|
140
|
-
const note = stream[c * perCycle + i];
|
|
141
|
-
if (note === undefined) break;
|
|
142
|
-
if (guard.accept(note)) accepted.push(note);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
expect(accepted).toEqual(["Concrete-but-repeated nit: x"]);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
3
|
-
import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
|
|
4
|
-
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import { ModelRegistry } from "../../config/model-registry";
|
|
6
|
-
import { indexModelsByRequestId } from "../auth-gateway-cli";
|
|
7
|
-
|
|
8
|
-
function stubAuthStorage(configKeys?: string[]): AuthStorage {
|
|
9
|
-
const stub = {
|
|
10
|
-
setFallbackResolver: () => {},
|
|
11
|
-
clearConfigApiKeys: () => {},
|
|
12
|
-
setConfigApiKey: (provider: string) => configKeys?.push(provider),
|
|
13
|
-
removeConfigApiKey: () => {},
|
|
14
|
-
hasAuth: () => true,
|
|
15
|
-
getAll: () => ({ anthropic: {} }),
|
|
16
|
-
};
|
|
17
|
-
return stub as unknown as AuthStorage;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe("indexModelsByRequestId (auth-gateway catalog)", () => {
|
|
21
|
-
test("resolves a discovery-only model absent from the bundled catalog", () => {
|
|
22
|
-
const registry = new ModelRegistry(stubAuthStorage());
|
|
23
|
-
// Simulate a model reached via provider discovery but not compiled into
|
|
24
|
-
// the bundle (e.g. a post-release id). registerProvider merges it into
|
|
25
|
-
// getAll() exactly as runtime discovery does.
|
|
26
|
-
registry.registerProvider("anthropic", {
|
|
27
|
-
baseUrl: "https://api.anthropic.com",
|
|
28
|
-
api: "anthropic-messages",
|
|
29
|
-
apiKey: "test-key",
|
|
30
|
-
models: [
|
|
31
|
-
{
|
|
32
|
-
id: "claude-opus-5-repro",
|
|
33
|
-
name: "Claude Opus 5 (repro)",
|
|
34
|
-
reasoning: false,
|
|
35
|
-
input: ["text"],
|
|
36
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
37
|
-
contextWindow: 200000,
|
|
38
|
-
maxTokens: 8192,
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
});
|
|
42
|
-
expect(getBundledModels("anthropic").map(m => m.id)).not.toContain("claude-opus-5-repro");
|
|
43
|
-
|
|
44
|
-
const index = indexModelsByRequestId(registry.getAll(), new Set(["anthropic"]));
|
|
45
|
-
|
|
46
|
-
// The gateway can now resolve it by qualified id and bare id — was
|
|
47
|
-
// "Unknown model" when the index was built from getBundledModels only.
|
|
48
|
-
expect(index.get("anthropic/claude-opus-5-repro")?.id).toBe("claude-opus-5-repro");
|
|
49
|
-
expect(index.get("claude-opus-5-repro")?.id).toBe("claude-opus-5-repro");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("gateway registry ignores local models.yml credential and routing overrides", async () => {
|
|
53
|
-
using tempDir = TempDir.createSync("@omp-auth-gateway-catalog-");
|
|
54
|
-
const modelsPath = tempDir.join("models.yml");
|
|
55
|
-
// anthropic: a plain credential/baseUrl override (no transport) — the
|
|
56
|
-
// reviewer's leak. openai: a pi-native gateway route — the self-routing loop.
|
|
57
|
-
await Bun.write(
|
|
58
|
-
modelsPath,
|
|
59
|
-
[
|
|
60
|
-
"providers:",
|
|
61
|
-
" anthropic:",
|
|
62
|
-
" baseUrl: http://127.0.0.1:18899",
|
|
63
|
-
" apiKey: gateway-token",
|
|
64
|
-
" openai:",
|
|
65
|
-
" baseUrl: http://127.0.0.1:18899",
|
|
66
|
-
" apiKey: gateway-token",
|
|
67
|
-
" transport: pi-native",
|
|
68
|
-
"",
|
|
69
|
-
].join("\n"),
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// A normal client registry applies the local overrides and installs the
|
|
73
|
-
// config API keys into AuthStorage.
|
|
74
|
-
const clientKeys: string[] = [];
|
|
75
|
-
const clientRegistry = new ModelRegistry(stubAuthStorage(clientKeys), modelsPath);
|
|
76
|
-
expect(clientRegistry.find("anthropic", "claude-sonnet-4-5")?.baseUrl).toBe("http://127.0.0.1:18899");
|
|
77
|
-
expect(clientRegistry.getAll().find(model => model.provider === "openai")?.transport).toBe("pi-native");
|
|
78
|
-
expect(clientKeys).toContain("anthropic");
|
|
79
|
-
|
|
80
|
-
// The gateway registry ignores models.yml entirely: bundled routing wins,
|
|
81
|
-
// no config key reaches AuthStorage, and no pi-native self-route survives.
|
|
82
|
-
const gatewayKeys: string[] = [];
|
|
83
|
-
const gatewayRegistry = new ModelRegistry(stubAuthStorage(gatewayKeys), modelsPath, {
|
|
84
|
-
ignoreLocalModelConfig: true,
|
|
85
|
-
});
|
|
86
|
-
const gatewayModel = gatewayRegistry.find("anthropic", "claude-sonnet-4-5");
|
|
87
|
-
const bundledModel = getBundledModels("anthropic").find(model => model.id === "claude-sonnet-4-5");
|
|
88
|
-
if (!gatewayModel || !bundledModel) throw new Error("expected bundled Anthropic model");
|
|
89
|
-
|
|
90
|
-
expect(gatewayModel.baseUrl).toBe(bundledModel.baseUrl);
|
|
91
|
-
expect(gatewayModel.transport).toBeUndefined();
|
|
92
|
-
expect(gatewayKeys).toHaveLength(0);
|
|
93
|
-
expect(gatewayRegistry.getAll().find(model => model.provider === "openai")?.transport).toBeUndefined();
|
|
94
|
-
expect(indexModelsByRequestId(gatewayRegistry.getAll(), new Set(["anthropic"])).get(gatewayModel.id)).toBe(
|
|
95
|
-
gatewayModel,
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("scopes the catalog to providers with credentials", () => {
|
|
100
|
-
const registry = new ModelRegistry(stubAuthStorage());
|
|
101
|
-
const all = registry.getAll();
|
|
102
|
-
const anthropicModel = all.find(m => m.provider === "anthropic");
|
|
103
|
-
const foreignModel = all.find(m => m.provider !== "anthropic");
|
|
104
|
-
if (!anthropicModel || !foreignModel) throw new Error("expected mixed-provider bundled catalog");
|
|
105
|
-
|
|
106
|
-
const index = indexModelsByRequestId(all, new Set(["anthropic"]));
|
|
107
|
-
|
|
108
|
-
expect(index.get(`anthropic/${anthropicModel.id}`)).toBeDefined();
|
|
109
|
-
expect(index.get(`${foreignModel.provider}/${foreignModel.id}`)).toBeUndefined();
|
|
110
|
-
});
|
|
111
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
-
import { runUpdateCommand } from "./update-cli";
|
|
3
|
-
|
|
4
|
-
type FetchInput = string | URL | Request;
|
|
5
|
-
type FetchInit = RequestInit | BunFetchRequestInit;
|
|
6
|
-
|
|
7
|
-
describe("runUpdateCommand fetch cancellation", () => {
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
vi.restoreAllMocks();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("checks release metadata with a timeout signal", async () => {
|
|
13
|
-
let requestSignal: AbortSignal | undefined;
|
|
14
|
-
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
15
|
-
const fetchStub = Object.assign(
|
|
16
|
-
async (_input: FetchInput, init?: FetchInit) => {
|
|
17
|
-
requestSignal = init?.signal ?? undefined;
|
|
18
|
-
return Response.json({ version: "999.0.0" });
|
|
19
|
-
},
|
|
20
|
-
{ preconnect: globalThis.fetch.preconnect },
|
|
21
|
-
);
|
|
22
|
-
vi.spyOn(globalThis, "fetch").mockImplementation(fetchStub);
|
|
23
|
-
|
|
24
|
-
await runUpdateCommand({ force: false, check: true });
|
|
25
|
-
|
|
26
|
-
expect(requestSignal).toBeInstanceOf(AbortSignal);
|
|
27
|
-
});
|
|
28
|
-
});
|