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

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,457 @@
1
+ import type { SlatesProtocolClientOptions } from '@slates/client';
2
+ import {
3
+ createLocalSlateTransport,
4
+ createSlatesClient,
5
+ SlateProtocolError
6
+ } from '@slates/client';
7
+ import {
8
+ createSlatesClientFromProfile,
9
+ openSlatesCliStore,
10
+ type 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
+ authMethodId: string | null;
18
+ profile: SlatesProfileRecord | null;
19
+ rootDir: string;
20
+ storePath: string;
21
+ cliDir: string;
22
+ }
23
+
24
+ export type SlatesTestClient = ReturnType<typeof createSlatesClient>;
25
+
26
+ type LocalSlate = Parameters<typeof createLocalSlateTransport>[0]['slate'];
27
+ interface LocalSlateLike {
28
+ readonly spec: unknown;
29
+ readonly actions: readonly unknown[];
30
+ }
31
+ type SlatesAction = Awaited<ReturnType<SlatesTestClient['listActions']>>['actions'][number];
32
+ type SlatesToolAction = Extract<SlatesAction, { type: 'action.tool' }>;
33
+ type SlatesTriggerAction = Extract<SlatesAction, { type: 'action.trigger' }>;
34
+ type SlateAuthenticationMethod = Awaited<
35
+ ReturnType<SlatesTestClient['listAuthMethods']>
36
+ >['authenticationMethods'][number];
37
+
38
+ let selectProfileAuth = (profile: SlatesProfileRecord | null, authMethodId: string | null) => {
39
+ if (!profile || !authMethodId) {
40
+ return profile;
41
+ }
42
+
43
+ let selectedAuth = profile.auth[authMethodId];
44
+ if (!selectedAuth) {
45
+ let availableAuthMethods = Object.keys(profile.auth);
46
+ throw new Error(
47
+ `No stored authentication found for auth method "${authMethodId}" in profile "${profile.name}".` +
48
+ (availableAuthMethods.length > 0
49
+ ? ` Available auth methods: ${availableAuthMethods.join(', ')}.`
50
+ : ' No auth methods are stored for this profile.')
51
+ );
52
+ }
53
+
54
+ return {
55
+ ...profile,
56
+ auth: {
57
+ [selectedAuth.authMethodId]: selectedAuth
58
+ }
59
+ };
60
+ };
61
+
62
+ export interface ExpectedSlateAction {
63
+ id: string;
64
+ name?: string;
65
+ description?: string;
66
+ readOnly?: boolean;
67
+ destructive?: boolean;
68
+ }
69
+
70
+ export interface ExpectedSlateTriggerGroup {
71
+ id: string;
72
+ name?: string;
73
+ description?: string;
74
+ invocationType?: 'polling' | 'webhook';
75
+ }
76
+
77
+ export let getVitestExpect = () => {
78
+ let maybeExpect = (
79
+ globalThis as typeof globalThis & { expect?: typeof import('vitest').expect }
80
+ ).expect;
81
+
82
+ if (!maybeExpect) {
83
+ throw new Error('Vitest expect is not available in the current runtime.');
84
+ }
85
+
86
+ return maybeExpect;
87
+ };
88
+
89
+ export let loadSlatesRuntimeContext = async (
90
+ opts: { cwd?: string; profile?: string | null } = {}
91
+ ): Promise<SlatesRuntimeContext> => {
92
+ let runtimeContextPath = process.env.SLATES_TEST_CONTEXT_PATH;
93
+ if (runtimeContextPath) {
94
+ let raw = await readFile(runtimeContextPath, 'utf-8');
95
+ let parsed = JSON.parse(raw) as {
96
+ integration?: string | null;
97
+ profileId: string | null;
98
+ authMethodId?: string | null;
99
+ rootDir?: string;
100
+ storePath: string;
101
+ cliDir: string;
102
+ };
103
+ let store = await openSlatesCliStore({
104
+ storePath: parsed.storePath,
105
+ rootDir: parsed.rootDir ?? process.env.SLATES_STORE_ROOT_DIR
106
+ });
107
+ let authMethodId = parsed.authMethodId ?? null;
108
+ let profile = selectProfileAuth(
109
+ store.getProfile(opts.profile ?? parsed.profileId ?? null),
110
+ authMethodId
111
+ );
112
+
113
+ return {
114
+ integration: parsed.integration ?? store.scope?.key ?? null,
115
+ profileId: profile?.id ?? parsed.profileId ?? null,
116
+ authMethodId,
117
+ profile,
118
+ rootDir: parsed.rootDir ?? store.rootDir,
119
+ storePath: parsed.storePath,
120
+ cliDir: parsed.cliDir
121
+ };
122
+ }
123
+
124
+ let store = process.env.SLATES_STORE_PATH
125
+ ? await openSlatesCliStore({
126
+ storePath: process.env.SLATES_STORE_PATH,
127
+ rootDir: process.env.SLATES_STORE_ROOT_DIR
128
+ })
129
+ : await openSlatesCliStore({ cwd: opts.cwd });
130
+ let profileId = opts.profile ?? process.env.SLATES_PROFILE_ID ?? null;
131
+ let authMethodId: any = null;
132
+ let profile = selectProfileAuth(store.getProfile(profileId), authMethodId);
133
+
134
+ return {
135
+ integration: process.env.SLATES_INTEGRATION ?? store.scope?.key ?? null,
136
+ profileId: profile?.id ?? null,
137
+ authMethodId,
138
+ profile,
139
+ rootDir: store.rootDir,
140
+ storePath: store.storePath,
141
+ cliDir: store.dirPath
142
+ };
143
+ };
144
+
145
+ export let loadSlatesProfile = async (
146
+ opts: { cwd?: string; profile?: string | null } = {}
147
+ ) => {
148
+ let context = await loadSlatesRuntimeContext(opts);
149
+ if (!context.profile) {
150
+ throw new Error('No Slates profile is available for the current test context.');
151
+ }
152
+
153
+ return context.profile;
154
+ };
155
+
156
+ export let createSlatesTestClient = async (
157
+ opts: { cwd?: string; profile?: string | null } = {}
158
+ ) => {
159
+ let context = await loadSlatesRuntimeContext(opts);
160
+ if (!context.profile) {
161
+ throw new Error('No Slates profile is available for the current test context.');
162
+ }
163
+
164
+ let store = await openSlatesCliStore({
165
+ storePath: context.storePath,
166
+ rootDir: context.rootDir
167
+ });
168
+ return createSlatesClientFromProfile(context.profile, { cwd: opts.cwd, store });
169
+ };
170
+
171
+ export let withSlateProfile = async <T>(
172
+ profileName: string | null | undefined,
173
+ cb: (ctx: { profile: SlatesProfileRecord }) => Promise<T>
174
+ ) => {
175
+ let profile = await loadSlatesProfile({ profile: profileName });
176
+ return cb({ profile });
177
+ };
178
+
179
+ export let expectToolCall = async (d: {
180
+ client?: Awaited<ReturnType<typeof createSlatesTestClient>>;
181
+ profile?: string | null;
182
+ toolId: string;
183
+ input: Record<string, any>;
184
+ output?: Record<string, any>;
185
+ }) => {
186
+ let expect = getVitestExpect();
187
+ let client = d.client ?? (await createSlatesTestClient({ profile: d.profile }));
188
+ let result = await client.invokeTool(d.toolId, d.input);
189
+
190
+ if (d.output) {
191
+ expect(result.output).toMatchObject(d.output);
192
+ }
193
+
194
+ return result;
195
+ };
196
+
197
+ export let createLocalSlateTestClient = (opts: {
198
+ slate: LocalSlateLike;
199
+ state?: SlatesProtocolClientOptions['state'];
200
+ participants?: SlatesProtocolClientOptions['participants'];
201
+ }) =>
202
+ createSlatesClient({
203
+ transport: createLocalSlateTransport({ slate: opts.slate as LocalSlate }),
204
+ state: {
205
+ capabilities: { triggers: true },
206
+ ...opts.state
207
+ },
208
+ participants: opts.participants
209
+ });
210
+
211
+ export let getSlateContract = async (client: SlatesTestClient) => {
212
+ let [provider, actions, authMethods, configSchema, triggerGroups] = await Promise.all([
213
+ client.identify(),
214
+ client.listActions(),
215
+ client.listAuthMethods(),
216
+ client.getConfigSchema(),
217
+ client.listTriggerGroups()
218
+ ]);
219
+
220
+ return {
221
+ provider: provider.provider,
222
+ actions: actions.actions,
223
+ tools: actions.actions.filter(
224
+ (action: SlatesAction): action is SlatesToolAction => action.type === 'action.tool'
225
+ ),
226
+ triggers: actions.actions.filter(
227
+ (action: SlatesAction): action is SlatesTriggerAction => action.type === 'action.trigger'
228
+ ),
229
+ triggerGroups: triggerGroups.triggerGroups,
230
+ authMethods: authMethods.authenticationMethods,
231
+ configSchema: configSchema.schema
232
+ };
233
+ };
234
+
235
+ let expectActionMatches = (
236
+ actual: Record<string, any> | undefined,
237
+ expected: ExpectedSlateAction
238
+ ) => {
239
+ let expect = getVitestExpect();
240
+ expect(actual).toBeTruthy();
241
+ expect(actual?.id).toBe(expected.id);
242
+
243
+ if (expected.name !== undefined) {
244
+ expect(actual?.name).toBe(expected.name);
245
+ }
246
+
247
+ if (expected.description !== undefined) {
248
+ expect(actual?.description).toBe(expected.description);
249
+ }
250
+
251
+ if (expected.readOnly !== undefined) {
252
+ expect(actual?.tags?.readOnly ?? false).toBe(expected.readOnly);
253
+ }
254
+
255
+ if (expected.destructive !== undefined) {
256
+ expect(actual?.tags?.destructive ?? false).toBe(expected.destructive);
257
+ }
258
+ };
259
+
260
+ let expectTriggerGroupMatches = (
261
+ actual: Record<string, any> | undefined,
262
+ expected: ExpectedSlateTriggerGroup
263
+ ) => {
264
+ let expect = getVitestExpect();
265
+ expect(actual).toBeTruthy();
266
+ expect(actual?.id).toBe(expected.id);
267
+
268
+ if (expected.name !== undefined) {
269
+ expect(actual?.name).toBe(expected.name);
270
+ }
271
+
272
+ if (expected.description !== undefined) {
273
+ expect(actual?.description).toBe(expected.description);
274
+ }
275
+
276
+ if (expected.invocationType !== undefined) {
277
+ expect((actual as { invocation?: { type?: string } } | undefined)?.invocation?.type).toBe(
278
+ expected.invocationType
279
+ );
280
+ }
281
+ };
282
+
283
+ export let expectSlateContract = async (d: {
284
+ client: SlatesTestClient;
285
+ provider?: {
286
+ id: string;
287
+ name?: string;
288
+ description?: string;
289
+ };
290
+ toolIds?: string[];
291
+ triggerIds?: string[];
292
+ triggerGroupIds?: string[];
293
+ authMethodIds?: string[];
294
+ tools?: ExpectedSlateAction[];
295
+ triggers?: ExpectedSlateAction[];
296
+ triggerGroups?: ExpectedSlateTriggerGroup[];
297
+ }) => {
298
+ let expect = getVitestExpect();
299
+ let contract = await getSlateContract(d.client);
300
+
301
+ if (d.provider) {
302
+ expect(contract.provider.id).toBe(d.provider.id);
303
+ if (d.provider.name !== undefined) {
304
+ expect(contract.provider.name).toBe(d.provider.name);
305
+ }
306
+ if (d.provider.description !== undefined) {
307
+ expect(contract.provider.description).toBe(d.provider.description);
308
+ }
309
+ }
310
+
311
+ if (d.toolIds) {
312
+ expect(contract.tools.map((action: SlatesToolAction) => action.id)).toEqual(d.toolIds);
313
+ }
314
+
315
+ if (d.triggerIds) {
316
+ expect(contract.triggers.map((action: SlatesTriggerAction) => action.id)).toEqual(
317
+ d.triggerIds
318
+ );
319
+ }
320
+
321
+ if (d.triggerGroupIds) {
322
+ expect(contract.triggerGroups.map(group => group.id)).toEqual(d.triggerGroupIds);
323
+ }
324
+
325
+ if (d.authMethodIds) {
326
+ expect(contract.authMethods.map((method: SlateAuthenticationMethod) => method.id)).toEqual(
327
+ d.authMethodIds
328
+ );
329
+ }
330
+
331
+ for (let tool of d.tools ?? []) {
332
+ expectActionMatches(
333
+ contract.tools.find((action: SlatesToolAction) => action.id === tool.id),
334
+ tool
335
+ );
336
+ }
337
+
338
+ for (let trigger of d.triggers ?? []) {
339
+ expectActionMatches(
340
+ contract.triggers.find((action: SlatesTriggerAction) => action.id === trigger.id),
341
+ trigger
342
+ );
343
+ }
344
+
345
+ for (let triggerGroup of d.triggerGroups ?? []) {
346
+ expectTriggerGroupMatches(
347
+ contract.triggerGroups.find(group => group.id === triggerGroup.id),
348
+ triggerGroup
349
+ );
350
+ }
351
+
352
+ return contract;
353
+ };
354
+
355
+ export let registerSlateTriggerGroupWebhook = async (d: {
356
+ client: SlatesTestClient;
357
+ triggerGroupId: string;
358
+ webhookTargetIdentifier: string;
359
+ webhookTargetPayload: any;
360
+ webhookUrl: string;
361
+ }) =>
362
+ d.client.registerTriggerGroupWebhook({
363
+ triggerGroupId: d.triggerGroupId,
364
+ webhookTargetIdentifier: d.webhookTargetIdentifier,
365
+ webhookTargetPayload: d.webhookTargetPayload,
366
+ webhookUrl: d.webhookUrl
367
+ });
368
+
369
+ export let pollSlateTriggerGroupEvents = async (d: {
370
+ client: SlatesTestClient;
371
+ triggerGroupId: string;
372
+ state?: any;
373
+ }) => {
374
+ d.client.ensureSession();
375
+ return d.client.pollTriggerGroup({
376
+ triggerGroupId: d.triggerGroupId,
377
+ state: d.state ?? null
378
+ });
379
+ };
380
+
381
+ export let processSlateTriggerGroupWebhook = async (d: {
382
+ client: SlatesTestClient;
383
+ triggerGroupId: string;
384
+ url: string;
385
+ method?: string;
386
+ headers?: Record<string, string>;
387
+ body?: string | Uint8Array | null;
388
+ webhookRegistrationPayload?: any;
389
+ }) =>
390
+ d.client.processTriggerGroupWebhook({
391
+ triggerGroupId: d.triggerGroupId,
392
+ url: d.url,
393
+ method: d.method ?? 'POST',
394
+ headers: d.headers,
395
+ body: d.body,
396
+ webhookRegistrationPayload: d.webhookRegistrationPayload ?? null
397
+ });
398
+
399
+ export let unregisterSlateTriggerGroupWebhook = async (d: {
400
+ client: SlatesTestClient;
401
+ triggerGroupId: string;
402
+ webhookRegistrationIdentifier: string;
403
+ webhookRegistrationPayload: any;
404
+ }) =>
405
+ d.client.unregisterTriggerGroupWebhook({
406
+ triggerGroupId: d.triggerGroupId,
407
+ webhookRegistrationIdentifier: d.webhookRegistrationIdentifier,
408
+ webhookRegistrationPayload: d.webhookRegistrationPayload
409
+ });
410
+
411
+ export let mapSlateTriggerEvent = async (d: {
412
+ client: SlatesTestClient;
413
+ triggerId: string;
414
+ input: Record<string, any>;
415
+ output?: Record<string, any>;
416
+ type?: string;
417
+ }) => {
418
+ let expect = getVitestExpect();
419
+ let result = await d.client.mapTriggerEvent(d.triggerId, d.input);
420
+
421
+ if (d.type !== undefined) {
422
+ expect(result.type).toBe(d.type);
423
+ }
424
+
425
+ if (d.output) {
426
+ expect(result.output).toMatchObject(d.output);
427
+ }
428
+
429
+ return result;
430
+ };
431
+
432
+ export let expectSlateError = async (
433
+ run: Promise<unknown> | (() => Promise<unknown>),
434
+ expected: Record<string, any> | string | RegExp
435
+ ) => {
436
+ let expect = getVitestExpect();
437
+ try {
438
+ await (typeof run === 'function' ? run() : run);
439
+ } catch (error) {
440
+ if (typeof expected === 'string' || expected instanceof RegExp) {
441
+ let message = error instanceof Error ? error.message : String(error);
442
+ if (expected instanceof RegExp) {
443
+ expect(message).toMatch(expected);
444
+ } else {
445
+ expect(message).toContain(expected);
446
+ }
447
+ } else if (error instanceof SlateProtocolError) {
448
+ expect(error.data).toMatchObject(expected);
449
+ } else {
450
+ expect(error).toMatchObject(expected);
451
+ }
452
+
453
+ return error;
454
+ }
455
+
456
+ throw new Error('Expected the operation to fail, but it completed successfully.');
457
+ };
package/src/schema.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { z } from 'zod';
3
+
4
+ type ToolSchemaTarget = {
5
+ key: string;
6
+ inputSchema: z.ZodType;
7
+ };
8
+
9
+ type ToolSchemaAction = {
10
+ type?: string;
11
+ key?: string;
12
+ inputSchema?: z.ZodType;
13
+ build?: () => unknown;
14
+ };
15
+
16
+ type ToolSchemaSource =
17
+ | readonly unknown[]
18
+ | {
19
+ actions: readonly unknown[];
20
+ };
21
+
22
+ let isRecord = (value: unknown): value is Record<string, unknown> =>
23
+ typeof value === 'object' && value !== null && !Array.isArray(value);
24
+
25
+ let isToolSchemaTarget = (value: unknown): value is ToolSchemaTarget =>
26
+ isRecord(value) && typeof value.key === 'string' && value.inputSchema !== undefined;
27
+
28
+ let buildToolSchemaTarget = (action: ToolSchemaAction) => {
29
+ if (typeof action.build !== 'function') {
30
+ return action;
31
+ }
32
+
33
+ return action.build();
34
+ };
35
+
36
+ let isToolAction = (action: unknown): action is ToolSchemaAction => {
37
+ if (!isRecord(action)) return false;
38
+ return action.type === 'tool' || action.type === 'action.tool';
39
+ };
40
+
41
+ let getToolSchemaActions = (source: ToolSchemaSource): readonly unknown[] => {
42
+ if (Array.isArray(source)) {
43
+ return source;
44
+ }
45
+
46
+ return (source as { actions: readonly unknown[] }).actions;
47
+ };
48
+
49
+ export let getMcpCompatibleToolSchemaCases = (
50
+ source: ToolSchemaSource
51
+ ): ReadonlyArray<readonly [string, ToolSchemaTarget]> =>
52
+ getToolSchemaActions(source)
53
+ .filter(isToolAction)
54
+ .map(buildToolSchemaTarget)
55
+ .filter(isToolSchemaTarget)
56
+ .map(tool => [tool.key, tool] as const);
57
+
58
+ export let expectMcpCompatibleToolSchema = (tool: ToolSchemaTarget) => {
59
+ let jsonSchema = z.toJSONSchema(tool.inputSchema) as Record<string, unknown>;
60
+
61
+ expect(jsonSchema.type).toBe('object');
62
+ expect(jsonSchema).not.toHaveProperty('oneOf');
63
+ expect(jsonSchema).not.toHaveProperty('anyOf');
64
+ expect(jsonSchema).not.toHaveProperty('allOf');
65
+ };
66
+
67
+ export let describeMcpCompatibleToolSchemas = (name: string, source: ToolSchemaSource) => {
68
+ describe(name, () => {
69
+ for (let [key, tool] of getMcpCompatibleToolSchemaCases(source)) {
70
+ it(`${key} uses an MCP-compatible top-level object schema`, () => {
71
+ expectMcpCompatibleToolSchema(tool);
72
+ });
73
+ }
74
+ });
75
+ };