@slates/test 1.0.0-rc.2 → 1.0.0-rc.4

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/src/runtime.ts ADDED
@@ -0,0 +1,352 @@
1
+ import {
2
+ createLocalSlateTransport,
3
+ createSlatesClient,
4
+ SlateProtocolError
5
+ } from '@slates/client';
6
+ import type { SlatesProtocolClientOptions } from '@slates/client';
7
+ import {
8
+ createSlatesClientFromProfile,
9
+ openSlatesCliStore,
10
+ SlatesProfileRecord
11
+ } from '@slates/profiles';
12
+ import { readFile } from 'fs/promises';
13
+
14
+ export interface SlatesRuntimeContext {
15
+ integration: string | null;
16
+ profileId: string | null;
17
+ profile: SlatesProfileRecord | null;
18
+ rootDir: string;
19
+ storePath: string;
20
+ cliDir: string;
21
+ }
22
+
23
+ export type SlatesTestClient = ReturnType<typeof createSlatesClient>;
24
+
25
+ type LocalSlate = Parameters<typeof createLocalSlateTransport>[0]['slate'];
26
+
27
+ export interface ExpectedSlateAction {
28
+ id: string;
29
+ name?: string;
30
+ description?: string;
31
+ readOnly?: boolean;
32
+ destructive?: boolean;
33
+ invocationType?: 'polling' | 'webhook';
34
+ }
35
+
36
+ export let getVitestExpect = () => {
37
+ let maybeExpect = (globalThis as typeof globalThis & { expect?: typeof import('vitest').expect })
38
+ .expect;
39
+
40
+ if (!maybeExpect) {
41
+ throw new Error('Vitest expect is not available in the current runtime.');
42
+ }
43
+
44
+ return maybeExpect;
45
+ };
46
+
47
+ export let loadSlatesRuntimeContext = async (
48
+ opts: {
49
+ cwd?: string;
50
+ profile?: string | null;
51
+ } = {}
52
+ ): Promise<SlatesRuntimeContext> => {
53
+ let runtimeContextPath = process.env.SLATES_TEST_CONTEXT_PATH;
54
+ if (runtimeContextPath) {
55
+ let raw = await readFile(runtimeContextPath, 'utf-8');
56
+ let parsed = JSON.parse(raw) as {
57
+ integration?: string | null;
58
+ profileId: string | null;
59
+ rootDir?: string;
60
+ storePath: string;
61
+ cliDir: string;
62
+ };
63
+ let store = await openSlatesCliStore({
64
+ storePath: parsed.storePath,
65
+ rootDir: parsed.rootDir ?? process.env.SLATES_STORE_ROOT_DIR
66
+ });
67
+ let profile = store.getProfile(opts.profile ?? parsed.profileId ?? null);
68
+
69
+ return {
70
+ integration: parsed.integration ?? store.scope?.key ?? null,
71
+ profileId: profile?.id ?? parsed.profileId ?? null,
72
+ profile,
73
+ rootDir: parsed.rootDir ?? store.rootDir,
74
+ storePath: parsed.storePath,
75
+ cliDir: parsed.cliDir
76
+ };
77
+ }
78
+
79
+ let store = process.env.SLATES_STORE_PATH
80
+ ? await openSlatesCliStore({
81
+ storePath: process.env.SLATES_STORE_PATH,
82
+ rootDir: process.env.SLATES_STORE_ROOT_DIR
83
+ })
84
+ : await openSlatesCliStore({ cwd: opts.cwd });
85
+ let profileId = opts.profile ?? process.env.SLATES_PROFILE_ID ?? null;
86
+ let profile = store.getProfile(profileId);
87
+
88
+ return {
89
+ integration: process.env.SLATES_INTEGRATION ?? store.scope?.key ?? null,
90
+ profileId: profile?.id ?? null,
91
+ profile,
92
+ rootDir: store.rootDir,
93
+ storePath: store.storePath,
94
+ cliDir: store.dirPath
95
+ };
96
+ };
97
+
98
+ export let loadSlatesProfile = async (
99
+ opts: { cwd?: string; profile?: string | null } = {}
100
+ ) => {
101
+ let context = await loadSlatesRuntimeContext(opts);
102
+ if (!context.profile) {
103
+ throw new Error('No Slates profile is available for the current test context.');
104
+ }
105
+
106
+ return context.profile;
107
+ };
108
+
109
+ export let createSlatesTestClient = async (
110
+ opts: { cwd?: string; profile?: string | null } = {}
111
+ ) => {
112
+ let context = await loadSlatesRuntimeContext(opts);
113
+ if (!context.profile) {
114
+ throw new Error('No Slates profile is available for the current test context.');
115
+ }
116
+
117
+ let store = await openSlatesCliStore({
118
+ storePath: context.storePath,
119
+ rootDir: context.rootDir
120
+ });
121
+ return createSlatesClientFromProfile(context.profile, { cwd: opts.cwd, store });
122
+ };
123
+
124
+ export let withSlateProfile = async <T>(
125
+ profileName: string | null | undefined,
126
+ cb: (ctx: { profile: SlatesProfileRecord }) => Promise<T>
127
+ ) => {
128
+ let profile = await loadSlatesProfile({ profile: profileName });
129
+ return cb({ profile });
130
+ };
131
+
132
+ export let expectToolCall = async (d: {
133
+ client?: Awaited<ReturnType<typeof createSlatesTestClient>>;
134
+ profile?: string | null;
135
+ toolId: string;
136
+ input: Record<string, any>;
137
+ output?: Record<string, any>;
138
+ }) => {
139
+ let expect = getVitestExpect();
140
+ let client = d.client ?? (await createSlatesTestClient({ profile: d.profile }));
141
+ let result = await client.invokeTool(d.toolId, d.input);
142
+
143
+ if (d.output) {
144
+ expect(result.output).toMatchObject(d.output);
145
+ }
146
+
147
+ return result;
148
+ };
149
+
150
+ export let createLocalSlateTestClient = (opts: {
151
+ slate: LocalSlate;
152
+ state?: SlatesProtocolClientOptions['state'];
153
+ participants?: SlatesProtocolClientOptions['participants'];
154
+ }) =>
155
+ createSlatesClient({
156
+ transport: createLocalSlateTransport({ slate: opts.slate }),
157
+ state: opts.state,
158
+ participants: opts.participants
159
+ });
160
+
161
+ export let getSlateContract = async (client: SlatesTestClient) => {
162
+ let [provider, actions, authMethods, configSchema] = await Promise.all([
163
+ client.identify(),
164
+ client.listActions(),
165
+ client.listAuthMethods(),
166
+ client.getConfigSchema()
167
+ ]);
168
+
169
+ return {
170
+ provider: provider.provider,
171
+ actions: actions.actions,
172
+ tools: actions.actions.filter(action => action.type === 'action.tool'),
173
+ triggers: actions.actions.filter(action => action.type === 'action.trigger'),
174
+ authMethods: authMethods.authenticationMethods,
175
+ configSchema: configSchema.schema
176
+ };
177
+ };
178
+
179
+ let expectActionMatches = (actual: Record<string, any> | undefined, expected: ExpectedSlateAction) => {
180
+ let expect = getVitestExpect();
181
+ expect(actual).toBeTruthy();
182
+ expect(actual?.id).toBe(expected.id);
183
+
184
+ if (expected.name !== undefined) {
185
+ expect(actual?.name).toBe(expected.name);
186
+ }
187
+
188
+ if (expected.description !== undefined) {
189
+ expect(actual?.description).toBe(expected.description);
190
+ }
191
+
192
+ if (expected.readOnly !== undefined) {
193
+ expect(actual?.tags?.readOnly ?? false).toBe(expected.readOnly);
194
+ }
195
+
196
+ if (expected.destructive !== undefined) {
197
+ expect(actual?.tags?.destructive ?? false).toBe(expected.destructive);
198
+ }
199
+
200
+ if (expected.invocationType !== undefined) {
201
+ expect((actual as { invocation?: { type?: string } } | undefined)?.invocation?.type).toBe(
202
+ expected.invocationType
203
+ );
204
+ }
205
+ };
206
+
207
+ export let expectSlateContract = async (d: {
208
+ client: SlatesTestClient;
209
+ provider?: {
210
+ id: string;
211
+ name?: string;
212
+ description?: string;
213
+ };
214
+ toolIds?: string[];
215
+ triggerIds?: string[];
216
+ authMethodIds?: string[];
217
+ tools?: ExpectedSlateAction[];
218
+ triggers?: ExpectedSlateAction[];
219
+ }) => {
220
+ let expect = getVitestExpect();
221
+ let contract = await getSlateContract(d.client);
222
+
223
+ if (d.provider) {
224
+ expect(contract.provider.id).toBe(d.provider.id);
225
+ if (d.provider.name !== undefined) {
226
+ expect(contract.provider.name).toBe(d.provider.name);
227
+ }
228
+ if (d.provider.description !== undefined) {
229
+ expect(contract.provider.description).toBe(d.provider.description);
230
+ }
231
+ }
232
+
233
+ if (d.toolIds) {
234
+ expect(contract.tools.map(action => action.id)).toEqual(d.toolIds);
235
+ }
236
+
237
+ if (d.triggerIds) {
238
+ expect(contract.triggers.map(action => action.id)).toEqual(d.triggerIds);
239
+ }
240
+
241
+ if (d.authMethodIds) {
242
+ expect(contract.authMethods.map(method => method.id)).toEqual(d.authMethodIds);
243
+ }
244
+
245
+ for (let tool of d.tools ?? []) {
246
+ expectActionMatches(contract.tools.find(action => action.id === tool.id), tool);
247
+ }
248
+
249
+ for (let trigger of d.triggers ?? []) {
250
+ expectActionMatches(contract.triggers.find(action => action.id === trigger.id), trigger);
251
+ }
252
+
253
+ return contract;
254
+ };
255
+
256
+ export let registerSlateTriggerWebhook = async (d: {
257
+ client: SlatesTestClient;
258
+ triggerId: string;
259
+ webhookBaseUrl: string;
260
+ }) => d.client.registerTriggerWebhook(d.triggerId, d.webhookBaseUrl);
261
+
262
+ export let pollSlateTriggerEvents = async (d: {
263
+ client: SlatesTestClient;
264
+ triggerId: string;
265
+ state?: any;
266
+ }) => {
267
+ d.client.ensureSession();
268
+ return d.client.request('slates/action.trigger.poll_events', {
269
+ actionId: d.triggerId,
270
+ state: d.state ?? null
271
+ });
272
+ };
273
+
274
+ export let handleSlateTriggerWebhook = async (d: {
275
+ client: SlatesTestClient;
276
+ triggerId: string;
277
+ url: string;
278
+ method?: string;
279
+ headers?: Record<string, string>;
280
+ body?: string | Uint8Array | null;
281
+ state?: any;
282
+ }) =>
283
+ d.client.handleTriggerWebhook({
284
+ actionId: d.triggerId,
285
+ url: d.url,
286
+ method: d.method ?? 'POST',
287
+ headers: d.headers,
288
+ body: d.body,
289
+ state: d.state
290
+ });
291
+
292
+ export let unregisterSlateTriggerWebhook = async (d: {
293
+ client: SlatesTestClient;
294
+ triggerId: string;
295
+ webhookBaseUrl: string;
296
+ registrationDetails: any;
297
+ state?: any;
298
+ }) =>
299
+ d.client.unregisterTriggerWebhook({
300
+ actionId: d.triggerId,
301
+ webhookBaseUrl: d.webhookBaseUrl,
302
+ registrationDetails: d.registrationDetails,
303
+ state: d.state
304
+ });
305
+
306
+ export let mapSlateTriggerEvent = async (d: {
307
+ client: SlatesTestClient;
308
+ triggerId: string;
309
+ input: Record<string, any>;
310
+ output?: Record<string, any>;
311
+ type?: string;
312
+ }) => {
313
+ let expect = getVitestExpect();
314
+ let result = await d.client.mapTriggerEvent(d.triggerId, d.input);
315
+
316
+ if (d.type !== undefined) {
317
+ expect(result.type).toBe(d.type);
318
+ }
319
+
320
+ if (d.output) {
321
+ expect(result.output).toMatchObject(d.output);
322
+ }
323
+
324
+ return result;
325
+ };
326
+
327
+ export let expectSlateError = async (
328
+ run: Promise<unknown> | (() => Promise<unknown>),
329
+ expected: Record<string, any> | string | RegExp
330
+ ) => {
331
+ let expect = getVitestExpect();
332
+ try {
333
+ await (typeof run === 'function' ? run() : run);
334
+ } catch (error) {
335
+ if (typeof expected === 'string' || expected instanceof RegExp) {
336
+ let message = error instanceof Error ? error.message : String(error);
337
+ if (expected instanceof RegExp) {
338
+ expect(message).toMatch(expected);
339
+ } else {
340
+ expect(message).toContain(expected);
341
+ }
342
+ } else if (error instanceof SlateProtocolError) {
343
+ expect(error.data).toMatchObject(expected);
344
+ } else {
345
+ expect(error).toMatchObject(expected);
346
+ }
347
+
348
+ return error;
349
+ }
350
+
351
+ throw new Error('Expected the operation to fail, but it completed successfully.');
352
+ };