@aithos/sdk 0.1.0-alpha.5 → 0.1.0-alpha.51

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.
Files changed (67) hide show
  1. package/README.md +245 -7
  2. package/dist/src/apps.d.ts +224 -0
  3. package/dist/src/apps.js +432 -0
  4. package/dist/src/assets.d.ts +209 -0
  5. package/dist/src/assets.js +534 -0
  6. package/dist/src/auth-api.d.ts +219 -0
  7. package/dist/src/auth-api.js +248 -0
  8. package/dist/src/auth.d.ts +543 -0
  9. package/dist/src/auth.js +937 -31
  10. package/dist/src/compute.d.ts +464 -6
  11. package/dist/src/compute.js +746 -20
  12. package/dist/src/data-schema-contacts-v1.d.ts +14 -0
  13. package/dist/src/data-schema-contacts-v1.js +28 -0
  14. package/dist/src/data.d.ts +342 -0
  15. package/dist/src/data.js +1002 -0
  16. package/dist/src/endpoints.d.ts +25 -0
  17. package/dist/src/endpoints.js +7 -0
  18. package/dist/src/ethos.d.ts +85 -0
  19. package/dist/src/ethos.js +463 -7
  20. package/dist/src/index.d.ts +17 -6
  21. package/dist/src/index.js +25 -3
  22. package/dist/src/internal/delegate-bundle.js +7 -2
  23. package/dist/src/internal/envelope.d.ts +93 -0
  24. package/dist/src/internal/envelope.js +59 -0
  25. package/dist/src/mandates.d.ts +111 -2
  26. package/dist/src/mandates.js +150 -7
  27. package/dist/src/react/AithosAsset.d.ts +66 -0
  28. package/dist/src/react/AithosAsset.js +67 -0
  29. package/dist/src/react/context.d.ts +29 -0
  30. package/dist/src/react/context.js +31 -0
  31. package/dist/src/react/index.d.ts +29 -0
  32. package/dist/src/react/index.js +31 -0
  33. package/dist/src/react/use-aithos-asset.d.ts +39 -0
  34. package/dist/src/react/use-aithos-asset.js +118 -0
  35. package/dist/src/react/use-transcribe-pending.d.ts +21 -0
  36. package/dist/src/react/use-transcribe-pending.js +47 -0
  37. package/dist/src/sdk.d.ts +10 -0
  38. package/dist/src/sdk.js +22 -0
  39. package/dist/src/transcribe-resilience.d.ts +57 -0
  40. package/dist/src/transcribe-resilience.js +203 -0
  41. package/dist/src/web.d.ts +279 -0
  42. package/dist/src/web.js +186 -0
  43. package/dist/test/auth-j3.test.js +32 -1
  44. package/dist/test/canonical-conformance.test.d.ts +2 -0
  45. package/dist/test/canonical-conformance.test.js +86 -0
  46. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  47. package/dist/test/compute-delegate-path.test.js +183 -0
  48. package/dist/test/compute.test.js +4 -0
  49. package/dist/test/endpoints.test.js +30 -1
  50. package/dist/test/envelope-core-conformance.test.d.ts +2 -0
  51. package/dist/test/envelope-core-conformance.test.js +75 -0
  52. package/dist/test/envelope.test.d.ts +2 -0
  53. package/dist/test/envelope.test.js +318 -0
  54. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  55. package/dist/test/ethos-first-edition.test.js +371 -0
  56. package/dist/test/mandates-compute.test.d.ts +2 -0
  57. package/dist/test/mandates-compute.test.js +256 -0
  58. package/dist/test/sdk.test.js +11 -2
  59. package/dist/test/signup-bootstrap.test.d.ts +2 -0
  60. package/dist/test/signup-bootstrap.test.js +311 -0
  61. package/dist/test/transcribe-invoke.test.d.ts +2 -0
  62. package/dist/test/transcribe-invoke.test.js +204 -0
  63. package/dist/test/transcribe.test.d.ts +2 -0
  64. package/dist/test/transcribe.test.js +186 -0
  65. package/dist/test/web.test.d.ts +2 -0
  66. package/dist/test/web.test.js +270 -0
  67. package/package.json +20 -3
