@copilotkit/runtime 1.56.2 → 1.56.4
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/dist/graphql/resolvers/copilot.resolver.cjs +2 -1
- package/dist/graphql/resolvers/copilot.resolver.cjs.map +1 -1
- package/dist/graphql/resolvers/copilot.resolver.mjs +2 -1
- package/dist/graphql/resolvers/copilot.resolver.mjs.map +1 -1
- package/dist/graphql/resolvers/resolve-message-id.cjs +19 -0
- package/dist/graphql/resolvers/resolve-message-id.cjs.map +1 -0
- package/dist/graphql/resolvers/resolve-message-id.mjs +18 -0
- package/dist/graphql/resolvers/resolve-message-id.mjs.map +1 -0
- package/dist/lib/runtime/copilot-runtime.cjs +4 -2
- package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.mjs +4 -2
- package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
- package/dist/package.cjs +2 -2
- package/dist/package.mjs +2 -2
- package/dist/v2/index.d.cts +2 -2
- package/dist/v2/index.d.mts +2 -2
- package/dist/v2/runtime/core/debug-event-bus.cjs +36 -0
- package/dist/v2/runtime/core/debug-event-bus.cjs.map +1 -0
- package/dist/v2/runtime/core/debug-event-bus.d.cts +19 -0
- package/dist/v2/runtime/core/debug-event-bus.d.cts.map +1 -0
- package/dist/v2/runtime/core/debug-event-bus.d.mts +19 -0
- package/dist/v2/runtime/core/debug-event-bus.d.mts.map +1 -0
- package/dist/v2/runtime/core/debug-event-bus.mjs +35 -0
- package/dist/v2/runtime/core/debug-event-bus.mjs.map +1 -0
- package/dist/v2/runtime/core/fetch-handler.cjs +8 -0
- package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
- package/dist/v2/runtime/core/fetch-handler.mjs +8 -0
- package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.cjs +1 -0
- package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
- package/dist/v2/runtime/core/fetch-router.mjs +1 -0
- package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
- package/dist/v2/runtime/core/hooks.cjs.map +1 -1
- package/dist/v2/runtime/core/hooks.d.cts +2 -0
- package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
- package/dist/v2/runtime/core/hooks.d.mts +2 -0
- package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
- package/dist/v2/runtime/core/hooks.mjs.map +1 -1
- package/dist/v2/runtime/core/runtime.cjs +5 -0
- package/dist/v2/runtime/core/runtime.cjs.map +1 -1
- package/dist/v2/runtime/core/runtime.d.cts +5 -0
- package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
- package/dist/v2/runtime/core/runtime.d.mts +5 -1
- package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
- package/dist/v2/runtime/core/runtime.mjs +5 -0
- package/dist/v2/runtime/core/runtime.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.cjs +3 -2
- package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.mjs +3 -2
- package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-debug-events.cjs +33 -0
- package/dist/v2/runtime/handlers/handle-debug-events.cjs.map +1 -0
- package/dist/v2/runtime/handlers/handle-debug-events.mjs +32 -0
- package/dist/v2/runtime/handlers/handle-debug-events.mjs.map +1 -0
- package/dist/v2/runtime/handlers/handle-run.cjs +1 -0
- package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-run.mjs +1 -0
- package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/connect.cjs +24 -4
- package/dist/v2/runtime/handlers/intelligence/connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/connect.mjs +25 -5
- package/dist/v2/runtime/handlers/intelligence/connect.mjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/run.cjs +111 -26
- package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/run.mjs +111 -26
- package/dist/v2/runtime/handlers/intelligence/run.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/intelligence-utils.cjs +7 -3
- package/dist/v2/runtime/handlers/shared/intelligence-utils.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/intelligence-utils.mjs +7 -3
- package/dist/v2/runtime/handlers/shared/intelligence-utils.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs +5 -1
- package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs +5 -1
- package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/sse-response.cjs +21 -1
- package/dist/v2/runtime/handlers/shared/sse-response.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/sse-response.mjs +21 -1
- package/dist/v2/runtime/handlers/shared/sse-response.mjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/connect.cjs +3 -1
- package/dist/v2/runtime/handlers/sse/connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/connect.mjs +3 -1
- package/dist/v2/runtime/handlers/sse/connect.mjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/run.cjs +3 -1
- package/dist/v2/runtime/handlers/sse/run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/run.mjs +3 -1
- package/dist/v2/runtime/handlers/sse/run.mjs.map +1 -1
- package/dist/v2/runtime/index.d.cts +1 -1
- package/dist/v2/runtime/index.d.mts +1 -2
- package/dist/v2/runtime/index.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.cjs +6 -8
- package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.cts +16 -21
- package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.mts +16 -21
- package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.mjs +6 -8
- package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
- package/dist/v2/runtime/runner/agent-runner.cjs.map +1 -1
- package/dist/v2/runtime/runner/agent-runner.d.cts +0 -1
- package/dist/v2/runtime/runner/agent-runner.d.cts.map +1 -1
- package/dist/v2/runtime/runner/agent-runner.d.mts +0 -1
- package/dist/v2/runtime/runner/agent-runner.d.mts.map +1 -1
- package/dist/v2/runtime/runner/agent-runner.mjs.map +1 -1
- package/dist/v2/runtime/runner/index.d.cts +1 -1
- package/dist/v2/runtime/runner/index.d.mts +1 -1
- package/dist/v2/runtime/runner/intelligence.cjs +47 -10
- package/dist/v2/runtime/runner/intelligence.cjs.map +1 -1
- package/dist/v2/runtime/runner/intelligence.d.cts +8 -1
- package/dist/v2/runtime/runner/intelligence.d.cts.map +1 -1
- package/dist/v2/runtime/runner/intelligence.d.mts +8 -1
- package/dist/v2/runtime/runner/intelligence.d.mts.map +1 -1
- package/dist/v2/runtime/runner/intelligence.mjs +47 -10
- package/dist/v2/runtime/runner/intelligence.mjs.map +1 -1
- package/dist/v2/runtime/telemetry/instance-created.cjs +33 -0
- package/dist/v2/runtime/telemetry/instance-created.cjs.map +1 -0
- package/dist/v2/runtime/telemetry/instance-created.mjs +33 -0
- package/dist/v2/runtime/telemetry/instance-created.mjs.map +1 -0
- package/dist/v2/runtime/telemetry/telemetry-client.cjs +1 -38
- package/dist/v2/runtime/telemetry/telemetry-client.cjs.map +1 -1
- package/dist/v2/runtime/telemetry/telemetry-client.mjs +1 -37
- package/dist/v2/runtime/telemetry/telemetry-client.mjs.map +1 -1
- package/package.json +3 -3
- package/src/agents/langgraph/__tests__/event-source.test.ts +256 -0
- package/src/graphql/resolvers/__tests__/resolve-message-id.test.ts +25 -0
- package/src/graphql/resolvers/copilot.resolver.ts +2 -1
- package/src/graphql/resolvers/resolve-message-id.ts +14 -0
- package/src/lib/runtime/__tests__/handle-service-adapter.test.ts +108 -0
- package/src/lib/runtime/__tests__/retry-utils.test.ts +137 -0
- package/src/lib/runtime/agent-integrations/langgraph/__tests__/dispatch-event-filtering.test.ts +190 -0
- package/src/lib/runtime/copilot-runtime.ts +20 -4
- package/src/lib/runtime/retry-utils.ts +41 -1
- package/src/v2/runtime/__tests__/express-single-telemetry.integration.test.ts +65 -0
- package/src/v2/runtime/__tests__/express-telemetry.integration.test.ts +101 -0
- package/src/v2/runtime/__tests__/fetch-router.test.ts +22 -0
- package/src/v2/runtime/__tests__/handle-connect.test.ts +183 -23
- package/src/v2/runtime/__tests__/handle-run.test.ts +411 -33
- package/src/v2/runtime/__tests__/handle-threads.test.ts +66 -4
- package/src/v2/runtime/__tests__/hono-single-telemetry.integration.test.ts +46 -0
- package/src/v2/runtime/__tests__/hono-telemetry.integration.test.ts +99 -0
- package/src/v2/runtime/__tests__/integration/node-servers.integration.test.ts +19 -0
- package/src/v2/runtime/__tests__/integration/suites/debug-events.suite.ts +253 -0
- package/src/v2/runtime/__tests__/intelligence-run-telemetry.test.ts +194 -0
- package/src/v2/runtime/__tests__/runtime.test.ts +3 -1
- package/src/v2/runtime/__tests__/sse-response-telemetry.test.ts +108 -0
- package/src/v2/runtime/__tests__/telemetry.test.ts +0 -61
- package/src/v2/runtime/core/__tests__/debug-event-bus.test.ts +156 -0
- package/src/v2/runtime/core/debug-event-bus.ts +45 -0
- package/src/v2/runtime/core/fetch-handler.ts +7 -0
- package/src/v2/runtime/core/fetch-router.ts +11 -0
- package/src/v2/runtime/core/hooks.ts +2 -1
- package/src/v2/runtime/core/runtime.ts +12 -0
- package/src/v2/runtime/handlers/__tests__/handle-debug-events.test.ts +176 -0
- package/src/v2/runtime/handlers/handle-connect.ts +2 -1
- package/src/v2/runtime/handlers/handle-debug-events.ts +52 -0
- package/src/v2/runtime/handlers/handle-run.ts +1 -0
- package/src/v2/runtime/handlers/intelligence/connect.ts +48 -11
- package/src/v2/runtime/handlers/intelligence/run.ts +162 -21
- package/src/v2/runtime/handlers/shared/intelligence-utils.ts +21 -1
- package/src/v2/runtime/handlers/shared/resolve-intelligence-user.ts +4 -1
- package/src/v2/runtime/handlers/shared/sse-response.ts +46 -0
- package/src/v2/runtime/handlers/sse/__tests__/sse-connect-agent-id.test.ts +71 -0
- package/src/v2/runtime/handlers/sse/connect.ts +6 -0
- package/src/v2/runtime/handlers/sse/run.ts +4 -0
- package/src/v2/runtime/intelligence-platform/__tests__/client.test.ts +33 -37
- package/src/v2/runtime/intelligence-platform/client.ts +37 -40
- package/src/v2/runtime/runner/__tests__/intelligence-runner.test.ts +66 -8
- package/src/v2/runtime/runner/agent-runner.ts +0 -1
- package/src/v2/runtime/runner/intelligence.ts +74 -15
- package/src/v2/runtime/telemetry/__tests__/instance-created.test.ts +96 -0
- package/src/v2/runtime/telemetry/instance-created.ts +44 -0
- package/src/v2/runtime/telemetry/telemetry-client.ts +1 -57
- package/dist/v2/runtime/intelligence-platform/index.d.mts +0 -2
- package/dist/v2/runtime/telemetry/utils.cjs +0 -15
- package/dist/v2/runtime/telemetry/utils.cjs.map +0 -1
- package/dist/v2/runtime/telemetry/utils.mjs +0 -14
- package/dist/v2/runtime/telemetry/utils.mjs.map +0 -1
- package/src/v2/runtime/telemetry/utils.ts +0 -15
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry-client.mjs","names":["
|
|
1
|
+
{"version":3,"file":"telemetry-client.mjs","names":["scarfClient"],"sources":["../../../../src/v2/runtime/telemetry/telemetry-client.ts"],"sourcesContent":["import { AnalyticsEvents } from \"./events\";\nimport scarfClient from \"./scarf-client\";\n\nexport function isTelemetryDisabled(): boolean {\n return (\n (process.env as Record<string, string | undefined>)\n .COPILOTKIT_TELEMETRY_DISABLED === \"true\" ||\n (process.env as Record<string, string | undefined>)\n .COPILOTKIT_TELEMETRY_DISABLED === \"1\" ||\n (process.env as Record<string, string | undefined>).DO_NOT_TRACK ===\n \"true\" ||\n (process.env as Record<string, string | undefined>).DO_NOT_TRACK === \"1\"\n );\n}\n\nexport class TelemetryClient {\n private telemetryDisabled: boolean = false;\n private sampleRate: number = 0.05;\n\n constructor({\n telemetryDisabled,\n sampleRate,\n }: {\n telemetryDisabled?: boolean;\n sampleRate?: number;\n } = {}) {\n this.telemetryDisabled = telemetryDisabled ?? isTelemetryDisabled();\n this.setSampleRate(sampleRate);\n }\n\n private shouldSendEvent() {\n if (this.telemetryDisabled) {\n return false;\n }\n const randomNumber = Math.random();\n return randomNumber < this.sampleRate;\n }\n\n async capture<K extends keyof AnalyticsEvents>(\n event: K,\n _properties: AnalyticsEvents[K],\n ) {\n if (!this.shouldSendEvent()) {\n return;\n }\n\n await scarfClient.logEvent({\n event,\n });\n }\n\n private setSampleRate(sampleRate: number | undefined) {\n let _sampleRate: number;\n\n _sampleRate = sampleRate ?? 0.05;\n\n if (process.env.COPILOTKIT_TELEMETRY_SAMPLE_RATE) {\n _sampleRate = parseFloat(process.env.COPILOTKIT_TELEMETRY_SAMPLE_RATE);\n }\n\n if (_sampleRate < 0 || _sampleRate > 1) {\n throw new Error(\"Sample rate must be between 0 and 1\");\n }\n\n this.sampleRate = _sampleRate;\n }\n}\n\nconst telemetry = new TelemetryClient();\nexport default telemetry;\n"],"mappings":";;;;AAGA,SAAgB,sBAA+B;AAC7C,QACG,QAAQ,IACN,kCAAkC,UACpC,QAAQ,IACN,kCAAkC,OACpC,QAAQ,IAA2C,iBAClD,UACD,QAAQ,IAA2C,iBAAiB;;AAIzE,IAAa,kBAAb,MAA6B;CAI3B,YAAY,EACV,mBACA,eAIE,EAAE,EAAE;2BAT6B;oBACR;AAS3B,OAAK,oBAAoB,qBAAqB,qBAAqB;AACnE,OAAK,cAAc,WAAW;;CAGhC,AAAQ,kBAAkB;AACxB,MAAI,KAAK,kBACP,QAAO;AAGT,SADqB,KAAK,QAAQ,GACZ,KAAK;;CAG7B,MAAM,QACJ,OACA,aACA;AACA,MAAI,CAAC,KAAK,iBAAiB,CACzB;AAGF,QAAMA,qBAAY,SAAS,EACzB,OACD,CAAC;;CAGJ,AAAQ,cAAc,YAAgC;EACpD,IAAI;AAEJ,gBAAc,cAAc;AAE5B,MAAI,QAAQ,IAAI,iCACd,eAAc,WAAW,QAAQ,IAAI,iCAAiC;AAGxE,MAAI,cAAc,KAAK,cAAc,EACnC,OAAM,IAAI,MAAM,sCAAsC;AAGxD,OAAK,aAAa;;;AAItB,MAAM,YAAY,IAAI,iBAAiB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/runtime",
|
|
3
|
-
"version": "1.56.
|
|
3
|
+
"version": "1.56.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"@ai-sdk/google-vertex": "^3.0.97",
|
|
88
88
|
"@ai-sdk/mcp": "^1.0.21",
|
|
89
89
|
"@ai-sdk/openai": "^3.0.36",
|
|
90
|
-
"@copilotkit/license-verifier": "0.0
|
|
90
|
+
"@copilotkit/license-verifier": "0.2.0",
|
|
91
91
|
"@graphql-yoga/plugin-defer-stream": "^3.3.1",
|
|
92
92
|
"@hono/node-server": "^1.13.5",
|
|
93
93
|
"@modelcontextprotocol/sdk": "^1.18.2",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"uuid": "^10.0.0",
|
|
116
116
|
"ws": "^8.18.0",
|
|
117
117
|
"zod": "^3.23.3",
|
|
118
|
-
"@copilotkit/shared": "1.56.
|
|
118
|
+
"@copilotkit/shared": "1.56.4"
|
|
119
119
|
},
|
|
120
120
|
"devDependencies": {
|
|
121
121
|
"@copilotkit/aimock": "^1.10.0",
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { RemoteLangGraphEventSource } from "../event-source";
|
|
2
|
+
import { LangGraphEventTypes } from "../events";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Access private methods for testing via a helper.
|
|
6
|
+
* These are pure functions that extract data from LangGraph events.
|
|
7
|
+
*/
|
|
8
|
+
function getSource() {
|
|
9
|
+
return new RemoteLangGraphEventSource();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Helper to call private methods. Returns `any` — type safety is traded for
|
|
13
|
+
// access to private implementation details that have no public test surface.
|
|
14
|
+
function callPrivate(
|
|
15
|
+
source: RemoteLangGraphEventSource,
|
|
16
|
+
method: string,
|
|
17
|
+
...args: any[]
|
|
18
|
+
) {
|
|
19
|
+
return (source as any)[method](...args);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("shouldEmitToolCall", () => {
|
|
23
|
+
const source = getSource();
|
|
24
|
+
|
|
25
|
+
it("returns true when shouldEmitToolCalls is true (boolean)", () => {
|
|
26
|
+
expect(callPrivate(source, "shouldEmitToolCall", true, "anyTool")).toBe(
|
|
27
|
+
true,
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns false when shouldEmitToolCalls is false (boolean)", () => {
|
|
32
|
+
expect(callPrivate(source, "shouldEmitToolCall", false, "anyTool")).toBe(
|
|
33
|
+
false,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns true when tool name matches string", () => {
|
|
38
|
+
expect(
|
|
39
|
+
callPrivate(source, "shouldEmitToolCall", "SearchTool", "SearchTool"),
|
|
40
|
+
).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns false when tool name does not match string", () => {
|
|
44
|
+
expect(
|
|
45
|
+
callPrivate(source, "shouldEmitToolCall", "SearchTool", "OtherTool"),
|
|
46
|
+
).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns true when tool name is in array", () => {
|
|
50
|
+
expect(
|
|
51
|
+
callPrivate(
|
|
52
|
+
source,
|
|
53
|
+
"shouldEmitToolCall",
|
|
54
|
+
["SearchTool", "FetchTool"],
|
|
55
|
+
"FetchTool",
|
|
56
|
+
),
|
|
57
|
+
).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns false when tool name is not in array", () => {
|
|
61
|
+
expect(
|
|
62
|
+
callPrivate(
|
|
63
|
+
source,
|
|
64
|
+
"shouldEmitToolCall",
|
|
65
|
+
["SearchTool", "FetchTool"],
|
|
66
|
+
"OtherTool",
|
|
67
|
+
),
|
|
68
|
+
).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("getCurrentMessageId", () => {
|
|
73
|
+
const source = getSource();
|
|
74
|
+
|
|
75
|
+
it("extracts id from standard kwargs layout", () => {
|
|
76
|
+
const event = {
|
|
77
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
78
|
+
data: { chunk: { kwargs: { id: "msg-std-123" } } },
|
|
79
|
+
};
|
|
80
|
+
expect(callPrivate(source, "getCurrentMessageId", event)).toBe(
|
|
81
|
+
"msg-std-123",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("extracts id from LangGraph Platform layout (no kwargs)", () => {
|
|
86
|
+
const event = {
|
|
87
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
88
|
+
data: { chunk: { id: "msg-plat-456" } },
|
|
89
|
+
};
|
|
90
|
+
expect(callPrivate(source, "getCurrentMessageId", event)).toBe(
|
|
91
|
+
"msg-plat-456",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("prefers kwargs layout when both present", () => {
|
|
96
|
+
const event = {
|
|
97
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
98
|
+
data: { chunk: { kwargs: { id: "kwargs-id" }, id: "direct-id" } },
|
|
99
|
+
};
|
|
100
|
+
expect(callPrivate(source, "getCurrentMessageId", event)).toBe("kwargs-id");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns undefined when neither layout is present", () => {
|
|
104
|
+
const event = {
|
|
105
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
106
|
+
data: { chunk: {} },
|
|
107
|
+
};
|
|
108
|
+
expect(callPrivate(source, "getCurrentMessageId", event)).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("handles missing data gracefully", () => {
|
|
112
|
+
const event = { event: LangGraphEventTypes.OnChatModelStream };
|
|
113
|
+
expect(callPrivate(source, "getCurrentMessageId", event)).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("getCurrentToolCallChunks", () => {
|
|
118
|
+
const source = getSource();
|
|
119
|
+
|
|
120
|
+
it("extracts chunks from standard kwargs layout", () => {
|
|
121
|
+
const chunks = [{ name: "tool1", args: "{}", id: "tc-1", index: 0 }];
|
|
122
|
+
const event = {
|
|
123
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
124
|
+
data: { chunk: { kwargs: { tool_call_chunks: chunks } } },
|
|
125
|
+
};
|
|
126
|
+
expect(callPrivate(source, "getCurrentToolCallChunks", event)).toEqual(
|
|
127
|
+
chunks,
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("extracts chunks from LangGraph Platform layout", () => {
|
|
132
|
+
const chunks = [{ name: "tool2", args: "{}", id: "tc-2", index: 0 }];
|
|
133
|
+
const event = {
|
|
134
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
135
|
+
data: { chunk: { tool_call_chunks: chunks } },
|
|
136
|
+
};
|
|
137
|
+
expect(callPrivate(source, "getCurrentToolCallChunks", event)).toEqual(
|
|
138
|
+
chunks,
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns undefined when no chunks present", () => {
|
|
143
|
+
const event = {
|
|
144
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
145
|
+
data: { chunk: {} },
|
|
146
|
+
};
|
|
147
|
+
expect(
|
|
148
|
+
callPrivate(source, "getCurrentToolCallChunks", event),
|
|
149
|
+
).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("getResponseMetadata", () => {
|
|
154
|
+
const source = getSource();
|
|
155
|
+
|
|
156
|
+
it("extracts metadata from standard kwargs layout", () => {
|
|
157
|
+
const meta = { finish_reason: "stop" };
|
|
158
|
+
const event = {
|
|
159
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
160
|
+
data: { chunk: { kwargs: { response_metadata: meta } } },
|
|
161
|
+
};
|
|
162
|
+
expect(callPrivate(source, "getResponseMetadata", event)).toEqual(meta);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("extracts metadata from LangGraph Platform layout", () => {
|
|
166
|
+
const meta = { finish_reason: "tool_calls" };
|
|
167
|
+
const event = {
|
|
168
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
169
|
+
data: { chunk: { response_metadata: meta } },
|
|
170
|
+
};
|
|
171
|
+
expect(callPrivate(source, "getResponseMetadata", event)).toEqual(meta);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("returns undefined when no metadata present", () => {
|
|
175
|
+
const event = {
|
|
176
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
177
|
+
data: { chunk: {} },
|
|
178
|
+
};
|
|
179
|
+
expect(callPrivate(source, "getResponseMetadata", event)).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("getCurrentContent", () => {
|
|
184
|
+
const source = getSource();
|
|
185
|
+
|
|
186
|
+
it("extracts string content from kwargs layout", () => {
|
|
187
|
+
const event = {
|
|
188
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
189
|
+
data: { chunk: { kwargs: { content: "hello world" } } },
|
|
190
|
+
};
|
|
191
|
+
expect(callPrivate(source, "getCurrentContent", event)).toBe("hello world");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("extracts string content from Platform layout", () => {
|
|
195
|
+
const event = {
|
|
196
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
197
|
+
data: { chunk: { content: "platform content" } },
|
|
198
|
+
};
|
|
199
|
+
expect(callPrivate(source, "getCurrentContent", event)).toBe(
|
|
200
|
+
"platform content",
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("extracts text from array content (Anthropic format)", () => {
|
|
205
|
+
const event = {
|
|
206
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
207
|
+
data: {
|
|
208
|
+
chunk: {
|
|
209
|
+
kwargs: {
|
|
210
|
+
content: [{ text: "array text", type: "text", index: 0 }],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
expect(callPrivate(source, "getCurrentContent", event)).toBe("array text");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("returns null when no content and no tool call chunks", () => {
|
|
219
|
+
const event = {
|
|
220
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
221
|
+
data: { chunk: { kwargs: {} } },
|
|
222
|
+
};
|
|
223
|
+
expect(callPrivate(source, "getCurrentContent", event)).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("falls back to tool_call_chunks args when no content", () => {
|
|
227
|
+
const event = {
|
|
228
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
229
|
+
data: {
|
|
230
|
+
chunk: {
|
|
231
|
+
kwargs: {
|
|
232
|
+
content: "",
|
|
233
|
+
tool_call_chunks: [{ args: '{"key":"val"}' }],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
expect(callPrivate(source, "getCurrentContent", event)).toBe(
|
|
239
|
+
'{"key":"val"}',
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("handles missing data gracefully", () => {
|
|
244
|
+
const event = { event: LangGraphEventTypes.OnChatModelStream };
|
|
245
|
+
expect(callPrivate(source, "getCurrentContent", event)).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("returns empty string when content is empty string", () => {
|
|
249
|
+
const event = {
|
|
250
|
+
event: LangGraphEventTypes.OnChatModelStream,
|
|
251
|
+
data: { chunk: { kwargs: { content: "" } } },
|
|
252
|
+
};
|
|
253
|
+
// Empty string is a valid string, so typeof === "string" returns it as-is
|
|
254
|
+
expect(callPrivate(source, "getCurrentContent", event)).toBe("");
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveMessageId } from "../resolve-message-id";
|
|
3
|
+
|
|
4
|
+
describe("resolveMessageId (#2118)", () => {
|
|
5
|
+
it("preserves a provided non-empty id verbatim", () => {
|
|
6
|
+
expect(resolveMessageId("msg-123")).toBe("msg-123");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it.each(["", null, undefined] as const)(
|
|
10
|
+
"falls back to a generated id when the event id is %p",
|
|
11
|
+
(input) => {
|
|
12
|
+
const id = resolveMessageId(input);
|
|
13
|
+
// randomId() always produces the "ck-<uuid>" shape; the important
|
|
14
|
+
// contract for #2118 is that the returned value is a non-empty string,
|
|
15
|
+
// never null/undefined.
|
|
16
|
+
expect(id).toMatch(/^ck-[0-9a-f-]{36}$/);
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
it("generates a fresh id on each fallback call", () => {
|
|
21
|
+
const a = resolveMessageId(undefined);
|
|
22
|
+
const b = resolveMessageId(undefined);
|
|
23
|
+
expect(a).not.toBe(b);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
} from "../types/converted";
|
|
59
59
|
import telemetry from "../../lib/telemetry-client";
|
|
60
60
|
import { randomId } from "@copilotkit/shared";
|
|
61
|
+
import { resolveMessageId } from "./resolve-message-id";
|
|
61
62
|
import { AgentsResponse } from "../types/agents-response.type";
|
|
62
63
|
import { LangGraphEventTypes } from "../../agents/langgraph/events";
|
|
63
64
|
import {
|
|
@@ -488,7 +489,7 @@ export class CopilotResolver {
|
|
|
488
489
|
typeof MessageStatusUnion
|
|
489
490
|
>();
|
|
490
491
|
|
|
491
|
-
const messageId = event.messageId;
|
|
492
|
+
const messageId = resolveMessageId(event.messageId);
|
|
492
493
|
// push the new message
|
|
493
494
|
pushMessage({
|
|
494
495
|
id: messageId,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { randomId } from "@copilotkit/shared";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the id to use for a streamed TextMessageOutput.
|
|
5
|
+
*
|
|
6
|
+
* Upstream events occasionally arrive with `messageId` missing, null, or an
|
|
7
|
+
* empty string. We fall back to a freshly generated id in those cases so the
|
|
8
|
+
* resulting output never surfaces a null id to the GraphQL client (#2118).
|
|
9
|
+
*/
|
|
10
|
+
export function resolveMessageId(
|
|
11
|
+
eventMessageId: string | null | undefined,
|
|
12
|
+
): string {
|
|
13
|
+
return eventMessageId || randomId();
|
|
14
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { LanguageModel } from "ai";
|
|
2
|
+
import { CopilotKitMisuseError } from "@copilotkit/shared";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { BuiltInAgent } from "../../../agent";
|
|
5
|
+
import type { CopilotServiceAdapter } from "../../../service-adapters";
|
|
6
|
+
import { CopilotRuntime } from "../copilot-runtime";
|
|
7
|
+
|
|
8
|
+
function makeAdapter(
|
|
9
|
+
overrides?: Partial<CopilotServiceAdapter>,
|
|
10
|
+
): CopilotServiceAdapter {
|
|
11
|
+
return {
|
|
12
|
+
name: "TestAdapter",
|
|
13
|
+
async process() {
|
|
14
|
+
throw new Error("process() is not expected to be called in these tests");
|
|
15
|
+
},
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function getDefaultAgent(runtime: CopilotRuntime) {
|
|
21
|
+
const agents = await runtime.instance.agents;
|
|
22
|
+
return agents.default;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// `BuiltInAgent.config` is private; reading it is the only way to verify the
|
|
26
|
+
// correct model was passed through without running the entire agent pipeline.
|
|
27
|
+
// This narrow accessor is the Rule 2 exception, documented here once rather
|
|
28
|
+
// than inline at each call site.
|
|
29
|
+
function getBuiltInAgentModel(agent: BuiltInAgent): unknown {
|
|
30
|
+
return (agent as unknown as { config: { model: unknown } }).config.model;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("CopilotRuntime#handleServiceAdapter (#3217)", () => {
|
|
34
|
+
it("uses the adapter's pre-configured LanguageModel when getLanguageModel() returns one", async () => {
|
|
35
|
+
const fakeLanguageModel = {
|
|
36
|
+
specificationVersion: "v1",
|
|
37
|
+
} as unknown as LanguageModel;
|
|
38
|
+
const runtime = new CopilotRuntime();
|
|
39
|
+
|
|
40
|
+
runtime.handleServiceAdapter(
|
|
41
|
+
makeAdapter({
|
|
42
|
+
name: "OpenAIAdapter",
|
|
43
|
+
provider: "openai",
|
|
44
|
+
model: "gpt-4o",
|
|
45
|
+
getLanguageModel: () => fakeLanguageModel,
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const agent = await getDefaultAgent(runtime);
|
|
50
|
+
expect(agent).toBeInstanceOf(BuiltInAgent);
|
|
51
|
+
expect(getBuiltInAgentModel(agent as BuiltInAgent)).toBe(fakeLanguageModel);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("builds a 'provider/model' string when only provider+model are exposed", async () => {
|
|
55
|
+
const runtime = new CopilotRuntime();
|
|
56
|
+
|
|
57
|
+
runtime.handleServiceAdapter(
|
|
58
|
+
makeAdapter({
|
|
59
|
+
name: "GroqAdapter",
|
|
60
|
+
provider: "groq",
|
|
61
|
+
model: "llama-3.3-70b-versatile",
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const agent = await getDefaultAgent(runtime);
|
|
66
|
+
expect(agent).toBeInstanceOf(BuiltInAgent);
|
|
67
|
+
expect(getBuiltInAgentModel(agent as BuiltInAgent)).toBe(
|
|
68
|
+
"groq/llama-3.3-70b-versatile",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("throws CopilotKitMisuseError when no model source is available (LangChainAdapter regression)", async () => {
|
|
73
|
+
const runtime = new CopilotRuntime();
|
|
74
|
+
|
|
75
|
+
runtime.handleServiceAdapter(makeAdapter({ name: "LangChainAdapter" }));
|
|
76
|
+
|
|
77
|
+
await expect(runtime.instance.agents).rejects.toBeInstanceOf(
|
|
78
|
+
CopilotKitMisuseError,
|
|
79
|
+
);
|
|
80
|
+
await expect(runtime.instance.agents).rejects.toThrow(
|
|
81
|
+
/Service adapter "LangChainAdapter" does not provide model information/,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("falls back to 'unknown' in the thrown error when the adapter has no name", async () => {
|
|
86
|
+
const runtime = new CopilotRuntime();
|
|
87
|
+
|
|
88
|
+
runtime.handleServiceAdapter(makeAdapter({ name: undefined }));
|
|
89
|
+
|
|
90
|
+
await expect(runtime.instance.agents).rejects.toThrow(
|
|
91
|
+
/Service adapter "unknown" does not provide model information/,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("does not throw when provider is set without a model — but must not emit 'undefined/undefined'", async () => {
|
|
96
|
+
// Guards the specific #3217 regression: when only one half of the pair is
|
|
97
|
+
// present, we must NOT synthesize a bogus "provider/undefined" string.
|
|
98
|
+
const runtime = new CopilotRuntime();
|
|
99
|
+
|
|
100
|
+
runtime.handleServiceAdapter(
|
|
101
|
+
makeAdapter({ name: "PartialAdapter", provider: "openai" }),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
await expect(runtime.instance.agents).rejects.toThrow(
|
|
105
|
+
CopilotKitMisuseError,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { fetchWithRetry, parseRetryAfter, RETRY_CONFIG } from "../retry-utils";
|
|
3
|
+
|
|
4
|
+
function responseWithRetryAfter(
|
|
5
|
+
headerValue: string | null,
|
|
6
|
+
status = 429,
|
|
7
|
+
): Response {
|
|
8
|
+
const headers = new Headers();
|
|
9
|
+
if (headerValue !== null) headers.set("Retry-After", headerValue);
|
|
10
|
+
return new Response(null, { status, headers });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("parseRetryAfter", () => {
|
|
14
|
+
it("returns undefined when the Retry-After header is absent", () => {
|
|
15
|
+
expect(parseRetryAfter(responseWithRetryAfter(null))).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("parses integer seconds into milliseconds", () => {
|
|
19
|
+
expect(parseRetryAfter(responseWithRetryAfter("5"))).toBe(5000);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("treats zero seconds as zero delay", () => {
|
|
23
|
+
expect(parseRetryAfter(responseWithRetryAfter("0"))).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("clamps a negative numeric value to zero", () => {
|
|
27
|
+
// `-1` fails the `seconds >= 0` guard and falls through to Date.parse,
|
|
28
|
+
// which interprets it as a year-in-the-past timestamp; the past-date
|
|
29
|
+
// branch then clamps to 0. The behavior is lenient rather than strict.
|
|
30
|
+
expect(parseRetryAfter(responseWithRetryAfter("-1"))).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("parses an HTTP-date in the future as the delta to now", () => {
|
|
34
|
+
const now = Date.parse("2026-04-22T12:00:00Z");
|
|
35
|
+
vi.useFakeTimers();
|
|
36
|
+
vi.setSystemTime(now);
|
|
37
|
+
try {
|
|
38
|
+
const future = new Date(now + 30_000).toUTCString();
|
|
39
|
+
expect(parseRetryAfter(responseWithRetryAfter(future))).toBe(30_000);
|
|
40
|
+
} finally {
|
|
41
|
+
vi.useRealTimers();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("clamps an HTTP-date in the past to zero", () => {
|
|
46
|
+
const now = Date.parse("2026-04-22T12:00:00Z");
|
|
47
|
+
vi.useFakeTimers();
|
|
48
|
+
vi.setSystemTime(now);
|
|
49
|
+
try {
|
|
50
|
+
const past = new Date(now - 60_000).toUTCString();
|
|
51
|
+
expect(parseRetryAfter(responseWithRetryAfter(past))).toBe(0);
|
|
52
|
+
} finally {
|
|
53
|
+
vi.useRealTimers();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns undefined for unparseable values", () => {
|
|
58
|
+
expect(parseRetryAfter(responseWithRetryAfter("soon"))).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("fetchWithRetry Retry-After handling (#3637)", () => {
|
|
63
|
+
let fetchMock: ReturnType<typeof vi.fn>;
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
vi.useFakeTimers();
|
|
67
|
+
fetchMock = vi.fn();
|
|
68
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
vi.useRealTimers();
|
|
73
|
+
vi.unstubAllGlobals();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("honors Retry-After within the allowed maximum on 429", async () => {
|
|
77
|
+
fetchMock
|
|
78
|
+
.mockResolvedValueOnce(responseWithRetryAfter("2"))
|
|
79
|
+
.mockResolvedValueOnce(new Response("ok", { status: 200 }));
|
|
80
|
+
|
|
81
|
+
const promise = fetchWithRetry("https://example.com", {});
|
|
82
|
+
await vi.advanceTimersByTimeAsync(1999);
|
|
83
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
84
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
85
|
+
const response = await promise;
|
|
86
|
+
|
|
87
|
+
expect(response.status).toBe(200);
|
|
88
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("throws when Retry-After exceeds maxRetryAfterSeconds", async () => {
|
|
92
|
+
const excessive = RETRY_CONFIG.maxRetryAfterSeconds + 1;
|
|
93
|
+
fetchMock.mockResolvedValue(responseWithRetryAfter(String(excessive)));
|
|
94
|
+
|
|
95
|
+
// The oversized-Retry-After branch throws before sleeping, and the
|
|
96
|
+
// resulting Error doesn't match any retryable pattern, so the loop
|
|
97
|
+
// breaks out without consuming the remaining attempts.
|
|
98
|
+
await expect(fetchWithRetry("https://example.com", {})).rejects.toThrow(
|
|
99
|
+
new RegExp(
|
|
100
|
+
`Retry-After of ${excessive}s.*exceeds the maximum of ${RETRY_CONFIG.maxRetryAfterSeconds}s`,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("falls back to exponential backoff when Retry-After is missing on 429", async () => {
|
|
107
|
+
fetchMock
|
|
108
|
+
.mockResolvedValueOnce(new Response(null, { status: 429 }))
|
|
109
|
+
.mockResolvedValueOnce(new Response("ok", { status: 200 }));
|
|
110
|
+
|
|
111
|
+
const promise = fetchWithRetry("https://example.com", {});
|
|
112
|
+
// calculateDelay(0) === RETRY_CONFIG.baseDelayMs
|
|
113
|
+
await vi.advanceTimersByTimeAsync(RETRY_CONFIG.baseDelayMs - 1);
|
|
114
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
115
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
116
|
+
const response = await promise;
|
|
117
|
+
|
|
118
|
+
expect(response.status).toBe(200);
|
|
119
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("ignores Retry-After on non-429 retryable responses (e.g. 503)", async () => {
|
|
123
|
+
const longRetryAfter = String(RETRY_CONFIG.maxRetryAfterSeconds + 600);
|
|
124
|
+
fetchMock
|
|
125
|
+
.mockResolvedValueOnce(responseWithRetryAfter(longRetryAfter, 503))
|
|
126
|
+
.mockResolvedValueOnce(new Response("ok", { status: 200 }));
|
|
127
|
+
|
|
128
|
+
const promise = fetchWithRetry("https://example.com", {});
|
|
129
|
+
// Exponential backoff applies, not the header value — otherwise this
|
|
130
|
+
// would wait 10 minutes and the test would time out.
|
|
131
|
+
await vi.advanceTimersByTimeAsync(RETRY_CONFIG.baseDelayMs);
|
|
132
|
+
const response = await promise;
|
|
133
|
+
|
|
134
|
+
expect(response.status).toBe(200);
|
|
135
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
136
|
+
});
|
|
137
|
+
});
|