@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/index.test.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ createTextAttachment,
2
3
  Slate,
3
4
  SlateAuth,
4
5
  SlateConfig,
@@ -20,6 +21,7 @@ import {
20
21
  handleSlateTriggerWebhook,
21
22
  loadSlatesRuntimeContext,
22
23
  mapSlateTriggerEvent,
24
+ pollSlateTriggerEvents,
23
25
  registerSlateTriggerWebhook,
24
26
  unregisterSlateTriggerWebhook
25
27
  } from './index';
@@ -121,6 +123,53 @@ let createDemoSlate = () => {
121
123
  })
122
124
  .build();
123
125
 
126
+ let attachmentEcho = SlateTool.create(spec, {
127
+ key: 'attachment_echo',
128
+ name: 'Attachment Echo',
129
+ tags: {
130
+ readOnly: true
131
+ }
132
+ })
133
+ .input(
134
+ z.object({
135
+ mimeType: z.string().optional()
136
+ })
137
+ )
138
+ .output(
139
+ z.object({
140
+ size: z.number()
141
+ })
142
+ )
143
+ .handleInvocation(async ctx => ({
144
+ output: {
145
+ size: 5
146
+ },
147
+ message: 'attached',
148
+ attachments: [createTextAttachment('hello', ctx.input.mimeType)]
149
+ }))
150
+ .build();
151
+
152
+ let downloadLink = SlateTool.create(spec, {
153
+ key: 'download_link',
154
+ name: 'Download Link',
155
+ tags: {
156
+ readOnly: true
157
+ }
158
+ })
159
+ .input(z.object({}))
160
+ .output(
161
+ z.object({
162
+ downloadUrl: z.string()
163
+ })
164
+ )
165
+ .handleInvocation(async () => ({
166
+ output: {
167
+ downloadUrl: 'https://example.com/files/demo.txt'
168
+ },
169
+ message: 'linked'
170
+ }))
171
+ .build();
172
+
124
173
  let webhookEcho = SlateTrigger.create(spec, {
125
174
  key: 'webhook_echo',
126
175
  name: 'Webhook Echo'
@@ -176,10 +225,41 @@ let createDemoSlate = () => {
176
225
  })
177
226
  .build();
178
227
 
228
+ let pollEcho = SlateTrigger.create(spec, {
229
+ key: 'poll_echo',
230
+ name: 'Poll Echo'
231
+ })
232
+ .input(
233
+ z.object({
234
+ value: z.string()
235
+ })
236
+ )
237
+ .output(
238
+ z.object({
239
+ echoed: z.string()
240
+ })
241
+ )
242
+ .polling({
243
+ pollEvents: async ctx => ({
244
+ inputs: ctx.input.state?.seen ? [] : [{ value: 'poll-value' }],
245
+ updatedState: {
246
+ seen: true
247
+ }
248
+ }),
249
+ handleEvent: async ctx => ({
250
+ type: 'demo.poll',
251
+ id: `poll-${ctx.input.value}`,
252
+ output: {
253
+ echoed: ctx.input.value
254
+ }
255
+ })
256
+ })
257
+ .build();
258
+
179
259
  return Slate.create({
180
260
  spec,
181
- tools: [echo, fail],
182
- triggers: [webhookEcho]
261
+ tools: [echo, fail, attachmentEcho, downloadLink],
262
+ triggers: [webhookEcho, pollEcho]
183
263
  });
184
264
  };
185
265
 
@@ -247,14 +327,19 @@ describe('@slates/test', () => {
247
327
  name: 'Demo Slate',
248
328
  description: 'A tiny test slate'
249
329
  },
250
- toolIds: ['echo', 'fail'],
251
- triggerIds: ['webhook_echo'],
330
+ toolIds: ['echo', 'fail', 'attachment_echo', 'download_link'],
331
+ triggerIds: ['webhook_echo', 'poll_echo'],
252
332
  authMethodIds: ['token_auth'],
253
333
  tools: [
254
334
  { id: 'echo', readOnly: false, destructive: false },
255
- { id: 'fail', readOnly: false, destructive: false }
335
+ { id: 'fail', readOnly: false, destructive: false },
336
+ { id: 'attachment_echo', readOnly: true, destructive: false },
337
+ { id: 'download_link', readOnly: true, destructive: false }
256
338
  ],
257
- triggers: [{ id: 'webhook_echo', invocationType: 'webhook' }]
339
+ triggers: [
340
+ { id: 'webhook_echo', invocationType: 'webhook' },
341
+ { id: 'poll_echo', invocationType: 'polling' }
342
+ ]
258
343
  });
