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

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 (93) hide show
  1. package/lib/EnvironmentsApi.d.ts +2 -1
  2. package/lib/EnvironmentsApi.d.ts.map +1 -1
  3. package/lib/EnvironmentsApi.js +3 -0
  4. package/lib/EnvironmentsApi.js.map +1 -1
  5. package/lib/InferenceProfilesApi.d.ts +14 -0
  6. package/lib/InferenceProfilesApi.d.ts.map +1 -0
  7. package/lib/InferenceProfilesApi.js +33 -0
  8. package/lib/InferenceProfilesApi.js.map +1 -0
  9. package/lib/InteractionBase.d.ts +2 -954
  10. package/lib/InteractionBase.d.ts.map +1 -1
  11. package/lib/InteractionConfigurationsApi.d.ts +9 -0
  12. package/lib/InteractionConfigurationsApi.d.ts.map +1 -0
  13. package/lib/InteractionConfigurationsApi.js +16 -0
  14. package/lib/InteractionConfigurationsApi.js.map +1 -0
  15. package/lib/InteractionsApi.d.ts +2 -7
  16. package/lib/InteractionsApi.d.ts.map +1 -1
  17. package/lib/InteractionsApi.js.map +1 -1
  18. package/lib/InternalRunBudgetsApi.d.ts +8 -0
  19. package/lib/InternalRunBudgetsApi.d.ts.map +1 -0
  20. package/lib/InternalRunBudgetsApi.js +11 -0
  21. package/lib/InternalRunBudgetsApi.js.map +1 -0
  22. package/lib/client.d.ts +8 -10
  23. package/lib/client.d.ts.map +1 -1
  24. package/lib/client.js +7 -0
  25. package/lib/client.js.map +1 -1
  26. package/lib/index.d.ts +1 -0
  27. package/lib/index.d.ts.map +1 -1
  28. package/lib/index.js +1 -0
  29. package/lib/index.js.map +1 -1
  30. package/lib/store/AgentsApi.d.ts +6 -1
  31. package/lib/store/AgentsApi.d.ts.map +1 -1
  32. package/lib/store/AgentsApi.js +7 -0
  33. package/lib/store/AgentsApi.js.map +1 -1
  34. package/lib/store/ContentApi.d.ts +111 -0
  35. package/lib/store/ContentApi.d.ts.map +1 -0
  36. package/lib/store/ContentApi.js +83 -0
  37. package/lib/store/ContentApi.js.map +1 -0
  38. package/lib/store/DataApi.d.ts +1 -1
  39. package/lib/store/DataApi.js +1 -1
  40. package/lib/store/IndexingApi.d.ts +1 -1
  41. package/lib/store/IndexingApi.d.ts.map +1 -1
  42. package/lib/store/IndexingApi.js +2 -1
  43. package/lib/store/IndexingApi.js.map +1 -1
  44. package/lib/store/client.d.ts +2 -0
  45. package/lib/store/client.d.ts.map +1 -1
  46. package/lib/store/client.js +2 -0
  47. package/lib/store/client.js.map +1 -1
  48. package/lib/store/index.d.ts +1 -0
  49. package/lib/store/index.d.ts.map +1 -1
  50. package/lib/store/index.js +1 -0
  51. package/lib/store/index.js.map +1 -1
  52. package/lib/vertesia-client.js +4 -4
  53. package/lib/vertesia-client.js.map +1 -1
  54. package/package.json +11 -10
  55. package/src/EnvironmentsApi.ts +6 -0
  56. package/src/InferenceProfilesApi.ts +47 -0
  57. package/src/InteractionConfigurationsApi.ts +20 -0
  58. package/src/InteractionsApi.ts +2 -9
  59. package/src/InternalRunBudgetsApi.ts +13 -0
  60. package/src/client.ts +7 -0
  61. package/src/index.ts +1 -0
  62. package/src/store/AgentsApi.ts +9 -0
  63. package/src/store/ContentApi.ts +158 -0
  64. package/src/store/DataApi.ts +1 -1
  65. package/src/store/IndexingApi.ts +2 -0
  66. package/src/store/client.ts +2 -0
  67. package/src/store/index.ts +1 -0
  68. package/src/AccountApi.test.ts +0 -30
  69. package/src/AppsApi.test.ts +0 -132
  70. package/src/EnvironmentsApi.test.ts +0 -39
  71. package/src/InteractionOutput.test.ts +0 -338
  72. package/src/OAuthServerApi.test.ts +0 -56
  73. package/src/ProjectsApi.test.ts +0 -33
  74. package/src/RunsApi.test.ts +0 -24
  75. package/src/ViewsApi.test.ts +0 -38
  76. package/src/app-session.test.ts +0 -23
  77. package/src/bundle-isolation.test.ts +0 -37
  78. package/src/client.test.ts +0 -332
  79. package/src/execute.test.ts +0 -22
  80. package/src/store/AgentsApi.artifacts.test.ts +0 -119
  81. package/src/store/AgentsApi.history.test.ts +0 -29
  82. package/src/store/AgentsApi.stream.test.ts +0 -386
  83. package/src/store/DataApi.test.ts +0 -42
  84. package/src/store/EmbeddingsApi.test.ts +0 -31
  85. package/src/store/EventsApi.test.ts +0 -131
  86. package/src/store/FilesApi.test.ts +0 -118
  87. package/src/store/ObjectsApi.test.ts +0 -50
  88. package/src/store/ProcessApi.test.ts +0 -24
  89. package/src/store/ViewsApi.test.ts +0 -33
  90. package/src/store/signed-url.test.ts +0 -204
  91. package/src/store/stream-gap.test.ts +0 -230
  92. package/src/store/stream-termination.test.ts +0 -82
  93. package/src/store/uploadMimeType.test.ts +0 -22
