@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slates/test",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.20",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -15,10 +15,18 @@
15
15
  "type": "module",
16
16
  "source": "src/index.ts",
17
17
  "exports": {
18
- "types": "./dist/index.d.ts",
19
- "require": "./dist/index.cjs",
20
- "import": "./dist/index.module.js",
21
- "default": "./dist/index.module.js"
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "require": "./dist/index.cjs",
21
+ "import": "./dist/index.module.js",
22
+ "default": "./dist/index.module.js"
23
+ },
24
+ "./config": {
25
+ "types": "./dist/config.d.ts",
26
+ "require": "./dist/config.cjs",
27
+ "import": "./dist/config.module.js",
28
+ "default": "./dist/config.module.js"
29
+ }
22
30
  },
23
31
  "main": "./dist/index.cjs",
24
32
  "module": "./dist/index.module.js",
@@ -26,17 +34,17 @@
26
34
  "unpkg": "./dist/index.module.js",
27
35
  "scripts": {
28
36
  "test": "vitest run --passWithNoTests",
29
- "lint": "prettier src/**/*.ts --check",
30
- "build": "tsup --config ../../tsup.packages.config.ts --external async_hooks",
31
- "typecheck": "tsc --noEmit"
37
+ "build": "tsup --config tsup.config.ts",
38
+ "typecheck": "tsc --noEmit",
39
+ "biome:check": "biome check --write src"
32
40
  },
33
41
  "dependencies": {
34
42
  "@lowerdeck/testing-tools": "latest",
35
- "@slates/client": "1.0.0-rc.2",
36
- "@slates/profiles": "1.0.0-rc.2"
43
+ "@slates/client": "1.0.0-rc.23",
44
+ "@slates/profiles": "1.0.0-rc.21"
37
45
  },
38
46
  "devDependencies": {
39
- "@slates/provider": "1.0.0-rc.8",
47
+ "@slates/provider": "1.0.0-rc.23",
40
48
  "@slates/tsconfig": "1.0.0-rc.1",
41
49
  "typescript": "5.8.2",
42
50
  "vitest": "^3.1.2",
package/src/config.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { createVitestConfig } from '@lowerdeck/testing-tools';
2
+
3
+ export let createSlatesVitestConfig = (
4
+ config: Parameters<typeof createVitestConfig>[0] = {}
5
+ ) => createVitestConfig(config);
package/src/index.test.ts CHANGED
@@ -1,12 +1,14 @@
1
+ import { openSlatesCliStore } from '@slates/profiles';
1
2
  import {
3
+ createTextAttachment,
2
4
  Slate,
3
5
  SlateAuth,
4
6
  SlateConfig,
5
7
  SlateSpecification,
6
8
  SlateTool,
7
- SlateTrigger
9
+ SlateTrigger,
10
+ SlateTriggerGroup
8
11
  } from '@slates/provider';
9
- import { openSlatesCliStore } from '@slates/profiles';
10
12
  import { mkdtemp, rm, writeFile } from 'fs/promises';
11
13
  import { tmpdir } from 'os';
12
14
  import path from 'path';
@@ -17,11 +19,12 @@ import {
17
19
  expectSlateContract,
18
20
  expectSlateError,
19
21
  expectToolCall,
20
- handleSlateTriggerWebhook,
21
22
  loadSlatesRuntimeContext,
22
23
  mapSlateTriggerEvent,
23
- registerSlateTriggerWebhook,
24
- unregisterSlateTriggerWebhook
24
+ pollSlateTriggerGroupEvents,
25
+ processSlateTriggerGroupWebhook,
26
+ registerSlateTriggerGroupWebhook,
27
+ unregisterSlateTriggerGroupWebhook
25
28
  } from './index';
26
29
 
27
30
  (globalThis as typeof globalThis & { expect?: typeof expect }).expect = expect;
@@ -38,6 +41,7 @@ afterEach(async () => {
38
41
  delete process.env.SLATES_INTEGRATION;
39
42
  delete process.env.SLATES_PROFILE_ID;
40
43
  delete process.env.SLATES_STORE_PATH;
44
+ delete process.env.SLATES_STORE_ROOT_DIR;
41
45
  delete process.env.SLATES_TEST_CONTEXT_PATH;
42
46
  await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })));
43
47
  });