259
344
 
260
345
  expect(contract.configSchema.properties.prefix.type).toBe('string');
@@ -270,6 +355,30 @@ describe('@slates/test', () => {
270
355
  token: 'secret-token'
271
356
  }
272
357
  });
358
+
359
+ let attachmentResult = await client.invokeTool('attachment_echo', {
360
+ mimeType: 'text/plain'
361
+ });
362
+ expect(attachmentResult.attachments).toEqual([
363
+ {
364
+ mimeType: 'text/plain',
365
+ content: {
366
+ type: 'content',
367
+ encoding: 'utf-8',
368
+ content: 'hello'
369
+ }
370
+ }
371
+ ]);
372
+
373
+ let downloadResult = await client.invokeTool('download_link', {});
374
+ expect(downloadResult.attachments).toEqual([
375
+ {
376
+ content: {
377
+ type: 'url',
378
+ url: 'https://example.com/files/demo.txt'
379
+ }
380
+ }
381
+ ]);
273
382
  });
274
383
 
275
384
  it('wraps trigger webhook flows and error assertions', async () => {
@@ -357,4 +466,53 @@ describe('@slates/test', () => {
357
466
  { code: 'internal.unexpected', kind: 'internal', status: 500 }
358
467
  );
359
468
  });
469
+
470
+ it('wraps trigger polling flows', async () => {
471
+ let client = createLocalSlateTestClient({
472
+ slate: createDemoSlate(),
473
+ state: {
474
+ config: {
475
+ prefix: 'Hi'
476
+ },
477
+ auth: {
478
+ authenticationMethodId: 'token_auth',
479
+ output: {
480
+ token: 'secret-token'
481
+ }
482
+ }
483
+ }
484
+ });
485
+
486
+ let initialPoll = await pollSlateTriggerEvents({
487
+ client,
488
+ triggerId: 'poll_echo'
489
+ });
490
+ expect(initialPoll).toMatchObject({
491
+ inputs: [{ value: 'poll-value' }],
492
+ updatedState: {
493
+ seen: true
494
+ }
495
+ });
496
+
497
+ let mapped = await mapSlateTriggerEvent({
498
+ client,
499
+ triggerId: 'poll_echo',
500
+ input: initialPoll.inputs[0]!,
501
+ type: 'demo.poll',
502
+ output: {
503
+ echoed: 'poll-value'
504
+ }
505
+ });
506
+ expect(mapped.id).toBe('poll-poll-value');
507
+
508
+ let repeatedPoll = await pollSlateTriggerEvents({
509
+ client,
510
+ triggerId: 'poll_echo',
511
+ state: initialPoll.updatedState
512
+ });
513
+ expect(repeatedPoll.inputs).toEqual([]);
514
+ expect(repeatedPoll.updatedState).toEqual({
515
+ seen: true
516
+ });
517
+ });
360
518
  });
