@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,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Characterization test for the ChatAnthropic `createClient` ->
|
|
3
|
+
* AnthropicFoundry seam — the integration the T05 foundry backend adapter
|
|
4
|
+
* is built on. Sibling of vertex-seam.test.ts and bedrock-seam.test.ts;
|
|
5
|
+
* same rules: this pins REAL cross-package behavior (`@langchain/anthropic`
|
|
6
|
+
* bundling @anthropic-ai/sdk 0.95.x driving `@anthropic-ai/foundry-sdk`
|
|
7
|
+
* bundling its own nested >=0.115 copy), so a future bump of either side
|
|
8
|
+
* fails here in CI instead of in production. See
|
|
9
|
+
* scripts/check-langchain-deps.sh for why the two @anthropic-ai/sdk
|
|
10
|
+
* versions coexist and when they collapse to one.
|
|
11
|
+
*
|
|
12
|
+
* Determinism: zero live credentials, zero network. API-key auth is a
|
|
13
|
+
* static test key; Entra ID auth is a fake `azureADTokenProvider` (the
|
|
14
|
+
* SDK's supported constructor option — a plain `() => Promise<string>`,
|
|
15
|
+
* so no @azure/identity is needed here); transport is a recording `fetch`
|
|
16
|
+
* injected through the SDK's own `fetch` option.
|
|
17
|
+
*
|
|
18
|
+
* Foundry-specific wire facts this suite pins (vs. its two siblings):
|
|
19
|
+
* - The deployment name stays in the BODY as `model` — Foundry routes by
|
|
20
|
+
* deployment name, not by URL path (Vertex/Bedrock move the id into
|
|
21
|
+
* the path). One /v1/messages URL serves both modes; `stream: true`
|
|
22
|
+
* in the body selects streaming.
|
|
23
|
+
* - The constructor enforces its config contract itself: a credential
|
|
24
|
+
* (apiKey XOR azureADTokenProvider) and an endpoint (resource XOR
|
|
25
|
+
* baseURL) are both mandatory, each pair mutually exclusive. These
|
|
26
|
+
* throws are what checkFoundryPrerequisites front-runs at boot.
|
|
27
|
+
* - withStructuredOutput compiles to forced tool use (tools +
|
|
28
|
+
* tool_choice), NOT Anthropic's native structured-output parameter —
|
|
29
|
+
* which is why it works on "Hosted on Azure" Foundry deployments,
|
|
30
|
+
* where the native feature returns 400 by design (operator doc,
|
|
31
|
+
* docs/guides/runners/model-backends.mdx).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
35
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
36
|
+
import { AnthropicFoundry } from "@anthropic-ai/foundry-sdk";
|
|
37
|
+
import { HumanMessage, AIMessage, AIMessageChunk } from "@langchain/core/messages";
|
|
38
|
+
|
|
39
|
+
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Foundry's default deployment names are the DATELESS Claude ids — this is
|
|
43
|
+
* a dated registry id after the strip-date translation in llm-backend.ts
|
|
44
|
+
* (claude-sonnet-4-5-20250929 -> claude-sonnet-4-5).
|
|
45
|
+
*/
|
|
46
|
+
const FOUNDRY_DEPLOYMENT = "claude-sonnet-4-5";
|
|
47
|
+
const RESOURCE = "seam-test-resource";
|
|
48
|
+
const API_KEY = "foundry-seam-test-key";
|
|
49
|
+
|
|
50
|
+
const WEATHER_TOOL = {
|
|
51
|
+
name: "get_weather",
|
|
52
|
+
description: "Get the current weather for a city.",
|
|
53
|
+
input_schema: {
|
|
54
|
+
type: "object" as const,
|
|
55
|
+
properties: { city: { type: "string" } },
|
|
56
|
+
required: ["city"],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Non-streaming response: text + tool_use + usage. */
|
|
61
|
+
const MESSAGE_RESPONSE = {
|
|
62
|
+
id: "msg_foundry_test_01",
|
|
63
|
+
type: "message",
|
|
64
|
+
role: "assistant",
|
|
65
|
+
model: FOUNDRY_DEPLOYMENT,
|
|
66
|
+
content: [
|
|
67
|
+
{ type: "text", text: "I'll check the weather." },
|
|
68
|
+
{ type: "tool_use", id: "toolu_test_01", name: "get_weather", input: { city: "Chennai" } },
|
|
69
|
+
],
|
|
70
|
+
stop_reason: "tool_use",
|
|
71
|
+
stop_sequence: null,
|
|
72
|
+
usage: { input_tokens: 25, output_tokens: 17 },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Streaming response as Anthropic SSE — Foundry speaks the standard
|
|
77
|
+
* Messages API event grammar (no transcoding layer like Bedrock's binary
|
|
78
|
+
* EventStream): a text block, a tool_use block assembled from
|
|
79
|
+
* input_json_delta, then cumulative usage in message_delta.
|
|
80
|
+
*/
|
|
81
|
+
const SSE_EVENTS: ReadonlyArray<[string, object]> = [
|
|
82
|
+
["message_start", {
|
|
83
|
+
type: "message_start",
|
|
84
|
+
message: {
|
|
85
|
+
id: "msg_foundry_test_02", type: "message", role: "assistant",
|
|
86
|
+
model: FOUNDRY_DEPLOYMENT, content: [], stop_reason: null, stop_sequence: null,
|
|
87
|
+
usage: { input_tokens: 25, output_tokens: 1 },
|
|
88
|
+
},
|
|
89
|
+
}],
|
|
90
|
+
["content_block_start", { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }],
|
|
91
|
+
["content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "I'll check the weather." } }],
|
|
92
|
+
["content_block_stop", { type: "content_block_stop", index: 0 }],
|
|
93
|
+
["content_block_start", { type: "content_block_start", index: 1, content_block: { type: "tool_use", id: "toolu_test_02", name: "get_weather", input: {} } }],
|
|
94
|
+
["content_block_delta", { type: "content_block_delta", index: 1, delta: { type: "input_json_delta", partial_json: '{"city":"Chennai"}' } }],
|
|
95
|
+
["content_block_stop", { type: "content_block_stop", index: 1 }],
|
|
96
|
+
["message_delta", { type: "message_delta", delta: { stop_reason: "tool_use", stop_sequence: null }, usage: { output_tokens: 17 } }],
|
|
97
|
+
["message_stop", { type: "message_stop" }],
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
function sseBody(): string {
|
|
101
|
+
return SSE_EVENTS
|
|
102
|
+
.map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
|
103
|
+
.join("");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Env hygiene ─────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The AnthropicFoundry constructor reads these as option defaults, so a
|
|
110
|
+
* developer machine with a real Foundry setup would silently change what
|
|
111
|
+
* the constructor-contract tests observe. Cleared for every test; the
|
|
112
|
+
* harness passes everything explicitly.
|
|
113
|
+
*/
|
|
114
|
+
const FOUNDRY_ENV_VARS = [
|
|
115
|
+
"ANTHROPIC_FOUNDRY_API_KEY",
|
|
116
|
+
"ANTHROPIC_FOUNDRY_RESOURCE",
|
|
117
|
+
"ANTHROPIC_FOUNDRY_BASE_URL",
|
|
118
|
+
] as const;
|
|
119
|
+
|
|
120
|
+
const savedEnv = new Map<string, string | undefined>();
|
|
121
|
+
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
for (const name of FOUNDRY_ENV_VARS) {
|
|
124
|
+
savedEnv.set(name, process.env[name]);
|
|
125
|
+
delete process.env[name];
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
afterEach(() => {
|
|
130
|
+
for (const name of FOUNDRY_ENV_VARS) {
|
|
131
|
+
const value = savedEnv.get(name);
|
|
132
|
+
if (value === undefined) delete process.env[name];
|
|
133
|
+
else process.env[name] = value;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ─── Harness ─────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
interface RecordedRequest {
|
|
140
|
+
url: string;
|
|
141
|
+
method: string;
|
|
142
|
+
headers: Record<string, string>;
|
|
143
|
+
body: Record<string, unknown>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface SeamHarness {
|
|
147
|
+
model: ChatAnthropic;
|
|
148
|
+
requests: RecordedRequest[];
|
|
149
|
+
/** Options ChatAnthropic passed to the createClient factory. */
|
|
150
|
+
factoryOptions: Array<{ maxRetries?: number }>;
|
|
151
|
+
/** The AnthropicFoundry instances the factory constructed. */
|
|
152
|
+
clients: AnthropicFoundry[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build a real ChatAnthropic wired to a real AnthropicFoundry through the
|
|
157
|
+
* `createClient` seam, with transport replaced by a recording fetch.
|
|
158
|
+
*
|
|
159
|
+
* The factory honors `maxRetries` from the incoming options: LangChain owns
|
|
160
|
+
* retrying (its AsyncCaller wraps every request) and passes `maxRetries: 0`
|
|
161
|
+
* so the underlying SDK must not retry underneath it — same contract the
|
|
162
|
+
* vertex and bedrock seams pin, preserved by the T05 adapter.
|
|
163
|
+
*
|
|
164
|
+
* Auth is either the static test API key or a caller-supplied Entra token
|
|
165
|
+
* provider — the same either/or the production adapter selects between.
|
|
166
|
+
*/
|
|
167
|
+
function buildSeamHarness(auth?: { azureADTokenProvider: () => Promise<string> }): SeamHarness {
|
|
168
|
+
const requests: RecordedRequest[] = [];
|
|
169
|
+
const factoryOptions: SeamHarness["factoryOptions"] = [];
|
|
170
|
+
const clients: AnthropicFoundry[] = [];
|
|
171
|
+
|
|
172
|
+
const recordingFetch: typeof fetch = async (input, init) => {
|
|
173
|
+
const headers: Record<string, string> = {};
|
|
174
|
+
new Headers(init?.headers).forEach((value, key) => {
|
|
175
|
+
headers[key] = value;
|
|
176
|
+
});
|
|
177
|
+
const body = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
|
178
|
+
requests.push({
|
|
179
|
+
url: String(input),
|
|
180
|
+
method: init?.method ?? "GET",
|
|
181
|
+
headers,
|
|
182
|
+
body,
|
|
183
|
+
});
|
|
184
|
+
// One URL serves both modes on Foundry — the body's stream flag picks.
|
|
185
|
+
return body.stream === true
|
|
186
|
+
? new Response(sseBody(), {
|
|
187
|
+
status: 200,
|
|
188
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
189
|
+
})
|
|
190
|
+
: new Response(JSON.stringify(MESSAGE_RESPONSE), {
|
|
191
|
+
status: 200,
|
|
192
|
+
headers: { "Content-Type": "application/json" },
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const model = new ChatAnthropic({
|
|
197
|
+
model: FOUNDRY_DEPLOYMENT,
|
|
198
|
+
temperature: 0,
|
|
199
|
+
maxTokens: 1024,
|
|
200
|
+
createClient: (options) => {
|
|
201
|
+
factoryOptions.push({ maxRetries: options.maxRetries });
|
|
202
|
+
const client = new AnthropicFoundry({
|
|
203
|
+
resource: RESOURCE,
|
|
204
|
+
...(auth ?? { apiKey: API_KEY }),
|
|
205
|
+
fetch: recordingFetch,
|
|
206
|
+
maxRetries: options.maxRetries,
|
|
207
|
+
});
|
|
208
|
+
clients.push(client);
|
|
209
|
+
return client;
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return { model, requests, factoryOptions, clients };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Foundry's one Messages URL: resource-derived host, /anthropic prefix. */
|
|
217
|
+
const EXPECTED_URL =
|
|
218
|
+
`https://${RESOURCE}.services.ai.azure.com/anthropic/v1/messages`;
|
|
219
|
+
|
|
220
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
describe("ChatAnthropic createClient -> AnthropicFoundry seam", () => {
|
|
223
|
+
it("constructs without an Anthropic API key when createClient is provided", () => {
|
|
224
|
+
// chat_models.js waives the "Anthropic API key not found" check for
|
|
225
|
+
// factory-constructed clients — the waiver the foundry backend depends on.
|
|
226
|
+
expect(() => buildSeamHarness()).not.toThrow();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("enforces its config contract in the constructor (what preflight front-runs)", () => {
|
|
230
|
+
// These four throws are the SDK's own; checkFoundryPrerequisites
|
|
231
|
+
// (llm-backend.ts) exists to catch the endpoint half at boot with the
|
|
232
|
+
// catalog message instead of at the first model call. The credential
|
|
233
|
+
// half resolves at construction (key if present, else Entra chain), so
|
|
234
|
+
// the "missing credentials" arm is unreachable in production — pinned
|
|
235
|
+
// here so an SDK contract change surfaces as a failing pin.
|
|
236
|
+
expect(() => new AnthropicFoundry({ resource: RESOURCE })).toThrow(
|
|
237
|
+
/Missing credentials/,
|
|
238
|
+
);
|
|
239
|
+
expect(
|
|
240
|
+
() =>
|
|
241
|
+
new AnthropicFoundry({
|
|
242
|
+
resource: RESOURCE,
|
|
243
|
+
apiKey: API_KEY,
|
|
244
|
+
azureADTokenProvider: async () => "token",
|
|
245
|
+
}),
|
|
246
|
+
).toThrow(/mutually exclusive/);
|
|
247
|
+
expect(() => new AnthropicFoundry({ apiKey: API_KEY })).toThrow(
|
|
248
|
+
/baseURL.*resource|resource.*baseURL/,
|
|
249
|
+
);
|
|
250
|
+
expect(
|
|
251
|
+
() =>
|
|
252
|
+
new AnthropicFoundry({
|
|
253
|
+
apiKey: API_KEY,
|
|
254
|
+
resource: RESOURCE,
|
|
255
|
+
baseURL: "https://example.services.ai.azure.com/anthropic/",
|
|
256
|
+
}),
|
|
257
|
+
).toThrow(/mutually exclusive/);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("shapes a non-streaming request: deployment name in the BODY, key in x-api-key", async () => {
|
|
261
|
+
const h = buildSeamHarness();
|
|
262
|
+
|
|
263
|
+
const result = await h.model.invoke([new HumanMessage("Weather in Chennai?")]);
|
|
264
|
+
|
|
265
|
+
expect(h.requests).toHaveLength(1);
|
|
266
|
+
const req = h.requests[0];
|
|
267
|
+
|
|
268
|
+
// Unlike Vertex/Bedrock, the model field survives in the body — it IS
|
|
269
|
+
// the routing key (the deployment name). The URL never carries it.
|
|
270
|
+
expect(req.url).toBe(EXPECTED_URL);
|
|
271
|
+
expect(req.method).toBe("POST");
|
|
272
|
+
expect(req.body.model).toBe(FOUNDRY_DEPLOYMENT);
|
|
273
|
+
expect(req.body.max_tokens).toBe(1024);
|
|
274
|
+
|
|
275
|
+
// API-key mode authenticates with x-api-key (Entra uses Authorization:
|
|
276
|
+
// Bearer — pinned separately); the standard version header rides along.
|
|
277
|
+
expect(req.headers["x-api-key"]).toBe(API_KEY);
|
|
278
|
+
expect(req.headers.authorization).toBeUndefined();
|
|
279
|
+
expect(req.headers["anthropic-version"]).toBe("2023-06-01");
|
|
280
|
+
|
|
281
|
+
expect(result).toBeInstanceOf(AIMessage);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("round-trips tool definitions and tool_use blocks into tool_calls", async () => {
|
|
285
|
+
const h = buildSeamHarness();
|
|
286
|
+
const withTools = h.model.bindTools([WEATHER_TOOL]);
|
|
287
|
+
|
|
288
|
+
const result = (await withTools.invoke([
|
|
289
|
+
new HumanMessage("Weather in Chennai?"),
|
|
290
|
+
])) as AIMessage;
|
|
291
|
+
|
|
292
|
+
// Tool definition survived request shaping through the Foundry client.
|
|
293
|
+
const tools = h.requests[0].body.tools as Array<{ name: string }>;
|
|
294
|
+
expect(tools).toHaveLength(1);
|
|
295
|
+
expect(tools[0].name).toBe("get_weather");
|
|
296
|
+
|
|
297
|
+
// The tool_use response block became a LangChain tool_call with its id —
|
|
298
|
+
// the identity the HITL approval flow keys on.
|
|
299
|
+
expect(result.tool_calls).toHaveLength(1);
|
|
300
|
+
expect(result.tool_calls?.[0]).toMatchObject({
|
|
301
|
+
id: "toolu_test_01",
|
|
302
|
+
name: "get_weather",
|
|
303
|
+
args: { city: "Chennai" },
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("reports usage_metadata on non-streaming responses (what billing reads)", async () => {
|
|
308
|
+
const h = buildSeamHarness();
|
|
309
|
+
|
|
310
|
+
const result = (await h.model.invoke([
|
|
311
|
+
new HumanMessage("Weather in Chennai?"),
|
|
312
|
+
])) as AIMessage;
|
|
313
|
+
|
|
314
|
+
expect(result.usage_metadata).toMatchObject({
|
|
315
|
+
input_tokens: 25,
|
|
316
|
+
output_tokens: 17,
|
|
317
|
+
total_tokens: 42,
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("streams over the same URL with stream: true, assembling tool_calls and usage from SSE", async () => {
|
|
322
|
+
const h = buildSeamHarness();
|
|
323
|
+
|
|
324
|
+
let final: AIMessageChunk | undefined;
|
|
325
|
+
for await (const chunk of await h.model.stream([
|
|
326
|
+
new HumanMessage("Weather in Chennai?"),
|
|
327
|
+
])) {
|
|
328
|
+
final = final === undefined ? chunk : final.concat(chunk);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
expect(h.requests).toHaveLength(1);
|
|
332
|
+
expect(h.requests[0].url).toBe(EXPECTED_URL);
|
|
333
|
+
expect(h.requests[0].body.stream).toBe(true);
|
|
334
|
+
expect(h.requests[0].body.model).toBe(FOUNDRY_DEPLOYMENT);
|
|
335
|
+
|
|
336
|
+
expect(final).toBeDefined();
|
|
337
|
+
expect(final?.text).toBe("I'll check the weather.");
|
|
338
|
+
expect(final?.tool_calls).toHaveLength(1);
|
|
339
|
+
expect(final?.tool_calls?.[0]).toMatchObject({
|
|
340
|
+
id: "toolu_test_02",
|
|
341
|
+
name: "get_weather",
|
|
342
|
+
args: { city: "Chennai" },
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// input from message_start; output accumulated across message events.
|
|
346
|
+
// Pinned to the observed accumulation (same math the vertex and bedrock
|
|
347
|
+
// seams pin) so a LangChain bump that changes usage accounting (billing
|
|
348
|
+
// input) fails here first.
|
|
349
|
+
expect(final?.usage_metadata).toMatchObject({
|
|
350
|
+
input_tokens: 25,
|
|
351
|
+
output_tokens: 18,
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("passes maxRetries: 0 to the factory and the honored value reaches the client", async () => {
|
|
356
|
+
const h = buildSeamHarness();
|
|
357
|
+
|
|
358
|
+
await h.model.invoke([new HumanMessage("hi")]);
|
|
359
|
+
for await (const chunk of await h.model.stream([new HumanMessage("hi")])) {
|
|
360
|
+
void chunk;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// LangChain owns retrying: it must hand the factory maxRetries: 0, once
|
|
364
|
+
// per cached client (batch + streaming are constructed independently).
|
|
365
|
+
expect(h.factoryOptions).toHaveLength(2);
|
|
366
|
+
expect(h.factoryOptions.every((o) => o.maxRetries === 0)).toBe(true);
|
|
367
|
+
expect(h.clients).toHaveLength(2);
|
|
368
|
+
expect(h.clients.every((c) => c.maxRetries === 0)).toBe(true);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("Entra mode: sends Authorization: Bearer, invoking the provider per request", async () => {
|
|
372
|
+
// The provider must be consulted on EVERY request, never cached by the
|
|
373
|
+
// SDK — Entra tokens expire, and a long-lived runner that cached one
|
|
374
|
+
// would start failing an hour in. Pinned by counting invocations.
|
|
375
|
+
let calls = 0;
|
|
376
|
+
const h = buildSeamHarness({
|
|
377
|
+
azureADTokenProvider: async () => {
|
|
378
|
+
calls += 1;
|
|
379
|
+
return `entra-token-${calls}`;
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await h.model.invoke([new HumanMessage("hi")]);
|
|
384
|
+
await h.model.invoke([new HumanMessage("hi again")]);
|
|
385
|
+
|
|
386
|
+
expect(calls).toBe(2);
|
|
387
|
+
expect(h.requests[0].headers.authorization).toBe("Bearer entra-token-1");
|
|
388
|
+
expect(h.requests[1].headers.authorization).toBe("Bearer entra-token-2");
|
|
389
|
+
expect(h.requests[0].headers["x-api-key"]).toBeUndefined();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("Entra mode: a failing token provider throws the message model-error.ts matches", async () => {
|
|
393
|
+
// The exact prefix is the LLM_BACKEND_CREDENTIALS matcher in
|
|
394
|
+
// model-error.ts (isFoundryCredentialMessage) — it arrives with no HTTP
|
|
395
|
+
// status, so classification must catch it by prose, like the Google ADC
|
|
396
|
+
// and AWS chain failures. If an SDK bump rewords it, this pin fails
|
|
397
|
+
// before the classifier silently degrades.
|
|
398
|
+
const client = new AnthropicFoundry({
|
|
399
|
+
resource: RESOURCE,
|
|
400
|
+
azureADTokenProvider: async () => {
|
|
401
|
+
throw new Error("ManagedIdentityCredential: no managed identity endpoint found");
|
|
402
|
+
},
|
|
403
|
+
maxRetries: 0,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
await expect(
|
|
407
|
+
client.messages.create({
|
|
408
|
+
model: FOUNDRY_DEPLOYMENT,
|
|
409
|
+
max_tokens: 16,
|
|
410
|
+
messages: [{ role: "user", content: "hi" }],
|
|
411
|
+
}),
|
|
412
|
+
).rejects.toThrow(/Failed to get token from azureADTokenProvider/);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("withStructuredOutput compiles to forced tool use — no native structured-output param", async () => {
|
|
416
|
+
// Load-bearing for the hosted-on-Azure deployment mode: Foundry
|
|
417
|
+
// deployments hosted on Azure return 400 for Anthropic's NATIVE
|
|
418
|
+
// structured-output feature, but plain tool use is fully supported.
|
|
419
|
+
// LangChain implements withStructuredOutput as tools + tool_choice, so
|
|
420
|
+
// every runner call site (call-llm, classify-tool-approvals, Cursor
|
|
421
|
+
// extraction) works on either hosting mode. If a LangChain bump swaps
|
|
422
|
+
// the implementation to the native parameter, this pin fails and the
|
|
423
|
+
// operator doc's hosting-mode guidance must be revisited.
|
|
424
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
425
|
+
const structuredFetch: typeof fetch = async (_input, init) => {
|
|
426
|
+
const body = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
|
427
|
+
requests.push(body);
|
|
428
|
+
return new Response(
|
|
429
|
+
JSON.stringify({
|
|
430
|
+
...MESSAGE_RESPONSE,
|
|
431
|
+
content: [{
|
|
432
|
+
type: "tool_use",
|
|
433
|
+
id: "toolu_so_01",
|
|
434
|
+
name: "record_weather",
|
|
435
|
+
input: { city: "Chennai", temperature_c: 31 },
|
|
436
|
+
}],
|
|
437
|
+
}),
|
|
438
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
439
|
+
);
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const model = new ChatAnthropic({
|
|
443
|
+
model: FOUNDRY_DEPLOYMENT,
|
|
444
|
+
temperature: 0,
|
|
445
|
+
maxTokens: 1024,
|
|
446
|
+
createClient: (options) =>
|
|
447
|
+
new AnthropicFoundry({
|
|
448
|
+
resource: RESOURCE,
|
|
449
|
+
apiKey: API_KEY,
|
|
450
|
+
fetch: structuredFetch,
|
|
451
|
+
maxRetries: options.maxRetries,
|
|
452
|
+
}),
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const structured = model.withStructuredOutput(
|
|
456
|
+
{
|
|
457
|
+
type: "object",
|
|
458
|
+
properties: {
|
|
459
|
+
city: { type: "string" },
|
|
460
|
+
temperature_c: { type: "number" },
|
|
461
|
+
},
|
|
462
|
+
required: ["city", "temperature_c"],
|
|
463
|
+
},
|
|
464
|
+
{ name: "record_weather" },
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const result = await structured.invoke([new HumanMessage("Weather in Chennai?")]);
|
|
468
|
+
|
|
469
|
+
expect(requests).toHaveLength(1);
|
|
470
|
+
const body = requests[0];
|
|
471
|
+
// The schema rides as a tool with a forced tool_choice…
|
|
472
|
+
const tools = body.tools as Array<{ name: string }>;
|
|
473
|
+
expect(tools).toHaveLength(1);
|
|
474
|
+
expect(tools[0].name).toBe("record_weather");
|
|
475
|
+
expect(body.tool_choice).toMatchObject({ type: "tool", name: "record_weather" });
|
|
476
|
+
// …and never as the native structured-output request fields.
|
|
477
|
+
expect(body).not.toHaveProperty("output_format");
|
|
478
|
+
expect(body).not.toHaveProperty("response_format");
|
|
479
|
+
|
|
480
|
+
expect(result).toMatchObject({ city: "Chennai", temperature_c: 31 });
|
|
481
|
+
});
|
|
482
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { isRetryableHttpStatus, isRetryableFetchError } from "../http-retry.js";
|
|
3
|
+
|
|
4
|
+
// The bounded-backoff loop that consumes these classifiers lives in
|
|
5
|
+
// checkpointer/http-saver.ts (fetchWithRetry) and is exercised by
|
|
6
|
+
// http-saver.test.ts ("retry behavior"). This file covers the classification
|
|
7
|
+
// policy in isolation — the grpc-retry.test.ts twin.
|
|
8
|
+
|
|
9
|
+
describe("isRetryableHttpStatus", () => {
|
|
10
|
+
it("returns true for 408 (request timeout)", () => {
|
|
11
|
+
expect(isRetryableHttpStatus(408)).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns true for 429 (throttled)", () => {
|
|
15
|
+
expect(isRetryableHttpStatus(429)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it.each([500, 502, 503, 504])("returns true for %i", (status) => {
|
|
19
|
+
expect(isRetryableHttpStatus(status)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// The four deterministic errors CheckpointerProxyController actually emits:
|
|
23
|
+
// 400 malformed, 403 FGA deny, 404 missing, 413 over the 4 MB cap. Retrying
|
|
24
|
+
// any of them would repeat the same failure (and 404 carries semantics —
|
|
25
|
+
// getTuple maps it to "no checkpoint yet").
|
|
26
|
+
it.each([400, 401, 403, 404, 413])("returns false for deterministic %i", (status) => {
|
|
27
|
+
expect(isRetryableHttpStatus(status)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns false for success statuses", () => {
|
|
31
|
+
expect(isRetryableHttpStatus(200)).toBe(false);
|
|
32
|
+
expect(isRetryableHttpStatus(204)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("isRetryableFetchError", () => {
|
|
37
|
+
it("returns true for undici's network-failure shape (TypeError: fetch failed)", () => {
|
|
38
|
+
const err = new TypeError("fetch failed");
|
|
39
|
+
(err as TypeError & { cause?: unknown }).cause = new Error("connect ECONNREFUSED");
|
|
40
|
+
expect(isRetryableFetchError(err)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns true for a bare TypeError (no cause)", () => {
|
|
44
|
+
expect(isRetryableFetchError(new TypeError("fetch failed"))).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns true for AbortSignal.timeout's rejection shape (TimeoutError)", () => {
|
|
48
|
+
// DOMException with name "TimeoutError" is what AbortSignal.timeout()
|
|
49
|
+
// produces; construct the real shape rather than a look-alike.
|
|
50
|
+
const err = new DOMException("The operation was aborted due to timeout", "TimeoutError");
|
|
51
|
+
expect(isRetryableFetchError(err)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns false for a manual abort (AbortError) — the caller cancelled on purpose", () => {
|
|
55
|
+
const err = new DOMException("This operation was aborted", "AbortError");
|
|
56
|
+
expect(isRetryableFetchError(err)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns false for a generic Error", () => {
|
|
60
|
+
expect(isRetryableFetchError(new Error("random"))).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns false for non-Error values", () => {
|
|
64
|
+
expect(isRetryableFetchError("boom")).toBe(false);
|
|
65
|
+
expect(isRetryableFetchError(undefined)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|