@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.
- package/lib/AuditTrailApi.d.ts +3 -1
- package/lib/AuditTrailApi.d.ts.map +1 -1
- package/lib/AuditTrailApi.js +6 -0
- package/lib/AuditTrailApi.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 -954
- 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/InteractionsApi.d.ts +2 -7
- package/lib/InteractionsApi.d.ts.map +1 -1
- package/lib/InteractionsApi.js.map +1 -1
- package/lib/client.d.ts +6 -12
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +4 -3
- package/lib/client.js.map +1 -1
- package/lib/execute.d.ts.map +1 -1
- package/lib/execute.js +9 -2
- package/lib/execute.js.map +1 -1
- package/lib/index.d.ts +0 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +0 -2
- package/lib/index.js.map +1 -1
- package/lib/managed-sse.d.ts.map +1 -1
- package/lib/managed-sse.js +2 -3
- package/lib/managed-sse.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 +12 -4
- 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/WorkflowsApi.d.ts.map +1 -1
- package/lib/store/WorkflowsApi.js +6 -1
- package/lib/store/WorkflowsApi.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/store/signed-url.d.ts +13 -0
- package/lib/store/signed-url.d.ts.map +1 -1
- package/lib/store/signed-url.js +29 -1
- package/lib/store/signed-url.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/AuditTrailApi.ts +12 -0
- package/src/EnvironmentsApi.ts +6 -0
- package/src/InferenceProfilesApi.ts +47 -0
- package/src/InteractionConfigurationsApi.ts +20 -0
- package/src/InteractionsApi.ts +2 -9
- package/src/client.ts +4 -3
- package/src/execute.ts +15 -4
- package/src/index.ts +0 -2
- package/src/managed-sse.ts +2 -5
- package/src/store/AgentsApi.ts +14 -4
- package/src/store/ContentApi.ts +158 -0
- package/src/store/DataApi.ts +1 -1
- package/src/store/IndexingApi.ts +2 -0
- package/src/store/WorkflowsApi.ts +5 -1
- package/src/store/client.ts +2 -0
- package/src/store/index.ts +1 -0
- package/src/store/signed-url.ts +32 -1
- package/lib/InternalAccountsApi.d.ts +0 -7
- package/lib/InternalAccountsApi.d.ts.map +0 -1
- package/lib/InternalAccountsApi.js +0 -10
- package/lib/InternalAccountsApi.js.map +0 -1
- package/lib/InternalSecretsApi.d.ts +0 -18
- package/lib/InternalSecretsApi.d.ts.map +0 -1
- package/lib/InternalSecretsApi.js +0 -29
- package/lib/InternalSecretsApi.js.map +0 -1
- 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 -338
- package/src/InternalAccountsApi.ts +0 -12
- package/src/InternalSecretsApi.ts +0 -63
- 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/app-session.test.ts +0 -23
- 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 -386
- 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 -118
- 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,119 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { escapeArtifactPathDelimiters } from './AgentsApi.js';
|
|
3
|
-
import { ZenoClient } from './client.js';
|
|
4
|
-
|
|
5
|
-
const SERVER_URL = 'https://store.test';
|
|
6
|
-
const RUN_ID = 'run-1';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* A client whose injected fetch records the URL of every request it is handed. The URL is read off
|
|
10
|
-
* the Request object, so the assertions see the path after WHATWG URL parsing — which is where an
|
|
11
|
-
* unescaped `#` was being dropped.
|
|
12
|
-
*/
|
|
13
|
-
function clientRecordingUrls(body: unknown = { url: 'https://signed', path: 'p' }) {
|
|
14
|
-
const urls: string[] = [];
|
|
15
|
-
const client = new ZenoClient({
|
|
16
|
-
serverUrl: SERVER_URL,
|
|
17
|
-
apikey: 'token',
|
|
18
|
-
fetch: async (input: RequestInfo) => {
|
|
19
|
-
urls.push(input instanceof Request ? input.url : String(input));
|
|
20
|
-
return new Response(JSON.stringify(body), {
|
|
21
|
-
status: 200,
|
|
22
|
-
headers: { 'content-type': 'application/json' },
|
|
23
|
-
});
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
return { client, urls };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** The artifact path the server reconstructs: the URL path after the route prefix, decoded per segment. */
|
|
30
|
-
function serverPath(url: string, prefix: string): string {
|
|
31
|
-
const tail = new URL(url).pathname.split(`/${prefix}/`)[1] ?? '';
|
|
32
|
-
return tail.split('/').map(decodeURIComponent).join('/');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('escapeArtifactPathDelimiters', () => {
|
|
36
|
-
it('escapes the two characters that truncate a path', () => {
|
|
37
|
-
expect(escapeArtifactPathDelimiters('files/tpl (YYYY-0#).docx')).toBe('files/tpl (YYYY-0%23).docx');
|
|
38
|
-
expect(escapeArtifactPathDelimiters('files/report?v2.docx')).toBe('files/report%3Fv2.docx');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Scope guard. Anything this helper touches changes the bytes on the wire for filenames that
|
|
42
|
-
// work today, so it must leave everything except `#` and `?` exactly as it found it.
|
|
43
|
-
it.each([
|
|
44
|
-
['percent', 'files/100% done.docx'],
|
|
45
|
-
['spaces', 'files/MHA - Client IQ FSD.docx'],
|
|
46
|
-
['non-ascii and accents', 'files/café résumé.docx'],
|
|
47
|
-
['backslash', 'files/back\\slash.docx'],
|
|
48
|
-
['segment separators', 'files/nested/a b.txt'],
|
|
49
|
-
['already-encoded sequence', 'files/report%23.md'],
|
|
50
|
-
])('leaves %s untouched', (_label, path) => {
|
|
51
|
-
expect(escapeArtifactPathDelimiters(path)).toBe(path);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('AgentsApi artifact paths', () => {
|
|
56
|
-
afterEach(() => {
|
|
57
|
-
vi.restoreAllMocks();
|
|
58
|
-
vi.unstubAllGlobals();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// A `#` in an uploaded filename used to be parsed as the URL fragment, so the server saw a
|
|
62
|
-
// truncated path and stored the object under a key nothing could read back.
|
|
63
|
-
it('uploadArtifact preserves a path containing #', async () => {
|
|
64
|
-
vi.stubGlobal(
|
|
65
|
-
'fetch',
|
|
66
|
-
vi.fn(async () => new Response(null, { status: 200 })),
|
|
67
|
-
);
|
|
68
|
-
const { client, urls } = clientRecordingUrls();
|
|
69
|
-
const path = 'files/Design Template (YYYY-0#).docx';
|
|
70
|
-
|
|
71
|
-
await client.agents.uploadArtifact(RUN_ID, path, 'content');
|
|
72
|
-
|
|
73
|
-
expect(urls).toHaveLength(1);
|
|
74
|
-
expect(urls[0]).not.toContain('#');
|
|
75
|
-
expect(serverPath(urls[0], 'artifacts')).toBe(path);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// The query is concatenated onto the path, so an unescaped delimiter took it down too.
|
|
79
|
-
it.each([
|
|
80
|
-
['hash', 'files/Design (YYYY-0#).docx'],
|
|
81
|
-
['question mark', 'files/report?v2.docx'],
|
|
82
|
-
])('getArtifactUrl round-trips %s and keeps the query intact', async (_label, path) => {
|
|
83
|
-
const { client, urls } = clientRecordingUrls();
|
|
84
|
-
|
|
85
|
-
await client.agents.getArtifactUrl(RUN_ID, path, 'attachment', 'download-name.docx');
|
|
86
|
-
|
|
87
|
-
const url = new URL(urls[0]);
|
|
88
|
-
expect(serverPath(urls[0], 'artifacts')).toBe(path);
|
|
89
|
-
expect(url.hash).toBe('');
|
|
90
|
-
expect(url.searchParams.get('url')).toBe('1');
|
|
91
|
-
expect(url.searchParams.get('disposition')).toBe('attachment');
|
|
92
|
-
expect(url.searchParams.get('filename')).toBe('download-name.docx');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Pins the scope boundary: `%` must go on the wire exactly as it does with no escaping at all.
|
|
96
|
-
it('sends a path containing % exactly as it did before, unescaped', async () => {
|
|
97
|
-
const { client, urls } = clientRecordingUrls();
|
|
98
|
-
const path = 'files/100% done.docx';
|
|
99
|
-
|
|
100
|
-
await client.agents.getArtifactUrl(RUN_ID, path);
|
|
101
|
-
|
|
102
|
-
const unescaped = new URL(`${SERVER_URL}/api/v1/agents/${RUN_ID}/artifacts/${path}?url=1`);
|
|
103
|
-
expect(new URL(urls[0]).pathname).toBe(unescaped.pathname);
|
|
104
|
-
expect(new URL(urls[0]).pathname).toContain('100%%20done.docx');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('getArtifactContent and updateArtifactContent preserve a path containing #', async () => {
|
|
108
|
-
const { client, urls } = clientRecordingUrls({ content: '', generation: '1' });
|
|
109
|
-
const path = 'files/notes (v0#1).md';
|
|
110
|
-
|
|
111
|
-
await client.agents.getArtifactContent(RUN_ID, path);
|
|
112
|
-
await client.agents.updateArtifactContent(RUN_ID, path, { content: 'x', generation: '1' });
|
|
113
|
-
|
|
114
|
-
expect(urls).toHaveLength(2);
|
|
115
|
-
for (const url of urls) {
|
|
116
|
-
expect(serverPath(url, 'artifact-content')).toBe(path);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { VertesiaClient } from '../client.js';
|
|
3
|
-
|
|
4
|
-
describe('agent history cursor transport', () => {
|
|
5
|
-
it('passes the cursor and response metadata through for main and child details', async () => {
|
|
6
|
-
const urls: URL[] = [];
|
|
7
|
-
const response = { history: { type: 'agent', mode: 'delta', next_from: 'next', agentTasks: [] } };
|
|
8
|
-
const client = new VertesiaClient({
|
|
9
|
-
serverUrl: 'https://studio.test',
|
|
10
|
-
storeUrl: 'https://store.test',
|
|
11
|
-
fetch: (async (input: Request | string) => {
|
|
12
|
-
urls.push(new URL(typeof input === 'string' ? input : input.url));
|
|
13
|
-
return Response.json(response);
|
|
14
|
-
}) as typeof fetch,
|
|
15
|
-
});
|
|
16
|
-
const options = { includeHistory: true, hydratePayloads: true, from: 'opaque/+=' };
|
|
17
|
-
expect(await client.agents.getRunDetails('agent', options)).toEqual(response);
|
|
18
|
-
expect(await client.agents.getChildDetails('agent', 'child', options)).toEqual(response);
|
|
19
|
-
expect(urls.map((url) => url.pathname)).toEqual([
|
|
20
|
-
'/api/v1/agents/agent/details',
|
|
21
|
-
'/api/v1/agents/agent/children/child/details',
|
|
22
|
-
]);
|
|
23
|
-
for (const url of urls) {
|
|
24
|
-
expect(url.searchParams.get('from')).toBe(options.from);
|
|
25
|
-
expect(url.searchParams.get('include_history')).toBe('true');
|
|
26
|
-
expect(url.searchParams.get('hydrate_payloads')).toBe('true');
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
});
|
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
import { type AgentMessage, AgentMessageType, type CompactMessage } from '@vertesia/common';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { VertesiaClient } from '../client.js';
|
|
4
|
-
|
|
5
|
-
const AGENT_RUN_ID = 'run-1';
|
|
6
|
-
const STORE_URL = 'https://store.example.test';
|
|
7
|
-
|
|
8
|
-
class FakeEventSource {
|
|
9
|
-
static readonly CONNECTING = 0;
|
|
10
|
-
static readonly OPEN = 1;
|
|
11
|
-
static readonly CLOSED = 2;
|
|
12
|
-
|
|
13
|
-
static instances: FakeEventSource[] = [];
|
|
14
|
-
static urls: string[] = [];
|
|
15
|
-
|
|
16
|
-
readonly url: string;
|
|
17
|
-
readonly withCredentials = false;
|
|
18
|
-
readyState = FakeEventSource.CONNECTING;
|
|
19
|
-
onopen: ((event: Event) => void) | null = null;
|
|
20
|
-
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
21
|
-
onerror: ((event: Event) => void) | null = null;
|
|
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() {}
|
|
34
|
-
removeEventListener() {}
|
|
35
|
-
dispatchEvent() {
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
fail() {
|
|
40
|
-
this.onerror?.(new Event('error'));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
emit(message: CompactMessage) {
|
|
44
|
-
this.onmessage?.({ data: JSON.stringify(message) } as MessageEvent);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface Harness {
|
|
49
|
-
client: VertesiaClient;
|
|
50
|
-
/** `since` query value of every GET /updates call after the initial history fetch. */
|
|
51
|
-
pollSince: number[];
|
|
52
|
-
setHistory: (messages: CompactMessage[]) => void;
|
|
53
|
-
setPollResponse: (handler: (since: number) => CompactMessage[]) => void;
|
|
54
|
-
setRunStatus: (status: string) => void;
|
|
55
|
-
issuedTokens: string[];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function createHarness(): Harness {
|
|
59
|
-
let history: CompactMessage[] = [];
|
|
60
|
-
let pollResponse: (since: number) => CompactMessage[] = () => [];
|
|
61
|
-
let runStatus = 'running';
|
|
62
|
-
let historyServed = false;
|
|
63
|
-
const pollSince: number[] = [];
|
|
64
|
-
const issuedTokens: string[] = [];
|
|
65
|
-
|
|
66
|
-
const fetchMock = vi.fn(async (input: Request | string) => {
|
|
67
|
-
const url = new URL(typeof input === 'string' ? input : input.url);
|
|
68
|
-
if (url.pathname.endsWith('/updates')) {
|
|
69
|
-
if (!historyServed) {
|
|
70
|
-
historyServed = true;
|
|
71
|
-
return Response.json({ messages: history });
|
|
72
|
-
}
|
|
73
|
-
const since = Number(url.searchParams.get('since') ?? 0);
|
|
74
|
-
pollSince.push(since);
|
|
75
|
-
try {
|
|
76
|
-
return Response.json({ messages: pollResponse(since) });
|
|
77
|
-
} catch (err) {
|
|
78
|
-
// A thrown error carrying `status` models an HTTP error response (e.g. 404).
|
|
79
|
-
const status = (err as { status?: unknown }).status;
|
|
80
|
-
if (typeof status === 'number') {
|
|
81
|
-
return Response.json({ error: (err as Error).message }, { status });
|
|
82
|
-
}
|
|
83
|
-
throw err;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (url.pathname.endsWith(`/${AGENT_RUN_ID}`)) {
|
|
87
|
-
return Response.json({ id: AGENT_RUN_ID, status: runStatus });
|
|
88
|
-
}
|
|
89
|
-
return Response.json({ error: 'not found' }, { status: 404 });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const client = new VertesiaClient({
|
|
93
|
-
serverUrl: 'https://studio.example.test',
|
|
94
|
-
storeUrl: STORE_URL,
|
|
95
|
-
fetch: fetchMock as unknown as typeof fetch,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Mimics a refreshing credential provider: every call hands out a newer token.
|
|
99
|
-
client.withAuthCallback(async () => {
|
|
100
|
-
const token = `token-${issuedTokens.length + 1}`;
|
|
101
|
-
issuedTokens.push(token);
|
|
102
|
-
return `Bearer ${token}`;
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
client,
|
|
107
|
-
pollSince,
|
|
108
|
-
issuedTokens,
|
|
109
|
-
setHistory: (messages) => {
|
|
110
|
-
history = messages;
|
|
111
|
-
},
|
|
112
|
-
setPollResponse: (handler) => {
|
|
113
|
-
pollResponse = handler;
|
|
114
|
-
},
|
|
115
|
-
setRunStatus: (status) => {
|
|
116
|
-
runStatus = status;
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Advance fake timers and drain the microtask queue so awaited fetches settle. */
|
|
122
|
-
async function settle(ms = 0) {
|
|
123
|
-
await vi.advanceTimersByTimeAsync(ms);
|
|
124
|
-
for (let i = 0; i < 20; i++) {
|
|
125
|
-
await Promise.resolve();
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Longer than the capped backoff delay (30s + jitter). */
|
|
130
|
-
const BACKOFF_CEILING_MS = 40000;
|
|
131
|
-
|
|
132
|
-
async function failCurrentConnection() {
|
|
133
|
-
FakeEventSource.instances[FakeEventSource.instances.length - 1].fail();
|
|
134
|
-
await settle(BACKOFF_CEILING_MS);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
describe('AgentsApi.streamMessages reconnection', () => {
|
|
138
|
-
let logs: string[];
|
|
139
|
-
let warnings: string[];
|
|
140
|
-
|
|
141
|
-
beforeEach(() => {
|
|
142
|
-
vi.useFakeTimers();
|
|
143
|
-
FakeEventSource.instances = [];
|
|
144
|
-
FakeEventSource.urls = [];
|
|
145
|
-
logs = [];
|
|
146
|
-
warnings = [];
|
|
147
|
-
vi.stubGlobal('EventSource', FakeEventSource);
|
|
148
|
-
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
149
|
-
logs.push(args.map(String).join(' '));
|
|
150
|
-
});
|
|
151
|
-
vi.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => {
|
|
152
|
-
warnings.push(args.map(String).join(' '));
|
|
153
|
-
});
|
|
154
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
afterEach(() => {
|
|
158
|
-
vi.unstubAllGlobals();
|
|
159
|
-
vi.restoreAllMocks();
|
|
160
|
-
vi.useRealTimers();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('keeps interactive streams open on root IDLE by default', async () => {
|
|
164
|
-
const harness = createHarness();
|
|
165
|
-
const settled = vi.fn();
|
|
166
|
-
const received: AgentMessage[] = [];
|
|
167
|
-
const done = harness.client.agents
|
|
168
|
-
.streamMessages(AGENT_RUN_ID, (message) => received.push(message))
|
|
169
|
-
.then(settled);
|
|
170
|
-
await settle();
|
|
171
|
-
|
|
172
|
-
FakeEventSource.instances[0].emit({
|
|
173
|
-
t: AgentMessageType.IDLE,
|
|
174
|
-
m: 'Waiting for your command...',
|
|
175
|
-
w: 'main',
|
|
176
|
-
ts: 1_000,
|
|
177
|
-
});
|
|
178
|
-
await settle();
|
|
179
|
-
expect(settled).not.toHaveBeenCalled();
|
|
180
|
-
|
|
181
|
-
FakeEventSource.instances[0].emit({
|
|
182
|
-
t: AgentMessageType.ANSWER,
|
|
183
|
-
m: 'Follow-up response',
|
|
184
|
-
w: 'main',
|
|
185
|
-
ts: 1_500,
|
|
186
|
-
});
|
|
187
|
-
await settle();
|
|
188
|
-
expect(received.map((message) => message.message)).toEqual([
|
|
189
|
-
'Waiting for your command...',
|
|
190
|
-
'Follow-up response',
|
|
191
|
-
]);
|
|
192
|
-
|
|
193
|
-
FakeEventSource.instances[0].emit({
|
|
194
|
-
t: AgentMessageType.COMPLETE,
|
|
195
|
-
m: 'Conversation complete',
|
|
196
|
-
w: 'main',
|
|
197
|
-
ts: 2_000,
|
|
198
|
-
});
|
|
199
|
-
await expect(done).resolves.toBeUndefined();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('lets headless followers opt into closing on root IDLE', async () => {
|
|
203
|
-
const harness = createHarness();
|
|
204
|
-
const done = harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, undefined, {
|
|
205
|
-
closeOnIdle: true,
|
|
206
|
-
});
|
|
207
|
-
await settle();
|
|
208
|
-
|
|
209
|
-
FakeEventSource.instances[0].emit({
|
|
210
|
-
t: AgentMessageType.IDLE,
|
|
211
|
-
m: 'Waiting for your command...',
|
|
212
|
-
w: 'main',
|
|
213
|
-
ts: 1_000,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
await expect(done).resolves.toBeNull();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('resolves a fresh auth token for every reconnection attempt', async () => {
|
|
220
|
-
const harness = createHarness();
|
|
221
|
-
const abort = new AbortController();
|
|
222
|
-
void harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, abort.signal);
|
|
223
|
-
await settle();
|
|
224
|
-
|
|
225
|
-
expect(FakeEventSource.instances).toHaveLength(1);
|
|
226
|
-
await failCurrentConnection();
|
|
227
|
-
await failCurrentConnection();
|
|
228
|
-
expect(FakeEventSource.instances).toHaveLength(3);
|
|
229
|
-
|
|
230
|
-
const tokens = FakeEventSource.urls.map((url) => new URL(url).searchParams.get('access_token'));
|
|
231
|
-
expect(tokens).toHaveLength(3);
|
|
232
|
-
expect(new Set(tokens).size).toBe(3);
|
|
233
|
-
expect(tokens.every((token) => token?.startsWith('token-'))).toBe(true);
|
|
234
|
-
|
|
235
|
-
abort.abort();
|
|
236
|
-
await settle();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('never announces an attempt number above the stated maximum', async () => {
|
|
240
|
-
const harness = createHarness();
|
|
241
|
-
const abort = new AbortController();
|
|
242
|
-
void harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, abort.signal);
|
|
243
|
-
await settle();
|
|
244
|
-
|
|
245
|
-
// 10 reconnects are allowed; the 11th failure must degrade instead of announcing 11/10.
|
|
246
|
-
for (let i = 0; i < 11; i++) {
|
|
247
|
-
await failCurrentConnection();
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const announced = logs
|
|
251
|
-
.map((line) => /\(attempt (\d+)\/(\d+)\)/.exec(line))
|
|
252
|
-
.filter((match): match is RegExpExecArray => match !== null)
|
|
253
|
-
.map((match) => ({ attempt: Number(match[1]), max: Number(match[2]) }));
|
|
254
|
-
|
|
255
|
-
expect(announced).toHaveLength(10);
|
|
256
|
-
expect(announced.map((entry) => entry.attempt)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
|
257
|
-
expect(announced.every((entry) => entry.attempt <= entry.max)).toBe(true);
|
|
258
|
-
expect(FakeEventSource.instances).toHaveLength(11);
|
|
259
|
-
expect(warnings.some((line) => line.includes('SSE unavailable after 10 attempts'))).toBe(true);
|
|
260
|
-
|
|
261
|
-
abort.abort();
|
|
262
|
-
await settle();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('polls GET /updates from the last received timestamp and emits the messages', async () => {
|
|
266
|
-
const harness = createHarness();
|
|
267
|
-
harness.setHistory([{ t: AgentMessageType.UPDATE, m: 'history', ts: 1000 }]);
|
|
268
|
-
harness.setPollResponse((since) =>
|
|
269
|
-
since < 2000 ? [{ t: AgentMessageType.UPDATE, m: 'polled', ts: 2000 }] : [],
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
const received: AgentMessage[] = [];
|
|
273
|
-
const abort = new AbortController();
|
|
274
|
-
void harness.client.agents.streamMessages(
|
|
275
|
-
AGENT_RUN_ID,
|
|
276
|
-
(message) => received.push(message),
|
|
277
|
-
undefined,
|
|
278
|
-
abort.signal,
|
|
279
|
-
);
|
|
280
|
-
await settle();
|
|
281
|
-
|
|
282
|
-
for (let i = 0; i < 11; i++) {
|
|
283
|
-
await failCurrentConnection();
|
|
284
|
-
}
|
|
285
|
-
await settle(6000);
|
|
286
|
-
|
|
287
|
-
// The first poll resumes at the history cursor, later polls at the last polled message.
|
|
288
|
-
expect(harness.pollSince[0]).toBe(1000);
|
|
289
|
-
expect(harness.pollSince[1]).toBe(2000);
|
|
290
|
-
expect(received.map((message) => message.message)).toEqual(['history', 'polled']);
|
|
291
|
-
// Polled messages reach the caller as fully-formed AgentMessages, like the SSE path.
|
|
292
|
-
expect(received[1]).toMatchObject({
|
|
293
|
-
type: AgentMessageType.UPDATE,
|
|
294
|
-
workflow_run_id: AGENT_RUN_ID,
|
|
295
|
-
workstream_id: 'main',
|
|
296
|
-
timestamp: 2000,
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
abort.abort();
|
|
300
|
-
await settle();
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('warns on every polling failure instead of failing silently', async () => {
|
|
304
|
-
const harness = createHarness();
|
|
305
|
-
harness.setHistory([{ t: AgentMessageType.UPDATE, m: 'history', ts: 1000 }]);
|
|
306
|
-
harness.setPollResponse(() => {
|
|
307
|
-
throw new Error('boom');
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
const abort = new AbortController();
|
|
311
|
-
void harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, abort.signal);
|
|
312
|
-
await settle();
|
|
313
|
-
|
|
314
|
-
for (let i = 0; i < 11; i++) {
|
|
315
|
-
await failCurrentConnection();
|
|
316
|
-
}
|
|
317
|
-
await settle(6000);
|
|
318
|
-
|
|
319
|
-
const pollWarnings = warnings.filter((line) => line.includes('GET /updates failed while polling'));
|
|
320
|
-
expect(pollWarnings.length).toBeGreaterThanOrEqual(2);
|
|
321
|
-
expect(pollWarnings[0]).toContain('consecutive failures: 1');
|
|
322
|
-
expect(pollWarnings[1]).toContain('consecutive failures: 2');
|
|
323
|
-
|
|
324
|
-
abort.abort();
|
|
325
|
-
await settle();
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('retries SSE after the polling fallback so an outage is not permanent', async () => {
|
|
329
|
-
const harness = createHarness();
|
|
330
|
-
const abort = new AbortController();
|
|
331
|
-
void harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, abort.signal);
|
|
332
|
-
await settle();
|
|
333
|
-
|
|
334
|
-
for (let i = 0; i < 11; i++) {
|
|
335
|
-
await failCurrentConnection();
|
|
336
|
-
}
|
|
337
|
-
const connectionsBeforeRetry = FakeEventSource.instances.length;
|
|
338
|
-
expect(warnings.some((line) => line.includes('SSE retried every 60s'))).toBe(true);
|
|
339
|
-
|
|
340
|
-
await settle(61000);
|
|
341
|
-
|
|
342
|
-
expect(FakeEventSource.instances.length).toBe(connectionsBeforeRetry + 1);
|
|
343
|
-
expect(logs.some((line) => line.includes('retrying the SSE connection'))).toBe(true);
|
|
344
|
-
|
|
345
|
-
abort.abort();
|
|
346
|
-
await settle();
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it('stops the polling fallback when the run no longer exists (404)', async () => {
|
|
350
|
-
const harness = createHarness();
|
|
351
|
-
harness.setHistory([{ t: AgentMessageType.UPDATE, m: 'history', ts: 1000 }]);
|
|
352
|
-
harness.setPollResponse(() => {
|
|
353
|
-
throw Object.assign(new Error('Not Found: Agent run not found'), { status: 404 });
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
const abort = new AbortController();
|
|
357
|
-
const done = harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, abort.signal);
|
|
358
|
-
await settle();
|
|
359
|
-
|
|
360
|
-
for (let i = 0; i < 11; i++) {
|
|
361
|
-
await failCurrentConnection();
|
|
362
|
-
}
|
|
363
|
-
await settle(6000);
|
|
364
|
-
|
|
365
|
-
await expect(done).resolves.toBeNull();
|
|
366
|
-
expect(warnings.some((line) => line.includes('the run no longer exists (404)'))).toBe(true);
|
|
367
|
-
// A single 404 ends the loop: no retry warning, no further poll after the exit.
|
|
368
|
-
expect(warnings.filter((line) => line.includes('GET /updates failed while polling'))).toEqual([]);
|
|
369
|
-
expect(harness.pollSince).toHaveLength(1);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it('closes the stream when polling observes a terminal run status', async () => {
|
|
373
|
-
const harness = createHarness();
|
|
374
|
-
const abort = new AbortController();
|
|
375
|
-
const done = harness.client.agents.streamMessages(AGENT_RUN_ID, undefined, undefined, abort.signal);
|
|
376
|
-
await settle();
|
|
377
|
-
|
|
378
|
-
for (let i = 0; i < 11; i++) {
|
|
379
|
-
await failCurrentConnection();
|
|
380
|
-
}
|
|
381
|
-
harness.setRunStatus('completed');
|
|
382
|
-
await settle(6000);
|
|
383
|
-
|
|
384
|
-
await expect(done).resolves.toBeNull();
|
|
385
|
-
});
|
|
386
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { ImportDataPayload } from '@vertesia/common';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { ZenoClient } from './client.js';
|
|
4
|
-
|
|
5
|
-
describe('DataApi', () => {
|
|
6
|
-
// `import` is the name applications built against the 1.4 SDK call, through the client the
|
|
7
|
-
// platform serves them rather than one they bundle -- so renaming it (as 1.5 briefly did, to
|
|
8
|
-
// `importData`) breaks them at deploy time rather than at their next upgrade. This pins both
|
|
9
|
-
// the name and the request it issues.
|
|
10
|
-
it('exposes `import` as POST {store}/import with the data store header', async () => {
|
|
11
|
-
const requests: { url: string; method: string; storeHeader: string | null; body: string }[] = [];
|
|
12
|
-
const fetchImport = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
13
|
-
const request = new Request(input, init);
|
|
14
|
-
requests.push({
|
|
15
|
-
url: request.url,
|
|
16
|
-
method: request.method,
|
|
17
|
-
storeHeader: request.headers.get('x-data-store-id'),
|
|
18
|
-
body: await request.text(),
|
|
19
|
-
});
|
|
20
|
-
return new Response(JSON.stringify({ id: 'import-1', status: 'completed' }), {
|
|
21
|
-
status: 200,
|
|
22
|
-
headers: { 'content-type': 'application/json' },
|
|
23
|
-
});
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const client = new ZenoClient({ serverUrl: 'https://store.test', apikey: 'token', fetch: fetchImport });
|
|
27
|
-
const payload: ImportDataPayload = {
|
|
28
|
-
mode: 'append',
|
|
29
|
-
message: 'test import',
|
|
30
|
-
tables: { customers: { source: 'inline', data: [{ id: 1 }] } },
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const job = await client.data.import('store-1', payload);
|
|
34
|
-
|
|
35
|
-
expect(job).toEqual({ id: 'import-1', status: 'completed' });
|
|
36
|
-
expect(requests).toHaveLength(1);
|
|
37
|
-
expect(requests[0].method).toBe('POST');
|
|
38
|
-
expect(requests[0].url).toBe('https://store.test/api/v1/data/store-1/import');
|
|
39
|
-
expect(requests[0].storeHeader).toBe('store-1');
|
|
40
|
-
expect(JSON.parse(requests[0].body)).toEqual(payload);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { SupportedEmbeddingTypes } from '@vertesia/common';
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { ZenoClient } from './client.js';
|
|
4
|
-
|
|
5
|
-
describe('EmbeddingsApi', () => {
|
|
6
|
-
it('omits the mode by default and sends the explicit sync override when requested', async () => {
|
|
7
|
-
const requests: Request[] = [];
|
|
8
|
-
const client = new ZenoClient({
|
|
9
|
-
serverUrl: 'https://store.example.com',
|
|
10
|
-
apikey: 'token',
|
|
11
|
-
fetch: vi.fn(
|
|
12
|
-
async () =>
|
|
13
|
-
new Response(JSON.stringify({ status: 'success', message: 'started' }), {
|
|
14
|
-
status: 200,
|
|
15
|
-
headers: { 'content-type': 'application/json' },
|
|
16
|
-
}),
|
|
17
|
-
),
|
|
18
|
-
onRequest: (request) => requests.push(request),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
await client.embeddings.recalculate(SupportedEmbeddingTypes.text);
|
|
22
|
-
await client.embeddings.recalculate(SupportedEmbeddingTypes.text, { mode: 'sync' });
|
|
23
|
-
await client.embeddings.recalculate(SupportedEmbeddingTypes.image, { force: true });
|
|
24
|
-
|
|
25
|
-
expect(requests.map(({ url }) => url)).toEqual([
|
|
26
|
-
'https://store.example.com/api/v1/embeddings/text/recalculate',
|
|
27
|
-
'https://store.example.com/api/v1/embeddings/text/recalculate?mode=sync',
|
|
28
|
-
'https://store.example.com/api/v1/embeddings/image/recalculate?force=true',
|
|
29
|
-
]);
|
|
30
|
-
});
|
|
31
|
-
});
|