@vertesia/client 1.6.0-dev.20260923.085325Z → 1.6.0-dev.20261008.064737Z

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 (120) hide show
  1. package/lib/AuditTrailApi.d.ts +3 -1
  2. package/lib/AuditTrailApi.d.ts.map +1 -1
  3. package/lib/AuditTrailApi.js +6 -0
  4. package/lib/AuditTrailApi.js.map +1 -1
  5. package/lib/EnvironmentsApi.d.ts +2 -1
  6. package/lib/EnvironmentsApi.d.ts.map +1 -1
  7. package/lib/EnvironmentsApi.js +3 -0
  8. package/lib/EnvironmentsApi.js.map +1 -1
  9. package/lib/InferenceProfilesApi.d.ts +14 -0
  10. package/lib/InferenceProfilesApi.d.ts.map +1 -0
  11. package/lib/InferenceProfilesApi.js +33 -0
  12. package/lib/InferenceProfilesApi.js.map +1 -0
  13. package/lib/InteractionBase.d.ts +2 -954
  14. package/lib/InteractionBase.d.ts.map +1 -1
  15. package/lib/InteractionConfigurationsApi.d.ts +9 -0
  16. package/lib/InteractionConfigurationsApi.d.ts.map +1 -0
  17. package/lib/InteractionConfigurationsApi.js +16 -0
  18. package/lib/InteractionConfigurationsApi.js.map +1 -0
  19. package/lib/InteractionsApi.d.ts +2 -7
  20. package/lib/InteractionsApi.d.ts.map +1 -1
  21. package/lib/InteractionsApi.js.map +1 -1
  22. package/lib/client.d.ts +6 -12
  23. package/lib/client.d.ts.map +1 -1
  24. package/lib/client.js +4 -3
  25. package/lib/client.js.map +1 -1
  26. package/lib/execute.d.ts.map +1 -1
  27. package/lib/execute.js +9 -2
  28. package/lib/execute.js.map +1 -1
  29. package/lib/index.d.ts +0 -2
  30. package/lib/index.d.ts.map +1 -1
  31. package/lib/index.js +0 -2
  32. package/lib/index.js.map +1 -1
  33. package/lib/managed-sse.d.ts.map +1 -1
  34. package/lib/managed-sse.js +2 -3
  35. package/lib/managed-sse.js.map +1 -1
  36. package/lib/store/AgentsApi.d.ts +6 -1
  37. package/lib/store/AgentsApi.d.ts.map +1 -1
  38. package/lib/store/AgentsApi.js +12 -4
  39. package/lib/store/AgentsApi.js.map +1 -1
  40. package/lib/store/ContentApi.d.ts +111 -0
  41. package/lib/store/ContentApi.d.ts.map +1 -0
  42. package/lib/store/ContentApi.js +83 -0
  43. package/lib/store/ContentApi.js.map +1 -0
  44. package/lib/store/DataApi.d.ts +1 -1
  45. package/lib/store/DataApi.js +1 -1
  46. package/lib/store/IndexingApi.d.ts +1 -1
  47. package/lib/store/IndexingApi.d.ts.map +1 -1
  48. package/lib/store/IndexingApi.js +2 -1
  49. package/lib/store/IndexingApi.js.map +1 -1
  50. package/lib/store/WorkflowsApi.d.ts.map +1 -1
  51. package/lib/store/WorkflowsApi.js +6 -1
  52. package/lib/store/WorkflowsApi.js.map +1 -1
  53. package/lib/store/client.d.ts +2 -0
  54. package/lib/store/client.d.ts.map +1 -1
  55. package/lib/store/client.js +2 -0
  56. package/lib/store/client.js.map +1 -1
  57. package/lib/store/index.d.ts +1 -0
  58. package/lib/store/index.d.ts.map +1 -1
  59. package/lib/store/index.js +1 -0
  60. package/lib/store/index.js.map +1 -1
  61. package/lib/store/signed-url.d.ts +13 -0
  62. package/lib/store/signed-url.d.ts.map +1 -1
  63. package/lib/store/signed-url.js +29 -1
  64. package/lib/store/signed-url.js.map +1 -1
  65. package/lib/vertesia-client.js +4 -4
  66. package/lib/vertesia-client.js.map +1 -1
  67. package/package.json +12 -11
  68. package/src/AuditTrailApi.ts +12 -0
  69. package/src/EnvironmentsApi.ts +6 -0
  70. package/src/InferenceProfilesApi.ts +47 -0
  71. package/src/InteractionConfigurationsApi.ts +20 -0
  72. package/src/InteractionsApi.ts +2 -9
  73. package/src/client.ts +4 -3
  74. package/src/execute.ts +15 -4
  75. package/src/index.ts +0 -2
  76. package/src/managed-sse.ts +2 -5
  77. package/src/store/AgentsApi.ts +14 -4
  78. package/src/store/ContentApi.ts +158 -0
  79. package/src/store/DataApi.ts +1 -1
  80. package/src/store/IndexingApi.ts +2 -0
  81. package/src/store/WorkflowsApi.ts +5 -1
  82. package/src/store/client.ts +2 -0
  83. package/src/store/index.ts +1 -0
  84. package/src/store/signed-url.ts +32 -1
  85. package/lib/InternalAccountsApi.d.ts +0 -7
  86. package/lib/InternalAccountsApi.d.ts.map +0 -1
  87. package/lib/InternalAccountsApi.js +0 -10
  88. package/lib/InternalAccountsApi.js.map +0 -1
  89. package/lib/InternalSecretsApi.d.ts +0 -18
  90. package/lib/InternalSecretsApi.d.ts.map +0 -1
  91. package/lib/InternalSecretsApi.js +0 -29
  92. package/lib/InternalSecretsApi.js.map +0 -1
  93. package/src/AccountApi.test.ts +0 -30
  94. package/src/AppsApi.test.ts +0 -132
  95. package/src/EnvironmentsApi.test.ts +0 -39
  96. package/src/InteractionOutput.test.ts +0 -338
  97. package/src/InternalAccountsApi.ts +0 -12
  98. package/src/InternalSecretsApi.ts +0 -63
  99. package/src/OAuthServerApi.test.ts +0 -56
  100. package/src/ProjectsApi.test.ts +0 -33
  101. package/src/RunsApi.test.ts +0 -24
  102. package/src/ViewsApi.test.ts +0 -38
  103. package/src/app-session.test.ts +0 -23
  104. package/src/bundle-isolation.test.ts +0 -37
  105. package/src/client.test.ts +0 -332
  106. package/src/execute.test.ts +0 -22
  107. package/src/store/AgentsApi.artifacts.test.ts +0 -119
  108. package/src/store/AgentsApi.history.test.ts +0 -29
  109. package/src/store/AgentsApi.stream.test.ts +0 -386
  110. package/src/store/DataApi.test.ts +0 -42
  111. package/src/store/EmbeddingsApi.test.ts +0 -31
  112. package/src/store/EventsApi.test.ts +0 -131
  113. package/src/store/FilesApi.test.ts +0 -118
  114. package/src/store/ObjectsApi.test.ts +0 -50
  115. package/src/store/ProcessApi.test.ts +0 -24
  116. package/src/store/ViewsApi.test.ts +0 -33
  117. package/src/store/signed-url.test.ts +0 -204
  118. package/src/store/stream-gap.test.ts +0 -230
  119. package/src/store/stream-termination.test.ts +0 -82
  120. package/src/store/uploadMimeType.test.ts +0 -22
