@vertesia/client 1.6.0-dev.20260918.115658Z → 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.
- package/lib/AppsApi.d.ts +5 -0
- package/lib/AppsApi.d.ts.map +1 -1
- package/lib/AppsApi.js +9 -0
- package/lib/AppsApi.js.map +1 -1
- package/lib/EnvironmentsApi.d.ts +2 -1
- package/lib/EnvironmentsApi.d.ts.map +1 -1
- package/lib/EnvironmentsApi.js +3 -0
- package/lib/EnvironmentsApi.js.map +1 -1
- package/lib/InferenceProfilesApi.d.ts +14 -0
- package/lib/InferenceProfilesApi.d.ts.map +1 -0
- package/lib/InferenceProfilesApi.js +33 -0
- package/lib/InferenceProfilesApi.js.map +1 -0
- package/lib/InteractionBase.d.ts +2 -878
- package/lib/InteractionBase.d.ts.map +1 -1
- package/lib/InteractionConfigurationsApi.d.ts +9 -0
- package/lib/InteractionConfigurationsApi.d.ts.map +1 -0
- package/lib/InteractionConfigurationsApi.js +16 -0
- package/lib/InteractionConfigurationsApi.js.map +1 -0
- package/lib/InteractionOutput.d.ts +4 -0
- package/lib/InteractionOutput.d.ts.map +1 -1
- package/lib/InteractionOutput.js +13 -0
- package/lib/InteractionOutput.js.map +1 -1
- package/lib/InteractionsApi.d.ts +2 -7
- package/lib/InteractionsApi.d.ts.map +1 -1
- package/lib/InteractionsApi.js.map +1 -1
- package/lib/InternalAccountsApi.d.ts +7 -0
- package/lib/InternalAccountsApi.d.ts.map +1 -0
- package/lib/InternalAccountsApi.js +10 -0
- package/lib/InternalAccountsApi.js.map +1 -0
- package/lib/InternalRunBudgetsApi.d.ts +8 -0
- package/lib/InternalRunBudgetsApi.d.ts.map +1 -0
- package/lib/InternalRunBudgetsApi.js +11 -0
- package/lib/InternalRunBudgetsApi.js.map +1 -0
- package/lib/OAuthServerApi.d.ts +2 -1
- package/lib/OAuthServerApi.d.ts.map +1 -1
- package/lib/OAuthServerApi.js +3 -0
- package/lib/OAuthServerApi.js.map +1 -1
- package/lib/client.d.ts +10 -10
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +10 -0
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/store/AgentsApi.d.ts +6 -1
- package/lib/store/AgentsApi.d.ts.map +1 -1
- package/lib/store/AgentsApi.js +22 -0
- package/lib/store/AgentsApi.js.map +1 -1
- package/lib/store/ContentApi.d.ts +111 -0
- package/lib/store/ContentApi.d.ts.map +1 -0
- package/lib/store/ContentApi.js +83 -0
- package/lib/store/ContentApi.js.map +1 -0
- package/lib/store/DataApi.d.ts +1 -1
- package/lib/store/DataApi.js +1 -1
- package/lib/store/IndexingApi.d.ts +1 -1
- package/lib/store/IndexingApi.d.ts.map +1 -1
- package/lib/store/IndexingApi.js +2 -1
- package/lib/store/IndexingApi.js.map +1 -1
- package/lib/store/client.d.ts +2 -0
- package/lib/store/client.d.ts.map +1 -1
- package/lib/store/client.js +2 -0
- package/lib/store/client.js.map +1 -1
- package/lib/store/index.d.ts +1 -0
- package/lib/store/index.d.ts.map +1 -1
- package/lib/store/index.js +1 -0
- package/lib/store/index.js.map +1 -1
- package/lib/vertesia-client.js +4 -4
- package/lib/vertesia-client.js.map +1 -1
- package/package.json +12 -11
- package/src/AppsApi.ts +11 -0
- package/src/EnvironmentsApi.ts +6 -0
- package/src/InferenceProfilesApi.ts +47 -0
- package/src/InteractionConfigurationsApi.ts +20 -0
- package/src/InteractionOutput.ts +15 -0
- package/src/InteractionsApi.ts +2 -9
- package/src/InternalAccountsApi.ts +12 -0
- package/src/InternalRunBudgetsApi.ts +13 -0
- package/src/OAuthServerApi.ts +6 -0
- package/src/client.ts +12 -0
- package/src/index.ts +2 -0
- package/src/store/AgentsApi.ts +26 -0
- package/src/store/ContentApi.ts +158 -0
- package/src/store/DataApi.ts +1 -1
- package/src/store/IndexingApi.ts +2 -0
- package/src/store/client.ts +2 -0
- package/src/store/index.ts +1 -0
- package/src/AccountApi.test.ts +0 -30
- package/src/AppsApi.test.ts +0 -132
- package/src/EnvironmentsApi.test.ts +0 -39
- package/src/InteractionOutput.test.ts +0 -324
- package/src/OAuthServerApi.test.ts +0 -56
- package/src/ProjectsApi.test.ts +0 -33
- package/src/RunsApi.test.ts +0 -24
- package/src/ViewsApi.test.ts +0 -38
- package/src/bundle-isolation.test.ts +0 -37
- package/src/client.test.ts +0 -332
- package/src/execute.test.ts +0 -22
- package/src/store/AgentsApi.artifacts.test.ts +0 -119
- package/src/store/AgentsApi.history.test.ts +0 -29
- package/src/store/AgentsApi.stream.test.ts +0 -354
- package/src/store/DataApi.test.ts +0 -42
- package/src/store/EmbeddingsApi.test.ts +0 -31
- package/src/store/EventsApi.test.ts +0 -131
- package/src/store/FilesApi.test.ts +0 -105
- package/src/store/ObjectsApi.test.ts +0 -50
- package/src/store/ProcessApi.test.ts +0 -24
- package/src/store/ViewsApi.test.ts +0 -33
- package/src/store/signed-url.test.ts +0 -204
- package/src/store/stream-gap.test.ts +0 -230
- package/src/store/stream-termination.test.ts +0 -82
- package/src/store/uploadMimeType.test.ts +0 -22
|
@@ -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
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { type AgentMessage, AgentMessageType, type CompactMessage } from '@vertesia/common';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { shouldCloseAgentRunStream, shouldCloseCompactRunStream } from './stream-termination.js';
|
|
4
|
-
|
|
5
|
-
describe('stream termination', () => {
|
|
6
|
-
const rootRunId = 'root-run';
|
|
7
|
-
|
|
8
|
-
it('closes agent streams on the root run completion', () => {
|
|
9
|
-
const message: AgentMessage = {
|
|
10
|
-
type: AgentMessageType.COMPLETE,
|
|
11
|
-
timestamp: Date.now(),
|
|
12
|
-
workflow_run_id: rootRunId,
|
|
13
|
-
message: 'root complete',
|
|
14
|
-
workstream_id: 'main',
|
|
15
|
-
details: {
|
|
16
|
-
process_run_id: rootRunId,
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
expect(shouldCloseAgentRunStream(message, rootRunId)).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('keeps agent streams open for child process completion on main', () => {
|
|
24
|
-
const message: AgentMessage = {
|
|
25
|
-
type: AgentMessageType.COMPLETE,
|
|
26
|
-
timestamp: Date.now(),
|
|
27
|
-
workflow_run_id: rootRunId,
|
|
28
|
-
message: 'child complete',
|
|
29
|
-
workstream_id: 'main',
|
|
30
|
-
details: {
|
|
31
|
-
process_run_id: 'child-run',
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
expect(shouldCloseAgentRunStream(message, rootRunId)).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('keeps compact streams open for child process completion on main', () => {
|
|
39
|
-
const message: CompactMessage = {
|
|
40
|
-
t: AgentMessageType.COMPLETE,
|
|
41
|
-
m: 'child complete',
|
|
42
|
-
d: {
|
|
43
|
-
process_run_id: 'child-run',
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
expect(shouldCloseCompactRunStream(message, rootRunId)).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('still closes streams for main completion without process metadata', () => {
|
|
51
|
-
const message: CompactMessage = {
|
|
52
|
-
t: AgentMessageType.COMPLETE,
|
|
53
|
-
m: 'conversation complete',
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
expect(shouldCloseCompactRunStream(message, rootRunId)).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('keeps interactive root streams open when the conversation becomes idle', () => {
|
|
60
|
-
const message: AgentMessage = {
|
|
61
|
-
type: AgentMessageType.IDLE,
|
|
62
|
-
timestamp: Date.now(),
|
|
63
|
-
workflow_run_id: rootRunId,
|
|
64
|
-
message: 'Waiting for your command...',
|
|
65
|
-
workstream_id: 'main',
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
expect(shouldCloseAgentRunStream(message, rootRunId)).toBe(false);
|
|
69
|
-
expect(shouldCloseAgentRunStream(message, rootRunId, true)).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('keeps compact streams open when a child workstream becomes idle', () => {
|
|
73
|
-
const message: CompactMessage = {
|
|
74
|
-
t: AgentMessageType.IDLE,
|
|
75
|
-
m: 'Child waiting',
|
|
76
|
-
w: 'implementation',
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
expect(shouldCloseCompactRunStream(message, rootRunId)).toBe(false);
|
|
80
|
-
expect(shouldCloseCompactRunStream(message, rootRunId, true)).toBe(false);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { getUploadMimeTypeHint, resolveUploadMimeType } from './uploadMimeType.js';
|
|
3
|
-
|
|
4
|
-
describe('upload MIME type resolution', () => {
|
|
5
|
-
it('keeps a specific browser-provided content type', () => {
|
|
6
|
-
expect(getUploadMimeTypeHint('text/plain')).toBe('text/plain');
|
|
7
|
-
expect(resolveUploadMimeType('text/plain', 'text/markdown')).toBe('text/plain');
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('uses filename inference when the browser reports generic binary content', () => {
|
|
11
|
-
expect(getUploadMimeTypeHint('application/octet-stream')).toBeUndefined();
|
|
12
|
-
expect(resolveUploadMimeType('application/octet-stream', 'text/markdown')).toBe('text/markdown');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('uses filename inference when the browser omits the content type', () => {
|
|
16
|
-
expect(resolveUploadMimeType('', 'text/markdown')).toBe('text/markdown');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('retains octet-stream when no more specific type can be inferred', () => {
|
|
20
|
-
expect(resolveUploadMimeType('application/octet-stream', undefined)).toBe('application/octet-stream');
|
|
21
|
-
});
|
|
22
|
-
});
|