@codespar/sdk 0.10.11 → 0.12.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 +72 -1
- package/dist/__tests__/abort.test.d.ts +2 -0
- package/dist/__tests__/abort.test.d.ts.map +1 -0
- package/dist/__tests__/abort.test.js +70 -0
- package/dist/__tests__/abort.test.js.map +1 -0
- package/dist/__tests__/api-client.test.d.ts +7 -0
- package/dist/__tests__/api-client.test.d.ts.map +1 -0
- package/dist/__tests__/api-client.test.js +286 -0
- package/dist/__tests__/api-client.test.js.map +1 -0
- package/dist/__tests__/close-timeout.test.d.ts +2 -0
- package/dist/__tests__/close-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/close-timeout.test.js +57 -0
- package/dist/__tests__/close-timeout.test.js.map +1 -0
- package/dist/__tests__/codespar.test.js +32 -0
- package/dist/__tests__/codespar.test.js.map +1 -1
- package/dist/__tests__/connections-timeout.test.d.ts +2 -0
- package/dist/__tests__/connections-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/connections-timeout.test.js +58 -0
- package/dist/__tests__/connections-timeout.test.js.map +1 -0
- package/dist/__tests__/fetch-timeout.test.d.ts +2 -0
- package/dist/__tests__/fetch-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/fetch-timeout.test.js +137 -0
- package/dist/__tests__/fetch-timeout.test.js.map +1 -0
- package/dist/__tests__/loop.test.d.ts +2 -0
- package/dist/__tests__/loop.test.d.ts.map +1 -0
- package/dist/__tests__/loop.test.js +237 -0
- package/dist/__tests__/loop.test.js.map +1 -0
- package/dist/__tests__/mandate.test.d.ts +2 -0
- package/dist/__tests__/mandate.test.d.ts.map +1 -0
- package/dist/__tests__/mandate.test.js +141 -0
- package/dist/__tests__/mandate.test.js.map +1 -0
- package/dist/__tests__/openapi-snapshot.test.d.ts +13 -0
- package/dist/__tests__/openapi-snapshot.test.d.ts.map +1 -0
- package/dist/__tests__/openapi-snapshot.test.js +79 -0
- package/dist/__tests__/openapi-snapshot.test.js.map +1 -0
- package/dist/__tests__/shop.test.js +1 -1
- package/dist/__tests__/shop.test.js.map +1 -1
- package/dist/__tests__/stream-timeout.test.d.ts +2 -0
- package/dist/__tests__/stream-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/stream-timeout.test.js +335 -0
- package/dist/__tests__/stream-timeout.test.js.map +1 -0
- package/dist/__tests__/wait-for-connections.test.d.ts +2 -0
- package/dist/__tests__/wait-for-connections.test.d.ts.map +1 -0
- package/dist/__tests__/wait-for-connections.test.js +94 -0
- package/dist/__tests__/wait-for-connections.test.js.map +1 -0
- package/dist/api/client.d.ts +56 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +221 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/types.d.ts +110 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +10 -0
- package/dist/api/types.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -1
- package/dist/generated/openapi.d.ts +22004 -0
- package/dist/generated/openapi.d.ts.map +1 -0
- package/dist/generated/openapi.js +2 -0
- package/dist/generated/openapi.js.map +1 -0
- package/dist/generated/operations.d.ts +1084 -0
- package/dist/generated/operations.d.ts.map +1 -0
- package/dist/generated/operations.js +225 -0
- package/dist/generated/operations.js.map +1 -0
- package/dist/index.d.ts +24 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -4
- package/dist/index.js.map +1 -1
- package/dist/internal/abort.d.ts +18 -0
- package/dist/internal/abort.d.ts.map +1 -0
- package/dist/internal/abort.js +42 -0
- package/dist/internal/abort.js.map +1 -0
- package/dist/internal/fetch.d.ts +23 -0
- package/dist/internal/fetch.d.ts.map +1 -0
- package/dist/internal/fetch.js +57 -0
- package/dist/internal/fetch.js.map +1 -0
- package/dist/mandate/index.d.ts +111 -0
- package/dist/mandate/index.d.ts.map +1 -0
- package/dist/mandate/index.js +217 -0
- package/dist/mandate/index.js.map +1 -0
- package/dist/session.d.ts +3 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +343 -162
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -1
- package/dist/types.js.map +1 -1
- package/package.json +11 -3
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import { agentDidFromKid, decodeMandateToken, reconstructSigningString, verifyEd25519, verifyMandateToken, } from "../mandate/index.js";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const FIXTURE_PATH = join(__dirname, "fixtures/canonical.v3.fixture.json");
|
|
8
|
+
const fx = JSON.parse(readFileSync(FIXTURE_PATH, "utf8"));
|
|
9
|
+
/** Reproduce the enterprise `computeToken` envelope so we have a real token. */
|
|
10
|
+
function makeToken(fields, envelope) {
|
|
11
|
+
return Buffer.from(JSON.stringify({ ...fields, ...envelope }), "utf8").toString("base64url");
|
|
12
|
+
}
|
|
13
|
+
const TOKEN = makeToken(fx.input, {
|
|
14
|
+
signature: fx.hmac_sha256_hex,
|
|
15
|
+
agent_sig: fx.agent_sig_b64url,
|
|
16
|
+
issuer_sig: fx.issuer_sig_b64url,
|
|
17
|
+
kid: fx.input.agent_kid,
|
|
18
|
+
});
|
|
19
|
+
describe("@codespar/sdk/mandate — decode", () => {
|
|
20
|
+
it("round-trips the V3 envelope and separates it from the signed fields", () => {
|
|
21
|
+
const res = decodeMandateToken(TOKEN);
|
|
22
|
+
expect(res.ok).toBe(true);
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
return;
|
|
25
|
+
expect(res.token.signature).toBe(fx.hmac_sha256_hex);
|
|
26
|
+
expect(res.token.agent_sig).toBe(fx.agent_sig_b64url);
|
|
27
|
+
expect(res.token.issuer_sig).toBe(fx.issuer_sig_b64url);
|
|
28
|
+
expect(res.token.kid).toBe(fx.input.agent_kid);
|
|
29
|
+
const m = res.token.mandate;
|
|
30
|
+
expect(m.signature).toBeUndefined();
|
|
31
|
+
expect(m.agent_sig).toBeUndefined();
|
|
32
|
+
expect(m.issuer_sig).toBeUndefined();
|
|
33
|
+
expect(m.kid).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
it("rejects a non-base64url / non-JSON blob", () => {
|
|
36
|
+
const res = decodeMandateToken("!!!nope!!!");
|
|
37
|
+
expect(res.ok).toBe(false);
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
expect(res.error).toBe("invalid_payload");
|
|
40
|
+
});
|
|
41
|
+
it("rejects an unsupported (v1) format_version", () => {
|
|
42
|
+
const res = decodeMandateToken(makeToken({ ...fx.input, format_version: 1 }, { signature: "x" }));
|
|
43
|
+
expect(res.ok).toBe(false);
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
expect(res.error).toBe("mandate_format_unsupported");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe("@codespar/sdk/mandate — reconstructSigningString (byte-lock)", () => {
|
|
49
|
+
it("reproduces the frozen 14-field canonical string byte-for-byte", () => {
|
|
50
|
+
expect(reconstructSigningString(fx.input)).toBe(fx.canonical_string);
|
|
51
|
+
});
|
|
52
|
+
it("sorts + escapes purposes independent of input order", () => {
|
|
53
|
+
expect(reconstructSigningString({
|
|
54
|
+
...fx.input,
|
|
55
|
+
purposes: ["utility", "refund"],
|
|
56
|
+
})).toBe(fx.canonical_string);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe("@codespar/sdk/mandate — verifyEd25519", () => {
|
|
60
|
+
const agentPub = () => Buffer.from(fx.agent_pubkey_hex, "hex");
|
|
61
|
+
const issuerPub = () => Buffer.from(fx.issuer_pubkey_hex, "hex");
|
|
62
|
+
it("verifies agent + issuer signatures with only the DID public keys", () => {
|
|
63
|
+
expect(verifyEd25519(fx.canonical_string, fx.agent_sig_b64url, agentPub())).toBe(true);
|
|
64
|
+
expect(verifyEd25519(fx.canonical_string, fx.issuer_sig_b64url, issuerPub())).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
it("fails a one-byte tamper of a signed field", () => {
|
|
67
|
+
const forged = reconstructSigningString({
|
|
68
|
+
...fx.input,
|
|
69
|
+
amount: "9999",
|
|
70
|
+
});
|
|
71
|
+
expect(verifyEd25519(forged, fx.agent_sig_b64url, agentPub())).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it("fails against the wrong key and never throws on a malformed key", () => {
|
|
74
|
+
expect(verifyEd25519(fx.canonical_string, fx.agent_sig_b64url, issuerPub())).toBe(false);
|
|
75
|
+
expect(verifyEd25519(fx.canonical_string, fx.agent_sig_b64url, Buffer.alloc(5))).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("@codespar/sdk/mandate — verifyMandateToken (high-level)", () => {
|
|
79
|
+
it("verifies both signatures from hex keys and exposes governed fields", () => {
|
|
80
|
+
const res = verifyMandateToken(TOKEN, {
|
|
81
|
+
agentPublicKey: fx.agent_pubkey_hex,
|
|
82
|
+
issuerPublicKey: fx.issuer_pubkey_hex,
|
|
83
|
+
});
|
|
84
|
+
expect(res.verified).toBe(true);
|
|
85
|
+
expect(res.agent.status).toBe("verified");
|
|
86
|
+
expect(res.issuer.status).toBe("verified");
|
|
87
|
+
expect(res.agent.kid).toBe(fx.input.agent_kid);
|
|
88
|
+
expect(res.agentDid).toBe("did:web:id.codespar.dev:org_demo:a1");
|
|
89
|
+
expect(res.kid).toBe(fx.input.agent_kid);
|
|
90
|
+
expect(res.mandate.purposes).toContain("refund");
|
|
91
|
+
expect(res.mandate.amount).toBe("5000");
|
|
92
|
+
});
|
|
93
|
+
it("accepts raw byte keys as well as hex", () => {
|
|
94
|
+
const res = verifyMandateToken(TOKEN, {
|
|
95
|
+
agentPublicKey: Buffer.from(fx.agent_pubkey_hex, "hex"),
|
|
96
|
+
issuerPublicKey: Buffer.from(fx.issuer_pubkey_hex, "hex"),
|
|
97
|
+
});
|
|
98
|
+
expect(res.verified).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
it("skips a signature when its key is not supplied (still verified overall)", () => {
|
|
101
|
+
const res = verifyMandateToken(TOKEN, { agentPublicKey: fx.agent_pubkey_hex });
|
|
102
|
+
expect(res.agent.status).toBe("verified");
|
|
103
|
+
expect(res.issuer.status).toBe("skipped");
|
|
104
|
+
expect(res.verified).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
it("reports NOT verified when a supplied key fails", () => {
|
|
107
|
+
// Issuer key given for the agent slot → agent signature fails.
|
|
108
|
+
const res = verifyMandateToken(TOKEN, { agentPublicKey: fx.issuer_pubkey_hex });
|
|
109
|
+
expect(res.agent.status).toBe("failed");
|
|
110
|
+
expect(res.verified).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
it("reports NOT verified for a tampered token", () => {
|
|
113
|
+
const tampered = makeToken({ ...fx.input, amount: "9999" }, {
|
|
114
|
+
signature: fx.hmac_sha256_hex,
|
|
115
|
+
agent_sig: fx.agent_sig_b64url,
|
|
116
|
+
issuer_sig: fx.issuer_sig_b64url,
|
|
117
|
+
kid: fx.input.agent_kid,
|
|
118
|
+
});
|
|
119
|
+
const res = verifyMandateToken(tampered, {
|
|
120
|
+
agentPublicKey: fx.agent_pubkey_hex,
|
|
121
|
+
issuerPublicKey: fx.issuer_pubkey_hex,
|
|
122
|
+
});
|
|
123
|
+
expect(res.verified).toBe(false);
|
|
124
|
+
expect(res.agent.status).toBe("failed");
|
|
125
|
+
expect(res.issuer.status).toBe("failed");
|
|
126
|
+
});
|
|
127
|
+
it("throws on a token that cannot be decoded", () => {
|
|
128
|
+
expect(() => verifyMandateToken("not-a-token")).toThrow(/cannot decode/);
|
|
129
|
+
});
|
|
130
|
+
it("rejects a malformed hex key by treating it as no key (skipped)", () => {
|
|
131
|
+
const res = verifyMandateToken(TOKEN, { agentPublicKey: "xyz" });
|
|
132
|
+
expect(res.agent.status).toBe("skipped");
|
|
133
|
+
expect(res.verified).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe("@codespar/sdk/mandate — helpers", () => {
|
|
137
|
+
it("agentDidFromKid strips the #fragment", () => {
|
|
138
|
+
expect(agentDidFromKid("did:web:id.codespar.dev:org_demo:a1#1")).toBe("did:web:id.codespar.dev:org_demo:a1");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
//# sourceMappingURL=mandate.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mandate.test.js","sourceRoot":"","sources":["../../src/__tests__/mandate.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated client is only as true as the chain behind it:
|
|
3
|
+
*
|
|
4
|
+
* served document == openapi-snapshot.json (scripts/openapi-spec.mjs check, network)
|
|
5
|
+
* openapi-snapshot.json == src/generated/* (this file, hermetic)
|
|
6
|
+
* src/generated/operations.ts == paths type (tsc, via `satisfies`)
|
|
7
|
+
* every row of the table is dispatchable (api-client.test.ts)
|
|
8
|
+
*
|
|
9
|
+
* This file pins the hermetic links and proves the comparison itself can
|
|
10
|
+
* fail: a snapshot with one operation removed must be reported as drift.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=openapi-snapshot.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-snapshot.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/openapi-snapshot.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated client is only as true as the chain behind it:
|
|
3
|
+
*
|
|
4
|
+
* served document == openapi-snapshot.json (scripts/openapi-spec.mjs check, network)
|
|
5
|
+
* openapi-snapshot.json == src/generated/* (this file, hermetic)
|
|
6
|
+
* src/generated/operations.ts == paths type (tsc, via `satisfies`)
|
|
7
|
+
* every row of the table is dispatchable (api-client.test.ts)
|
|
8
|
+
*
|
|
9
|
+
* This file pins the hermetic links and proves the comparison itself can
|
|
10
|
+
* fail: a snapshot with one operation removed must be reported as drift.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import { OPERATIONS_PATH, SNAPSHOT_PATH, TYPES_PATH, diffOperations, documentSha256, listOperations, operationTable, readSnapshot, renderGenerated, } from "../../scripts/openapi-spec.mjs";
|
|
15
|
+
import { API_OPERATIONS } from "../generated/operations.js";
|
|
16
|
+
import { ApiClient } from "../api/client.js";
|
|
17
|
+
const snapshot = readSnapshot(SNAPSHOT_PATH);
|
|
18
|
+
const key = (op) => `${op.method.toUpperCase()} ${op.path}`;
|
|
19
|
+
describe("openapi-snapshot.json", () => {
|
|
20
|
+
it("records the sha256 of its own document (a hand edit breaks the seal)", () => {
|
|
21
|
+
expect(documentSha256(snapshot.document)).toBe(snapshot.snapshot.sha256);
|
|
22
|
+
});
|
|
23
|
+
it("records its own operation and path counts", () => {
|
|
24
|
+
expect(listOperations(snapshot.document)).toHaveLength(snapshot.snapshot.operations);
|
|
25
|
+
expect(Object.keys(snapshot.document.paths ?? {})).toHaveLength(snapshot.snapshot.paths);
|
|
26
|
+
});
|
|
27
|
+
it("names the served URL and a fetch timestamp", () => {
|
|
28
|
+
expect(snapshot.snapshot.source).toMatch(/^https:\/\/.+\/openapi\.json$/);
|
|
29
|
+
expect(new Date(snapshot.snapshot.fetched_at).toString()).not.toBe("Invalid Date");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("src/generated/", () => {
|
|
33
|
+
// Regenerates 1.1 MB of types in-process: ~250 ms on a laptop, but past
|
|
34
|
+
// vitest's 5 s default on a CI runner where turbo runs every package's
|
|
35
|
+
// suite at once (it timed out there once with the code unchanged).
|
|
36
|
+
it("equals what the snapshot generates (no hand edits, no stale output)", async () => {
|
|
37
|
+
const rendered = await renderGenerated(snapshot);
|
|
38
|
+
expect(fs.readFileSync(TYPES_PATH, "utf8")).toBe(rendered.types);
|
|
39
|
+
expect(fs.readFileSync(OPERATIONS_PATH, "utf8")).toBe(rendered.operations);
|
|
40
|
+
}, 60_000);
|
|
41
|
+
it("carries every operation of the snapshot, once, in document order", () => {
|
|
42
|
+
const fromSnapshot = operationTable(snapshot.document);
|
|
43
|
+
expect(API_OPERATIONS.map(key)).toEqual(fromSnapshot.map(key));
|
|
44
|
+
expect(new Set(API_OPERATIONS.map(key)).size).toBe(API_OPERATIONS.length);
|
|
45
|
+
expect([...API_OPERATIONS]).toEqual(fromSnapshot);
|
|
46
|
+
});
|
|
47
|
+
it("is what the client enumerates as reachable", () => {
|
|
48
|
+
expect(ApiClient.operations().map(key)).toEqual(API_OPERATIONS.map(key));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe("diffOperations (the comparison the served check relies on)", () => {
|
|
52
|
+
it("reports nothing for an identical document", () => {
|
|
53
|
+
expect(diffOperations(snapshot.document, snapshot.document)).toEqual({
|
|
54
|
+
added: [],
|
|
55
|
+
removed: [],
|
|
56
|
+
changed: [],
|
|
57
|
+
schemas: [],
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
it("goes red when one operation is removed from the snapshot (positive control)", () => {
|
|
61
|
+
const first = listOperations(snapshot.document)[0];
|
|
62
|
+
const edited = structuredClone(snapshot.document);
|
|
63
|
+
delete edited.paths[first.path][first.method];
|
|
64
|
+
const diff = diffOperations(edited, snapshot.document);
|
|
65
|
+
expect(diff.added).toEqual([key(first)]);
|
|
66
|
+
expect(diff.removed).toEqual([]);
|
|
67
|
+
expect(documentSha256(edited)).not.toBe(snapshot.snapshot.sha256);
|
|
68
|
+
});
|
|
69
|
+
it("reports an operation whose definition changed, and a schema that changed", () => {
|
|
70
|
+
const edited = structuredClone(snapshot.document);
|
|
71
|
+
const wallet = edited.paths["/v1/wallets/{id}"].get;
|
|
72
|
+
wallet.description = "edited";
|
|
73
|
+
edited.components.schemas.Wallet.description = "edited";
|
|
74
|
+
const diff = diffOperations(snapshot.document, edited);
|
|
75
|
+
expect(diff.changed).toEqual(["GET /v1/wallets/{id}"]);
|
|
76
|
+
expect(diff.schemas).toEqual(["Wallet"]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=openapi-snapshot.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-snapshot.test.js","sourceRoot":"","sources":["../../src/__tests__/openapi-snapshot.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EACL,eAAe,EACf,aAAa,EACb,UAAU,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,cAAc,EACd,YAAY,EACZ,eAAe,GAChB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,MAAM,QAAQ,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;AAC7C,MAAM,GAAG,GAAG,CAAC,EAAoC,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;AAE9F,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACrF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC3F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC;QAC1E,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,wEAAwE;IACxE,uEAAuE;IACvE,mEAAmE;IACnE,EAAE,CACA,qEAAqE,EACrE,KAAK,IAAI,EAAE;QACT,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,QAAQ,CAAC,CAAC;QACjD,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACjE,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC7E,CAAC,EACD,MAAM,CACP,CAAC;IAEF,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,YAAY,GAAG,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACvD,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/D,MAAM,CAAC,IAAI,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAC1E,MAAM,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,4DAA4D,EAAE,GAAG,EAAE;IAC1E,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;YACnE,KAAK,EAAE,EAAE;YACT,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,EAAE;SACZ,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6EAA6E,EAAE,GAAG,EAAE;QACrF,MAAM,KAAK,GAAG,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAE,CAAC;QACpD,MAAM,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAClD,OAAO,MAAM,CAAC,KAAM,CAAC,KAAK,CAAC,IAAI,CAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAChD,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACvD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACjC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAM,CAAC,kBAAkB,CAAE,CAAC,GAA8B,CAAC;QACjF,MAAM,CAAC,WAAW,GAAG,QAAQ,CAAC;QAC7B,MAAM,CAAC,UAAW,CAAC,OAAQ,CAAC,MAAkC,CAAC,WAAW,GAAG,QAAQ,CAAC;QACvF,MAAM,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACvD,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC;QACvD,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -63,7 +63,7 @@ async function sessionReturning(over) {
|
|
|
63
63
|
};
|
|
64
64
|
});
|
|
65
65
|
globalThis.fetch = fetchMock;
|
|
66
|
-
const session = await createSession("consumer_123", { servers: ["cobasi"] }, { baseUrl: "https://api.example.com", apiKey: "csk_live_test" });
|
|
66
|
+
const session = await createSession("consumer_123", { servers: ["cobasi"] }, { baseUrl: "https://api.example.com", apiKey: "csk_live_test", timeout: 60000 });
|
|
67
67
|
return { session, calls };
|
|
68
68
|
}
|
|
69
69
|
const originalFetch = globalThis.fetch;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shop.test.js","sourceRoot":"","sources":["../../src/__tests__/shop.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAU7D,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAO9C,SAAS,QAAQ,CAAC,IAAyB;IACzC,OAAO;QACL,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,IAAI;QACV,KAAK,EAAE,IAAI;QACX,QAAQ,EAAE,CAAC;QACX,MAAM,EAAE,WAAW;QACnB,IAAI,EAAE,eAAe;QACrB,GAAG,IAAI;KACR,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,gBAAgB,CAAC,IAAyB;IAIvD,MAAM,KAAK,GAAe,EAAE,CAAC;IAC7B,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC3B,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,GAAW,EAAE,IAAwB,EAAE,EAAE;QACtE,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,IAAI,IAAI,CAGzC,CAAC;YACF,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;YACxD,OAAO;gBACL,EAAE,EAAE,IAAI;gBACR,MAAM,EAAE,GAAG;gBACX,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,EAAE;gBACpB,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,GAAG;aACtB,CAAC;QACJ,CAAC;QACD,iBAAiB;QACjB,OAAO;YACL,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,GAAG;YACX,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,EAAE;YACpB,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;gBACjB,EAAE,EAAE,UAAU;gBACd,MAAM,EAAE,UAAU;gBAClB,OAAO,EAAE,cAAc;gBACvB,OAAO,EAAE,CAAC,QAAQ,CAAC;gBACnB,MAAM,EAAE,QAAQ;gBAChB,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACpC,SAAS,EAAE,IAAI;aAChB,CAAC;SACH,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,UAAU,CAAC,KAAK,GAAG,SAAoC,CAAC;IAExD,MAAM,OAAO,GAAG,MAAM,aAAa,CACjC,cAAc,EACd,EAAE,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,EACvB,EAAE,OAAO,EAAE,yBAAyB,EAAE,MAAM,EAAE,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"shop.test.js","sourceRoot":"","sources":["../../src/__tests__/shop.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-timeout.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/stream-timeout.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
|
+
import { CodeSpar } from "../index.js";
|
|
3
|
+
import { TimeoutError } from "../errors.js";
|
|
4
|
+
const realFetch = globalThis.fetch;
|
|
5
|
+
afterEach(() => { globalThis.fetch = realFetch; vi.useRealTimers(); });
|
|
6
|
+
function sse(chunks, gapMs) {
|
|
7
|
+
const enc = new TextEncoder();
|
|
8
|
+
let i = 0;
|
|
9
|
+
return new ReadableStream({
|
|
10
|
+
pull(ctrl) {
|
|
11
|
+
if (i >= chunks.length)
|
|
12
|
+
return new Promise(() => { }); // then idle forever
|
|
13
|
+
const c = chunks[i++];
|
|
14
|
+
return new Promise((r) => setTimeout(() => { ctrl.enqueue(enc.encode(c)); r(); }, gapMs));
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function sessionCreate() {
|
|
19
|
+
return { ok: true, status: 201, text: async () => "", json: async () => ({
|
|
20
|
+
id: "ses_s", org_id: "o", user_id: "u", servers: [],
|
|
21
|
+
status: "active", created_at: new Date().toISOString(), closed_at: null,
|
|
22
|
+
}) };
|
|
23
|
+
}
|
|
24
|
+
function trackedSse(chunks, gapMs) {
|
|
25
|
+
const enc = new TextEncoder();
|
|
26
|
+
let i = 0;
|
|
27
|
+
const state = { cancelled: false };
|
|
28
|
+
const stream = new ReadableStream({
|
|
29
|
+
pull(ctrl) {
|
|
30
|
+
if (i >= chunks.length)
|
|
31
|
+
return new Promise(() => { });
|
|
32
|
+
const c = chunks[i++];
|
|
33
|
+
return new Promise((r) => setTimeout(() => { ctrl.enqueue(enc.encode(c)); r(); }, gapMs));
|
|
34
|
+
},
|
|
35
|
+
cancel() { state.cancelled = true; },
|
|
36
|
+
});
|
|
37
|
+
return { stream, state };
|
|
38
|
+
}
|
|
39
|
+
describe("stream idle timeout", () => {
|
|
40
|
+
it("delivers TimeoutError even when reader.cancel() never resolves", async () => {
|
|
41
|
+
// Pathological/degraded stream: cancel() returns a promise that
|
|
42
|
+
// never settles. Parser cleanup must not block delivery of the
|
|
43
|
+
// timeout to the caller.
|
|
44
|
+
const enc = new TextEncoder();
|
|
45
|
+
let sent = false;
|
|
46
|
+
const stream = new ReadableStream({
|
|
47
|
+
pull(ctrl) {
|
|
48
|
+
if (sent)
|
|
49
|
+
return new Promise(() => { }); // idle forever after 1 event
|
|
50
|
+
sent = true;
|
|
51
|
+
ctrl.enqueue(enc.encode('event: assistant_text\ndata: {"content":"hi","iteration":1}\n\n'));
|
|
52
|
+
},
|
|
53
|
+
cancel() { return new Promise(() => { }); }, // never resolves
|
|
54
|
+
});
|
|
55
|
+
globalThis.fetch = ((url) => {
|
|
56
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
57
|
+
return Promise.resolve(sessionCreate());
|
|
58
|
+
return Promise.resolve({ ok: true, body: stream });
|
|
59
|
+
});
|
|
60
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 50 });
|
|
61
|
+
const session = await cs.create("u");
|
|
62
|
+
await expect(async () => {
|
|
63
|
+
for await (const _ of session.sendStream("hi")) { /* drain */ }
|
|
64
|
+
}).rejects.toBeInstanceOf(TimeoutError);
|
|
65
|
+
}, 5000);
|
|
66
|
+
it("sendStream throws TimeoutError when the stream goes idle past the window", async () => {
|
|
67
|
+
globalThis.fetch = ((url) => {
|
|
68
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
69
|
+
return Promise.resolve(sessionCreate());
|
|
70
|
+
return Promise.resolve({ ok: true, body: sse(['event: assistant_text\ndata: {"content":"hi","iteration":1}\n\n'], 0) });
|
|
71
|
+
});
|
|
72
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 50 });
|
|
73
|
+
const session = await cs.create("u");
|
|
74
|
+
await expect(async () => {
|
|
75
|
+
for await (const _ of session.sendStream("hi")) { /* drain; stream idles after 1 event */ }
|
|
76
|
+
}).rejects.toBeInstanceOf(TimeoutError);
|
|
77
|
+
}, 5000);
|
|
78
|
+
it("sendStream times out when the initial fetch never returns headers/body", async () => {
|
|
79
|
+
// Backend accepts the connection but never sends SSE headers/body.
|
|
80
|
+
// Mirrors native fetch: hangs until the signal aborts, then rejects
|
|
81
|
+
// with the signal reason.
|
|
82
|
+
globalThis.fetch = ((url, init) => {
|
|
83
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
84
|
+
return Promise.resolve(sessionCreate());
|
|
85
|
+
return new Promise((_res, rej) => {
|
|
86
|
+
init?.signal?.addEventListener("abort", () => rej(init.signal.reason), { once: true });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 50 });
|
|
90
|
+
const session = await cs.create("u");
|
|
91
|
+
await expect(async () => {
|
|
92
|
+
for await (const _ of session.sendStream("hi")) { /* never reached */ }
|
|
93
|
+
}).rejects.toBeInstanceOf(TimeoutError);
|
|
94
|
+
}, 5000);
|
|
95
|
+
it("paymentStatusStream times out when the initial fetch never returns headers/body", async () => {
|
|
96
|
+
globalThis.fetch = ((url, init) => {
|
|
97
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
98
|
+
return Promise.resolve(sessionCreate());
|
|
99
|
+
return new Promise((_res, rej) => {
|
|
100
|
+
init?.signal?.addEventListener("abort", () => rej(init.signal.reason), { once: true });
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 10_000 });
|
|
104
|
+
const session = await cs.create("u");
|
|
105
|
+
await expect(session.paymentStatusStream("tc1", { timeout: 50, onUpdate() { } })).rejects.toBeInstanceOf(TimeoutError);
|
|
106
|
+
}, 5000);
|
|
107
|
+
it("paymentStatusStream stays alive while only heartbeat frames arrive", async () => {
|
|
108
|
+
// Healthy long-pending flow: snapshot, then several SSE comment
|
|
109
|
+
// heartbeats spaced below the idle window across more than one
|
|
110
|
+
// window, then a terminal done. Heartbeats are complete frames —
|
|
111
|
+
// they must keep the stream alive (parity with Python httpx, whose
|
|
112
|
+
// read timeout is reset by any incoming bytes).
|
|
113
|
+
const snapshot = 'event: snapshot\ndata: {"tool_call_id":"tc1","payment_status":"pending","idempotency_key":null,"original_status":"success","hosted_url":null,"events":[]}\n\n';
|
|
114
|
+
const done = 'event: done\ndata: {}\n\n';
|
|
115
|
+
const enc = new TextEncoder();
|
|
116
|
+
let stopped = false;
|
|
117
|
+
let step = 0;
|
|
118
|
+
// timeout 80ms; heartbeat every 40ms × 5 = 200ms (> 2 windows).
|
|
119
|
+
const stream = new ReadableStream({
|
|
120
|
+
pull(ctrl) {
|
|
121
|
+
return new Promise((r) => setTimeout(() => {
|
|
122
|
+
if (stopped)
|
|
123
|
+
return r();
|
|
124
|
+
if (step === 0)
|
|
125
|
+
ctrl.enqueue(enc.encode(snapshot));
|
|
126
|
+
else if (step <= 5)
|
|
127
|
+
ctrl.enqueue(enc.encode(":heartbeat\n\n"));
|
|
128
|
+
else {
|
|
129
|
+
ctrl.enqueue(enc.encode(done));
|
|
130
|
+
ctrl.close();
|
|
131
|
+
}
|
|
132
|
+
step++;
|
|
133
|
+
r();
|
|
134
|
+
}, step === 0 ? 0 : 40));
|
|
135
|
+
},
|
|
136
|
+
cancel() { stopped = true; },
|
|
137
|
+
});
|
|
138
|
+
globalThis.fetch = ((url) => {
|
|
139
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
140
|
+
return Promise.resolve(sessionCreate());
|
|
141
|
+
return Promise.resolve({ ok: true, body: stream });
|
|
142
|
+
});
|
|
143
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 10_000 });
|
|
144
|
+
const session = await cs.create("u");
|
|
145
|
+
const result = await session.paymentStatusStream("tc1", { timeout: 80, onUpdate() { } });
|
|
146
|
+
expect(result.payment_status).toBe("pending");
|
|
147
|
+
}, 5000);
|
|
148
|
+
it("cancels the response body when the idle timeout fires", async () => {
|
|
149
|
+
const { stream, state } = trackedSse(['event: assistant_text\ndata: {"content":"hi","iteration":1}\n\n'], 0);
|
|
150
|
+
globalThis.fetch = ((url) => {
|
|
151
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
152
|
+
return Promise.resolve(sessionCreate());
|
|
153
|
+
return Promise.resolve({ ok: true, body: stream });
|
|
154
|
+
});
|
|
155
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 50 });
|
|
156
|
+
const session = await cs.create("u");
|
|
157
|
+
await expect(async () => {
|
|
158
|
+
for await (const _ of session.sendStream("hi")) { /* drain */ }
|
|
159
|
+
}).rejects.toBeInstanceOf(TimeoutError);
|
|
160
|
+
expect(state.cancelled).toBe(true);
|
|
161
|
+
}, 5000);
|
|
162
|
+
it("propagates the caller's abort reason (not TimeoutError) and cancels the body", async () => {
|
|
163
|
+
const { stream, state } = trackedSse([], 0); // never emits
|
|
164
|
+
globalThis.fetch = ((url) => {
|
|
165
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
166
|
+
return Promise.resolve(sessionCreate());
|
|
167
|
+
return Promise.resolve({ ok: true, body: stream });
|
|
168
|
+
});
|
|
169
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 10_000 });
|
|
170
|
+
const session = await cs.create("u");
|
|
171
|
+
const ac = new AbortController();
|
|
172
|
+
const reason = new DOMException("user cancelled", "AbortError");
|
|
173
|
+
const iterate = (async () => {
|
|
174
|
+
// CallOptions is part of the public Session contract — no cast needed.
|
|
175
|
+
for await (const _ of session.sendStream("hi", { signal: ac.signal })) { /* drain */ }
|
|
176
|
+
})();
|
|
177
|
+
queueMicrotask(() => ac.abort(reason));
|
|
178
|
+
await expect(iterate).rejects.toBe(reason);
|
|
179
|
+
expect(state.cancelled).toBe(true);
|
|
180
|
+
}, 5000);
|
|
181
|
+
it("paymentStatusStream: per-call timeout overrides the client default", async () => {
|
|
182
|
+
// Stream emits one snapshot frame then idles forever.
|
|
183
|
+
const snapshotFrame = 'event: snapshot\ndata: {"tool_call_id":"tc1","payment_status":"pending","idempotency_key":null,"original_status":"success","hosted_url":null,"events":[]}\n\n';
|
|
184
|
+
globalThis.fetch = ((url) => {
|
|
185
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
186
|
+
return Promise.resolve(sessionCreate());
|
|
187
|
+
// Any payment-status/stream URL: emit one frame then idle forever.
|
|
188
|
+
return Promise.resolve({ ok: true, body: sse([snapshotFrame], 0) });
|
|
189
|
+
});
|
|
190
|
+
// Client default is intentionally large; per-call override is 50 ms.
|
|
191
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 10_000 });
|
|
192
|
+
const session = await cs.create("u");
|
|
193
|
+
await expect(session.paymentStatusStream("tc1", { timeout: 50, onUpdate() { } })).rejects.toBeInstanceOf(TimeoutError);
|
|
194
|
+
}, 5000);
|
|
195
|
+
it("drains a steady multi-frame stream without timing out", async () => {
|
|
196
|
+
const frames = Array.from({ length: 12 }, (_, n) => `event: assistant_text\ndata: {"content":"c${n}","iteration":${n}}\n\n`);
|
|
197
|
+
// Use a stream that closes (ctrl.close()) after all frames so the parser
|
|
198
|
+
// sees done=true rather than idling forever post-last-frame.
|
|
199
|
+
const enc = new TextEncoder();
|
|
200
|
+
let fi = 0;
|
|
201
|
+
const closingStream = new ReadableStream({
|
|
202
|
+
pull(ctrl) {
|
|
203
|
+
if (fi >= frames.length) {
|
|
204
|
+
ctrl.close();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const c = frames[fi++];
|
|
208
|
+
return new Promise((r) => setTimeout(() => { ctrl.enqueue(enc.encode(c)); r(); }, 5));
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
globalThis.fetch = ((url) => {
|
|
212
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
213
|
+
return Promise.resolve(sessionCreate());
|
|
214
|
+
return Promise.resolve({ ok: true, body: closingStream });
|
|
215
|
+
});
|
|
216
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 500 });
|
|
217
|
+
const session = await cs.create("u");
|
|
218
|
+
let count = 0;
|
|
219
|
+
for await (const ev of session.sendStream("hi")) {
|
|
220
|
+
if (ev.type === "assistant_text")
|
|
221
|
+
count++;
|
|
222
|
+
}
|
|
223
|
+
expect(count).toBe(12);
|
|
224
|
+
}, 5000);
|
|
225
|
+
it("does not time out when the consumer is slow between events", async () => {
|
|
226
|
+
// Network delivers two frames promptly; the consumer spends longer
|
|
227
|
+
// than the idle window processing the first one. The idle timer is
|
|
228
|
+
// a TRANSPORT idle timeout — consumer processing time must not count
|
|
229
|
+
// against it, otherwise healthy streams with heavy per-event work
|
|
230
|
+
// false-timeout.
|
|
231
|
+
const enc = new TextEncoder();
|
|
232
|
+
const frames = [
|
|
233
|
+
'event: assistant_text\ndata: {"content":"a","iteration":0}\n\n',
|
|
234
|
+
'event: assistant_text\ndata: {"content":"b","iteration":1}\n\n',
|
|
235
|
+
];
|
|
236
|
+
let fi = 0;
|
|
237
|
+
// highWaterMark 0 → pull only fires on an actual read(), so the
|
|
238
|
+
// post-sleep read genuinely waits on the network instead of
|
|
239
|
+
// replaying a pre-buffered frame (which would mask the bug).
|
|
240
|
+
const closing = new ReadableStream({
|
|
241
|
+
pull(ctrl) {
|
|
242
|
+
if (fi >= frames.length) {
|
|
243
|
+
ctrl.close();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const c = frames[fi++];
|
|
247
|
+
return new Promise((r) => setTimeout(() => { ctrl.enqueue(enc.encode(c)); r(); }, 10));
|
|
248
|
+
},
|
|
249
|
+
}, { highWaterMark: 0 });
|
|
250
|
+
globalThis.fetch = ((url) => {
|
|
251
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
252
|
+
return Promise.resolve(sessionCreate());
|
|
253
|
+
return Promise.resolve({ ok: true, body: closing });
|
|
254
|
+
});
|
|
255
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 80 });
|
|
256
|
+
const session = await cs.create("u");
|
|
257
|
+
let count = 0;
|
|
258
|
+
for await (const ev of session.sendStream("hi")) {
|
|
259
|
+
if (ev.type === "assistant_text") {
|
|
260
|
+
count++;
|
|
261
|
+
// Heavy per-event work, longer than the 80ms idle window.
|
|
262
|
+
await new Promise((r) => setTimeout(r, 160));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
expect(count).toBe(2);
|
|
266
|
+
}, 5000);
|
|
267
|
+
it("handles a final unterminated frame with a slow consumer without firing the idle timer", async () => {
|
|
268
|
+
// Last frame has no trailing "\n\n"; the stream then closes, so the
|
|
269
|
+
// event is emitted from the buffer.trim() branch. A slow consumer
|
|
270
|
+
// there must not trip the idle timer (no unhandled rejection / no
|
|
271
|
+
// false timeout) — the trailing yield pauses idle too.
|
|
272
|
+
const enc = new TextEncoder();
|
|
273
|
+
const chunks = [
|
|
274
|
+
'event: assistant_text\ndata: {"content":"a","iteration":0}\n\n',
|
|
275
|
+
'event: assistant_text\ndata: {"content":"b","iteration":1}', // no \n\n
|
|
276
|
+
];
|
|
277
|
+
let ci = 0;
|
|
278
|
+
const closing = new ReadableStream({
|
|
279
|
+
pull(ctrl) {
|
|
280
|
+
if (ci >= chunks.length) {
|
|
281
|
+
ctrl.close();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const c = chunks[ci++];
|
|
285
|
+
return new Promise((r) => setTimeout(() => { ctrl.enqueue(enc.encode(c)); r(); }, 10));
|
|
286
|
+
},
|
|
287
|
+
}, { highWaterMark: 0 });
|
|
288
|
+
globalThis.fetch = ((url) => {
|
|
289
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
290
|
+
return Promise.resolve(sessionCreate());
|
|
291
|
+
return Promise.resolve({ ok: true, body: closing });
|
|
292
|
+
});
|
|
293
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 60 });
|
|
294
|
+
const session = await cs.create("u");
|
|
295
|
+
let count = 0;
|
|
296
|
+
for await (const ev of session.sendStream("hi")) {
|
|
297
|
+
if (ev.type === "assistant_text") {
|
|
298
|
+
count++;
|
|
299
|
+
await new Promise((r) => setTimeout(r, 120)); // > idle window
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
expect(count).toBe(2);
|
|
303
|
+
}, 5000);
|
|
304
|
+
it("times out on a byte trickle that never completes an SSE frame", async () => {
|
|
305
|
+
// Server dribbles bytes faster than the idle window but never emits
|
|
306
|
+
// a complete "\n\n" frame. The idle timer must reset per PARSED
|
|
307
|
+
// event, not per raw read — otherwise the trickle resets it forever
|
|
308
|
+
// and the hang is never detected.
|
|
309
|
+
const enc = new TextEncoder();
|
|
310
|
+
const partial = "event: assistant_text\n"; // no blank line → no frame
|
|
311
|
+
let stopped = false;
|
|
312
|
+
const trickle = new ReadableStream({
|
|
313
|
+
pull(ctrl) {
|
|
314
|
+
return new Promise((r) => setTimeout(() => {
|
|
315
|
+
if (stopped)
|
|
316
|
+
return r();
|
|
317
|
+
ctrl.enqueue(enc.encode(partial));
|
|
318
|
+
r();
|
|
319
|
+
}, 20));
|
|
320
|
+
},
|
|
321
|
+
cancel() { stopped = true; },
|
|
322
|
+
});
|
|
323
|
+
globalThis.fetch = ((url) => {
|
|
324
|
+
if (String(url).endsWith("/v1/sessions"))
|
|
325
|
+
return Promise.resolve(sessionCreate());
|
|
326
|
+
return Promise.resolve({ ok: true, body: trickle });
|
|
327
|
+
});
|
|
328
|
+
const cs = new CodeSpar({ apiKey: "csk_live_t", baseUrl: "https://x", timeout: 120 });
|
|
329
|
+
const session = await cs.create("u");
|
|
330
|
+
await expect(async () => {
|
|
331
|
+
for await (const _ of session.sendStream("hi")) { /* never a full frame */ }
|
|
332
|
+
}).rejects.toBeInstanceOf(TimeoutError);
|
|
333
|
+
}, 5000);
|
|
334
|
+
});
|
|
335
|
+
//# sourceMappingURL=stream-timeout.test.js.map
|