@@ -1,131 +0,0 @@
1
- import type { EventDeliveryStreamSnapshot } from '@vertesia/common';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { VertesiaClient } from '../client.js';
4
-
5
- type Listener = (event: MessageEvent) => void;
6
-
7
- class FakeEventSource {
8
- static readonly CONNECTING = 0;
9
- static readonly OPEN = 1;
10
- static readonly CLOSED = 2;
11
-
12
- static instances: FakeEventSource[] = [];
13
- static urls: string[] = [];
14
-
15
- readonly url: string;
16
- readonly withCredentials = false;
17
- readyState = FakeEventSource.CONNECTING;
18
- onopen: ((event: Event) => void) | null = null;
19
- onmessage: ((event: MessageEvent) => void) | null = null;
20
- onerror: ((event: Event) => void) | null = null;
21
- private readonly listeners = new Map<string, Listener[]>();
22
-
23
- constructor(url: string) {
24
- this.url = url;
25
- FakeEventSource.urls.push(url);
26
- FakeEventSource.instances.push(this);
27
- }
28
-
29
- close() {
30
- this.readyState = FakeEventSource.CLOSED;
31
- }
32
-
33
- addEventListener(type: string, listener: Listener) {
34
- const listeners = this.listeners.get(type) ?? [];
35
- listeners.push(listener);
36
- this.listeners.set(type, listeners);
37
- }
38
-
39
- removeEventListener() {}
40
-
41
- dispatchEvent() {
42
- return true;
43
- }
44
-
45
- emit(type: string, data: unknown, lastEventId = '') {
46
- const event = { data: JSON.stringify(data), lastEventId } as MessageEvent;
47
- for (const listener of this.listeners.get(type) ?? []) {
48
- listener(event);
49
- }
50
- if (type === 'message') {
51
- this.onmessage?.(event);
52
- }
53
- }
54
- }
55
-
56
- function createClient(): VertesiaClient {
57
- const client = new VertesiaClient({
58
- serverUrl: 'https://studio.example.test',
59
- storeUrl: 'https://store.example.test',
60
- });
61
- client.withAuthCallback(async () => 'Bearer test-token');
62
- return client;
63
- }
64
-
65
- describe('EventsApi.subscribeDeliveries', () => {
66
- beforeEach(() => {
67
- FakeEventSource.instances = [];
68
- FakeEventSource.urls = [];
69
- vi.stubGlobal('EventSource', FakeEventSource);
70
- });
71
-
72
- afterEach(() => {
73
- vi.unstubAllGlobals();
74
- vi.restoreAllMocks();
75
- });
76
-
77
- it('serializes stream filters and auth token into the EventSource URL', async () => {
78
- const client = createClient();
79
- const subscription = client.events.subscribeDeliveries({
80
- resource_id: 'agentic-coworker-platform',
81
- resource_type: ['app', 'workflow_run'],
82
- event_category: ['workflow', 'system'],
83
- action: ['create', 'workflow_completed'],
84
- outbox_status: ['failed'],
85
- include_event: true,
86
- poll_interval_ms: 1500,
87
- on_envelope: () => {},
88
- });
89
-
90
- await vi.waitFor(() => expect(FakeEventSource.urls).toHaveLength(1));
91
- const url = new URL(FakeEventSource.urls[0]);
92
-
93
- expect(url.href).toContain('https://store.example.test/api/v1/events/stream');
94
- expect(url.searchParams.get('access_token')).toBe('test-token');
95
- expect(url.searchParams.get('resource_id')).toBe('agentic-coworker-platform');
96
- expect(url.searchParams.get('resource_type')).toBe('app,workflow_run');
97
- expect(url.searchParams.get('event_category')).toBe('workflow,system');
98
- expect(url.searchParams.get('action')).toBe('create,workflow_completed');
99
- expect(url.searchParams.get('outbox_status')).toBe('failed');
100
- expect(url.searchParams.get('include_event')).toBe('true');
101
- expect(url.searchParams.get('poll_interval_ms')).toBe('1500');
102
-
103
- subscription.close();
104
- });
105
-
106
- it('dispatches typed snapshot envelopes to the snapshot callback', async () => {
107
- const client = createClient();
108
- const snapshots: EventDeliveryStreamSnapshot[] = [];
109
- const subscription = client.events.subscribeDeliveries({
110
- resource_id: 'demo-app',
111
- on_snapshot: (_items, envelope) => snapshots.push(envelope),
112
- });
113
-
114
- await vi.waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
115
- FakeEventSource.instances[0].emit(
116
- 'snapshot',
117
- {
118
- type: 'snapshot',
119
- emitted_at: '2026-06-27T00:00:00.000Z',
120
- cursor: 'evt_1',
121
- deliveries: [],
122
- } satisfies EventDeliveryStreamSnapshot,
123
- 'evt_1',
124
- );
125
-
126
- expect(snapshots).toHaveLength(1);
127
- expect(snapshots[0].cursor).toBe('evt_1');
128
-
129
- subscription.close();
130
- });
131
- });
@@ -1,118 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import { ZenoClient } from './client.js';
3
-
4
- describe('FilesApi', () => {
5
- it('retrieves a durable audio result through the authenticated file API', async () => {
6
- const bytes = new Uint8Array([82, 73, 70, 70, 1, 2, 3]);
7
- const sign = vi.fn(async () => Response.json({ url: 'https://signed.example/speech.wav' }));
8
- vi.stubGlobal(
9
- 'fetch',
10
- vi.fn(async () => new Response(bytes, { headers: { 'content-type': 'audio/wav' } })),
11
- );
12
- const client = new ZenoClient({ serverUrl: 'https://store.test', apikey: 'test-token', fetch: sign });
13
- const stream = await client.files.downloadFile('gs://bucket/runs/run-1/media/speech.wav');
14
- expect(new Uint8Array(await new Response(stream).arrayBuffer())).toEqual(bytes);
15
- expect(sign).toHaveBeenCalledOnce();
16
- });
17
-
18
- afterEach(() => {
19
- vi.restoreAllMocks();
20
- vi.unstubAllGlobals();
21
- });
22
-
23
- it('retries signed upload URL creation after a transient connection failure', async () => {
24
- let attempts = 0;
25
- const fetchSignedUploadUrl = async (): Promise<Response> => {
26
- attempts++;
27
- if (attempts === 1) {
28
- throw new TypeError('fetch failed');
29
- }
30
- return new Response(
31
- JSON.stringify({ id: 'file-1', path: 'agents/run/conversation.json', url: 'https://signed' }),
32
- {
33
- status: 200,
34
- headers: { 'content-type': 'application/json' },
35
- },
36
- );
37
- };
38
-
39
- const client = new ZenoClient({
40
- serverUrl: 'https://store.test',
41
- apikey: 'token',
42
- fetch: fetchSignedUploadUrl,
43
- });
44
-
45
- const result = await client.files.getUploadUrl({
46
- name: 'conversation.json',
47
- id: 'agents/run/conversation.json',
48
- });
49
-
50
- expect(result).toEqual({ id: 'file-1', path: 'agents/run/conversation.json', url: 'https://signed' });
51
- expect(attempts).toBe(2);
52
- });
53
-
54
- it('retries signed upload URL creation after a transient 503', async () => {
55
- let attempts = 0;
56
- const fetchSignedUploadUrl = async (): Promise<Response> => {
57
- attempts++;
58
- if (attempts === 1) {
59
- return new Response(JSON.stringify({ message: 'try again' }), {
60
- status: 503,
61
- headers: { 'content-type': 'application/json' },
62
- });
63
- }
64
- return new Response(
65
- JSON.stringify({ id: 'file-1', path: 'agents/run/conversation.json', url: 'https://signed' }),
66
- {
67
- status: 200,
68
- headers: { 'content-type': 'application/json' },
69
- },
70
- );
71
- };
72
-
73
- const client = new ZenoClient({
74
- serverUrl: 'https://store.test',
75
- apikey: 'token',
76
- fetch: fetchSignedUploadUrl,
77
- });
78
-
79
- const result = await client.files.getUploadUrl({
80
- name: 'conversation.json',
81
- id: 'agents/run/conversation.json',
82
- });
83
-
84
- expect(result).toEqual({ id: 'file-1', path: 'agents/run/conversation.json', url: 'https://signed' });
85
- expect(attempts).toBe(2);
86
- });
87
-
88
- it('does not expose a signed URL when a file download returns 404', async () => {
89
- const signedUrl =
90
- 'https://storage.googleapis.com/test-bucket/agents/run/files/missing.png?X-Goog-Credential=secret&X-Goog-Signature=do-not-log';
91
- const fetchDownloadUrl = (): Promise<Response> =>
92
- Promise.resolve(
93
- new Response(JSON.stringify({ url: signedUrl }), {
94
- status: 200,
95
- headers: { 'content-type': 'application/json' },
96
- }),
97
- );
98
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 404, statusText: 'Not Found' })));
99
- const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
100
- const client = new ZenoClient({
101
- serverUrl: 'https://store.test',
102
- apikey: 'token',
103
- fetch: fetchDownloadUrl,
104
- });
105
-
106
- const error = await client.files.downloadFile('agents/run/files/missing.png').catch((cause: unknown) => cause);
107
-
108
- expect(error).toMatchObject({
109
- name: 'FileDownloadError',
110
- status: 404,
111
- message: 'File download not found: agents/run/files/missing.png',
112
- });
113
- expect(String(error)).not.toContain('X-Goog-Credential');
114
- expect(String(error)).not.toContain('X-Goog-Signature');
115
- expect(String(error)).not.toContain('do-not-log');
116
- expect(consoleError).not.toHaveBeenCalled();
117
- });
118
- });
@@ -1,50 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { ZenoClient } from './client.js';
3
-
4
- describe('ObjectsApi revision labels', () => {
5
- it.each(['v1.2', 'Review — 日本語', 'Révision – café', '100% complete & reviewed + approved'])(
6
- 'preserves the revision label %s through the query contract',
7
- async (revisionLabel) => {
8
- const requests: Request[] = [];
9
- const client = new ZenoClient({
10
- serverUrl: 'https://api.example.com',
11
- fetch: vi.fn(async () => Response.json({ id: 'document-id' })),
12
- onRequest: (request) => requests.push(request),
13
- });
14
-
15
- await client.objects.update(
16
- 'document-id',
17
- { name: 'Updated document' },
18
- {
19
- createRevision: true,
20
- revisionLabel,
21
- ifMatch: 'previous-etag',
22
- },
23
- );
24
-
25
- expect(requests).toHaveLength(1);
26
- const request = requests[0];
27
- expect(request.method).toBe('PUT');
28
- expect(new URL(request.url).searchParams.get('revision_label')).toBe(revisionLabel);
29
- expect(request.headers.has('x-revision-label')).toBe(false);
30
- expect(request.headers.get('x-create-revision')).toBe('true');
31
- expect(request.headers.get('if-match')).toBe('previous-etag');
32
- expect(await request.clone().json()).toEqual({ name: 'Updated document' });
33
- },
34
- );
35
-
36
- it('does not send a label when updating in place', async () => {
37
- const requests: Request[] = [];
38
- const client = new ZenoClient({
39
- serverUrl: 'https://api.example.com',
40
- fetch: vi.fn(async () => Response.json({ id: 'document-id' })),
41
- onRequest: (request) => requests.push(request),
42
- });
43
-
44
- await client.objects.update('document-id', {}, { revisionLabel: 'Unused — label' });
45
-
46
- expect(new URL(requests[0].url).searchParams.has('revision_label')).toBe(false);
47
- expect(requests[0].headers.has('x-create-revision')).toBe(false);
48
- expect(requests[0].headers.has('x-revision-label')).toBe(false);
49
- });
50
- });
@@ -1,24 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { ZenoClient } from './client.js';
3
-
4
- function createClient(requests: Request[]) {
5
- return new ZenoClient({
6
- serverUrl: 'https://zeno.example.com',
7
- fetch: vi.fn(async () => Response.json([])),
8
- onRequest: (request) => requests.push(request),
9
- });
10
- }
11
-
12
- describe('ProcessApi.list', () => {
13
- it.each([
14
- ['canonical', { all_versions: true }],
15
- ['deprecated', { allVersions: true }],
16
- ])('maps the %s all-versions option to the wire query', async (_name, query) => {
17
- const requests: Request[] = [];
18
- const client = createClient(requests);
19
-
20
- await client.processes.list(query);
21
-
22
- expect(requests[0]?.url).toBe('https://zeno.example.com/api/v1/processes?all_versions=true');
23
- });
24
- });
@@ -1,33 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { ZenoClient } from './client.js';
3
-
4
- describe('StoreViewsApi', () => {
5
- it('uses the View execution resource for persisted and draft execution', async () => {
6
- const requests: Request[] = [];
7
- const fetchMock = vi.fn(async () => {
8
- return new Response(JSON.stringify({}), {
9
- status: 200,
10
- headers: { 'content-type': 'application/json' },
11
- });
12
- });
13
- const client = new ZenoClient({
14
- serverUrl: 'https://api.example.com',
15
- fetch: fetchMock,
16
- onRequest: (request) => requests.push(request),
17
- });
18
-
19
- await client.views.execute('app:content:document-library');
20
- await client.views.preview({
21
- configuration: {
22
- name: 'Document library',
23
- description: 'Browse documents.',
24
- scope: {},
25
- },
26
- });
27
-
28
- expect(requests.map((request) => request.url)).toEqual([
29
- 'https://api.example.com/api/v1/view-executions/app%3Acontent%3Adocument-library/execute',
30
- 'https://api.example.com/api/v1/view-executions/preview',
31
- ]);
32
- });
33
- });
@@ -1,204 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { fetchSignedUrl } from './signed-url.js';
3
-
4
- function streamOf(chunk: Uint8Array): ReadableStream {
5
- return new ReadableStream({
6
- start(controller) {
7
- controller.enqueue(chunk);
8
- controller.close();
9
- },
10
- });
11
- }
12
-
13
- function response(status: number, body = '', headers?: Record<string, string>): Response {
14
- return new Response(status === 204 ? null : body, { status, headers });
15
- }
16
-
17
- describe('fetchSignedUrl', () => {
18
- let fetchMock: ReturnType<typeof vi.fn>;
19
-
20
- beforeEach(() => {
21
- fetchMock = vi.fn();
22
- vi.stubGlobal('fetch', fetchMock);
23
- // Make backoff instantaneous so the tests don't actually wait.
24
- vi.useFakeTimers();
25
- });
26
-
27
- afterEach(() => {
28
- vi.useRealTimers();
29
- vi.unstubAllGlobals();
30
- vi.restoreAllMocks();
31
- });
32
-
33
- async function runAllTimers<T>(promise: Promise<T>): Promise<T> {
34
- await vi.runAllTimersAsync();
35
- return promise;
36
- }
37
-
38
- it('returns immediately on a successful response', async () => {
39
- fetchMock.mockResolvedValueOnce(response(200, 'ok'));
40
- const res = await runAllTimers(fetchSignedUrl('https://storage/x'));
41
- expect(res.status).toBe(200);
42
- expect(fetchMock).toHaveBeenCalledTimes(1);
43
- });
44
-
45
- it('retries on a 503 and succeeds', async () => {
46
- fetchMock
47
- .mockResolvedValueOnce(response(503, 'try later'))
48
- .mockResolvedValueOnce(response(503, 'try later'))
49
- .mockResolvedValueOnce(response(200, 'ok'));
50
- const res = await runAllTimers(fetchSignedUrl('https://storage/x', { method: 'PUT', body: 'data' }));
51
- expect(res.status).toBe(200);
52
- expect(fetchMock).toHaveBeenCalledTimes(3);
53
- });
54
-
55
- it('retries on connection errors and succeeds', async () => {
56
- fetchMock.mockRejectedValueOnce(new TypeError('network error')).mockResolvedValueOnce(response(200, 'ok'));
57
- const res = await runAllTimers(fetchSignedUrl('https://storage/x'));
58
- expect(res.status).toBe(200);
59
- expect(fetchMock).toHaveBeenCalledTimes(2);
60
- });
61
-
62
- it('does not retry non-retryable statuses (404)', async () => {
63
- fetchMock.mockResolvedValueOnce(response(404, 'missing'));
64
- const res = await runAllTimers(fetchSignedUrl('https://storage/x'));
65
- expect(res.status).toBe(404);
66
- expect(fetchMock).toHaveBeenCalledTimes(1);
67
- });
68
-
69
- it('returns the last retryable response after exhausting attempts', async () => {
70
- fetchMock.mockResolvedValue(response(503, 'down'));
71
- const res = await runAllTimers(fetchSignedUrl('https://storage/x', { attempts: 3 }));
72
- expect(res.status).toBe(503);
73
- expect(fetchMock).toHaveBeenCalledTimes(3);
74
- });
75
-
76
- it('throws the connection error after exhausting attempts', async () => {
77
- const err = new TypeError('network down');
78
- fetchMock.mockRejectedValue(err);
79
- // Attach the rejection expectation before advancing timers to avoid an
80
- // unhandled rejection while the backoff timer is pending.
81
- const assertion = expect(fetchSignedUrl('https://storage/x', { attempts: 2 })).rejects.toThrow('network down');
82
- await vi.runAllTimersAsync();
83
- await assertion;
84
- expect(fetchMock).toHaveBeenCalledTimes(2);
85
- });
86
-
87
- it('buffers a ReadableStream body so it can be replayed across retries', async () => {
88
- const bodies: unknown[] = [];
89
- fetchMock.mockImplementation((_url: string, init: RequestInit) => {
90
- bodies.push(init.body);
91
- return Promise.resolve(bodies.length < 2 ? response(503, 'retry') : response(200, 'ok'));
92
- });
93
-
94
- const stream = streamOf(new TextEncoder().encode('hello'));
95
- const res = await runAllTimers(
96
- fetchSignedUrl('https://storage/x', { method: 'PUT', body: stream as unknown as BodyInit }),
97
- );
98
-
99
- expect(res.status).toBe(200);
100
- expect(fetchMock).toHaveBeenCalledTimes(2);
101
- // The stream was buffered into a replayable Blob, and the same instance was reused on retry.
102
- expect(bodies[0]).toBeInstanceOf(Blob);
103
- expect(bodies[0]).toBe(bodies[1]);
104
- });
105
-
106
- it('buffers a string-chunk ReadableStream body without throwing (regression: non-Uint8Array chunk)', async () => {
107
- const bodies: unknown[] = [];
108
- fetchMock.mockImplementation((_url: string, init: RequestInit) => {
109
- bodies.push(init.body);
110
- return Promise.resolve(response(200, 'ok'));
111
- });
112
-
113
- // A web stream that yields *string* chunks — what `Readable.toWeb(Readable.from(text))`
114
- // produces (NodeStreamSource wraps text bodies this way). `new Response(stream).blob()`
115
- // rejects these under undici with "Received non-Uint8Array chunk"; the helper must
116
- // encode them to bytes instead.
117
- const stream = new ReadableStream<string>({
118
- start(controller) {
119
- controller.enqueue('hello ');
120
- controller.enqueue('world');
121
- controller.close();
122
- },
123
- });
124
-
125
- const res = await runAllTimers(
126
- fetchSignedUrl('https://storage/x', { method: 'PUT', body: stream as unknown as BodyInit }),
127
- );
128
-
129
- expect(res.status).toBe(200);
130
- expect(bodies[0]).toBeInstanceOf(Blob);
131
- expect(await (bodies[0] as Blob).text()).toBe('hello world');
132
- });
133
-
134
- it('buffers a mixed-chunk ReadableStream body, encoding strings and keeping bytes', async () => {
135
- const bodies: unknown[] = [];
136
- fetchMock.mockImplementation((_url: string, init: RequestInit) => {
137
- bodies.push(init.body);
138
- return Promise.resolve(response(200, 'ok'));
139
- });
140
-
141
- const stream = new ReadableStream<string | Uint8Array>({
142
- start(controller) {
143
- controller.enqueue('a');
144
- controller.enqueue(new TextEncoder().encode('b'));
145
- controller.close();
146
- },
147
- });
148
-
149
- const res = await runAllTimers(
150
- fetchSignedUrl('https://storage/x', { method: 'PUT', body: stream as unknown as BodyInit }),
151
- );
152
-
153
- expect(res.status).toBe(200);
154
- expect(bodies[0]).toBeInstanceOf(Blob);
155
- expect(await (bodies[0] as Blob).text()).toBe('ab');
156
- });
157
-
158
- it('throws on an object-mode stream chunk instead of silently corrupting the upload', async () => {
159
- const stream = new ReadableStream<unknown>({
160
- start(controller) {
161
- controller.enqueue({ not: 'bytes' });
162
- controller.close();
163
- },
164
- });
165
-
166
- await expect(
167
- fetchSignedUrl('https://storage/x', { method: 'PUT', body: stream as unknown as BodyInit }),
168
- ).rejects.toThrow(TypeError);
169
- // The bad body is rejected before any network call is attempted.
170
- expect(fetchMock).not.toHaveBeenCalled();
171
- expect(stream.locked).toBe(false);
172
- });
173
-
174
- it('honors a numeric Retry-After header when scheduling the retry', async () => {
175
- fetchMock
176
- .mockResolvedValueOnce(response(503, 'slow down', { 'retry-after': '30' }))
177
- .mockResolvedValueOnce(response(200, 'ok'));
178
- const pending = fetchSignedUrl('https://storage/x');
179
- await vi.advanceTimersByTimeAsync(29999);
180
- expect(fetchMock).toHaveBeenCalledTimes(1);
181
- await vi.advanceTimersByTimeAsync(1);
182
- const res = await pending;
183
- expect(res.status).toBe(200);
184
- expect(fetchMock).toHaveBeenCalledTimes(2);
185
- });
186
- });
187
-
188
- describe('Retry-After request budget', () => {
189
- it.each(['3600', '2147484', '1e308', new Date(Date.now() + 3600000).toUTCString()])(
190
- 'returns the intact response without an early retry for %s',
191
- async (retryAfter) => {
192
- const res = new Response('retry later', { status: 503, headers: { 'Retry-After': retryAfter } });
193
- const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(res);
194
- try {
195
- const result = await fetchSignedUrl('https://storage/x');
196
- expect(result).toBe(res);
197
- expect(await result.text()).toBe('retry later');
198
- expect(fetch).toHaveBeenCalledTimes(1);
199
- } finally {
200
- fetch.mockRestore();
201
- }
202
- },
203
- );
204
- });