@@ -121,65 +125,181 @@ let createDemoSlate = () => {
121
125
  })
122
126
  .build();
123
127
 
124
- let webhookEcho = SlateTrigger.create(spec, {
125
- key: 'webhook_echo',
126
- name: 'Webhook Echo'
128
+ let attachmentEcho = SlateTool.create(spec, {
129
+ key: 'attachment_echo',
130
+ name: 'Attachment Echo',
131
+ tags: {
132
+ readOnly: true
133
+ }
127
134
  })
128
135
  .input(
129
136
  z.object({
130
- value: z.string()
137
+ mimeType: z.string().optional()
131
138
  })
132
139
  )
133
140
  .output(
134
141
  z.object({
135
- echoed: z.string()
142
+ size: z.number()
143
+ })
144
+ )
145
+ .handleInvocation(async ctx => ({
146
+ output: {
147
+ size: 5
148
+ },
149
+ message: 'attached',
150
+ attachments: [createTextAttachment('hello', ctx.input.mimeType)]
151
+ }))
152
+ .build();
153
+
154
+ let downloadLink = SlateTool.create(spec, {
155
+ key: 'download_link',
156
+ name: 'Download Link',
157
+ tags: {
158
+ readOnly: true
159
+ }
160
+ })
161
+ .input(z.object({}))
162
+ .output(
163
+ z.object({
164
+ downloadUrl: z.string()
136
165
  })
137
166
  )
167
+ .handleInvocation(async () => ({
168
+ output: {
169
+ downloadUrl: 'https://example.com/files/demo.txt'
170
+ },
171
+ message: 'linked'
172
+ }))
173
+ .build();
174
+
175
+ let webhookGroup = SlateTriggerGroup.create(spec, {
176
+ key: 'webhook_group',
177
+ name: 'Webhook Group'
178
+ })
138
179
  .webhook({
139
- autoRegisterWebhook: async ctx => ({
140
- registrationDetails: {
141
- webhookBaseUrl: ctx.input.webhookBaseUrl,
142
- channelId: 'channel-1'
143
- },
144
- state: {
145
- registered: true
146
- }
147
- }),
148
- autoUnregisterWebhook: async ctx => {
149
- if (ctx.input.registrationDetails?.channelId !== 'channel-1') {
150
- throw new Error('Unexpected channel');
180
+ autoRegistration: {
181
+ webhookTargetList: async () => ({
182
+ targets: [
183
+ {
184
+ webhookTargetIdentifier: 'default',
185
+ name: 'Default',
186
+ metadata: {},
187
+ webhookTargetPayload: {},
188
+ targetOwnership: 'single_user'
189
+ }
190
+ ],
191
+ nextPageToken: null
192
+ }),
193
+ webhookRegister: async ctx => ({
194
+ webhookRegistrationIdentifier: 'reg-1',
195
+ webhookRegistrationPayload: {
196
+ webhookBaseUrl: ctx.input.webhookUrl,
197
+ channelId: 'channel-1'
198
+ }
199
+ }),
200
+ webhookUnregister: async ctx => {
201
+ if (ctx.input.webhookRegistrationPayload?.channelId !== 'channel-1') {
202
+ throw new Error('Unexpected channel');
203
+ }
151
204
  }
152
205
  },
153
- handleRequest: async ctx => {
154
- if (ctx.request.headers.get('x-demo-event') === 'ignore') {
155
- return { inputs: [] };
206
+ process: async ctx => {
207
+ if (ctx.input.request.headers.get('x-demo-event') === 'ignore') {
208
+ return { events: [] };
156
209
  }
157
210
 
158
211
  return {
159
- inputs: [
212
+ events: [
160
213
  {
161
- value: (await ctx.request.text()) || 'empty'
214
+ matchers: [],
215
+ payload: {
216
+ value: (await ctx.input.request.text()) || 'empty'
217
+ }
162
218
  }
163
219
  ],
164
- updatedState: {
165
- lastEvent: ctx.request.headers.get('x-demo-event') ?? 'unknown'
166
- }
220
+ response: new Response(ctx.input.webhookRegistrationPayload?.channelId ?? 'missing', {
221
+ status: 201,
222
+ headers: {
223
+ 'content-type': 'text/plain',
224
+ 'x-demo-response': 'accepted'
225
+ }
226
+ })
167
227
  };
168
- },
169
- handleEvent: async ctx => ({
170
- type: 'demo.webhook',
171
- id: `webhook-${ctx.input.value}`,
172
- output: {
173
- echoed: ctx.input.value
228
+ }
229
+ })
230
+ .routingMatchers(async () => [])
231
+ .build();
232
+
233
+ let webhookEcho = SlateTrigger.create(spec, {
234
+ key: 'webhook_echo',
235
+ name: 'Webhook Echo'
236
+ })
237
+ .input(
238
+ z.object({
239
+ value: z.string()
240
+ })
241
+ )
242
+ .output(
243
+ z.object({
244
+ echoed: z.string()
245
+ })
246
+ )
247
+ .triggerGroup(webhookGroup)
248
+ .matches(() => true)
249
+ .map(async ctx => ({
250
+ type: 'demo.webhook',
251
+ id: `webhook-${ctx.input.value}`,
252
+ output: {
253
+ echoed: ctx.input.value
254
+ }
255
+ }))
256
+ .build();
257
+
258
+ let pollGroup = SlateTriggerGroup.create(spec, {
259
+ key: 'poll_group',
260
+ name: 'Poll Group'
261
+ })
262
+ .polling({
263
+ pollEvents: async ctx => ({
264
+ events: ctx.input.state?.seen ? [] : [{ payload: { value: 'poll-value' } }],
265
+ updatedState: {
266
+ seen: true
174
267
  }
175
268
  })
176
269
  })
270
+ .routingMatchers(async () => [])
271
+ .build();
272
+
273
+ let pollEcho = SlateTrigger.create(spec, {
274
+ key: 'poll_echo',
275
+ name: 'Poll Echo'
276
+ })
277
+ .input(
278
+ z.object({
279
+ value: z.string()
280
+ })
281
+ )
282
+ .output(
283
+ z.object({
284
+ echoed: z.string()
285
+ })
286
+ )
287
+ .triggerGroup(pollGroup)
288
+ .matches(() => true)
289
+ .map(async ctx => ({
290
+ type: 'demo.poll',
291
+ id: `poll-${ctx.input.value}`,
292
+ output: {
293
+ echoed: ctx.input.value
294
+ }
295
+ }))
177
296
  .build();
178
297
 
179
298
  return Slate.create({
180
299
  spec,
181
- tools: [echo, fail],
182
- triggers: [webhookEcho]
300
+ tools: [echo, fail, attachmentEcho, downloadLink],
301
+ triggers: [webhookEcho, pollEcho],
302
+ triggerGroups: [webhookGroup, pollGroup]
183
303
  });
184
304
  };
185
305
 
@@ -224,6 +344,66 @@ describe('@slates/test', () => {
224
344
  expect(context.storePath).toBe(store.storePath);
225
345
  });
226
346
 
347
+ it('loads only the selected auth method from the runtime context', async () => {
348
+ let cwd = await createTempDir();
349
+ let store = await openSlatesCliStore({
350
+ cwd,
351
+ scope: {
352
+ key: 'integrations/demo',
353
+ name: 'demo'
354
+ }
355
+ });
356
+ let profile = store.upsertProfile({
357
+ name: 'Demo',
358
+ target: {
359
+ type: 'local',
360
+ entry: './demo-slate.mjs',
361
+ exportName: 'provider'
362
+ }
363
+ });
364
+ store.upsertAuth(profile.id, {
365
+ authMethodId: 'oauth',
366
+ authMethodName: 'Bot OAuth',
367
+ authType: 'auth.oauth',
368
+ input: {},
369
+ output: {
370
+ token: 'bot-token'
371
+ },
372
+ scopes: ['chat:write']
373
+ });
374
+ store.upsertAuth(profile.id, {
375
+ authMethodId: 'user_oauth',
376
+ authMethodName: 'User OAuth',
377
+ authType: 'auth.oauth',
378
+ input: {},
379
+ output: {
380
+ token: 'user-token'
381
+ },
382
+ scopes: ['search:read']
383
+ });
384
+ await store.save();
385
+
386
+ let runtimeContextPath = path.join(cwd, 'runtime.json');
387
+ await writeFile(
388
+ runtimeContextPath,
389
+ JSON.stringify({
390
+ integration: 'integrations/demo',
391
+ profileId: profile.id,
392
+ authMethodId: 'user_oauth',
393
+ storePath: store.storePath,
394
+ cliDir: store.dirPath
395
+ }),
396
+ 'utf-8'
397
+ );
398
+
399
+ process.env.SLATES_TEST_CONTEXT_PATH = runtimeContextPath;
400
+
401
+ let context = await loadSlatesRuntimeContext({ cwd });
402
+ expect(context.authMethodId).toBe('user_oauth');
403
+ expect(Object.keys(context.profile?.auth ?? {})).toEqual(['user_oauth']);
404
+ expect(context.profile?.auth.user_oauth?.output.token).toBe('user-token');
405
+ });
406
+
227
407
  it('creates local slate clients and asserts provider contracts', async () => {
228
408
  let client = createLocalSlateTestClient({
229
409
  slate: createDemoSlate(),
@@ -247,14 +427,21 @@ describe('@slates/test', () => {
247
427
  name: 'Demo Slate',
248
428
  description: 'A tiny test slate'
249
429
  },
250
- toolIds: ['echo', 'fail'],
251
- triggerIds: ['webhook_echo'],
430
+ toolIds: ['echo', 'fail', 'attachment_echo', 'download_link'],
431
+ triggerIds: ['webhook_echo', 'poll_echo'],
432
+ triggerGroupIds: ['webhook_group', 'poll_group'],
252
433
  authMethodIds: ['token_auth'],
253
434
  tools: [
254
435
  { id: 'echo', readOnly: false, destructive: false },
255
- { id: 'fail', readOnly: false, destructive: false }
436
+ { id: 'fail', readOnly: false, destructive: false },
437
+ { id: 'attachment_echo', readOnly: true, destructive: false },
438
+ { id: 'download_link', readOnly: true, destructive: false }
256
439
  ],
257
- triggers: [{ id: 'webhook_echo', invocationType: 'webhook' }]
440
+ triggers: [{ id: 'webhook_echo' }, { id: 'poll_echo' }],
441
+ triggerGroups: [
442
+ { id: 'webhook_group', invocationType: 'webhook' },
443
+ { id: 'poll_group', invocationType: 'polling' }
444
+ ]
258
445
  });
259
446
 
260
447
  expect(contract.configSchema.properties.prefix.type).toBe('string');
@@ -270,6 +457,30 @@ describe('@slates/test', () => {
270
457
  token: 'secret-token'
271
458
  }
272
459
  });
460
+
461
+ let attachmentResult = await client.invokeTool('attachment_echo', {
462
+ mimeType: 'text/plain'
463
+ });
464
+ expect(attachmentResult.attachments).toEqual([
465
+ {
466
+ mimeType: 'text/plain',
467
+ content: {
468
+ type: 'content',
469
+ encoding: 'utf-8',
470
+ content: 'hello'
471
+ }
472
+ }
473
+ ]);
474
+
475
+ let downloadResult = await client.invokeTool('download_link', {});
476
+ expect(downloadResult.attachments).toEqual([
477
+ {
478
+ content: {
479
+ type: 'url',
480
+ url: 'https://example.com/files/demo.txt'
481
+ }
482
+ }
483
+ ]);
273
484
  });
274
485
 
275
486
  it('wraps trigger webhook flows and error assertions', async () => {
@@ -288,44 +499,54 @@ describe('@slates/test', () => {
288
499
  }
289
500
  });
290
501
 
291
- let registration = await registerSlateTriggerWebhook({
502
+ let registration = await registerSlateTriggerGroupWebhook({
292
503
  client,
293
- triggerId: 'webhook_echo',
294
- webhookBaseUrl: 'https://example.com/hooks/google-calendar'
504
+ triggerGroupId: 'webhook_group',
505
+ webhookTargetIdentifier: 'default',
506
+ webhookTargetPayload: {},
507
+ webhookUrl: 'https://example.com/hooks/google-calendar'
295
508
  });
296
509
  expect(registration).toMatchObject({
297
- registrationDetails: {
510
+ webhookRegistrationIdentifier: 'reg-1',
511
+ webhookRegistrationPayload: {
298
512
  webhookBaseUrl: 'https://example.com/hooks/google-calendar',
299
513
  channelId: 'channel-1'
300
- },
301
- state: {
302
- registered: true
303
514
  }
304
515
  });
305
516
 
306
- let ignored = await handleSlateTriggerWebhook({
517
+ let ignored = await processSlateTriggerGroupWebhook({
307
518
  client,
308
- triggerId: 'webhook_echo',
519
+ triggerGroupId: 'webhook_group',
309
520
  url: 'https://example.com/hooks/google-calendar',
310
521
  headers: {
311
522
  'x-demo-event': 'ignore'
312
- }
523
+ },
524
+ webhookRegistrationPayload: registration.webhookRegistrationPayload
313
525
  });
314
- expect(ignored.inputs).toEqual([]);
526
+ expect(ignored.events).toEqual([]);
315
527
 
316
- let handled = await handleSlateTriggerWebhook({
528
+ let handled = await processSlateTriggerGroupWebhook({
317
529
  client,
318
- triggerId: 'webhook_echo',
530
+ triggerGroupId: 'webhook_group',
319
531
  url: 'https://example.com/hooks/google-calendar',
320
532
  headers: {
321
533
  'x-demo-event': 'created'
322
534
  },
323
- body: 'payload-value'
535
+ body: 'payload-value',
536
+ webhookRegistrationPayload: registration.webhookRegistrationPayload
324
537
  });
325
538
  expect(handled).toMatchObject({
326
- inputs: [{ value: 'payload-value' }],
327
- updatedState: {
328
- lastEvent: 'created'
539
+ events: [{ matchers: [], payload: { value: 'payload-value' } }],
540
+ response: {
541
+ status: 201,
542
+ headers: {
543
+ 'content-type': 'text/plain',
544
+ 'x-demo-response': 'accepted'
545
+ },
546
+ body: {
547
+ encoding: 'base64',
548
+ content: Buffer.from('channel-1').toString('base64')
549
+ }
329
550
  }
330
551
  });
331
552
 
@@ -342,11 +563,11 @@ describe('@slates/test', () => {
342
563
  });
343
564
  expect(mapped.id).toBe('webhook-payload-value');
344
565
 
345
- await unregisterSlateTriggerWebhook({
566
+ await unregisterSlateTriggerGroupWebhook({
346
567
  client,
347
- triggerId: 'webhook_echo',
348
- webhookBaseUrl: 'https://example.com/hooks/google-calendar',
349
- registrationDetails: registration.registrationDetails
568
+ triggerGroupId: 'webhook_group',
569
+ webhookRegistrationIdentifier: registration.webhookRegistrationIdentifier,
570
+ webhookRegistrationPayload: registration.webhookRegistrationPayload
350
571
  });
351
572
 
352
573
  await expectSlateError(
@@ -357,4 +578,53 @@ describe('@slates/test', () => {
357
578
  { code: 'internal.unexpected', kind: 'internal', status: 500 }
358
579
  );
359
580
  });
581
+
582
+ it('wraps trigger polling flows', async () => {
583
+ let client = createLocalSlateTestClient({
584
+ slate: createDemoSlate(),
585
+ state: {
586
+ config: {
587
+ prefix: 'Hi'
588
+ },
589
+ auth: {
590
+ authenticationMethodId: 'token_auth',
591
+ output: {
592
+ token: 'secret-token'
593
+ }
594
+ }
595
+ }
596
+ });
597
+
598
+ let initialPoll = await pollSlateTriggerGroupEvents({
599
+ client,
600
+ triggerGroupId: 'poll_group'
601
+ });
602
+ expect(initialPoll).toMatchObject({
603
+ events: [{ payload: { value: 'poll-value' } }],
604
+ updatedState: {
605
+ seen: true
606
+ }
607
+ });
608
+
609
+ let mapped = await mapSlateTriggerEvent({
610
+ client,
611
+ triggerId: 'poll_echo',
612
+ input: initialPoll.events[0]!.payload,
613
+ type: 'demo.poll',
614
+ output: {
615
+ echoed: 'poll-value'
616
+ }
617
+ });
618
+ expect(mapped.id).toBe('poll-poll-value');
619
+
620
+ let repeatedPoll = await pollSlateTriggerGroupEvents({
621
+ client,
622
+ triggerGroupId: 'poll_group',
623
+ state: initialPoll.updatedState
624
+ });
625
+ expect(repeatedPoll.events).toEqual([]);
626
+ expect(repeatedPoll.updatedState).toEqual({
627
+ seen: true
628
+ });
629
+ });
360
630
  });