@stigmer/runner 3.9.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/call-llm.js +9 -10
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +2 -1
- package/dist/activities/classify-tool-approvals.js +28 -2
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +32 -0
- package/dist/activities/discover-mcp-server.js +162 -27
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
- package/dist/activities/execute-cursor/approval-state.js +7 -1
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +25 -4
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
- package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/env-resolver.js +3 -1
- package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +40 -3
- package/dist/activities/execute-cursor/error-classifier.js +81 -3
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
- package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
- package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
- package/dist/activities/execute-cursor/hook-script.js +72 -10
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +5 -1
- package/dist/activities/execute-cursor/index.js +59 -58
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
- package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
- package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +18 -4
- package/dist/activities/execute-cursor/prompt-builder.js +12 -7
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -1
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/environment.js +3 -1
- package/dist/activities/execute-deep-agent/environment.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +15 -0
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
- package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
- package/dist/activities/execute-deep-agent/setup.js +65 -26
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +8 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +11 -4
- package/dist/activities/execute-deep-agent/subagent-wiring.js +13 -4
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +3 -1
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/workflow-event-activities.d.ts +28 -10
- package/dist/activities/workflow-event-activities.js +87 -58
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +21 -1
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +9 -4
- package/dist/client/stigmer-client.js +28 -15
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/encryption/config.d.ts +32 -0
- package/dist/encryption/config.js +68 -0
- package/dist/encryption/config.js.map +1 -0
- package/dist/encryption/index.d.ts +3 -0
- package/dist/encryption/index.js +3 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/encryption/payload-codec.d.ts +41 -0
- package/dist/encryption/payload-codec.js +130 -0
- package/dist/encryption/payload-codec.js.map +1 -0
- package/dist/payload-codecs.d.ts +16 -0
- package/dist/payload-codecs.js +38 -0
- package/dist/payload-codecs.js.map +1 -0
- package/dist/preflight.d.ts +31 -0
- package/dist/preflight.js +43 -0
- package/dist/preflight.js.map +1 -1
- package/dist/runner-manager.js +5 -15
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +5 -16
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +9 -3
- package/dist/shared/approval-policy.js +15 -6
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/attachment-naming.d.ts +53 -0
- package/dist/shared/attachment-naming.js +59 -0
- package/dist/shared/attachment-naming.js.map +1 -0
- package/dist/shared/attachment-vision.d.ts +43 -2
- package/dist/shared/attachment-vision.js +72 -6
- package/dist/shared/attachment-vision.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +23 -2
- package/dist/shared/caller-identity.js +36 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +1 -0
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +42 -1
- package/dist/shared/checkpointer/http-saver.js +96 -8
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/conversation-attachment.js +1 -0
- package/dist/shared/conversation-attachment.js.map +1 -1
- package/dist/shared/datastore-attachment.d.ts +50 -7
- package/dist/shared/datastore-attachment.js +93 -11
- package/dist/shared/datastore-attachment.js.map +1 -1
- package/dist/shared/http-retry.d.ts +43 -0
- package/dist/shared/http-retry.js +50 -0
- package/dist/shared/http-retry.js.map +1 -0
- package/dist/shared/llm-backend.d.ts +275 -0
- package/dist/shared/llm-backend.js +425 -0
- package/dist/shared/llm-backend.js.map +1 -0
- package/dist/shared/llm-proxy.d.ts +8 -0
- package/dist/shared/llm-proxy.js +15 -0
- package/dist/shared/llm-proxy.js.map +1 -1
- package/dist/shared/mcp-enabled-tools.d.ts +57 -0
- package/dist/shared/mcp-enabled-tools.js +86 -0
- package/dist/shared/mcp-enabled-tools.js.map +1 -0
- package/dist/shared/mcp-manager.d.ts +17 -2
- package/dist/shared/mcp-manager.js +37 -25
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +39 -2
- package/dist/shared/mcp-resolver.js +38 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/model-client.d.ts +12 -5
- package/dist/shared/model-client.js +138 -18
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/model-error.js +198 -5
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/model-registry.d.ts +20 -2
- package/dist/shared/model-registry.js +37 -2
- package/dist/shared/model-registry.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +26 -0
- package/dist/shared/plan-mode-permissions.js +28 -0
- package/dist/shared/plan-mode-permissions.js.map +1 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +2 -4
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +18 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
- package/dist/workflows/call-agent-orchestrator.js +1 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.js +7 -0
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/engine-core.js +23 -2
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +1 -1
- package/dist/workflows/execute-from-execution.js +11 -1
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/package.json +8 -2
- package/src/__tests__/claimcheck-codec.test.ts +36 -0
- package/src/__tests__/encryption-codec.test.ts +234 -0
- package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
- package/src/__tests__/history-encryption-e2e.test.ts +243 -0
- package/src/__tests__/preflight.test.ts +50 -2
- package/src/activities/__tests__/call-llm.test.ts +75 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +203 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
- package/src/activities/call-llm.ts +9 -16
- package/src/activities/classify-tool-approvals.ts +34 -4
- package/src/activities/discover-mcp-server.ts +190 -32
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +92 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -14
- package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
- package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
- package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
- package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
- package/src/activities/execute-cursor/approval-state.ts +30 -1
- package/src/activities/execute-cursor/attachment-resolver.ts +38 -3
- package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
- package/src/activities/execute-cursor/env-resolver.ts +3 -1
- package/src/activities/execute-cursor/error-classifier.ts +91 -4
- package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
- package/src/activities/execute-cursor/hook-script.ts +74 -10
- package/src/activities/execute-cursor/index.ts +63 -72
- package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
- package/src/activities/execute-cursor/prompt-builder.ts +34 -9
- package/src/activities/execute-cursor/turn-stream.ts +5 -2
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +132 -8
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +173 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
- package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
- package/src/activities/execute-deep-agent/environment.ts +3 -1
- package/src/activities/execute-deep-agent/index.ts +20 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
- package/src/activities/execute-deep-agent/setup.ts +86 -31
- package/src/activities/execute-deep-agent/subagent-transformer.ts +23 -1
- package/src/activities/execute-deep-agent/subagent-wiring.ts +14 -4
- package/src/activities/hydrate-workflow-execution.ts +3 -1
- package/src/activities/workflow-event-activities.ts +96 -69
- package/src/claimcheck/payload-codec.ts +33 -1
- package/src/client/__tests__/stigmer-client.test.ts +8 -8
- package/src/client/stigmer-client.ts +32 -18
- package/src/encryption/config.ts +91 -0
- package/src/encryption/index.ts +3 -0
- package/src/encryption/payload-codec.ts +152 -0
- package/src/payload-codecs.ts +56 -0
- package/src/preflight.ts +45 -0
- package/src/runner-manager.ts +6 -24
- package/src/runner.ts +6 -25
- package/src/shared/__tests__/approval-policy.test.ts +82 -39
- package/src/shared/__tests__/attachment-naming.test.ts +159 -0
- package/src/shared/__tests__/attachment-vision.test.ts +97 -0
- package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
- package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
- package/src/shared/__tests__/caller-identity.test.ts +25 -0
- package/src/shared/__tests__/channel-attachment.test.ts +1 -1
- package/src/shared/__tests__/connect-backfill.test.ts +1 -0
- package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
- package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
- package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
- package/src/shared/__tests__/foundry-seam.test.ts +482 -0
- package/src/shared/__tests__/http-retry.test.ts +67 -0
- package/src/shared/__tests__/llm-backend.test.ts +616 -0
- package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
- package/src/shared/__tests__/mcp-manager.test.ts +170 -2
- package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
- package/src/shared/__tests__/model-client.test.ts +154 -0
- package/src/shared/__tests__/model-error.test.ts +289 -1
- package/src/shared/__tests__/model-registry.test.ts +71 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
- package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
- package/src/shared/__tests__/vertex-seam.test.ts +295 -0
- package/src/shared/approval-policy.ts +14 -7
- package/src/shared/attachment-naming.ts +78 -0
- package/src/shared/attachment-vision.ts +92 -9
- package/src/shared/caller-identity.ts +40 -5
- package/src/shared/channel-attachment.ts +1 -0
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
- package/src/shared/checkpointer/http-saver.ts +117 -9
- package/src/shared/conversation-attachment.ts +1 -0
- package/src/shared/datastore-attachment.ts +106 -11
- package/src/shared/http-retry.ts +50 -0
- package/src/shared/llm-backend.ts +544 -0
- package/src/shared/llm-proxy.ts +15 -0
- package/src/shared/mcp-enabled-tools.ts +105 -0
- package/src/shared/mcp-manager.ts +43 -26
- package/src/shared/mcp-resolver.ts +73 -2
- package/src/shared/model-client.ts +161 -19
- package/src/shared/model-error.ts +222 -4
- package/src/shared/model-registry.ts +50 -2
- package/src/shared/plan-mode-permissions.ts +30 -0
- package/src/worker.ts +4 -5
- package/src/workflow-engine/types.ts +18 -0
- package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
- package/src/workflows/call-agent-orchestrator.ts +10 -0
- package/src/workflows/connect-mcp-server.ts +7 -0
- package/src/workflows/engine-core.ts +23 -2
- package/src/workflows/execute-from-execution.ts +12 -2
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
effectiveEnabledTools,
|
|
4
|
+
filterToolsByEnabledTools,
|
|
5
|
+
enabledToolsBySlug,
|
|
6
|
+
} from "../mcp-enabled-tools.js";
|
|
7
|
+
|
|
8
|
+
describe("effectiveEnabledTools", () => {
|
|
9
|
+
it("uses the usage's non-empty enabled_tools verbatim", () => {
|
|
10
|
+
expect(effectiveEnabledTools(["a", "b"], ["c"])).toEqual(["a", "b"]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("falls back to default_enabled_tools when the usage list is empty", () => {
|
|
14
|
+
expect(effectiveEnabledTools([], ["c", "d"])).toEqual(["c", "d"]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("falls back to default_enabled_tools when the usage list is absent", () => {
|
|
18
|
+
expect(effectiveEnabledTools(undefined, ["c"])).toEqual(["c"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns undefined (unrestricted) when both lists are empty or absent", () => {
|
|
22
|
+
expect(effectiveEnabledTools([], [])).toBeUndefined();
|
|
23
|
+
expect(effectiveEnabledTools(undefined, undefined)).toBeUndefined();
|
|
24
|
+
expect(effectiveEnabledTools([], undefined)).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns copies, never the caller's arrays", () => {
|
|
28
|
+
const usage = ["a"];
|
|
29
|
+
const result = effectiveEnabledTools(usage, undefined)!;
|
|
30
|
+
result.push("b");
|
|
31
|
+
expect(usage).toEqual(["a"]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("filterToolsByEnabledTools", () => {
|
|
36
|
+
const tool = (name: string) => ({ name });
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
41
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("passes every tool through untouched when unrestricted (undefined)", () => {
|
|
45
|
+
const tools = [tool("a"), tool("b")];
|
|
46
|
+
expect(filterToolsByEnabledTools("srv", tools, undefined)).toEqual(tools);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("keeps only the tools on the allow-list (exact bare-name match)", () => {
|
|
50
|
+
const tools = [tool("read"), tool("write"), tool("delete")];
|
|
51
|
+
expect(
|
|
52
|
+
filterToolsByEnabledTools("srv", tools, ["read"]).map((t) => t.name),
|
|
53
|
+
).toEqual(["read"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("warns and intersects when an enabled name is not among the discovered tools", () => {
|
|
57
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
58
|
+
const tools = [tool("read"), tool("write")];
|
|
59
|
+
|
|
60
|
+
const filtered = filterToolsByEnabledTools("srv", tools, ["read", "reed"]);
|
|
61
|
+
|
|
62
|
+
// The restriction still holds (intersection), the run is not failed,
|
|
63
|
+
// and the operator gets a loud pointer at the mismatched name.
|
|
64
|
+
expect(filtered.map((t) => t.name)).toEqual(["read"]);
|
|
65
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("reed"));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("yields an empty toolset when no enabled name matches (restriction over availability)", () => {
|
|
69
|
+
const tools = [tool("read")];
|
|
70
|
+
expect(filterToolsByEnabledTools("srv", tools, ["ghost"])).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("enabledToolsBySlug", () => {
|
|
75
|
+
it("collects only restricted servers, keyed by slug", () => {
|
|
76
|
+
const map = enabledToolsBySlug([
|
|
77
|
+
{ slug: "restricted", enabledTools: ["a"] },
|
|
78
|
+
{ slug: "open" },
|
|
79
|
+
]);
|
|
80
|
+
expect(map).toEqual({ restricted: ["a"] });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns an empty object when nothing is restricted (the common case)", () => {
|
|
84
|
+
expect(enabledToolsBySlug([{ slug: "open" }])).toEqual({});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -1,18 +1,48 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
|
2
|
+
import type { Connection } from "@langchain/mcp-adapters";
|
|
3
|
+
import { connectMcpServers, toMcpClientConfig } from "../mcp-manager.js";
|
|
3
4
|
import type { ResolvedMcpServer } from "../mcp-resolver.js";
|
|
4
5
|
|
|
6
|
+
// The adapter client is mocked ONLY for the connectMcpServers tests below
|
|
7
|
+
// (initializeConnections returns the canned per-server tool map); the
|
|
8
|
+
// toMcpClientConfig suites are pure and never construct it.
|
|
9
|
+
const mockMcpClient = vi.hoisted(() => ({
|
|
10
|
+
toolMap: {} as Record<string, { name: string }[]>,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@langchain/mcp-adapters", () => ({
|
|
14
|
+
MultiServerMCPClient: class {
|
|
15
|
+
async initializeConnections() {
|
|
16
|
+
return mockMcpClient.toolMap;
|
|
17
|
+
}
|
|
18
|
+
async close() {}
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
5
22
|
function makeServer(overrides: Partial<ResolvedMcpServer>): ResolvedMcpServer {
|
|
6
23
|
return {
|
|
7
24
|
slug: "test-server",
|
|
8
25
|
connectionType: "stdio",
|
|
9
26
|
toolApprovals: [],
|
|
10
27
|
pinnedToolApprovals: [],
|
|
28
|
+
toolApprovalOverrides: [],
|
|
11
29
|
discoveredCapabilitiesEmpty: false,
|
|
12
30
|
...overrides,
|
|
13
31
|
};
|
|
14
32
|
}
|
|
15
33
|
|
|
34
|
+
/** Narrow a Connection to its stdio variant and return its env. */
|
|
35
|
+
function stdioEnv(
|
|
36
|
+
config: Record<string, Connection>,
|
|
37
|
+
slug: string,
|
|
38
|
+
): Record<string, string> | undefined {
|
|
39
|
+
const conn = config[slug];
|
|
40
|
+
if (!conn || !("command" in conn)) {
|
|
41
|
+
throw new Error(`expected a stdio connection for '${slug}'`);
|
|
42
|
+
}
|
|
43
|
+
return conn.env;
|
|
44
|
+
}
|
|
45
|
+
|
|
16
46
|
describe("toMcpClientConfig", () => {
|
|
17
47
|
it("maps stdio servers to the client config format", () => {
|
|
18
48
|
const servers = [
|
|
@@ -81,3 +111,141 @@ describe("toMcpClientConfig", () => {
|
|
|
81
111
|
expect(toMcpClientConfig([])).toEqual({});
|
|
82
112
|
});
|
|
83
113
|
});
|
|
114
|
+
|
|
115
|
+
// Runner-internal credentials that must never reach an MCP stdio subprocess
|
|
116
|
+
// (oss#256). Names mirror what the runner actually reads: config.ts,
|
|
117
|
+
// fingerprint-secret.ts, model-client.ts, registry-endpoint.ts.
|
|
118
|
+
const RUNNER_CREDENTIAL_KEYS = [
|
|
119
|
+
"STIGMER_RUNNER_HITL_SECRET",
|
|
120
|
+
"STIGMER_TOKEN",
|
|
121
|
+
"STIGMER_AUTH_TOKEN",
|
|
122
|
+
"CURSOR_API_KEY",
|
|
123
|
+
"ANTHROPIC_API_KEY",
|
|
124
|
+
"OPENAI_API_KEY",
|
|
125
|
+
] as const;
|
|
126
|
+
|
|
127
|
+
describe("toMcpClientConfig — stdio env isolation (oss#256)", () => {
|
|
128
|
+
afterEach(() => {
|
|
129
|
+
vi.unstubAllEnvs();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("gives a stdio server with no declared env none of the runner's variables", () => {
|
|
133
|
+
for (const key of RUNNER_CREDENTIAL_KEYS) {
|
|
134
|
+
vi.stubEnv(key, `leaked-${key}`);
|
|
135
|
+
}
|
|
136
|
+
vi.stubEnv("STIGMER_TEST_LEAK_SENTINEL", "canary");
|
|
137
|
+
|
|
138
|
+
const config = toMcpClientConfig([
|
|
139
|
+
makeServer({ slug: "bare", command: "some-mcp-server", env: undefined }),
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
// Passing no env means the MCP SDK supplies its minimal base environment
|
|
143
|
+
// (HOME, LOGNAME, PATH, SHELL, TERM, USER) — the runner must add nothing.
|
|
144
|
+
expect(stdioEnv(config, "bare")).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("never copies process.env into a stdio config (fallback-reintroduction tripwire)", () => {
|
|
148
|
+
vi.stubEnv("STIGMER_TEST_LEAK_SENTINEL", "canary");
|
|
149
|
+
|
|
150
|
+
const config = toMcpClientConfig([
|
|
151
|
+
makeServer({ slug: "bare", command: "some-mcp-server", env: undefined }),
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
const env = stdioEnv(config, "bare") ?? {};
|
|
155
|
+
for (const key of Object.keys(process.env)) {
|
|
156
|
+
expect(
|
|
157
|
+
env,
|
|
158
|
+
`process.env key '${key}' must not leak into an MCP stdio env`,
|
|
159
|
+
).not.toHaveProperty(key);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("passes a declared env through exactly, without merging process.env", () => {
|
|
164
|
+
vi.stubEnv("STIGMER_TOKEN", "runner-secret");
|
|
165
|
+
|
|
166
|
+
const config = toMcpClientConfig([
|
|
167
|
+
makeServer({
|
|
168
|
+
slug: "declared",
|
|
169
|
+
command: "some-mcp-server",
|
|
170
|
+
env: { API_KEY: "user-value" },
|
|
171
|
+
}),
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
expect(stdioEnv(config, "declared")).toEqual({ API_KEY: "user-value" });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("passes an empty declared env through unchanged (same child env as undeclared)", () => {
|
|
178
|
+
vi.stubEnv("STIGMER_TOKEN", "runner-secret");
|
|
179
|
+
|
|
180
|
+
const config = toMcpClientConfig([
|
|
181
|
+
makeServer({ slug: "empty", command: "some-mcp-server", env: {} }),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
expect(stdioEnv(config, "empty")).toEqual({});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("connectMcpServers — enabled_tools enforcement (issue #350)", () => {
|
|
189
|
+
const tool = (name: string) => ({ name }) as any;
|
|
190
|
+
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
193
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
194
|
+
mockMcpClient.toolMap = {};
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
afterEach(() => {
|
|
198
|
+
vi.restoreAllMocks();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("filters a restricted server's discovered tools down to the allow-list, in BOTH the flat list and the per-server map", async () => {
|
|
202
|
+
mockMcpClient.toolMap = { planton: [tool("get"), tool("apply"), tool("destroy")] };
|
|
203
|
+
|
|
204
|
+
const result = await connectMcpServers([
|
|
205
|
+
makeServer({ slug: "planton", command: "npx", enabledTools: ["get"] }),
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
// Both result shapes must narrow together: `tools` feeds the model and
|
|
209
|
+
// the built-in general-purpose sub-agent, `serverToolMap` feeds the
|
|
210
|
+
// approval gate's toolServerMap and the sub-agent McpAccess subset check.
|
|
211
|
+
expect(result.tools.map((t) => t.name)).toEqual(["get"]);
|
|
212
|
+
expect(result.serverToolMap.planton.map((t) => t.name)).toEqual(["get"]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("leaves an unrestricted server (enabledTools absent — e.g. a synthesized attachment) untouched", async () => {
|
|
216
|
+
mockMcpClient.toolMap = { open: [tool("a"), tool("b")] };
|
|
217
|
+
|
|
218
|
+
const result = await connectMcpServers([
|
|
219
|
+
makeServer({ slug: "open", command: "npx" }),
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
expect(result.tools.map((t) => t.name)).toEqual(["a", "b"]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("restricts per server: one restricted server never narrows its unrestricted sibling", async () => {
|
|
226
|
+
mockMcpClient.toolMap = {
|
|
227
|
+
restricted: [tool("get"), tool("apply")],
|
|
228
|
+
open: [tool("get"), tool("apply")],
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const result = await connectMcpServers([
|
|
232
|
+
makeServer({ slug: "restricted", command: "npx", enabledTools: ["get"] }),
|
|
233
|
+
makeServer({ slug: "open", command: "npx" }),
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
expect(result.serverToolMap.restricted.map((t) => t.name)).toEqual(["get"]);
|
|
237
|
+
expect(result.serverToolMap.open.map((t) => t.name)).toEqual(["get", "apply"]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("warns and intersects an allow-list naming a tool the server does not expose", async () => {
|
|
241
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
242
|
+
mockMcpClient.toolMap = { planton: [tool("get")] };
|
|
243
|
+
|
|
244
|
+
const result = await connectMcpServers([
|
|
245
|
+
makeServer({ slug: "planton", command: "npx", enabledTools: ["get", "ghost"] }),
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
expect(result.tools.map((t) => t.name)).toEqual(["get"]);
|
|
249
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("ghost"));
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { resolveMcpServers } from "../mcp-resolver.js";
|
|
2
|
+
import { mcpServerToResolved, mergeMcpServerUsages, resolveMcpServers } from "../mcp-resolver.js";
|
|
3
3
|
import { McpTransportError } from "../mcp-transport-guard.js";
|
|
4
4
|
|
|
5
|
-
function makeUsage(
|
|
5
|
+
function makeUsage(
|
|
6
|
+
slug: string,
|
|
7
|
+
org = "test-org",
|
|
8
|
+
enabledTools: string[] = [],
|
|
9
|
+
toolApprovalOverrides: Array<{ toolName: string; requiresApproval: boolean }> = [],
|
|
10
|
+
) {
|
|
6
11
|
return {
|
|
7
12
|
mcpServerRef: { slug, org, kind: 0 },
|
|
8
|
-
|
|
13
|
+
enabledTools,
|
|
14
|
+
toolApprovalOverrides,
|
|
9
15
|
} as any;
|
|
10
16
|
}
|
|
11
17
|
|
|
@@ -106,3 +112,140 @@ describe("resolveMcpServers — transport guard integration", () => {
|
|
|
106
112
|
expect(result.resolvedServers).toHaveLength(0);
|
|
107
113
|
});
|
|
108
114
|
});
|
|
115
|
+
|
|
116
|
+
describe("resolveMcpServers — enabled_tools threading (issue #350)", () => {
|
|
117
|
+
function withDefaults(server: any, defaultEnabledTools: string[]) {
|
|
118
|
+
server.spec.defaultEnabledTools = defaultEnabledTools;
|
|
119
|
+
return server;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
it("carries the usage's enabled_tools as the effective allow-list", async () => {
|
|
123
|
+
const client = clientReturning({ github: httpMcpServer("github") });
|
|
124
|
+
|
|
125
|
+
const result = await resolveMcpServers(
|
|
126
|
+
client, [makeUsage("github", "test-org", ["create_pr"])], {}, "stdio-allowed",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(result.resolvedServers[0].enabledTools).toEqual(["create_pr"]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("falls back to the server's default_enabled_tools for an empty usage list", async () => {
|
|
133
|
+
const client = clientReturning({
|
|
134
|
+
github: withDefaults(httpMcpServer("github"), ["search_code"]),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const result = await resolveMcpServers(
|
|
138
|
+
client, [makeUsage("github")], {}, "stdio-allowed",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(result.resolvedServers[0].enabledTools).toEqual(["search_code"]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("resolves unrestricted (absent field) when both lists are empty", async () => {
|
|
145
|
+
const client = clientReturning({ github: httpMcpServer("github") });
|
|
146
|
+
|
|
147
|
+
const result = await resolveMcpServers(
|
|
148
|
+
client, [makeUsage("github")], {}, "stdio-allowed",
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(result.resolvedServers[0].enabledTools).toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("mcpServerToResolved without a usage keeps only the default fallback (the discovery path)", () => {
|
|
155
|
+
const server = withDefaults(httpMcpServer("github"), ["search_code"]);
|
|
156
|
+
|
|
157
|
+
const resolved = mcpServerToResolved(server, "github", {});
|
|
158
|
+
|
|
159
|
+
// No usage in hand (discovery) — a per-agent restriction cannot apply,
|
|
160
|
+
// only the server-declared default does.
|
|
161
|
+
expect(resolved?.enabledTools).toEqual(["search_code"]);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("resolveMcpServers — tool_approval_overrides threading (issue #349)", () => {
|
|
166
|
+
it("carries the usage's overrides on its own resolved server only", async () => {
|
|
167
|
+
// Riding the server is the scoping mechanism: an override can no longer
|
|
168
|
+
// reach a same-named tool on another server, because it never exists
|
|
169
|
+
// anywhere but its own server's object.
|
|
170
|
+
const client = clientReturning({
|
|
171
|
+
github: httpMcpServer("github"),
|
|
172
|
+
slack: httpMcpServer("slack"),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const result = await resolveMcpServers(
|
|
176
|
+
client,
|
|
177
|
+
[
|
|
178
|
+
makeUsage("github", "test-org", [], [{ toolName: "delete_item", requiresApproval: false }]),
|
|
179
|
+
makeUsage("slack"),
|
|
180
|
+
],
|
|
181
|
+
{},
|
|
182
|
+
"stdio-allowed",
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const bySlug = new Map(result.resolvedServers.map((s) => [s.slug, s]));
|
|
186
|
+
expect(bySlug.get("github")!.toolApprovalOverrides).toEqual([
|
|
187
|
+
{ toolName: "delete_item", requiresApproval: false },
|
|
188
|
+
]);
|
|
189
|
+
expect(bySlug.get("slack")!.toolApprovalOverrides).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("mcpServerToResolved without a usage carries no overrides (the discovery path)", () => {
|
|
193
|
+
const resolved = mcpServerToResolved(httpMcpServer("github"), "github", {});
|
|
194
|
+
|
|
195
|
+
// No usage in hand (discovery) — there is no agent context, so no
|
|
196
|
+
// layer-3 overrides can exist.
|
|
197
|
+
expect(resolved?.toolApprovalOverrides).toEqual([]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("session-wins merge feeds the SESSION usage's overrides to the resolver (native-harness parity)", async () => {
|
|
201
|
+
// Pins the end-to-end path that was broken before #349: the deep-agent
|
|
202
|
+
// harness resolved servers from the merged usages but flattened
|
|
203
|
+
// overrides from the AGENT usages only, so a session's overrides were
|
|
204
|
+
// silently ignored there (and honored by the Cursor harness).
|
|
205
|
+
const client = clientReturning({ github: httpMcpServer("github") });
|
|
206
|
+
const merged = mergeMcpServerUsages(
|
|
207
|
+
[makeUsage("github", "test-org", [], [{ toolName: "push", requiresApproval: true }])],
|
|
208
|
+
[makeUsage("github", "test-org", [], [{ toolName: "push", requiresApproval: false }])],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const result = await resolveMcpServers(client, merged, {}, "stdio-allowed");
|
|
212
|
+
|
|
213
|
+
expect(result.resolvedServers[0].toolApprovalOverrides).toEqual([
|
|
214
|
+
{ toolName: "push", requiresApproval: false },
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("mergeMcpServerUsages — session-wins-per-slug (shared by both harnesses)", () => {
|
|
220
|
+
it("session usage replaces the agent's WHOLE usage for the same slug, including enabled_tools", () => {
|
|
221
|
+
const merged = mergeMcpServerUsages(
|
|
222
|
+
[makeUsage("github", "test-org", ["create_pr"])],
|
|
223
|
+
[makeUsage("github", "test-org", ["search_code"])],
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(merged).toHaveLength(1);
|
|
227
|
+
expect(merged[0].enabledTools).toEqual(["search_code"]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("a session usage with an empty list overrides to the server-default/unrestricted shape", () => {
|
|
231
|
+
// Session-wins is whole-usage precedence, not a list union: an empty
|
|
232
|
+
// session list means "back to the server's defaults", exactly as if the
|
|
233
|
+
// agent-level usage did not exist.
|
|
234
|
+
const merged = mergeMcpServerUsages(
|
|
235
|
+
[makeUsage("github", "test-org", ["create_pr"])],
|
|
236
|
+
[makeUsage("github")],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
expect(merged).toHaveLength(1);
|
|
240
|
+
expect(merged[0].enabledTools).toEqual([]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("unions distinct slugs and skips usages without one", () => {
|
|
244
|
+
const merged = mergeMcpServerUsages(
|
|
245
|
+
[makeUsage("github"), { toolApprovalOverrides: [] } as any],
|
|
246
|
+
[makeUsage("planton")],
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
expect(merged.map((u) => u.mcpServerRef?.slug)).toEqual(["github", "planton"]);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -53,6 +53,9 @@ describe("buildChatModel", () => {
|
|
|
53
53
|
vi.clearAllMocks();
|
|
54
54
|
delete process.env.ANTHROPIC_API_KEY;
|
|
55
55
|
delete process.env.OPENAI_API_KEY;
|
|
56
|
+
delete process.env.STIGMER_ANTHROPIC_BACKEND;
|
|
57
|
+
delete process.env.STIGMER_OPENAI_BACKEND;
|
|
58
|
+
delete process.env.CLOUD_ML_REGION;
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
afterEach(() => {
|
|
@@ -159,4 +162,155 @@ describe("buildChatModel", () => {
|
|
|
159
162
|
expect(provider).toBe("anthropic");
|
|
160
163
|
expect(apiModelId).toBe("claude-haiku-4.5");
|
|
161
164
|
});
|
|
165
|
+
|
|
166
|
+
// ─── Request timeout (STIGMER_LLM_REQUEST_TIMEOUT_MS) ──────────────────────
|
|
167
|
+
//
|
|
168
|
+
// The timeout lives in a different slot per wrapper: ChatOpenAI reads it
|
|
169
|
+
// as a constructor field; ChatAnthropic only honors clientOptions.timeout.
|
|
170
|
+
// Putting it in the shared constructor spread was the regression that made
|
|
171
|
+
// the env var inert on every Anthropic path (T02 finding 2, repaired in
|
|
172
|
+
// T06). The backend factories' half of the chain is pinned in the
|
|
173
|
+
// vertex/bedrock/foundry adapter tests.
|
|
174
|
+
|
|
175
|
+
describe("request timeout", () => {
|
|
176
|
+
it("places the OpenAI timeout at the constructor level with maxRetries 0", async () => {
|
|
177
|
+
mockRegistryResponse([
|
|
178
|
+
{ id: "gpt-4.1", apiModelId: "gpt-4.1", provider: "openai" },
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
await buildChatModel({ modelName: "gpt-4.1", timeoutMs: 5000 });
|
|
182
|
+
|
|
183
|
+
const args = mockOpenAICtor.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
|
184
|
+
expect(args).toMatchObject({ timeout: 5000, maxRetries: 0 });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("places the Anthropic timeout in clientOptions, never the inert constructor slot", async () => {
|
|
188
|
+
mockRegistryResponse([
|
|
189
|
+
{ id: "claude-haiku-4.5", apiModelId: "claude-haiku-4-5-20251001", provider: "anthropic" },
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
await buildChatModel({ modelName: "claude-haiku-4.5", timeoutMs: 5000 });
|
|
193
|
+
|
|
194
|
+
const args = lastAnthropicArgs();
|
|
195
|
+
expect(args).toMatchObject({ clientOptions: { timeout: 5000 }, maxRetries: 0 });
|
|
196
|
+
expect(args).not.toHaveProperty("timeout");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("merges the timeout with the proxy wiring inside one clientOptions block", async () => {
|
|
200
|
+
mockRegistryResponse([
|
|
201
|
+
{ id: "claude-haiku-4.5", apiModelId: "claude-haiku-4-5-20251001", provider: "anthropic" },
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
await buildChatModel({
|
|
205
|
+
modelName: "claude-haiku-4.5",
|
|
206
|
+
proxyEndpoint: "https://api.stigmer.ai",
|
|
207
|
+
stigmerToken: "tok-123",
|
|
208
|
+
timeoutMs: 5000,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(lastAnthropicArgs()).toMatchObject({
|
|
212
|
+
clientOptions: {
|
|
213
|
+
timeout: 5000,
|
|
214
|
+
baseURL: "https://api.stigmer.ai/v1/proxy/llm/anthropic",
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ─── Provider backends (STIGMER_ANTHROPIC_BACKEND) ─────────────────────────
|
|
221
|
+
|
|
222
|
+
describe("vertex backend", () => {
|
|
223
|
+
beforeEach(() => {
|
|
224
|
+
process.env.STIGMER_ANTHROPIC_BACKEND = "vertex";
|
|
225
|
+
process.env.CLOUD_ML_REGION = "asia-south1";
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("holds the canonical-id invariant: translated id on the wire, canonical id returned", async () => {
|
|
229
|
+
mockRegistryResponse([
|
|
230
|
+
{ id: "claude-sonnet-4.5", apiModelId: "claude-sonnet-4-5-20250929", provider: "anthropic" },
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
const { apiModelId } = await buildChatModel({ modelName: "claude-sonnet-4.5" });
|
|
234
|
+
|
|
235
|
+
// Pricing and usage metrics key on the canonical id; only the
|
|
236
|
+
// constructor (the wire) sees the Vertex `@date` form.
|
|
237
|
+
expect(apiModelId).toBe("claude-sonnet-4-5-20250929");
|
|
238
|
+
const args = lastAnthropicArgs();
|
|
239
|
+
expect(args.model).toBe("claude-sonnet-4-5@20250929");
|
|
240
|
+
expect(typeof args.createClient).toBe("function");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("passes dateless 4.6-generation ids to the wire untranslated", async () => {
|
|
244
|
+
mockRegistryResponse([
|
|
245
|
+
{ id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
await buildChatModel({ modelName: "claude-sonnet-4.6" });
|
|
249
|
+
|
|
250
|
+
expect(lastAnthropicArgs().model).toBe("claude-sonnet-4-6");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("constructs without ANTHROPIC_API_KEY (auth is Google's, not Anthropic's)", async () => {
|
|
254
|
+
mockRegistryResponse([
|
|
255
|
+
{ id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
|
|
256
|
+
]);
|
|
257
|
+
|
|
258
|
+
await buildChatModel({ modelName: "claude-sonnet-4.6" });
|
|
259
|
+
|
|
260
|
+
expect(lastAnthropicArgs().apiKey).toBe("");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("yields to the proxy: a proxied call never consults the backend var", async () => {
|
|
264
|
+
mockRegistryResponse([
|
|
265
|
+
{ id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
await buildChatModel({
|
|
269
|
+
modelName: "claude-sonnet-4.6",
|
|
270
|
+
proxyEndpoint: "https://api.stigmer.ai",
|
|
271
|
+
stigmerToken: "tok",
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const args = lastAnthropicArgs();
|
|
275
|
+
expect(args).not.toHaveProperty("createClient");
|
|
276
|
+
expect(args.model).toBe("claude-sonnet-4-6");
|
|
277
|
+
expect(args).toMatchObject({
|
|
278
|
+
clientOptions: { baseURL: "https://api.stigmer.ai/v1/proxy/llm/anthropic" },
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("leaves the OpenAI path untouched by the Anthropic backend var", async () => {
|
|
283
|
+
mockRegistryResponse([
|
|
284
|
+
{ id: "gpt-4.1", apiModelId: "gpt-4.1", provider: "openai" },
|
|
285
|
+
]);
|
|
286
|
+
|
|
287
|
+
await buildChatModel({ modelName: "gpt-4.1" });
|
|
288
|
+
|
|
289
|
+
const args = mockOpenAICtor.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
|
290
|
+
expect(args).not.toHaveProperty("createClient");
|
|
291
|
+
expect(mockAnthropicCtor).not.toHaveBeenCalled();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("fails at dispatch with the region message when CLOUD_ML_REGION is missing", async () => {
|
|
295
|
+
// Defense in depth for paths that bypass the factories' preflight.
|
|
296
|
+
delete process.env.CLOUD_ML_REGION;
|
|
297
|
+
mockRegistryResponse([
|
|
298
|
+
{ id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
await expect(buildChatModel({ modelName: "claude-sonnet-4.6" }))
|
|
302
|
+
.rejects.toThrow(/CLOUD_ML_REGION/);
|
|
303
|
+
expect(mockAnthropicCtor).not.toHaveBeenCalled();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("throws the parser's message on an invalid backend value (never a silent public fallback)", async () => {
|
|
308
|
+
process.env.STIGMER_ANTHROPIC_BACKEND = "verteks";
|
|
309
|
+
mockRegistryResponse([
|
|
310
|
+
{ id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic" },
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
await expect(buildChatModel({ modelName: "claude-sonnet-4.6" }))
|
|
314
|
+
.rejects.toThrow(/STIGMER_ANTHROPIC_BACKEND="verteks" is not a supported backend/);
|
|
315
|
+
});
|
|
162
316
|
});
|