@@ -0,0 +1,311 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Tests for the Ethos bootstrap step inside AithosAuth.signUp().
4
+ //
5
+ // The contract:
6
+ // 1. POST /auth/register → creates the auth user (auth.aithos.be).
7
+ // 2. POST /mcp/primitives/write with method=aithos.publish_identity →
8
+ // provisions the user's Ethos on api.aithos.be.
9
+ // 3. Hydrate local state ONLY after both steps succeed.
10
+ //
11
+ // Failure modes:
12
+ // - register fails → throw, no publish_identity attempted, no hydrate.
13
+ // - publish_identity rejected (JSON-RPC error) → throw immediately,
14
+ // no retry, no hydrate.
15
+ // - publish_identity 5xx / network error → 2 retries with backoff,
16
+ // then throw `ethos_bootstrap_failed` if all fail.
17
+ //
18
+ // We mock fetch end-to-end so the tests run offline. The protocol-client
19
+ // crypto runs for real — we want to assert that the envelope shape coming
20
+ // out of the SDK matches what api.aithos.be will accept.
21
+ import { strict as assert } from "node:assert";
22
+ import { describe, it } from "node:test";
23
+ import { AithosAuth, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
24
+ function makeMockFetch(handlers) {
25
+ const calls = [];
26
+ const fetchImpl = (async (input, init) => {
27
+ const url = String(input);
28
+ const method = init?.method ?? "GET";
29
+ const bodyText = typeof init?.body === "string"
30
+ ? init.body
31
+ : init?.body == null
32
+ ? null
33
+ : String(init.body);
34
+ const body = bodyText ? JSON.parse(bodyText) : null;
35
+ const call = { url, method, body };
36
+ calls.push(call);
37
+ for (const h of handlers) {
38
+ if (!url.includes(h.url))
39
+ continue;
40
+ if (h.method && h.method !== method)
41
+ continue;
42
+ if (h.remaining !== undefined && h.remaining <= 0)
43
+ continue;
44
+ if (h.remaining !== undefined)
45
+ h.remaining--;
46
+ const out = await h.respond(call);
47
+ const status = out.status ?? 200;
48
+ return new Response(JSON.stringify(out.json), {
49
+ status,
50
+ headers: { "content-type": "application/json" },
51
+ });
52
+ }
53
+ throw new Error(`unhandled fetch: ${method} ${url}`);
54
+ });
55
+ return { fetch: fetchImpl, calls };
56
+ }
57
+ function fakeRegisterOk() {
58
+ return {
59
+ json: {
60
+ session: "jwt-token-here",
61
+ exp: Math.floor(Date.now() / 1000) + 3600,
62
+ },
63
+ };
64
+ }
65
+ function fakePublishIdentityOk() {
66
+ return {
67
+ json: {
68
+ jsonrpc: "2.0",
69
+ id: "publish_identity",
70
+ result: { ok: true, did_document_url: "https://cdn.aithos.be/ethos/zABC/did.json" },
71
+ },
72
+ };
73
+ }
74
+ function makeAuth(fetchImpl) {
75
+ return new AithosAuth({
76
+ authBaseUrl: "https://auth.test",
77
+ apiBaseUrl: "https://api.test",
78
+ fetch: fetchImpl,
79
+ sessionStore: noopStore(),
80
+ keyStore: memoryKeyStore(),
81
+ });
82
+ }
83
+ const validInput = {
84
+ email: "alice@test.example",
85
+ password: "correct horse battery staple",
86
+ handle: "alice",
87
+ };
88
+ /* -------------------------------------------------------------------------- */
89
+ /* Happy path */
90
+ /* -------------------------------------------------------------------------- */
91
+ describe("AithosAuth.signUp — Ethos bootstrap", () => {
92
+ it("calls /auth/register THEN /mcp/primitives/write with publish_identity", async () => {
93
+ const { fetch: f, calls } = makeMockFetch([
94
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
95
+ {
96
+ url: "/mcp/primitives/write",
97
+ method: "POST",
98
+ respond: fakePublishIdentityOk,
99
+ },
100
+ ]);
101
+ const auth = makeAuth(f);
102
+ const r = await auth.signUp(validInput);
103
+ assert.equal(calls.length, 2, "should make exactly 2 calls");
104
+ assert.match(calls[0].url, /\/auth\/register$/);
105
+ assert.match(calls[1].url, /\/mcp\/primitives\/write$/);
106
+ assert.ok(r.session.session === "jwt-token-here");
107
+ assert.ok(auth.canSignAsOwner(), "must be hydrated as owner");
108
+ });
109
+ it("envelope is JSON-RPC publish_identity, signed by #root, with valid params", async () => {
110
+ let publishBody = null;
111
+ const { fetch: f } = makeMockFetch([
112
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
113
+ {
114
+ url: "/mcp/primitives/write",
115
+ method: "POST",
116
+ respond: (call) => {
117
+ publishBody = call.body;
118
+ return fakePublishIdentityOk();
119
+ },
120
+ },
121
+ ]);
122
+ await makeAuth(f).signUp(validInput);
123
+ // JSON-RPC envelope shape
124
+ assert.equal(publishBody.jsonrpc, "2.0");
125
+ assert.equal(publishBody.method, "aithos.publish_identity");
126
+ // params include the inline _envelope and the publish_identity payload
127
+ const params = publishBody.params;
128
+ assert.equal(typeof params.handle, "string");
129
+ assert.equal(params.handle, "alice");
130
+ assert.equal(typeof params.display_name, "string");
131
+ assert.ok(params.did_document, "did_document must be present");
132
+ assert.ok(params._envelope, "_envelope must be present");
133
+ // envelope is signed by #root
134
+ const env = params._envelope;
135
+ assert.equal(env["aithos-envelope"], "0.1.0");
136
+ assert.match(env.iss, /^did:aithos:/);
137
+ assert.equal(env.method, "aithos.publish_identity");
138
+ assert.equal(env.aud, "https://api.test/mcp/primitives/write");
139
+ assert.equal(env.proof.type, "Ed25519Signature2020");
140
+ assert.match(env.proof.verificationMethod, /#root$/);
141
+ assert.equal(typeof env.proof.proofValue, "string");
142
+ assert.ok(env.proof.proofValue.length > 0);
143
+ });
144
+ it("does NOT hydrate state when /auth/register fails", async () => {
145
+ const { fetch: f, calls } = makeMockFetch([
146
+ {
147
+ url: "/auth/register",
148
+ method: "POST",
149
+ respond: () => ({
150
+ status: 409,
151
+ json: { error: "email_taken" },
152
+ }),
153
+ },
154
+ ]);
155
+ const auth = makeAuth(f);
156
+ await assert.rejects(() => auth.signUp(validInput), AithosSDKError);
157
+ assert.equal(calls.length, 1, "publish_identity must NOT be called");
158
+ assert.equal(auth.canSignAsOwner(), false);
159
+ });
160
+ it("does NOT hydrate state when publish_identity returns a JSON-RPC error", async () => {
161
+ const { fetch: f, calls } = makeMockFetch([
162
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
163
+ {
164
+ url: "/mcp/primitives/write",
165
+ method: "POST",
166
+ respond: () => ({
167
+ json: {
168
+ jsonrpc: "2.0",
169
+ id: "publish_identity",
170
+ error: { code: -32600, message: "invalid envelope signature" },
171
+ },
172
+ }),
173
+ },
174
+ ]);
175
+ const auth = makeAuth(f);
176
+ await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError && e.code === "ethos_bootstrap_failed");
177
+ // No retry on JSON-RPC error: 1 register + 1 publish.
178
+ assert.equal(calls.length, 2);
179
+ assert.equal(auth.canSignAsOwner(), false);
180
+ });
181
+ it("retries publish_identity on 5xx, then succeeds", async () => {
182
+ let publishCalls = 0;
183
+ const { fetch: f } = makeMockFetch([
184
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
185
+ {
186
+ url: "/mcp/primitives/write",
187
+ method: "POST",
188
+ respond: () => {
189
+ publishCalls++;
190
+ if (publishCalls < 2) {
191
+ return { status: 503, json: { error: "transient" } };
192
+ }
193
+ return fakePublishIdentityOk();
194
+ },
195
+ },
196
+ ]);
197
+ const auth = makeAuth(f);
198
+ await auth.signUp(validInput);
199
+ assert.equal(publishCalls, 2);
200
+ assert.ok(auth.canSignAsOwner());
201
+ });
202
+ it("throws ethos_bootstrap_failed after all retries fail with 5xx", async () => {
203
+ let publishCalls = 0;
204
+ const { fetch: f } = makeMockFetch([
205
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
206
+ {
207
+ url: "/mcp/primitives/write",
208
+ method: "POST",
209
+ respond: () => {
210
+ publishCalls++;
211
+ return { status: 503, json: { error: "transient" } };
212
+ },
213
+ },
214
+ ]);
215
+ const auth = makeAuth(f);
216
+ await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError && e.code === "ethos_bootstrap_failed");
217
+ // 3 attempts total (initial + 2 retries).
218
+ assert.equal(publishCalls, 3);
219
+ assert.equal(auth.canSignAsOwner(), false);
220
+ });
221
+ /* ------------------------------------------------------------------------ */
222
+ /* alpha.36 — defense-in-depth for legacy backends without semantic-equal */
223
+ /* ------------------------------------------------------------------------ */
224
+ it("treats -32022 'different did.json already published' as a no-op success", async () => {
225
+ // This is the regression introduced in alpha.33: republishing the same
226
+ // identity returns -32022 because `signedDidDocument()` regenerates
227
+ // `aithos.created_at` on every call. For an honest signer, this is
228
+ // semantically a no-op — the Ethos is published, crypto material matches.
229
+ // The SDK swallows this specific case so chatty publish_identity callers
230
+ // (signInCustodial, verifyEmail) don't break on every subsequent sign-in.
231
+ const { fetch: f, calls } = makeMockFetch([
232
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
233
+ {
234
+ url: "/mcp/primitives/write",
235
+ method: "POST",
236
+ respond: () => ({
237
+ json: {
238
+ jsonrpc: "2.0",
239
+ id: "publish_identity",
240
+ error: {
241
+ code: -32022,
242
+ message: "different did.json already published for this DID",
243
+ data: { existing_doc_url: "https://cdn.aithos.be/did.json" },
244
+ },
245
+ },
246
+ }),
247
+ },
248
+ ]);
249
+ const auth = makeAuth(f);
250
+ // Must succeed, not throw — the republish-conflict is masked.
251
+ await auth.signUp(validInput);
252
+ // Hydrate worked: owner is loaded.
253
+ assert.equal(auth.canSignAsOwner(), true);
254
+ // No retry on -32022 (deterministic) — exactly 1 publish call.
255
+ assert.equal(calls.filter((c) => c.url.includes("/mcp/primitives/write")).length, 1, "publish_identity must not retry on -32022");
256
+ });
257
+ it("still throws ethos_bootstrap_failed on -32022 with a DIFFERENT message", async () => {
258
+ // The shim is narrow: it ONLY swallows the specific
259
+ // "different did.json already published" message. Other -32022 cases
260
+ // (server might use the same code for different semantics) must still
261
+ // bubble up as ethos_bootstrap_failed.
262
+ const { fetch: f } = makeMockFetch([
263
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
264
+ {
265
+ url: "/mcp/primitives/write",
266
+ method: "POST",
267
+ respond: () => ({
268
+ json: {
269
+ jsonrpc: "2.0",
270
+ id: "publish_identity",
271
+ error: {
272
+ code: -32022,
273
+ message: "subject identity is tombstoned",
274
+ },
275
+ },
276
+ }),
277
+ },
278
+ ]);
279
+ const auth = makeAuth(f);
280
+ await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError &&
281
+ e.code === "ethos_bootstrap_failed" &&
282
+ /tombstoned/i.test(e.message));
283
+ assert.equal(auth.canSignAsOwner(), false);
284
+ });
285
+ it("still throws ethos_bootstrap_failed on the conflict message under a different code", async () => {
286
+ // Conversely, an error matching the message but with a code OTHER than
287
+ // -32022 is not swallowed — we anchor on both (code, message) to keep
288
+ // the shim narrow.
289
+ const { fetch: f } = makeMockFetch([
290
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
291
+ {
292
+ url: "/mcp/primitives/write",
293
+ method: "POST",
294
+ respond: () => ({
295
+ json: {
296
+ jsonrpc: "2.0",
297
+ id: "publish_identity",
298
+ error: {
299
+ code: -32000,
300
+ message: "different did.json already published for this DID",
301
+ },
302
+ },
303
+ }),
304
+ },
305
+ ]);
306
+ const auth = makeAuth(f);
307
+ await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError && e.code === "ethos_bootstrap_failed");
308
+ assert.equal(auth.canSignAsOwner(), false);
309
+ });
310
+ });
311
+ //# sourceMappingURL=signup-bootstrap.test.js.map
@@ -0,0 +1,2 @@
1
+ import "fake-indexeddb/auto";
2
+ //# sourceMappingURL=transcribe-invoke.test.d.ts.map
@@ -0,0 +1,204 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Tests for the high-level invokeTranscribe flow (prepare -> upload -> start
4
+ // -> poll) and the framework-agnostic resilience helpers (local pending
5
+ // tracker + IndexedDB draft store). fake-indexeddb/auto installs an
6
+ // in-memory IndexedDB for the draft tests.
7
+ import "fake-indexeddb/auto";
8
+ import { strict as assert } from "node:assert";
9
+ import { describe, it } from "node:test";
10
+ import { createBrowserIdentity } from "@aithos/protocol-client";
11
+ import { AithosAuth, AithosSDK, LocalPendingTranscribeTracker, TranscribeDraftStore, memoryKeyStore, noopStore, } from "../src/index.js";
12
+ import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
13
+ const APP_DID = "did:aithos:app:test";
14
+ const UPLOAD_URL = "https://s3.example.test/upload?sig=abc";
15
+ async function makeSdk(fetchImpl) {
16
+ const id = createBrowserIdentity("test-handle", "Test User");
17
+ const auth = new AithosAuth({
18
+ authBaseUrl: "https://auth.test",
19
+ fetch: (() => {
20
+ throw new Error("auth not used");
21
+ }),
22
+ sessionStore: noopStore(),
23
+ keyStore: memoryKeyStore(),
24
+ });
25
+ const { text } = serializeRecoveryFile(id);
26
+ await auth.signInWithRecovery({ file: text });
27
+ return new AithosSDK({
28
+ auth,
29
+ appDid: APP_DID,
30
+ endpoints: { compute: "https://compute.example.test" },
31
+ fetch: fetchImpl,
32
+ });
33
+ }
34
+ function jsonRpc(result) {
35
+ return new Response(JSON.stringify({ result }), {
36
+ status: 200,
37
+ headers: { "content-type": "application/json" },
38
+ });
39
+ }
40
+ describe("compute.invokeTranscribe — full flow", () => {
41
+ it("prepares, uploads to S3, starts, polls and returns the transcript", async () => {
42
+ const calls = [];
43
+ let putBytes = 0;
44
+ let statusPolls = 0;
45
+ const fetchImpl = async (input, init) => {
46
+ const url = typeof input === "string" ? input : input.toString();
47
+ if (init?.method === "PUT" && url.startsWith("https://s3.example.test/")) {
48
+ // S3 upload.
49
+ calls.push("s3-put");
50
+ putBytes = init.body.size;
51
+ return new Response("", { status: 200 });
52
+ }
53
+ const method = JSON.parse(init?.body).method;
54
+ calls.push(method);
55
+ if (method === "aithos.compute_transcribe_prepare") {
56
+ return jsonRpc({
57
+ job_id: "tj_FLOW",
58
+ upload_url: UPLOAD_URL,
59
+ s3_object_key: "uploads/2026/05/tj_FLOW.webm",
60
+ expires_at: 1717000000,
61
+ });
62
+ }
63
+ if (method === "aithos.compute_transcribe_start") {
64
+ return jsonRpc({
65
+ job_id: "tj_FLOW",
66
+ status: "running",
67
+ estimated_credits: 60,
68
+ walletBalance: 49_940,
69
+ fundedBy: "purchase",
70
+ });
71
+ }
72
+ if (method === "aithos.compute_transcribe_status") {
73
+ statusPolls += 1;
74
+ if (statusPolls < 2) {
75
+ return jsonRpc({ job_id: "tj_FLOW", status: "running", elapsed_sec: 1 });
76
+ }
77
+ return jsonRpc({
78
+ job_id: "tj_FLOW",
79
+ status: "completed",
80
+ text: "Bonjour le monde",
81
+ segments: [{ start_sec: 0, end_sec: 1.4, text: "Bonjour le monde" }],
82
+ words: [{ start_sec: 0, end_sec: 0.4, content: "Bonjour", confidence: 0.99 }],
83
+ duration_sec_actual: 30,
84
+ language_code: "fr-FR",
85
+ creditsCharged: 60,
86
+ walletBalance: 49_940,
87
+ auditId: "audit_flow",
88
+ fundedBy: "purchase",
89
+ });
90
+ }
91
+ throw new Error(`unexpected method ${method}`);
92
+ };
93
+ const sdk = await makeSdk(fetchImpl);
94
+ const phases = [];
95
+ const audio = new Blob([new Uint8Array(2048)], { type: "audio/webm" });
96
+ const result = await sdk.compute.invokeTranscribe({
97
+ audio,
98
+ durationSecOverride: 30,
99
+ pollIntervalMs: 1,
100
+ onProgress: (s) => phases.push(s.phase),
101
+ });
102
+ assert.equal(result.text, "Bonjour le monde");
103
+ assert.equal(result.durationSec, 30);
104
+ assert.equal(result.creditsCharged, 60);
105
+ assert.equal(result.jobId, "tj_FLOW");
106
+ assert.equal(result.fundedBy, "purchase");
107
+ assert.equal(result.segments.length, 1);
108
+ // The flow visited every step in order.
109
+ assert.deepEqual(calls, [
110
+ "aithos.compute_transcribe_prepare",
111
+ "s3-put",
112
+ "aithos.compute_transcribe_start",
113
+ "aithos.compute_transcribe_status",
114
+ "aithos.compute_transcribe_status",
115
+ ]);
116
+ assert.equal(putBytes, 2048);
117
+ assert.ok(phases.includes("queued"));
118
+ assert.ok(phases.includes("uploading"));
119
+ assert.ok(phases.includes("starting"));
120
+ assert.ok(phases.includes("completed"));
121
+ // Job cleared from the local pending tracker on success.
122
+ assert.equal(sdk.compute.listLocalPendingTranscribes().length, 0);
123
+ });
124
+ it("throws an AithosSDKError when the job fails, leaving the job flagged failed", async () => {
125
+ const fetchImpl = async (input, init) => {
126
+ const url = typeof input === "string" ? input : input.toString();
127
+ if (init?.method === "PUT")
128
+ return new Response("", { status: 200 });
129
+ const method = JSON.parse(init?.body).method;
130
+ if (method === "aithos.compute_transcribe_prepare") {
131
+ return jsonRpc({ job_id: "tj_F", upload_url: UPLOAD_URL, s3_object_key: "k", expires_at: 1 });
132
+ }
133
+ if (method === "aithos.compute_transcribe_start") {
134
+ return jsonRpc({ job_id: "tj_F", status: "running", estimated_credits: 60, walletBalance: 1 });
135
+ }
136
+ return jsonRpc({
137
+ job_id: "tj_F",
138
+ status: "failed",
139
+ error: { code: "transcribe_provider_error", message: "bad audio" },
140
+ });
141
+ };
142
+ const sdk = await makeSdk(fetchImpl);
143
+ const audio = new Blob([new Uint8Array(10)], { type: "audio/webm" });
144
+ await assert.rejects(() => sdk.compute.invokeTranscribe({ audio, durationSecOverride: 30, pollIntervalMs: 1 }), (e) => {
145
+ assert.ok(e instanceof Error);
146
+ assert.match(e.message, /bad audio/);
147
+ return true;
148
+ });
149
+ const pending = sdk.compute.listLocalPendingTranscribes();
150
+ assert.equal(pending.find((p) => p.jobId === "tj_F")?.status, "failed");
151
+ });
152
+ });
153
+ describe("LocalPendingTranscribeTracker (vanilla, framework-agnostic)", () => {
154
+ it("upserts, lists, removes and notifies subscribers", () => {
155
+ const t = new LocalPendingTranscribeTracker();
156
+ t.clear();
157
+ let notifications = 0;
158
+ const unsub = t.subscribe(() => {
159
+ notifications += 1;
160
+ });
161
+ t.upsert("tj_1", "uploading", { model: "transcribe:aws-fr-standard" });
162
+ assert.equal(t.list().length, 1);
163
+ assert.equal(t.list()[0]?.status, "uploading");
164
+ t.upsert("tj_1", "running");
165
+ assert.equal(t.list().length, 1, "same job id updates in place");
166
+ assert.equal(t.list()[0]?.status, "running");
167
+ t.upsert("tj_2", "running");
168
+ assert.equal(t.list().length, 2);
169
+ t.remove("tj_1");
170
+ assert.equal(t.list().length, 1);
171
+ assert.equal(t.list()[0]?.jobId, "tj_2");
172
+ assert.ok(notifications >= 4, "subscriber fired on each mutation");
173
+ unsub();
174
+ const before = notifications;
175
+ t.upsert("tj_3", "running");
176
+ assert.equal(notifications, before, "no notification after unsubscribe");
177
+ });
178
+ it("getSnapshot returns a stable reference between mutations", () => {
179
+ const t = new LocalPendingTranscribeTracker();
180
+ t.clear();
181
+ const a = t.getSnapshot();
182
+ const b = t.getSnapshot();
183
+ assert.equal(a, b);
184
+ t.upsert("tj_x", "running");
185
+ assert.notEqual(t.getSnapshot(), a, "new reference after a mutation");
186
+ });
187
+ });
188
+ describe("TranscribeDraftStore (IndexedDB via fake-indexeddb)", () => {
189
+ it("saves, lists, gets and deletes a draft", async () => {
190
+ const store = new TranscribeDraftStore();
191
+ const blob = new Blob([new Uint8Array(64)], { type: "audio/webm" });
192
+ const { draftId } = await store.save(blob, { title: "séance 1" });
193
+ assert.ok(draftId.startsWith("draft_"));
194
+ const list = await store.list();
195
+ assert.ok(list.some((d) => d.draftId === draftId));
196
+ const got = await store.get(draftId);
197
+ assert.equal(got?.metadata.title, "séance 1");
198
+ assert.equal(got?.metadata.contentType, "audio/webm");
199
+ assert.equal(got?.blob.size, 64);
200
+ await store.delete(draftId);
201
+ assert.equal(await store.get(draftId), null);
202
+ });
203
+ });
204
+ //# sourceMappingURL=transcribe-invoke.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=transcribe.test.d.ts.map