@stigmer/runner 3.10.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 +18 -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 +51 -57
- 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 +55 -23
- 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/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 +3 -1
- package/dist/shared/mcp-manager.js +17 -4
- 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/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 +53 -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 +31 -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 +55 -71
- 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 +110 -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 +76 -28
- 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__/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 +84 -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__/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/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 +21 -4
- 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/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
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
* the desktop Run dialog via the IPC handshake.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { describe, expect, it } from "vitest";
|
|
15
|
-
import {
|
|
14
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
import {
|
|
16
|
+
isNodeSqliteAvailable,
|
|
17
|
+
preflightNodeRuntime,
|
|
18
|
+
assertLlmBackendsPreflight,
|
|
19
|
+
} from "../preflight.js";
|
|
16
20
|
|
|
17
21
|
describe("preflightNodeRuntime", () => {
|
|
18
22
|
it("passes (returns null) when node:sqlite is available", () => {
|
|
@@ -39,3 +43,47 @@ describe("preflightNodeRuntime", () => {
|
|
|
39
43
|
expect(preflightNodeRuntime() === null).toBe(isNodeSqliteAvailable());
|
|
40
44
|
});
|
|
41
45
|
});
|
|
46
|
+
|
|
47
|
+
describe("assertLlmBackendsPreflight", () => {
|
|
48
|
+
// This is the gate both runner factories run at construction — the throw
|
|
49
|
+
// is what a misconfigured deployment sees instead of accepting work. The
|
|
50
|
+
// per-case parse/prereq tables live in shared/__tests__/llm-backend.test.ts;
|
|
51
|
+
// here we pin the wrapper's contract: throw on fatal, warn on precedence,
|
|
52
|
+
// silent when clean, and options-proxy treated like the env proxy.
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.unstubAllEnvs();
|
|
55
|
+
vi.restoreAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/** Pin the vars this gate reads so ambient shell env cannot leak in. */
|
|
59
|
+
function stubBackendEnv(vars: Record<string, string>) {
|
|
60
|
+
vi.stubEnv("STIGMER_PROXY_ENDPOINT", "");
|
|
61
|
+
vi.stubEnv("STIGMER_ANTHROPIC_BACKEND", "");
|
|
62
|
+
vi.stubEnv("STIGMER_OPENAI_BACKEND", "");
|
|
63
|
+
vi.stubEnv("CLOUD_ML_REGION", "");
|
|
64
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
65
|
+
vi.stubEnv(key, value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
it("passes silently for an unconfigured deployment", () => {
|
|
70
|
+
stubBackendEnv({});
|
|
71
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
72
|
+
expect(() => assertLlmBackendsPreflight(null)).not.toThrow();
|
|
73
|
+
expect(warn).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("throws the actionable message for a misconfigured backend", () => {
|
|
77
|
+
stubBackendEnv({ STIGMER_ANTHROPIC_BACKEND: "vertex" });
|
|
78
|
+
expect(() => assertLlmBackendsPreflight(null)).toThrow(/CLOUD_ML_REGION/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("warns instead of throwing when an options-supplied proxy makes the var inert", () => {
|
|
82
|
+
stubBackendEnv({ STIGMER_ANTHROPIC_BACKEND: "vertex" });
|
|
83
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
84
|
+
expect(() => assertLlmBackendsPreflight("https://api.stigmer.ai")).not.toThrow();
|
|
85
|
+
expect(warn).toHaveBeenCalledWith(
|
|
86
|
+
expect.stringContaining("STIGMER_ANTHROPIC_BACKEND=vertex is ignored"),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -5,6 +5,45 @@ import { _resetRegistryCache } from "../../shared/model-registry.js";
|
|
|
5
5
|
const originalFetch = globalThis.fetch;
|
|
6
6
|
const originalEnv = { ...process.env };
|
|
7
7
|
|
|
8
|
+
const { vertexCreateRequests } = vi.hoisted(() => ({
|
|
9
|
+
vertexCreateRequests: [] as Array<Record<string, unknown>>,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Mocked at the SDK class (the vertex-adapter.test.ts pattern) so no real
|
|
13
|
+
// google-auth credential resolution ever runs inside a unit test — locally
|
|
14
|
+
// it fails fast, but in CI it can probe the GCE metadata server. ChatAnthropic
|
|
15
|
+
// consumes the client's parsed event stream, so the streaming arm returns a
|
|
16
|
+
// plain async generator of Anthropic stream events.
|
|
17
|
+
vi.mock("@anthropic-ai/vertex-sdk", () => ({
|
|
18
|
+
AnthropicVertex: class {
|
|
19
|
+
readonly messages = {
|
|
20
|
+
create: async (request: Record<string, unknown>) => {
|
|
21
|
+
vertexCreateRequests.push(request);
|
|
22
|
+
if (request.stream === true) {
|
|
23
|
+
return (async function* () {
|
|
24
|
+
yield { type: "message_start", message: { id: "msg_vertex_call_llm", type: "message", role: "assistant", model: request.model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 11, output_tokens: 1 } } };
|
|
25
|
+
yield { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } };
|
|
26
|
+
yield { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Vertex direct." } };
|
|
27
|
+
yield { type: "content_block_stop", index: 0 };
|
|
28
|
+
yield { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: 4 } };
|
|
29
|
+
yield { type: "message_stop" };
|
|
30
|
+
})();
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
id: "msg_vertex_call_llm",
|
|
34
|
+
type: "message",
|
|
35
|
+
role: "assistant",
|
|
36
|
+
model: request.model,
|
|
37
|
+
content: [{ type: "text", text: "Vertex direct." }],
|
|
38
|
+
stop_reason: "end_turn",
|
|
39
|
+
stop_sequence: null,
|
|
40
|
+
usage: { input_tokens: 11, output_tokens: 4 },
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
}));
|
|
46
|
+
|
|
8
47
|
describe("callLlmAction", () => {
|
|
9
48
|
let mockFetch: ReturnType<typeof vi.fn>;
|
|
10
49
|
|
|
@@ -16,6 +55,12 @@ describe("callLlmAction", () => {
|
|
|
16
55
|
delete process.env.STIGMER_TOKEN;
|
|
17
56
|
delete process.env.OPENAI_API_KEY;
|
|
18
57
|
delete process.env.ANTHROPIC_API_KEY;
|
|
58
|
+
// Deterministic regardless of the developer's shell: a leaked
|
|
59
|
+
// STIGMER_ANTHROPIC_BACKEND=vertex would reroute every direct-mode test.
|
|
60
|
+
delete process.env.STIGMER_ANTHROPIC_BACKEND;
|
|
61
|
+
delete process.env.STIGMER_OPENAI_BACKEND;
|
|
62
|
+
delete process.env.CLOUD_ML_REGION;
|
|
63
|
+
vertexCreateRequests.length = 0;
|
|
19
64
|
});
|
|
20
65
|
|
|
21
66
|
afterEach(() => {
|
|
@@ -234,6 +279,36 @@ describe("callLlmAction", () => {
|
|
|
234
279
|
});
|
|
235
280
|
});
|
|
236
281
|
|
|
282
|
+
describe("direct mode — Anthropic on the vertex backend", () => {
|
|
283
|
+
it("serves the call with no ANTHROPIC_API_KEY anywhere (ADC is the credential path)", async () => {
|
|
284
|
+
// The regression pin for the backend-blind key check: a correctly
|
|
285
|
+
// configured Vertex deployment holds no Anthropic key, and llm_call
|
|
286
|
+
// tasks must not fail with LLM_MISSING_API_KEY before construction.
|
|
287
|
+
process.env.STIGMER_ANTHROPIC_BACKEND = "vertex";
|
|
288
|
+
process.env.CLOUD_ML_REGION = "global";
|
|
289
|
+
mockFetchWithRegistry(() => {
|
|
290
|
+
throw new Error("unexpected fetch: vertex traffic must go through the SDK client");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const result = await callLlmAction(baseAnthropicConfig, {}, "exec-1");
|
|
294
|
+
|
|
295
|
+
expect(result.result).toBe("Vertex direct.");
|
|
296
|
+
expect(result.provider).toBe("anthropic");
|
|
297
|
+
expect(result.input_tokens).toBe(11);
|
|
298
|
+
expect(result.output_tokens).toBe(4);
|
|
299
|
+
expect(vertexCreateRequests.length).toBeGreaterThan(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("keeps the LLM_MISSING_API_KEY contract on the public backend, now naming the backend remedy", async () => {
|
|
303
|
+
const err = await callLlmAction(baseAnthropicConfig, {}, "exec-1").catch((e: unknown) => e as Error & { type?: string });
|
|
304
|
+
|
|
305
|
+
expect(err).toBeInstanceOf(Error);
|
|
306
|
+
expect((err as { type?: string }).type).toBe("LLM_MISSING_API_KEY");
|
|
307
|
+
expect((err as Error).message).toContain("ANTHROPIC_API_KEY");
|
|
308
|
+
expect((err as Error).message).toContain("STIGMER_ANTHROPIC_BACKEND=vertex");
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
237
312
|
describe("proxy mode", () => {
|
|
238
313
|
it("routes through proxy when endpoint is set", async () => {
|
|
239
314
|
process.env.STIGMER_PROXY_ENDPOINT = "https://proxy.stigmer.ai";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import type { ClassifyToolApprovalsInput, ToolDescriptor } from "../classify-tool-approvals.js";
|
|
3
3
|
|
|
4
4
|
vi.mock("../../idle-watchdog.js", () => ({
|
|
@@ -597,6 +597,122 @@ describe("ClassifyToolApprovals activity", () => {
|
|
|
597
597
|
expect(headers).not.toHaveProperty("X-Stigmer-Mcp-Server-Id");
|
|
598
598
|
});
|
|
599
599
|
});
|
|
600
|
+
|
|
601
|
+
describe("direct mode (no proxy)", () => {
|
|
602
|
+
// The regression suite for the stigmerBackendEndpoint-as-LLM-proxy bug:
|
|
603
|
+
// an unproxied runner must call the provider directly with the real key,
|
|
604
|
+
// and must fail closed with one clear message when it has none.
|
|
605
|
+
const directOptions = { proxyEndpoint: null, stigmerToken: null, primaryModel: "gpt-4.1" };
|
|
606
|
+
|
|
607
|
+
beforeEach(() => {
|
|
608
|
+
// Deterministic regardless of the developer's shell: blank reads as
|
|
609
|
+
// missing, and backend/proxy vars must not leak in from outside.
|
|
610
|
+
vi.stubEnv("OPENAI_API_KEY", "");
|
|
611
|
+
vi.stubEnv("ANTHROPIC_API_KEY", "");
|
|
612
|
+
vi.stubEnv("STIGMER_ANTHROPIC_BACKEND", "");
|
|
613
|
+
vi.stubEnv("STIGMER_OPENAI_BACKEND", "");
|
|
614
|
+
vi.stubEnv("STIGMER_PROXY_ENDPOINT", "");
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
afterEach(() => {
|
|
618
|
+
vi.unstubAllEnvs();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("fails closed without any credential path: every tool gated, no client constructed", async () => {
|
|
622
|
+
const { ChatAnthropic } = await import("@langchain/anthropic");
|
|
623
|
+
const { ChatOpenAI } = await import("@langchain/openai");
|
|
624
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
625
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
626
|
+
|
|
627
|
+
const tools: ToolDescriptor[] = [
|
|
628
|
+
{ name: "search_code", description: "Search code" },
|
|
629
|
+
{ name: "delete_file", description: "Delete a file" },
|
|
630
|
+
];
|
|
631
|
+
|
|
632
|
+
const result = await classifyTools(
|
|
633
|
+
{ tools, serverName: "github", serverDescription: "GitHub", mcpServerId: null },
|
|
634
|
+
directOptions,
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
expect(result).toEqual([
|
|
638
|
+
{ tool_name: "search_code", requires_approval: true, message: "Execute search_code" },
|
|
639
|
+
{ tool_name: "delete_file", requires_approval: true, message: "Execute delete_file" },
|
|
640
|
+
]);
|
|
641
|
+
expect(ChatOpenAI).not.toHaveBeenCalled();
|
|
642
|
+
expect(ChatAnthropic).not.toHaveBeenCalled();
|
|
643
|
+
expect(mockInvoke).not.toHaveBeenCalled();
|
|
644
|
+
// The message must name the model and provider (classification's
|
|
645
|
+
// provider follows the economy model, not the operator's key) and
|
|
646
|
+
// carry the predicate's remedy text.
|
|
647
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("'gpt-4o-mini' (openai)"));
|
|
648
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("OPENAI_API_KEY"));
|
|
649
|
+
warn.mockRestore();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("uses the provider key directly — no proxy baseURL, no Authorization header", async () => {
|
|
653
|
+
vi.stubEnv("OPENAI_API_KEY", "sk-direct-test");
|
|
654
|
+
const { ChatOpenAI } = await import("@langchain/openai");
|
|
655
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
656
|
+
|
|
657
|
+
mockInvoke.mockResolvedValueOnce({
|
|
658
|
+
approvals: [{ tool_name: "t", requires_approval: false, message: "" }],
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const result = await classifyTools(
|
|
662
|
+
{ tools: [{ name: "t", description: "d" }], serverName: "s", serverDescription: "", mcpServerId: null },
|
|
663
|
+
directOptions,
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
expect(result).toEqual([]);
|
|
667
|
+
const args = vi.mocked(ChatOpenAI).mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
|
668
|
+
expect(args.apiKey).toBe("sk-direct-test");
|
|
669
|
+
// The bug this suite pins: the gRPC control-plane endpoint must never
|
|
670
|
+
// reappear as a baseURL/Authorization transport override.
|
|
671
|
+
expect(args).not.toHaveProperty("configuration");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("routes an Anthropic economy model directly through ChatAnthropic with the real key", async () => {
|
|
675
|
+
vi.stubEnv("ANTHROPIC_API_KEY", "sk-ant-direct");
|
|
676
|
+
const { ChatAnthropic } = await import("@langchain/anthropic");
|
|
677
|
+
const { getSummarizationModel } = await import("../../shared/model-registry.js");
|
|
678
|
+
vi.mocked(getSummarizationModel).mockResolvedValueOnce("claude-haiku-4.5");
|
|
679
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
680
|
+
|
|
681
|
+
mockInvoke.mockResolvedValueOnce({
|
|
682
|
+
approvals: [{ tool_name: "t", requires_approval: false, message: "" }],
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
await classifyTools(
|
|
686
|
+
{ tools: [{ name: "t", description: "d" }], serverName: "s", serverDescription: "", mcpServerId: null },
|
|
687
|
+
directOptions,
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
const args = vi.mocked(ChatAnthropic).mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
|
691
|
+
expect(args.apiKey).toBe("sk-ant-direct");
|
|
692
|
+
expect(args).not.toHaveProperty("clientOptions");
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("defers an un-inferable economy model to the per-batch fail-closed path", async () => {
|
|
696
|
+
// Real scenario: the registry-empty fallback returns the primary model
|
|
697
|
+
// verbatim (model-registry.ts), whatever its name. The pre-check must
|
|
698
|
+
// not guess a provider — the batch path owns the precise error.
|
|
699
|
+
const { getSummarizationModel } = await import("../../shared/model-registry.js");
|
|
700
|
+
vi.mocked(getSummarizationModel).mockResolvedValueOnce("mystery-model");
|
|
701
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
702
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
703
|
+
|
|
704
|
+
const result = await classifyTools(
|
|
705
|
+
{ tools: [{ name: "t", description: "d" }], serverName: "s", serverDescription: "", mcpServerId: null },
|
|
706
|
+
directOptions,
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
expect(result).toEqual([
|
|
710
|
+
{ tool_name: "t", requires_approval: true, message: "Execute t" },
|
|
711
|
+
]);
|
|
712
|
+
expect(error).toHaveBeenCalled();
|
|
713
|
+
error.mockRestore();
|
|
714
|
+
});
|
|
715
|
+
});
|
|
600
716
|
});
|
|
601
717
|
|
|
602
718
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression pin for issue #239's hang mechanism, against the REAL MCP client
|
|
3
|
+
* stack (no @langchain/mcp-adapters mock — that's why this lives in its own
|
|
4
|
+
* file, apart from discover-mcp-server.test.ts's mocked suite).
|
|
5
|
+
*
|
|
6
|
+
* The monday.com failure shape: the endpoint answers the streamable-HTTP
|
|
7
|
+
* initialize POST with a 4xx, mcp-adapters automatically falls back to SSE at
|
|
8
|
+
* the same URL, the endpoint accepts the stream and never sends the legacy
|
|
9
|
+
* `endpoint` event — and the MCP SDK's SSEClientTransport has no timer of its
|
|
10
|
+
* own, so initialization hangs forever. Before the transport-aware init bound,
|
|
11
|
+
* that hang rode until Temporal killed the activity opaquely; this test pins
|
|
12
|
+
* that discovery now fails fast (in fake time) with an error that names the
|
|
13
|
+
* endpoint.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
16
|
+
import { createServer, type Server } from "node:http";
|
|
17
|
+
|
|
18
|
+
vi.mock("../../idle-watchdog.js", () => ({
|
|
19
|
+
activityStarted: vi.fn(),
|
|
20
|
+
activityFinished: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// classifyHttpOAuthFailure re-probes the endpoint on the failure path; keep it
|
|
24
|
+
// deterministic here (its own behavior is covered by mcp-oauth-detect tests).
|
|
25
|
+
vi.mock("../../shared/mcp-oauth-detect.js", () => ({
|
|
26
|
+
detectOAuthChallenge: vi.fn().mockResolvedValue(null),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
describe("discovery against a 4xx-then-silent-SSE endpoint (issue #239)", () => {
|
|
30
|
+
let server: Server;
|
|
31
|
+
let url: string;
|
|
32
|
+
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
server = createServer((req, res) => {
|
|
35
|
+
if (req.method === "POST") {
|
|
36
|
+
// monday-shaped: reject the streamable-HTTP initialize outright.
|
|
37
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
38
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// The SSE fallback: accept the stream, never send an `endpoint` event.
|
|
42
|
+
res.writeHead(200, {
|
|
43
|
+
"Content-Type": "text/event-stream",
|
|
44
|
+
"Cache-Control": "no-cache",
|
|
45
|
+
Connection: "keep-alive",
|
|
46
|
+
});
|
|
47
|
+
res.write(": silent stream\n\n");
|
|
48
|
+
});
|
|
49
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
50
|
+
const address = server.address();
|
|
51
|
+
if (typeof address === "string" || address === null) throw new Error("no port");
|
|
52
|
+
url = `http://127.0.0.1:${address.port}/mcp`;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
// closeAllConnections: the hung SSE stream is a live socket that would
|
|
57
|
+
// otherwise keep server.close() (and the vitest worker) waiting.
|
|
58
|
+
server.closeAllConnections();
|
|
59
|
+
server.close();
|
|
60
|
+
vi.useRealTimers();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("fails within the HTTP init bound with an endpoint-naming error", async () => {
|
|
64
|
+
const { discoverMcpServer } = await import("../discover-mcp-server.js");
|
|
65
|
+
|
|
66
|
+
const stigmerClient = {
|
|
67
|
+
getMcpServer: vi.fn().mockResolvedValue({
|
|
68
|
+
metadata: { slug: "monday", id: "mcp-monday" },
|
|
69
|
+
spec: {
|
|
70
|
+
serverType: {
|
|
71
|
+
case: "http",
|
|
72
|
+
value: { url, headers: {}, queryParams: {}, timeoutSeconds: 0 },
|
|
73
|
+
},
|
|
74
|
+
env: {},
|
|
75
|
+
pinnedToolApprovals: [],
|
|
76
|
+
},
|
|
77
|
+
status: undefined,
|
|
78
|
+
}),
|
|
79
|
+
getExecutionContextByExecutionId: vi.fn(),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// shouldAdvanceTime keeps real I/O flowing (the POST → 405 → SSE fallback
|
|
83
|
+
// happens over real sockets) while letting the test jump the 60s bound.
|
|
84
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
85
|
+
|
|
86
|
+
const promise = discoverMcpServer(
|
|
87
|
+
{ mcpServerId: "mcp-monday" },
|
|
88
|
+
{ stigmerClient: stigmerClient as never, transportPosture: "stdio-forbidden" },
|
|
89
|
+
);
|
|
90
|
+
// Attach the rejection expectation BEFORE advancing so the rejection is
|
|
91
|
+
// never momentarily unhandled.
|
|
92
|
+
const expectation = expect(promise).rejects.toThrow(
|
|
93
|
+
new RegExp(`at ${url.replaceAll(".", "\\.")}.*did not complete MCP initialization`, "s"),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Give the real client a beat to reach the hang (POST + fallback), then
|
|
97
|
+
// jump past the 30s HTTP bound in fake time.
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
99
|
+
await vi.advanceTimersByTimeAsync(31_000);
|
|
100
|
+
|
|
101
|
+
await expectation;
|
|
102
|
+
}, 15_000);
|
|
103
|
+
});
|
|
@@ -10,6 +10,14 @@ vi.mock("../../idle-watchdog.js", () => ({
|
|
|
10
10
|
activityFinished: vi.fn(),
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
|
+
// The factory starts a Temporal heartbeat loop; outside an activity context
|
|
14
|
+
// its ticks would throw, so replace it with a stop-spy and assert the
|
|
15
|
+
// start/stop contract instead.
|
|
16
|
+
const mockHeartbeatStop = vi.fn();
|
|
17
|
+
vi.mock("../../shared/heartbeat.js", () => ({
|
|
18
|
+
startHeartbeat: vi.fn(() => ({ stop: mockHeartbeatStop, cancelled: false, workerShutdown: false })),
|
|
19
|
+
}));
|
|
20
|
+
|
|
13
21
|
const mockInitializeConnections = vi.fn();
|
|
14
22
|
const mockGetClient = vi.fn();
|
|
15
23
|
const mockClose = vi.fn().mockResolvedValue(undefined);
|
|
@@ -600,6 +608,122 @@ describe("DiscoverMcpServer activity", () => {
|
|
|
600
608
|
expect(result.newToolsFingerprint).toBe("");
|
|
601
609
|
});
|
|
602
610
|
|
|
611
|
+
it("fails closed when the EC read errors and the server requires credentials", async () => {
|
|
612
|
+
// Issue #239: limping ahead with an empty env either died later as an
|
|
613
|
+
// opaque PlaceholderResolutionError or dialed the endpoint with a
|
|
614
|
+
// garbage credential and wedged in the 4xx → SSE-fallback limbo. The
|
|
615
|
+
// failure must name the root cause: credential delivery.
|
|
616
|
+
const { discoverMcpServer, CredentialResolutionError } = await import("../discover-mcp-server.js");
|
|
617
|
+
|
|
618
|
+
const spec = makeHttpSpec("https://mcp.monday.com/mcp");
|
|
619
|
+
spec.env = { MONDAY_ACCESS_TOKEN: { isSecret: true } };
|
|
620
|
+
const mockClient = makeMockStigmerClient({
|
|
621
|
+
mcpServer: makeMcpServer({ metadata: { slug: "monday" }, spec }),
|
|
622
|
+
});
|
|
623
|
+
mockClient.getExecutionContextByExecutionId = vi
|
|
624
|
+
.fn()
|
|
625
|
+
.mockRejectedValue(new Error("PERMISSION_DENIED: not scope-bound"));
|
|
626
|
+
|
|
627
|
+
await expect(
|
|
628
|
+
discoverMcpServer(
|
|
629
|
+
{ mcpServerId: "mcp-monday", executionContextId: "ctx-1" },
|
|
630
|
+
{ stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
|
|
631
|
+
),
|
|
632
|
+
).rejects.toThrow(CredentialResolutionError);
|
|
633
|
+
expect(mockInitializeConnections).not.toHaveBeenCalled();
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it("keeps the lenient path when the EC read errors but no env is declared", async () => {
|
|
637
|
+
const { discoverMcpServer } = await import("../discover-mcp-server.js");
|
|
638
|
+
|
|
639
|
+
const mockClient = makeMockStigmerClient({
|
|
640
|
+
mcpServer: makeMcpServer({
|
|
641
|
+
metadata: { slug: "no-env" },
|
|
642
|
+
spec: makeHttpSpec("https://mcp.example.com"),
|
|
643
|
+
}),
|
|
644
|
+
});
|
|
645
|
+
mockClient.getExecutionContextByExecutionId = vi
|
|
646
|
+
.fn()
|
|
647
|
+
.mockRejectedValue(new Error("transient"));
|
|
648
|
+
|
|
649
|
+
mockInitializeConnections.mockResolvedValue({});
|
|
650
|
+
mockGetClient.mockResolvedValue(makeMockMcpClient({ tools: [] }));
|
|
651
|
+
|
|
652
|
+
const result = await discoverMcpServer(
|
|
653
|
+
{ mcpServerId: "mcp-lenient", executionContextId: "ctx-2" },
|
|
654
|
+
{ stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
|
|
655
|
+
);
|
|
656
|
+
expect(result.tools).toEqual([]);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("keeps the lenient path when all declared env vars are optional", async () => {
|
|
660
|
+
// Caller-identity-style servers declare only optional keys; discovery
|
|
661
|
+
// legitimately proceeds without an EC for them (the injections below
|
|
662
|
+
// resolveEnvVarsForDiscovery supply the values).
|
|
663
|
+
const { discoverMcpServer } = await import("../discover-mcp-server.js");
|
|
664
|
+
|
|
665
|
+
const spec = makeHttpSpec("https://mcp.example.com");
|
|
666
|
+
spec.env = { STIGMER_CALLER_IDENTITY_KIND: { optional: true } };
|
|
667
|
+
const mockClient = makeMockStigmerClient({
|
|
668
|
+
mcpServer: makeMcpServer({ metadata: { slug: "optional-only" }, spec }),
|
|
669
|
+
});
|
|
670
|
+
mockClient.getExecutionContextByExecutionId = vi
|
|
671
|
+
.fn()
|
|
672
|
+
.mockRejectedValue(new Error("transient"));
|
|
673
|
+
|
|
674
|
+
mockInitializeConnections.mockResolvedValue({});
|
|
675
|
+
mockGetClient.mockResolvedValue(makeMockMcpClient({ tools: [] }));
|
|
676
|
+
|
|
677
|
+
const result = await discoverMcpServer(
|
|
678
|
+
{ mcpServerId: "mcp-optional", executionContextId: "ctx-3" },
|
|
679
|
+
{ stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
|
|
680
|
+
);
|
|
681
|
+
expect(result.tools).toEqual([]);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("fails closed when the EC is empty and the server requires credentials", async () => {
|
|
685
|
+
const { discoverMcpServer, CredentialResolutionError } = await import("../discover-mcp-server.js");
|
|
686
|
+
|
|
687
|
+
const spec = makeHttpSpec("https://mcp.monday.com/mcp");
|
|
688
|
+
spec.env = { MONDAY_ACCESS_TOKEN: { isSecret: true } };
|
|
689
|
+
const mockClient = makeMockStigmerClient({
|
|
690
|
+
mcpServer: makeMcpServer({ metadata: { slug: "monday" }, spec }),
|
|
691
|
+
executionContext: { spec: { data: {} } },
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const promise = discoverMcpServer(
|
|
695
|
+
{ mcpServerId: "mcp-monday", executionContextId: "ctx-4" },
|
|
696
|
+
{ stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
|
|
697
|
+
);
|
|
698
|
+
await expect(promise).rejects.toThrow(CredentialResolutionError);
|
|
699
|
+
await expect(promise).rejects.toThrow(/delivered no credentials/);
|
|
700
|
+
expect(mockInitializeConnections).not.toHaveBeenCalled();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it("fails closed when a delivered credential is the redaction sentinel", async () => {
|
|
704
|
+
// A redacted value means the server-side decrypt gate refused this
|
|
705
|
+
// runner's credential class/scope — dialing with the literal sentinel
|
|
706
|
+
// can only produce a misleading 401 (the issue-#239 M2 mechanism).
|
|
707
|
+
const { discoverMcpServer, CredentialResolutionError } = await import("../discover-mcp-server.js");
|
|
708
|
+
|
|
709
|
+
const spec = makeHttpSpec("https://mcp.monday.com/mcp");
|
|
710
|
+
spec.env = { MONDAY_ACCESS_TOKEN: { isSecret: true } };
|
|
711
|
+
const mockClient = makeMockStigmerClient({
|
|
712
|
+
mcpServer: makeMcpServer({ metadata: { slug: "monday" }, spec }),
|
|
713
|
+
executionContext: {
|
|
714
|
+
spec: { data: { MONDAY_ACCESS_TOKEN: { value: "***REDACTED***", isSecret: true } } },
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
const promise = discoverMcpServer(
|
|
719
|
+
{ mcpServerId: "mcp-monday", executionContextId: "ctx-5" },
|
|
720
|
+
{ stigmerClient: mockClient as any, transportPosture: "stdio-forbidden" },
|
|
721
|
+
);
|
|
722
|
+
await expect(promise).rejects.toThrow(CredentialResolutionError);
|
|
723
|
+
await expect(promise).rejects.toThrow(/MONDAY_ACCESS_TOKEN.*delivered redacted|delivered redacted.*MONDAY_ACCESS_TOKEN/s);
|
|
724
|
+
expect(mockInitializeConnections).not.toHaveBeenCalled();
|
|
725
|
+
});
|
|
726
|
+
|
|
603
727
|
it("closes MCP client even when discovery fails", async () => {
|
|
604
728
|
const { discoverMcpServer } = await import("../discover-mcp-server.js");
|
|
605
729
|
|
|
@@ -620,6 +744,85 @@ describe("DiscoverMcpServer activity", () => {
|
|
|
620
744
|
});
|
|
621
745
|
});
|
|
622
746
|
|
|
747
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
748
|
+
// Transport-aware init bounds (issue #239)
|
|
749
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
750
|
+
|
|
751
|
+
describe("transport-aware init bounds", () => {
|
|
752
|
+
it("bounds HTTP endpoints at 30s and stdio at 270s", async () => {
|
|
753
|
+
// HTTP has no cold-start excuse: a healthy endpoint completes the MCP
|
|
754
|
+
// handshake in seconds, so a short bound converts the silent-SSE hang
|
|
755
|
+
// into a fast actionable failure. stdio keeps the cold-start allowance
|
|
756
|
+
// (issue #243). The OSS server's connect-workflow budget (connectTimeout,
|
|
757
|
+
// controller/connect.go — pinned there) is derived from the stdio bound
|
|
758
|
+
// + the classification floor, so both bounds stay reachable under it.
|
|
759
|
+
const { initTimeoutMsFor } = await import("../discover-mcp-server.js");
|
|
760
|
+
expect(initTimeoutMsFor("http")).toBe(30_000);
|
|
761
|
+
expect(initTimeoutMsFor("sse")).toBe(30_000);
|
|
762
|
+
expect(initTimeoutMsFor("stdio")).toBe(270_000);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("names the endpoint URL in the HTTP timeout message", async () => {
|
|
766
|
+
const { initTimeoutMessageFor } = await import("../discover-mcp-server.js");
|
|
767
|
+
const message = initTimeoutMessageFor("monday", {
|
|
768
|
+
slug: "monday",
|
|
769
|
+
connectionType: "http",
|
|
770
|
+
url: "https://mcp.monday.com/mcp",
|
|
771
|
+
toolApprovals: [],
|
|
772
|
+
pinnedToolApprovals: [],
|
|
773
|
+
toolApprovalOverrides: [],
|
|
774
|
+
discoveredCapabilitiesEmpty: true,
|
|
775
|
+
});
|
|
776
|
+
expect(message).toContain("https://mcp.monday.com/mcp");
|
|
777
|
+
expect(message).toContain("30s");
|
|
778
|
+
expect(message).toContain("SSE fallback");
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("names the command in the stdio timeout message", async () => {
|
|
782
|
+
const { initTimeoutMessageFor } = await import("../discover-mcp-server.js");
|
|
783
|
+
const message = initTimeoutMessageFor("filesystem", {
|
|
784
|
+
slug: "filesystem",
|
|
785
|
+
connectionType: "stdio",
|
|
786
|
+
command: "npx",
|
|
787
|
+
toolApprovals: [],
|
|
788
|
+
pinnedToolApprovals: [],
|
|
789
|
+
toolApprovalOverrides: [],
|
|
790
|
+
discoveredCapabilitiesEmpty: true,
|
|
791
|
+
});
|
|
792
|
+
expect(message).toContain("npx");
|
|
793
|
+
expect(message).toContain("270s");
|
|
794
|
+
expect(message).toContain("cold start");
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
799
|
+
// Heartbeat contract (issue #239)
|
|
800
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
801
|
+
|
|
802
|
+
describe("heartbeat contract", () => {
|
|
803
|
+
it("starts a heartbeat for the activity and stops it on completion", async () => {
|
|
804
|
+
// The connect workflow proxies this activity with a 60s heartbeatTimeout.
|
|
805
|
+
// Without a heartbeat loop, any discovery slower than 60s was killed by
|
|
806
|
+
// Temporal with an opaque heartbeat timeout — never reaching the
|
|
807
|
+
// actionable init-timeout errors (issues #239/#243).
|
|
808
|
+
const { createDiscoverMcpServerActivities } = await import("../discover-mcp-server.js");
|
|
809
|
+
const { startHeartbeat } = await import("../../shared/heartbeat.js");
|
|
810
|
+
|
|
811
|
+
vi.mocked(startHeartbeat).mockClear();
|
|
812
|
+
mockHeartbeatStop.mockClear();
|
|
813
|
+
|
|
814
|
+
const { DiscoverMcpServerCapabilities } = createDiscoverMcpServerActivities(makeConfig());
|
|
815
|
+
try {
|
|
816
|
+
await DiscoverMcpServerCapabilities({ mcpServerId: "test" });
|
|
817
|
+
} catch {
|
|
818
|
+
// Expected — no real backend behind the factory's own client.
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
expect(startHeartbeat).toHaveBeenCalledTimes(1);
|
|
822
|
+
expect(mockHeartbeatStop).toHaveBeenCalledTimes(1);
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
|
|
623
826
|
// ─────────────────────────────────────────────────────────────────────────
|
|
624
827
|
// Idle Watchdog Integration
|
|
625
828
|
// ─────────────────────────────────────────────────────────────────────────
|