package/src/index.ts CHANGED
@@ -1,332 +1 @@
1
- import { createVitestConfig } from '@lowerdeck/testing-tools';
2
- import {
3
- createLocalSlateTransport,
4
- createSlatesClient,
5
- SlateProtocolError
6
- } from '@slates/client';
7
- import type { SlatesProtocolClientOptions } from '@slates/client';
8
- import {
9
- createSlatesClientFromProfile,
10
- openSlatesCliStore,
11
- SlatesProfileRecord
12
- } from '@slates/profiles';
13
- import { readFile } from 'fs/promises';
14
-
15
- export interface SlatesRuntimeContext {
16
- integration: string | null;
17
- profileId: string | null;
18
- profile: SlatesProfileRecord | null;
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
- let getExpect = () => {
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
- storePath: string;
60
- cliDir: string;
61
- };
62
- let store = await openSlatesCliStore({ storePath: parsed.storePath });
63
- let profile = store.getProfile(opts.profile ?? parsed.profileId ?? null);
64
-
65
- return {
66
- integration: parsed.integration ?? store.scope?.key ?? null,
67
- profileId: profile?.id ?? parsed.profileId ?? null,
68
- profile,
69
- storePath: parsed.storePath,
70
- cliDir: parsed.cliDir
71
- };
72
- }
73
-
74
- let store = process.env.SLATES_STORE_PATH
75
- ? await openSlatesCliStore({ storePath: process.env.SLATES_STORE_PATH })
76
- : await openSlatesCliStore({ cwd: opts.cwd });
77
- let profileId = opts.profile ?? process.env.SLATES_PROFILE_ID ?? null;
78
- let profile = store.getProfile(profileId);
79
-
80
- return {
81
- integration: process.env.SLATES_INTEGRATION ?? store.scope?.key ?? null,
82
- profileId: profile?.id ?? null,
83
- profile,
84
- storePath: store.storePath,
85
- cliDir: store.dirPath
86
- };
87
- };
88
-
89
- export let loadSlatesProfile = async (
90
- opts: { cwd?: string; profile?: string | null } = {}
91
- ) => {
92
- let context = await loadSlatesRuntimeContext(opts);
93
- if (!context.profile) {
94
- throw new Error('No Slates profile is available for the current test context.');
95
- }
96
-
97
- return context.profile;
98
- };
99
-
100
- export let createSlatesTestClient = async (
101
- opts: { cwd?: string; profile?: string | null } = {}
102
- ) => {
103
- let context = await loadSlatesRuntimeContext(opts);
104
- if (!context.profile) {
105
- throw new Error('No Slates profile is available for the current test context.');
106
- }
107
-
108
- let store = await openSlatesCliStore({ storePath: context.storePath });
109
- return createSlatesClientFromProfile(context.profile, { cwd: opts.cwd, store });
110
- };
111
-
112
- export let withSlateProfile = async <T>(
113
- profileName: string | null | undefined,
114
- cb: (ctx: { profile: SlatesProfileRecord }) => Promise<T>
115
- ) => {
116
- let profile = await loadSlatesProfile({ profile: profileName });
117
- return cb({ profile });
118
- };
119
-
120
- export let expectToolCall = async (d: {
121
- client?: Awaited<ReturnType<typeof createSlatesTestClient>>;
122
- profile?: string | null;
123
- toolId: string;
124
- input: Record<string, any>;
125
- output?: Record<string, any>;
126
- }) => {
127
- let expect = getExpect();
128
- let client = d.client ?? (await createSlatesTestClient({ profile: d.profile }));
129
- let result = await client.invokeTool(d.toolId, d.input);
130
-
131
- if (d.output) {
132
- expect(result.output).toMatchObject(d.output);
133
- }
134
-
135
- return result;
136
- };
137
-
138
- export let createLocalSlateTestClient = (opts: {
139
- slate: LocalSlate;
140
- state?: SlatesProtocolClientOptions['state'];
141
- participants?: SlatesProtocolClientOptions['participants'];
142
- }) =>
143
- createSlatesClient({
144
- transport: createLocalSlateTransport({ slate: opts.slate }),
145
- state: opts.state,
146
- participants: opts.participants
147
- });
148
-
149
- export let getSlateContract = async (client: SlatesTestClient) => {
150
- let [provider, actions, authMethods, configSchema] = await Promise.all([
151
- client.identify(),
152
- client.listActions(),
153
- client.listAuthMethods(),
154
- client.getConfigSchema()
155
- ]);
156
-
157
- return {
158
- provider: provider.provider,
159
- actions: actions.actions,
160
- tools: actions.actions.filter(action => action.type === 'action.tool'),
161
- triggers: actions.actions.filter(action => action.type === 'action.trigger'),
162
- authMethods: authMethods.authenticationMethods,
163
- configSchema: configSchema.schema
164
- };
165
- };
166
-
167
- let expectActionMatches = (actual: Record<string, any> | undefined, expected: ExpectedSlateAction) => {
168
- let expect = getExpect();
169
- expect(actual).toBeTruthy();
170
- expect(actual?.id).toBe(expected.id);
171
-
172
- if (expected.name !== undefined) {
173
- expect(actual?.name).toBe(expected.name);
174
- }
175
-
176
- if (expected.description !== undefined) {
177
- expect(actual?.description).toBe(expected.description);
178
- }
179
-
180
- if (expected.readOnly !== undefined) {
181
- expect(actual?.tags?.readOnly ?? false).toBe(expected.readOnly);
182
- }
183
-
184
- if (expected.destructive !== undefined) {
185
- expect(actual?.tags?.destructive ?? false).toBe(expected.destructive);
186
- }
187
-
188
- if (expected.invocationType !== undefined) {
189
- expect((actual as { invocation?: { type?: string } } | undefined)?.invocation?.type).toBe(
190
- expected.invocationType
191
- );
192
- }
193
- };
194
-
195
- export let expectSlateContract = async (d: {
196
- client: SlatesTestClient;
197
- provider?: {
198
- id: string;
199
- name?: string;
200
- description?: string;
201
- };
202
- toolIds?: string[];
203
- triggerIds?: string[];
204
- authMethodIds?: string[];
205
- tools?: ExpectedSlateAction[];
206
- triggers?: ExpectedSlateAction[];
207
- }) => {
208
- let expect = getExpect();
209
- let contract = await getSlateContract(d.client);
210
-
211
- if (d.provider) {
212
- expect(contract.provider.id).toBe(d.provider.id);
213
- if (d.provider.name !== undefined) {
214
- expect(contract.provider.name).toBe(d.provider.name);
215
- }
216
- if (d.provider.description !== undefined) {
217
- expect(contract.provider.description).toBe(d.provider.description);
218
- }
219
- }
220
-
221
- if (d.toolIds) {
222
- expect(contract.tools.map(action => action.id)).toEqual(d.toolIds);
223
- }
224
-
225
- if (d.triggerIds) {
226
- expect(contract.triggers.map(action => action.id)).toEqual(d.triggerIds);
227
- }
228
-
229
- if (d.authMethodIds) {
230
- expect(contract.authMethods.map(method => method.id)).toEqual(d.authMethodIds);
231
- }
232
-
233
- for (let tool of d.tools ?? []) {
234
- expectActionMatches(contract.tools.find(action => action.id === tool.id), tool);
235
- }
236
-
237
- for (let trigger of d.triggers ?? []) {
238
- expectActionMatches(contract.triggers.find(action => action.id === trigger.id), trigger);
239
- }
240
-
241
- return contract;
242
- };
243
-
244
- export let registerSlateTriggerWebhook = async (d: {
245
- client: SlatesTestClient;
246
- triggerId: string;
247
- webhookBaseUrl: string;
248
- }) => d.client.registerTriggerWebhook(d.triggerId, d.webhookBaseUrl);
249
-
250
- export let handleSlateTriggerWebhook = async (d: {
251
- client: SlatesTestClient;
252
- triggerId: string;
253
- url: string;
254
- method?: string;
255
- headers?: Record<string, string>;
256
- body?: string | Uint8Array | null;
257
- state?: any;
258
- }) =>
259
- d.client.handleTriggerWebhook({
260
- actionId: d.triggerId,
261
- url: d.url,
262
- method: d.method ?? 'POST',
263
- headers: d.headers,
264
- body: d.body,
265
- state: d.state
266
- });
267
-
268
- export let unregisterSlateTriggerWebhook = async (d: {
269
- client: SlatesTestClient;
270
- triggerId: string;
271
- webhookBaseUrl: string;
272
- registrationDetails: any;
273
- state?: any;
274
- }) =>
275
- d.client.unregisterTriggerWebhook({
276
- actionId: d.triggerId,
277
- webhookBaseUrl: d.webhookBaseUrl,
278
- registrationDetails: d.registrationDetails,
279
- state: d.state
280
- });
281
-
282
- export let mapSlateTriggerEvent = async (d: {
283
- client: SlatesTestClient;
284
- triggerId: string;
285
- input: Record<string, any>;
286
- output?: Record<string, any>;
287
- type?: string;
288
- }) => {
289
- let expect = getExpect();
290
- let result = await d.client.mapTriggerEvent(d.triggerId, d.input);
291
-
292
- if (d.type !== undefined) {
293
- expect(result.type).toBe(d.type);
294
- }
295
-
296
- if (d.output) {
297
- expect(result.output).toMatchObject(d.output);
298
- }
299
-
300
- return result;
301
- };
302
-
303
- export let expectSlateError = async (
304
- run: Promise<unknown> | (() => Promise<unknown>),
305
- expected: Record<string, any> | string | RegExp
306
- ) => {
307
- let expect = getExpect();
308
- try {
309
- await (typeof run === 'function' ? run() : run);
310
- } catch (error) {
311
- if (typeof expected === 'string' || expected instanceof RegExp) {
312
- let message = error instanceof Error ? error.message : String(error);
313
- if (expected instanceof RegExp) {
314
- expect(message).toMatch(expected);
315
- } else {
316
- expect(message).toContain(expected);
317
- }
318
- } else if (error instanceof SlateProtocolError) {
319
- expect(error.data).toMatchObject(expected);
320
- } else {
321
- expect(error).toMatchObject(expected);
322
- }
323
-
324
- return error;
325
- }
326
-
327
- throw new Error('Expected the operation to fail, but it completed successfully.');
328
- };
329
-
330
- export let createSlatesVitestConfig = (
331
- config: Parameters<typeof createVitestConfig>[0] = {}
332
- ) => createVitestConfig(config);
1
+ export * from './runtime';