@nkmc/agent-fs 0.1.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/dist/chunk-7LIZT7L3.js +966 -0
- package/dist/index.cjs +1278 -0
- package/dist/index.d.cts +96 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +419 -0
- package/dist/rpc-D1IHpjF_.d.cts +330 -0
- package/dist/rpc-D1IHpjF_.d.ts +330 -0
- package/dist/testing.cjs +842 -0
- package/dist/testing.d.cts +29 -0
- package/dist/testing.d.ts +29 -0
- package/dist/testing.js +10 -0
- package/package.json +25 -0
- package/src/agent-fs.ts +151 -0
- package/src/backends/http.ts +835 -0
- package/src/backends/memory.ts +183 -0
- package/src/backends/rpc.ts +456 -0
- package/src/index.ts +36 -0
- package/src/mount.ts +84 -0
- package/src/parser.ts +162 -0
- package/src/server.ts +158 -0
- package/src/testing.ts +3 -0
- package/src/types.ts +52 -0
- package/test/agent-fs.test.ts +325 -0
- package/test/http-204.test.ts +102 -0
- package/test/http-auth-prefix.test.ts +79 -0
- package/test/http-cloudflare.test.ts +533 -0
- package/test/http-form-encoding.test.ts +119 -0
- package/test/http-github.test.ts +580 -0
- package/test/http-listkey.test.ts +128 -0
- package/test/http-oauth2.test.ts +174 -0
- package/test/http-pagination.test.ts +200 -0
- package/test/http-param-styles.test.ts +98 -0
- package/test/http-passthrough.test.ts +282 -0
- package/test/http-retry.test.ts +132 -0
- package/test/http.test.ts +360 -0
- package/test/memory.test.ts +120 -0
- package/test/mount.test.ts +94 -0
- package/test/parser.test.ts +100 -0
- package/test/rpc-crud.test.ts +627 -0
- package/test/rpc-evm.test.ts +390 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +8 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { HttpBackend } from "../src/backends/http.js";
|
|
3
|
+
|
|
4
|
+
function createMockFetch(responses: Record<string, { status: number; body: any }>) {
|
|
5
|
+
return async (url: string, init?: RequestInit) => {
|
|
6
|
+
const method = init?.method ?? "GET";
|
|
7
|
+
const key = `${method} ${url}`;
|
|
8
|
+
// Try exact match first, then URL only
|
|
9
|
+
const resp = responses[key] ?? responses[url];
|
|
10
|
+
if (!resp) return new Response(JSON.stringify({ error: "not found" }), { status: 404 });
|
|
11
|
+
return new Response(JSON.stringify(resp.body), { status: resp.status, headers: { "Content-Type": "application/json" } });
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("HttpBackend Passthrough", () => {
|
|
16
|
+
it("should list root resources/endpoints when resources exist", async () => {
|
|
17
|
+
const backend = new HttpBackend({
|
|
18
|
+
baseUrl: "https://api.example.com",
|
|
19
|
+
resources: [{ name: "users" }],
|
|
20
|
+
fetch: createMockFetch({}) as any,
|
|
21
|
+
});
|
|
22
|
+
const entries = await backend.list("/");
|
|
23
|
+
expect(entries).toContain("users/");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should passthrough list when no resources or endpoints", async () => {
|
|
27
|
+
const backend = new HttpBackend({
|
|
28
|
+
baseUrl: "https://api.example.com",
|
|
29
|
+
resources: [],
|
|
30
|
+
endpoints: [],
|
|
31
|
+
fetch: createMockFetch({
|
|
32
|
+
"GET https://api.example.com/items": { status: 200, body: [{ id: "1" }, { id: "2" }] },
|
|
33
|
+
}) as any,
|
|
34
|
+
});
|
|
35
|
+
const entries = await backend.list("/items");
|
|
36
|
+
expect(entries).toEqual(["1", "2"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should passthrough read when no resources or endpoints", async () => {
|
|
40
|
+
const backend = new HttpBackend({
|
|
41
|
+
baseUrl: "https://api.example.com",
|
|
42
|
+
resources: [],
|
|
43
|
+
endpoints: [],
|
|
44
|
+
fetch: createMockFetch({
|
|
45
|
+
"GET https://api.example.com/items/1": { status: 200, body: { id: "1", name: "Test" } },
|
|
46
|
+
}) as any,
|
|
47
|
+
});
|
|
48
|
+
const result = await backend.read("/items/1");
|
|
49
|
+
expect(result).toEqual({ id: "1", name: "Test" });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should passthrough write when no resources or endpoints", async () => {
|
|
53
|
+
const backend = new HttpBackend({
|
|
54
|
+
baseUrl: "https://api.example.com",
|
|
55
|
+
resources: [],
|
|
56
|
+
endpoints: [],
|
|
57
|
+
fetch: createMockFetch({
|
|
58
|
+
"PUT https://api.example.com/items/1": { status: 200, body: { id: "1" } },
|
|
59
|
+
}) as any,
|
|
60
|
+
});
|
|
61
|
+
const result = await backend.write("/items/1", { name: "Updated" });
|
|
62
|
+
expect(result.id).toBe("1");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should passthrough POST to root path for create", async () => {
|
|
66
|
+
const backend = new HttpBackend({
|
|
67
|
+
baseUrl: "https://api.example.com",
|
|
68
|
+
resources: [],
|
|
69
|
+
endpoints: [],
|
|
70
|
+
fetch: createMockFetch({
|
|
71
|
+
"POST https://api.example.com/": { status: 201, body: { id: "new-1" } },
|
|
72
|
+
}) as any,
|
|
73
|
+
});
|
|
74
|
+
const result = await backend.write("/", { name: "New" });
|
|
75
|
+
expect(result.id).toBe("new-1");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should passthrough remove when no resources or endpoints", async () => {
|
|
79
|
+
const backend = new HttpBackend({
|
|
80
|
+
baseUrl: "https://api.example.com",
|
|
81
|
+
resources: [],
|
|
82
|
+
endpoints: [],
|
|
83
|
+
fetch: createMockFetch({
|
|
84
|
+
"DELETE https://api.example.com/items/1": { status: 200, body: { ok: true } },
|
|
85
|
+
}) as any,
|
|
86
|
+
});
|
|
87
|
+
await expect(backend.remove("/items/1")).resolves.toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should passthrough search when no resources or endpoints", async () => {
|
|
91
|
+
const backend = new HttpBackend({
|
|
92
|
+
baseUrl: "https://api.example.com",
|
|
93
|
+
resources: [],
|
|
94
|
+
endpoints: [],
|
|
95
|
+
fetch: createMockFetch({
|
|
96
|
+
"GET https://api.example.com/items?q=test": { status: 200, body: [{ id: "1" }] },
|
|
97
|
+
}) as any,
|
|
98
|
+
});
|
|
99
|
+
const results = await backend.search("/items", "test");
|
|
100
|
+
expect(results).toEqual([{ id: "1" }]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should still show root listing in passthrough mode", async () => {
|
|
104
|
+
const backend = new HttpBackend({
|
|
105
|
+
baseUrl: "https://api.example.com",
|
|
106
|
+
resources: [],
|
|
107
|
+
endpoints: [],
|
|
108
|
+
fetch: createMockFetch({}) as any,
|
|
109
|
+
});
|
|
110
|
+
const entries = await backend.list("/");
|
|
111
|
+
expect(entries).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("HttpBackend Passthrough Fallback (resources exist but path unmatched)", () => {
|
|
116
|
+
it("should fallback to passthrough for multi-segment paths that don't match resources", async () => {
|
|
117
|
+
// Simulates: repos resource exists, but /repos/facebook/react has 2 segments after resource
|
|
118
|
+
const backend = new HttpBackend({
|
|
119
|
+
baseUrl: "https://api.github.com",
|
|
120
|
+
resources: [{ name: "repos" }],
|
|
121
|
+
endpoints: [],
|
|
122
|
+
fetch: createMockFetch({
|
|
123
|
+
"GET https://api.github.com/orgs/facebook/repos": {
|
|
124
|
+
status: 200,
|
|
125
|
+
body: [{ id: 1, name: "react" }],
|
|
126
|
+
},
|
|
127
|
+
}) as any,
|
|
128
|
+
});
|
|
129
|
+
// "orgs" is NOT a defined resource, so it should passthrough
|
|
130
|
+
const result = await backend.read("/orgs/facebook/repos");
|
|
131
|
+
expect(result).toEqual([{ id: 1, name: "react" }]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should still use resource resolution for matching paths", async () => {
|
|
135
|
+
const backend = new HttpBackend({
|
|
136
|
+
baseUrl: "https://api.example.com",
|
|
137
|
+
resources: [{ name: "users" }],
|
|
138
|
+
fetch: createMockFetch({
|
|
139
|
+
"GET https://api.example.com/users": { status: 200, body: [{ id: "1" }] },
|
|
140
|
+
}) as any,
|
|
141
|
+
});
|
|
142
|
+
// "users" matches a resource — should use resource-list, not passthrough
|
|
143
|
+
const entries = await backend.list("/users/");
|
|
144
|
+
expect(entries).toEqual(["1.json"]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should passthrough write for unmatched paths even when resources exist", async () => {
|
|
148
|
+
const backend = new HttpBackend({
|
|
149
|
+
baseUrl: "https://api.example.com",
|
|
150
|
+
resources: [{ name: "users" }],
|
|
151
|
+
fetch: createMockFetch({
|
|
152
|
+
"PUT https://api.example.com/teams/5/members": { status: 200, body: { id: "m1" } },
|
|
153
|
+
}) as any,
|
|
154
|
+
});
|
|
155
|
+
// "teams" is NOT a defined resource
|
|
156
|
+
const result = await backend.write("/teams/5/members", { userId: "1" });
|
|
157
|
+
expect(result.id).toBe("m1");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should passthrough delete for unmatched paths even when resources exist", async () => {
|
|
161
|
+
const backend = new HttpBackend({
|
|
162
|
+
baseUrl: "https://api.example.com",
|
|
163
|
+
resources: [{ name: "users" }],
|
|
164
|
+
fetch: createMockFetch({
|
|
165
|
+
"DELETE https://api.example.com/teams/5/members/m1": { status: 200, body: { ok: true } },
|
|
166
|
+
}) as any,
|
|
167
|
+
});
|
|
168
|
+
await expect(backend.remove("/teams/5/members/m1")).resolves.toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should throw NotFoundError when passthrough gets 404 from upstream", async () => {
|
|
172
|
+
const backend = new HttpBackend({
|
|
173
|
+
baseUrl: "https://api.example.com",
|
|
174
|
+
resources: [{ name: "users" }],
|
|
175
|
+
fetch: createMockFetch({}) as any, // no matching response → 404
|
|
176
|
+
});
|
|
177
|
+
await expect(backend.read("/nonexistent/path")).rejects.toThrow();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("HttpBackend Passthrough — resource matches but unconsumed segments", () => {
|
|
182
|
+
// This is the critical case: first segment matches a resource, but extra
|
|
183
|
+
// segments remain that the resource tree cannot consume. The resource layer
|
|
184
|
+
// must yield to passthrough instead of silently dropping segments.
|
|
185
|
+
|
|
186
|
+
it("should passthrough read when matched resource has unconsumed segments", async () => {
|
|
187
|
+
// "repos" is a defined resource, but /repos/facebook/react has 2 segments
|
|
188
|
+
// after "repos" — resource layer can only consume 1 (the id).
|
|
189
|
+
const backend = new HttpBackend({
|
|
190
|
+
baseUrl: "https://api.github.com",
|
|
191
|
+
resources: [{ name: "repos" }],
|
|
192
|
+
fetch: createMockFetch({
|
|
193
|
+
"GET https://api.github.com/repos/facebook/react": {
|
|
194
|
+
status: 200,
|
|
195
|
+
body: { id: 1, full_name: "facebook/react" },
|
|
196
|
+
},
|
|
197
|
+
}) as any,
|
|
198
|
+
});
|
|
199
|
+
const result = await backend.read("/repos/facebook/react");
|
|
200
|
+
expect(result).toEqual({ id: 1, full_name: "facebook/react" });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should passthrough deeply nested paths past a matched resource", async () => {
|
|
204
|
+
const backend = new HttpBackend({
|
|
205
|
+
baseUrl: "https://api.github.com",
|
|
206
|
+
resources: [{ name: "repos" }],
|
|
207
|
+
fetch: createMockFetch({
|
|
208
|
+
"GET https://api.github.com/repos/facebook/react/issues": {
|
|
209
|
+
status: 200,
|
|
210
|
+
body: [{ id: 42, title: "Bug" }],
|
|
211
|
+
},
|
|
212
|
+
}) as any,
|
|
213
|
+
});
|
|
214
|
+
const result = await backend.read("/repos/facebook/react/issues");
|
|
215
|
+
expect(result).toEqual([{ id: 42, title: "Bug" }]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should passthrough list for unconsumed segments past a resource", async () => {
|
|
219
|
+
const backend = new HttpBackend({
|
|
220
|
+
baseUrl: "https://api.github.com",
|
|
221
|
+
resources: [{ name: "repos" }],
|
|
222
|
+
fetch: createMockFetch({
|
|
223
|
+
"GET https://api.github.com/repos/facebook/react/pulls": {
|
|
224
|
+
status: 200,
|
|
225
|
+
body: [{ id: 10, title: "PR" }, { id: 11, title: "PR2" }],
|
|
226
|
+
},
|
|
227
|
+
}) as any,
|
|
228
|
+
});
|
|
229
|
+
const entries = await backend.list("/repos/facebook/react/pulls");
|
|
230
|
+
expect(entries).toEqual(["10", "11"]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should passthrough write for unconsumed segments past a resource", async () => {
|
|
234
|
+
const backend = new HttpBackend({
|
|
235
|
+
baseUrl: "https://api.github.com",
|
|
236
|
+
resources: [{ name: "repos" }],
|
|
237
|
+
fetch: createMockFetch({
|
|
238
|
+
"PUT https://api.github.com/repos/facebook/react/issues/42": {
|
|
239
|
+
status: 200,
|
|
240
|
+
body: { id: 42 },
|
|
241
|
+
},
|
|
242
|
+
}) as any,
|
|
243
|
+
});
|
|
244
|
+
const result = await backend.write("/repos/facebook/react/issues/42", { state: "closed" });
|
|
245
|
+
expect(result.id).toBe("42");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should still use resource-item for exact 2-segment match", async () => {
|
|
249
|
+
// repos/123 → resource-item (only 1 segment after resource name, fully consumed)
|
|
250
|
+
const backend = new HttpBackend({
|
|
251
|
+
baseUrl: "https://api.example.com",
|
|
252
|
+
resources: [{ name: "repos" }],
|
|
253
|
+
fetch: createMockFetch({
|
|
254
|
+
"GET https://api.example.com/repos/123": {
|
|
255
|
+
status: 200,
|
|
256
|
+
body: { id: 123, name: "my-repo" },
|
|
257
|
+
},
|
|
258
|
+
}) as any,
|
|
259
|
+
});
|
|
260
|
+
const result = await backend.read("/repos/123");
|
|
261
|
+
expect(result).toEqual({ id: 123, name: "my-repo" });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should use children when defined, not passthrough", async () => {
|
|
265
|
+
// zones/{id}/dns_records → children match, should NOT passthrough
|
|
266
|
+
const backend = new HttpBackend({
|
|
267
|
+
baseUrl: "https://api.cloudflare.com/client/v4",
|
|
268
|
+
resources: [{
|
|
269
|
+
name: "zones",
|
|
270
|
+
children: [{ name: "dns_records" }],
|
|
271
|
+
}],
|
|
272
|
+
fetch: createMockFetch({
|
|
273
|
+
"GET https://api.cloudflare.com/client/v4/zones/z1/dns_records": {
|
|
274
|
+
status: 200,
|
|
275
|
+
body: [{ id: "r1", type: "A" }],
|
|
276
|
+
},
|
|
277
|
+
}) as any,
|
|
278
|
+
});
|
|
279
|
+
const entries = await backend.list("/zones/z1/dns_records/");
|
|
280
|
+
expect(entries).toEqual(["r1.json"]);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
|
+
import { HttpBackend } from "../src/backends/http.js";
|
|
4
|
+
|
|
5
|
+
describe("HttpBackend 429 retry with backoff", () => {
|
|
6
|
+
let server: Server;
|
|
7
|
+
let port: number;
|
|
8
|
+
let requestCount: number;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
await new Promise<void>((resolve) => {
|
|
12
|
+
server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
13
|
+
res.setHeader("Content-Type", "application/json");
|
|
14
|
+
requestCount++;
|
|
15
|
+
|
|
16
|
+
// /rate-limited → 429 twice, then 200
|
|
17
|
+
if (req.url === "/rate-limited") {
|
|
18
|
+
if (requestCount <= 2) {
|
|
19
|
+
res.writeHead(429, { "Retry-After": "0" }); // 0 seconds for fast tests
|
|
20
|
+
res.end(JSON.stringify({ error: "rate limited" }));
|
|
21
|
+
} else {
|
|
22
|
+
res.writeHead(200);
|
|
23
|
+
res.end(JSON.stringify({ id: "ok", data: "success" }));
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// /always-429 → always 429
|
|
29
|
+
if (req.url === "/always-429") {
|
|
30
|
+
res.writeHead(429, { "Retry-After": "0" });
|
|
31
|
+
res.end(JSON.stringify({ error: "rate limited" }));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// /server-error → 500 twice, then 200
|
|
36
|
+
if (req.url === "/server-error") {
|
|
37
|
+
if (requestCount <= 2) {
|
|
38
|
+
res.writeHead(500);
|
|
39
|
+
res.end(JSON.stringify({ error: "internal error" }));
|
|
40
|
+
} else {
|
|
41
|
+
res.writeHead(200);
|
|
42
|
+
res.end(JSON.stringify([{ id: "1" }]));
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// /client-error → 400 (should not retry)
|
|
48
|
+
if (req.url === "/client-error") {
|
|
49
|
+
res.writeHead(400);
|
|
50
|
+
res.end(JSON.stringify({ error: "bad request" }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
res.writeHead(200);
|
|
55
|
+
res.end(JSON.stringify({ ok: true }));
|
|
56
|
+
});
|
|
57
|
+
server.listen(0, () => {
|
|
58
|
+
port = (server.address() as any).port;
|
|
59
|
+
resolve();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterAll(() => server?.close());
|
|
65
|
+
|
|
66
|
+
beforeAll(() => {
|
|
67
|
+
requestCount = 0;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should retry on 429 and eventually succeed", async () => {
|
|
71
|
+
requestCount = 0;
|
|
72
|
+
const backend = new HttpBackend({
|
|
73
|
+
baseUrl: `http://localhost:${port}`,
|
|
74
|
+
retry: { maxRetries: 3, baseDelayMs: 10 },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = await backend.read("/rate-limited");
|
|
78
|
+
expect(result).toEqual({ id: "ok", data: "success" });
|
|
79
|
+
expect(requestCount).toBe(3); // 2 x 429 + 1 x 200
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should retry on 5xx and eventually succeed", async () => {
|
|
83
|
+
requestCount = 0;
|
|
84
|
+
const backend = new HttpBackend({
|
|
85
|
+
baseUrl: `http://localhost:${port}`,
|
|
86
|
+
retry: { maxRetries: 3, baseDelayMs: 10 },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const items = await backend.list("/server-error");
|
|
90
|
+
// Since it's passthrough mode, list returns parsed response
|
|
91
|
+
expect(requestCount).toBe(3);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should NOT retry on 400 client error", async () => {
|
|
95
|
+
requestCount = 0;
|
|
96
|
+
const backend = new HttpBackend({
|
|
97
|
+
baseUrl: `http://localhost:${port}`,
|
|
98
|
+
retry: { maxRetries: 3, baseDelayMs: 10 },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 400 should not be retried — passthrough list returns empty
|
|
102
|
+
await backend.list("/client-error");
|
|
103
|
+
expect(requestCount).toBe(1); // Only 1 request, no retry
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should give up after maxRetries exhausted", async () => {
|
|
107
|
+
requestCount = 0;
|
|
108
|
+
const backend = new HttpBackend({
|
|
109
|
+
baseUrl: `http://localhost:${port}`,
|
|
110
|
+
retry: { maxRetries: 2, baseDelayMs: 10 },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// always-429 will never succeed
|
|
114
|
+
await backend.list("/always-429");
|
|
115
|
+
expect(requestCount).toBe(3); // 1 initial + 2 retries
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should respect Retry-After header", async () => {
|
|
119
|
+
requestCount = 0;
|
|
120
|
+
const start = Date.now();
|
|
121
|
+
const backend = new HttpBackend({
|
|
122
|
+
baseUrl: `http://localhost:${port}`,
|
|
123
|
+
retry: { maxRetries: 3, baseDelayMs: 5000 }, // high base delay
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Retry-After: 0 should override the high base delay
|
|
127
|
+
await backend.read("/rate-limited");
|
|
128
|
+
const elapsed = Date.now() - start;
|
|
129
|
+
// With Retry-After: 0, should complete quickly despite high baseDelayMs
|
|
130
|
+
expect(elapsed).toBeLessThan(1000);
|
|
131
|
+
});
|
|
132
|
+
});
|