@@ -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
- });
@@ -1,230 +0,0 @@
1
- import { type AgentMessage, AgentMessageType } from '@vertesia/common';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { VertesiaClient } from '../client.js';
4
-
5
- class FakeEventSource {
6
- static readonly CONNECTING = 0;
7
- static readonly OPEN = 1;
8
- static readonly CLOSED = 2;
9
-
10
- static urls: string[] = [];
11
-
12
- readonly url: string;
13
- readonly withCredentials = false;
14
- readyState = FakeEventSource.CONNECTING;
15
- onopen: ((event: Event) => void) | null = null;
16
- onmessage: ((event: MessageEvent) => void) | null = null;
17
- onerror: ((event: Event) => void) | null = null;
18
-
19
- constructor(url: string) {
20
- this.url = url;
21
- FakeEventSource.urls.push(url);
22
- }
23
-
24
- close() {
25
- this.readyState = FakeEventSource.CLOSED;
26
- }
27
-
28
- addEventListener() {}
29
-
30
- removeEventListener() {}
31
-
32
- dispatchEvent() {
33
- return true;
34
- }
35
- }
36
-
37
- function createClient(): VertesiaClient {
38
- const client = new VertesiaClient({
39
- serverUrl: 'https://studio.example.test',
40
- storeUrl: 'https://store.example.test',
41
- });
42
- client.withAuthCallback(async () => 'Bearer test-token');
43
- return client;
44
- }
45
-
46
- function message(timestamp: number): AgentMessage {
47
- return {
48
- type: AgentMessageType.THOUGHT,
49
- timestamp,
50
- workflow_run_id: 'run-1',
51
- message: 'historical',
52
- workstream_id: 'main',
53
- };
54
- }
55
-
56
- function mockApiGet<T extends object>(api: T, response: unknown) {
57
- return vi
58
- .spyOn(api as unknown as { get: (path: string, options?: unknown) => Promise<unknown> }, 'get')
59
- .mockResolvedValue(response);
60
- }
61
-
62
- describe('streamMessages GET-to-SSE handoff', () => {
63
- beforeEach(() => {
64
- FakeEventSource.urls = [];
65
- vi.stubGlobal('EventSource', FakeEventSource);
66
- });
67
-
68
- afterEach(() => {
69
- vi.unstubAllGlobals();
70
- vi.restoreAllMocks();
71
- });
72
-
73
- it('uses a positive SSE since cursor for an agent run with empty initial history', async () => {
74
- const client = createClient();
75
- vi.spyOn(client.agents, 'retrieveMessages').mockResolvedValue([]);
76
- const abort = new AbortController();
77
-
78
- const stream = client.agents.streamMessages('agent-run-1', undefined, undefined, abort.signal);
79
-
80
- await vi.waitFor(() => expect(FakeEventSource.urls).toHaveLength(1));
81
-
82
- const streamUrl = new URL(FakeEventSource.urls[0]);
83
- expect(streamUrl.searchParams.get('skipHistory')).toBe('true');
84
- expect(Number(streamUrl.searchParams.get('since'))).toBeGreaterThan(0);
85
-
86
- abort.abort();
87
- await stream;
88
- });
89
-
90
- it('keeps the latest historical timestamp as the SSE since cursor for an agent run', async () => {
91
- const client = createClient();
92
- vi.spyOn(client.agents, 'retrieveMessages').mockResolvedValue([message(1_234)]);
93
- const abort = new AbortController();
94
-
95
- const stream = client.agents.streamMessages('agent-run-1', undefined, undefined, abort.signal);
96
-
97
- await vi.waitFor(() => expect(FakeEventSource.urls).toHaveLength(1));
98
-
99
- const streamUrl = new URL(FakeEventSource.urls[0]);
100
- expect(streamUrl.searchParams.get('since')).toBe('1234');
101
-
102
- abort.abort();
103
- await stream;
104
- });
105
-
106
- it('delivers full agent history before closing on a terminal historical message', async () => {
107
- const client = createClient();
108
- const complete = {
109
- ...message(1_000),
110
- type: AgentMessageType.COMPLETE,
111
- message: 'previous turn complete',
112
- };
113
- const latest = {
114
- ...message(2_000),
115
- message: 'newer historical message',
116
- };
117
- vi.spyOn(client.agents, 'retrieveMessages').mockResolvedValue([complete, latest]);
118
- const delivered: AgentMessage[] = [];
119
- const abort = new AbortController();
120
-
121
- const stream = client.agents.streamMessages(
122
- 'agent-run-1',
123
- (msg) => {
124
- delivered.push(msg);
125
- },
126
- undefined,
127
- abort.signal,
128
- );
129
-
130
- await vi.waitFor(() => expect(FakeEventSource.urls).toHaveLength(1));
131
-
132
- expect(delivered.map((msg) => msg.message)).toEqual(['previous turn complete', 'newer historical message']);
133
- expect(new URL(FakeEventSource.urls[0]).searchParams.get('since')).toBe('2000');
134
-
135
- abort.abort();
136
- await stream;
137
- });
138
-
139
- it('normalizes legacy agent history messages before returning them to the UI', async () => {
140
- const client = createClient();
141
- mockApiGet(client.agents, {
142
- messages: [
143
- {
144
- type: 8,
145
- timestamp: 1_000,
146
- workflow_run_id: 'legacy-run',
147
- message: 'What are the news headlines in France today?',
148
- workstream_id: 'main',
149
- details: { event_class: 'user_content' },
150
- },
151
- {
152
- type: 7,
153
- timestamp: 2_000,
154
- workflow_run_id: 'legacy-run',
155
- message: 'Here are the headlines.',
156
- workstream_id: 'main',
157
- details: { streamed: true },
158
- },
159
- ],
160
- });
161
-
162
- const messages = await client.agents.retrieveMessages('agent-run-1');
163
-
164
- expect(messages).toEqual([
165
- expect.objectContaining({
166
- type: AgentMessageType.QUESTION,
167
- timestamp: 1_000,
168
- workflow_run_id: 'agent-run-1',
169
- message: 'What are the news headlines in France today?',
170
- details: { event_class: 'user_content' },
171
- }),
172
- expect.objectContaining({
173
- type: AgentMessageType.ANSWER,
174
- timestamp: 2_000,
175
- workflow_run_id: 'agent-run-1',
176
- message: 'Here are the headlines.',
177
- details: { streamed: true },
178
- }),
179
- ]);
180
- });
181
-
182
- it('normalizes legacy workflow history messages before returning them to the UI', async () => {
183
- const client = createClient();
184
- mockApiGet(client.workflows, {
185
- messages: [
186
- {
187
- type: 'answer',
188
- timestamp: 3_000,
189
- workflow_run_id: 'legacy-run',
190
- message: 'Workflow answer',
191
- workstream_id: 'main',
192
- },
193
- ],
194
- });
195
-
196
- const messages = await client.workflows.retrieveMessages('workflow-1', 'workflow-run-1');
197
-
198
- expect(messages).toEqual([
199
- expect.objectContaining({
200
- type: AgentMessageType.ANSWER,
201
- timestamp: 3_000,
202
- workflow_run_id: 'workflow-run-1',
203
- message: 'Workflow answer',
204
- }),
205
- ]);
206
- });
207
-
208
- it('uses a positive SSE since cursor for a workflow run with empty initial history', async () => {
209
- const client = createClient();
210
- vi.spyOn(client.workflows, 'retrieveMessages').mockResolvedValue([]);
211
- const abort = new AbortController();
212
-
213
- const stream = client.workflows.streamMessages(
214
- 'workflow-1',
215
- 'workflow-run-1',
216
- undefined,
217
- undefined,
218
- abort.signal,
219
- );
220
-
221
- await vi.waitFor(() => expect(FakeEventSource.urls).toHaveLength(1));
222
-
223
- const streamUrl = new URL(FakeEventSource.urls[0]);
224
- expect(streamUrl.searchParams.get('skipHistory')).toBe('true');
225
- expect(Number(streamUrl.searchParams.get('since'))).toBeGreaterThan(0);
226
-
227
- abort.abort();
228
- await stream;
229
- });
230
- });