@slates/test 1.0.0-rc.2

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.
@@ -0,0 +1,209 @@
1
+ // src/index.ts
2
+ import { createVitestConfig } from "@lowerdeck/testing-tools";
3
+ import {
4
+ createLocalSlateTransport,
5
+ createSlatesClient,
6
+ SlateProtocolError
7
+ } from "@slates/client";
8
+ import {
9
+ createSlatesClientFromProfile,
10
+ openSlatesCliStore
11
+ } from "@slates/profiles";
12
+ import { readFile } from "fs/promises";
13
+ var getExpect = () => {
14
+ let maybeExpect = globalThis.expect;
15
+ if (!maybeExpect) {
16
+ throw new Error("Vitest expect is not available in the current runtime.");
17
+ }
18
+ return maybeExpect;
19
+ };
20
+ var loadSlatesRuntimeContext = async (opts = {}) => {
21
+ let runtimeContextPath = process.env.SLATES_TEST_CONTEXT_PATH;
22
+ if (runtimeContextPath) {
23
+ let raw = await readFile(runtimeContextPath, "utf-8");
24
+ let parsed = JSON.parse(raw);
25
+ let store2 = await openSlatesCliStore({ storePath: parsed.storePath });
26
+ let profile2 = store2.getProfile(opts.profile ?? parsed.profileId ?? null);
27
+ return {
28
+ integration: parsed.integration ?? store2.scope?.key ?? null,
29
+ profileId: profile2?.id ?? parsed.profileId ?? null,
30
+ profile: profile2,
31
+ storePath: parsed.storePath,
32
+ cliDir: parsed.cliDir
33
+ };
34
+ }
35
+ let store = process.env.SLATES_STORE_PATH ? await openSlatesCliStore({ storePath: process.env.SLATES_STORE_PATH }) : await openSlatesCliStore({ cwd: opts.cwd });
36
+ let profileId = opts.profile ?? process.env.SLATES_PROFILE_ID ?? null;
37
+ let profile = store.getProfile(profileId);
38
+ return {
39
+ integration: process.env.SLATES_INTEGRATION ?? store.scope?.key ?? null,
40
+ profileId: profile?.id ?? null,
41
+ profile,
42
+ storePath: store.storePath,
43
+ cliDir: store.dirPath
44
+ };
45
+ };
46
+ var loadSlatesProfile = async (opts = {}) => {
47
+ let context = await loadSlatesRuntimeContext(opts);
48
+ if (!context.profile) {
49
+ throw new Error("No Slates profile is available for the current test context.");
50
+ }
51
+ return context.profile;
52
+ };
53
+ var createSlatesTestClient = async (opts = {}) => {
54
+ let context = await loadSlatesRuntimeContext(opts);
55
+ if (!context.profile) {
56
+ throw new Error("No Slates profile is available for the current test context.");
57
+ }
58
+ let store = await openSlatesCliStore({ storePath: context.storePath });
59
+ return createSlatesClientFromProfile(context.profile, { cwd: opts.cwd, store });
60
+ };
61
+ var withSlateProfile = async (profileName, cb) => {
62
+ let profile = await loadSlatesProfile({ profile: profileName });
63
+ return cb({ profile });
64
+ };
65
+ var expectToolCall = async (d) => {
66
+ let expect = getExpect();
67
+ let client = d.client ?? await createSlatesTestClient({ profile: d.profile });
68
+ let result = await client.invokeTool(d.toolId, d.input);
69
+ if (d.output) {
70
+ expect(result.output).toMatchObject(d.output);
71
+ }
72
+ return result;
73
+ };
74
+ var createLocalSlateTestClient = (opts) => createSlatesClient({
75
+ transport: createLocalSlateTransport({ slate: opts.slate }),
76
+ state: opts.state,
77
+ participants: opts.participants
78
+ });
79
+ var getSlateContract = async (client) => {
80
+ let [provider, actions, authMethods, configSchema] = await Promise.all([
81
+ client.identify(),
82
+ client.listActions(),
83
+ client.listAuthMethods(),
84
+ client.getConfigSchema()
85
+ ]);
86
+ return {
87
+ provider: provider.provider,
88
+ actions: actions.actions,
89
+ tools: actions.actions.filter((action) => action.type === "action.tool"),
90
+ triggers: actions.actions.filter((action) => action.type === "action.trigger"),
91
+ authMethods: authMethods.authenticationMethods,
92
+ configSchema: configSchema.schema
93
+ };
94
+ };
95
+ var expectActionMatches = (actual, expected) => {
96
+ let expect = getExpect();
97
+ expect(actual).toBeTruthy();
98
+ expect(actual?.id).toBe(expected.id);
99
+ if (expected.name !== void 0) {
100
+ expect(actual?.name).toBe(expected.name);
101
+ }
102
+ if (expected.description !== void 0) {
103
+ expect(actual?.description).toBe(expected.description);
104
+ }
105
+ if (expected.readOnly !== void 0) {
106
+ expect(actual?.tags?.readOnly ?? false).toBe(expected.readOnly);
107
+ }
108
+ if (expected.destructive !== void 0) {
109
+ expect(actual?.tags?.destructive ?? false).toBe(expected.destructive);
110
+ }
111
+ if (expected.invocationType !== void 0) {
112
+ expect(actual?.invocation?.type).toBe(
113
+ expected.invocationType
114
+ );
115
+ }
116
+ };
117
+ var expectSlateContract = async (d) => {
118
+ let expect = getExpect();
119
+ let contract = await getSlateContract(d.client);
120
+ if (d.provider) {
121
+ expect(contract.provider.id).toBe(d.provider.id);
122
+ if (d.provider.name !== void 0) {
123
+ expect(contract.provider.name).toBe(d.provider.name);
124
+ }
125
+ if (d.provider.description !== void 0) {
126
+ expect(contract.provider.description).toBe(d.provider.description);
127
+ }
128
+ }
129
+ if (d.toolIds) {
130
+ expect(contract.tools.map((action) => action.id)).toEqual(d.toolIds);
131
+ }
132
+ if (d.triggerIds) {
133
+ expect(contract.triggers.map((action) => action.id)).toEqual(d.triggerIds);
134
+ }
135
+ if (d.authMethodIds) {
136
+ expect(contract.authMethods.map((method) => method.id)).toEqual(d.authMethodIds);
137
+ }
138
+ for (let tool of d.tools ?? []) {
139
+ expectActionMatches(contract.tools.find((action) => action.id === tool.id), tool);
140
+ }
141
+ for (let trigger of d.triggers ?? []) {
142
+ expectActionMatches(contract.triggers.find((action) => action.id === trigger.id), trigger);
143
+ }
144
+ return contract;
145
+ };
146
+ var registerSlateTriggerWebhook = async (d) => d.client.registerTriggerWebhook(d.triggerId, d.webhookBaseUrl);
147
+ var handleSlateTriggerWebhook = async (d) => d.client.handleTriggerWebhook({
148
+ actionId: d.triggerId,
149
+ url: d.url,
150
+ method: d.method ?? "POST",
151
+ headers: d.headers,
152
+ body: d.body,
153
+ state: d.state
154
+ });
155
+ var unregisterSlateTriggerWebhook = async (d) => d.client.unregisterTriggerWebhook({
156
+ actionId: d.triggerId,
157
+ webhookBaseUrl: d.webhookBaseUrl,
158
+ registrationDetails: d.registrationDetails,
159
+ state: d.state
160
+ });
161
+ var mapSlateTriggerEvent = async (d) => {
162
+ let expect = getExpect();
163
+ let result = await d.client.mapTriggerEvent(d.triggerId, d.input);
164
+ if (d.type !== void 0) {
165
+ expect(result.type).toBe(d.type);
166
+ }
167
+ if (d.output) {
168
+ expect(result.output).toMatchObject(d.output);
169
+ }
170
+ return result;
171
+ };
172
+ var expectSlateError = async (run, expected) => {
173
+ let expect = getExpect();
174
+ try {
175
+ await (typeof run === "function" ? run() : run);
176
+ } catch (error) {
177
+ if (typeof expected === "string" || expected instanceof RegExp) {
178
+ let message = error instanceof Error ? error.message : String(error);
179
+ if (expected instanceof RegExp) {
180
+ expect(message).toMatch(expected);
181
+ } else {
182
+ expect(message).toContain(expected);
183
+ }
184
+ } else if (error instanceof SlateProtocolError) {
185
+ expect(error.data).toMatchObject(expected);
186
+ } else {
187
+ expect(error).toMatchObject(expected);
188
+ }
189
+ return error;
190
+ }
191
+ throw new Error("Expected the operation to fail, but it completed successfully.");
192
+ };
193
+ var createSlatesVitestConfig = (config = {}) => createVitestConfig(config);
194
+ export {
195
+ createLocalSlateTestClient,
196
+ createSlatesTestClient,
197
+ createSlatesVitestConfig,
198
+ expectSlateContract,
199
+ expectSlateError,
200
+ expectToolCall,
201
+ getSlateContract,
202
+ handleSlateTriggerWebhook,
203
+ loadSlatesProfile,
204
+ loadSlatesRuntimeContext,
205
+ mapSlateTriggerEvent,
206
+ registerSlateTriggerWebhook,
207
+ unregisterSlateTriggerWebhook,
208
+ withSlateProfile
209
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@slates/test",
3
+ "version": "1.0.0-rc.2",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "files": [
8
+ "src/**",
9
+ "dist/**",
10
+ "README.md",
11
+ "package.json"
12
+ ],
13
+ "author": "Tobias Herber",
14
+ "license": "FSL 1.1",
15
+ "type": "module",
16
+ "source": "src/index.ts",
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"
22
+ },
23
+ "main": "./dist/index.cjs",
24
+ "module": "./dist/index.module.js",
25
+ "types": "dist/index.d.ts",
26
+ "unpkg": "./dist/index.module.js",
27
+ "scripts": {
28
+ "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"
32
+ },
33
+ "dependencies": {
34
+ "@lowerdeck/testing-tools": "latest",
35
+ "@slates/client": "1.0.0-rc.2",
36
+ "@slates/profiles": "1.0.0-rc.2"
37
+ },
38
+ "devDependencies": {
39
+ "@slates/provider": "1.0.0-rc.8",
40
+ "@slates/tsconfig": "1.0.0-rc.1",
41
+ "typescript": "5.8.2",
42
+ "vitest": "^3.1.2",
43
+ "zod": "^4.2"
44
+ }
45
+ }
@@ -0,0 +1,360 @@
1
+ import {
2
+ Slate,
3
+ SlateAuth,
4
+ SlateConfig,
5
+ SlateSpecification,
6
+ SlateTool,
7
+ SlateTrigger
8
+ } from '@slates/provider';
9
+ import { openSlatesCliStore } from '@slates/profiles';
10
+ import { mkdtemp, rm, writeFile } from 'fs/promises';
11
+ import { tmpdir } from 'os';
12
+ import path from 'path';
13
+ import { afterEach, describe, expect, it } from 'vitest';
14
+ import { z } from 'zod';
15
+ import {
16
+ createLocalSlateTestClient,
17
+ expectSlateContract,
18
+ expectSlateError,
19
+ expectToolCall,
20
+ handleSlateTriggerWebhook,
21
+ loadSlatesRuntimeContext,
22
+ mapSlateTriggerEvent,
23
+ registerSlateTriggerWebhook,
24
+ unregisterSlateTriggerWebhook
25
+ } from './index';
26
+
27
+ (globalThis as typeof globalThis & { expect?: typeof expect }).expect = expect;
28
+
29
+ let tempDirs: string[] = [];
30
+
31
+ let createTempDir = async () => {
32
+ let dir = await mkdtemp(path.join(tmpdir(), 'slates-test-'));
33
+ tempDirs.push(dir);
34
+ return dir;
35
+ };
36
+
37
+ afterEach(async () => {
38
+ delete process.env.SLATES_INTEGRATION;
39
+ delete process.env.SLATES_PROFILE_ID;
40
+ delete process.env.SLATES_STORE_PATH;
41
+ delete process.env.SLATES_TEST_CONTEXT_PATH;
42
+ await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })));
43
+ });
44
+
45
+ let createDemoSlate = () => {
46
+ let config = SlateConfig.create(
47
+ z.object({
48
+ prefix: z.string()
49
+ })
50
+ ).getDefaultConfig(() => ({
51
+ prefix: 'Hello'
52
+ }));
53
+
54
+ let auth = SlateAuth.create<{ token: string }>()
55
+ .output(
56
+ z.object({
57
+ token: z.string()
58
+ })
59
+ )
60
+ .addTokenAuth({
61
+ type: 'auth.token',
62
+ key: 'token_auth',
63
+ name: 'Token Auth',
64
+ inputSchema: z.object({
65
+ token: z.string()
66
+ }),
67
+ getOutput: async ctx => ({
68
+ output: {
69
+ token: ctx.input.token
70
+ }
71
+ })
72
+ });
73
+
74
+ let spec = SlateSpecification.create({
75
+ key: 'demo-slate',
76
+ name: 'Demo Slate',
77
+ description: 'A tiny test slate',
78
+ config,
79
+ auth
80
+ });
81
+
82
+ let echo = SlateTool.create(spec, {
83
+ key: 'echo',
84
+ name: 'Echo',
85
+ tags: {
86
+ readOnly: false
87
+ }
88
+ })
89
+ .input(
90
+ z.object({
91
+ name: z.string()
92
+ })
93
+ )
94
+ .output(
95
+ z.object({
96
+ greeting: z.string(),
97
+ token: z.string()
98
+ })
99
+ )
100
+ .handleInvocation(async ctx => ({
101
+ output: {
102
+ greeting: `${ctx.config.prefix} ${ctx.input.name}`,
103
+ token: ctx.auth.token
104
+ },
105
+ message: 'done'
106
+ }))
107
+ .build();
108
+
109
+ let fail = SlateTool.create(spec, {
110
+ key: 'fail',
111
+ name: 'Fail'
112
+ })
113
+ .input(
114
+ z.object({
115
+ reason: z.string()
116
+ })
117
+ )
118
+ .output(z.object({}))
119
+ .handleInvocation(async ctx => {
120
+ throw new Error(ctx.input.reason);
121
+ })
122
+ .build();
123
+
124
+ let webhookEcho = SlateTrigger.create(spec, {
125
+ key: 'webhook_echo',
126
+ name: 'Webhook Echo'
127
+ })
128
+ .input(
129
+ z.object({
130
+ value: z.string()
131
+ })
132
+ )
133
+ .output(
134
+ z.object({
135
+ echoed: z.string()
136
+ })
137
+ )
138
+ .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');
151
+ }
152
+ },
153
+ handleRequest: async ctx => {
154
+ if (ctx.request.headers.get('x-demo-event') === 'ignore') {
155
+ return { inputs: [] };
156
+ }
157
+
158
+ return {
159
+ inputs: [
160
+ {
161
+ value: (await ctx.request.text()) || 'empty'
162
+ }
163
+ ],
164
+ updatedState: {
165
+ lastEvent: ctx.request.headers.get('x-demo-event') ?? 'unknown'
166
+ }
167
+ };
168
+ },
169
+ handleEvent: async ctx => ({
170
+ type: 'demo.webhook',
171
+ id: `webhook-${ctx.input.value}`,
172
+ output: {
173
+ echoed: ctx.input.value
174
+ }
175
+ })
176
+ })
177
+ .build();
178
+
179
+ return Slate.create({
180
+ spec,
181
+ tools: [echo, fail],
182
+ triggers: [webhookEcho]
183
+ });
184
+ };
185
+
186
+ describe('@slates/test', () => {
187
+ it('loads runtime context from the CLI handoff file', async () => {
188
+ let cwd = await createTempDir();
189
+ let store = await openSlatesCliStore({
190
+ cwd,
191
+ scope: {
192
+ key: 'integrations/demo',
193
+ name: 'demo'
194
+ }
195
+ });
196
+ let profile = store.upsertProfile({
197
+ name: 'Demo',
198
+ target: {
199
+ type: 'local',
200
+ entry: './demo-slate.mjs',
201
+ exportName: 'provider'
202
+ }
203
+ });
204
+ await store.save();
205
+
206
+ let runtimeContextPath = path.join(cwd, 'runtime.json');
207
+ await writeFile(
208
+ runtimeContextPath,
209
+ JSON.stringify({
210
+ integration: 'integrations/demo',
211
+ profileId: profile.id,
212
+ storePath: store.storePath,
213
+ cliDir: store.dirPath
214
+ }),
215
+ 'utf-8'
216
+ );
217
+
218
+ process.env.SLATES_TEST_CONTEXT_PATH = runtimeContextPath;
219
+
220
+ let context = await loadSlatesRuntimeContext({ cwd });
221
+ expect(context.integration).toBe('integrations/demo');
222
+ expect(context.profileId).toBe(profile.id);
223
+ expect(context.profile?.target.type).toBe('local');
224
+ expect(context.storePath).toBe(store.storePath);
225
+ });
226
+
227
+ it('creates local slate clients and asserts provider contracts', async () => {
228
+ let client = createLocalSlateTestClient({
229
+ slate: createDemoSlate(),
230
+ state: {
231
+ config: {
232
+ prefix: 'Hi'
233
+ },
234
+ auth: {
235
+ authenticationMethodId: 'token_auth',
236
+ output: {
237
+ token: 'secret-token'
238
+ }
239
+ }
240
+ }
241
+ });
242
+
243
+ let contract = await expectSlateContract({
244
+ client,
245
+ provider: {
246
+ id: 'demo-slate',
247
+ name: 'Demo Slate',
248
+ description: 'A tiny test slate'
249
+ },
250
+ toolIds: ['echo', 'fail'],
251
+ triggerIds: ['webhook_echo'],
252
+ authMethodIds: ['token_auth'],
253
+ tools: [
254
+ { id: 'echo', readOnly: false, destructive: false },
255
+ { id: 'fail', readOnly: false, destructive: false }
256
+ ],
257
+ triggers: [{ id: 'webhook_echo', invocationType: 'webhook' }]
258
+ });
259
+
260
+ expect(contract.configSchema.properties.prefix.type).toBe('string');
261
+
262
+ await expectToolCall({
263
+ client,
264
+ toolId: 'echo',
265
+ input: {
266
+ name: 'Tobias'
267
+ },
268
+ output: {
269
+ greeting: 'Hi Tobias',
270
+ token: 'secret-token'
271
+ }
272
+ });
273
+ });
274
+
275
+ it('wraps trigger webhook flows and error assertions', async () => {
276
+ let client = createLocalSlateTestClient({
277
+ slate: createDemoSlate(),
278
+ state: {
279
+ config: {
280
+ prefix: 'Hi'
281
+ },
282
+ auth: {
283
+ authenticationMethodId: 'token_auth',
284
+ output: {
285
+ token: 'secret-token'
286
+ }
287
+ }
288
+ }
289
+ });
290
+
291
+ let registration = await registerSlateTriggerWebhook({
292
+ client,
293
+ triggerId: 'webhook_echo',
294
+ webhookBaseUrl: 'https://example.com/hooks/google-calendar'
295
+ });
296
+ expect(registration).toMatchObject({
297
+ registrationDetails: {
298
+ webhookBaseUrl: 'https://example.com/hooks/google-calendar',
299
+ channelId: 'channel-1'
300
+ },
301
+ state: {
302
+ registered: true
303
+ }
304
+ });
305
+
306
+ let ignored = await handleSlateTriggerWebhook({
307
+ client,
308
+ triggerId: 'webhook_echo',
309
+ url: 'https://example.com/hooks/google-calendar',
310
+ headers: {
311
+ 'x-demo-event': 'ignore'
312
+ }
313
+ });
314
+ expect(ignored.inputs).toEqual([]);
315
+
316
+ let handled = await handleSlateTriggerWebhook({
317
+ client,
318
+ triggerId: 'webhook_echo',
319
+ url: 'https://example.com/hooks/google-calendar',
320
+ headers: {
321
+ 'x-demo-event': 'created'
322
+ },
323
+ body: 'payload-value'
324
+ });
325
+ expect(handled).toMatchObject({
326
+ inputs: [{ value: 'payload-value' }],
327
+ updatedState: {
328
+ lastEvent: 'created'
329
+ }
330
+ });
331
+
332
+ let mapped = await mapSlateTriggerEvent({
333
+ client,
334
+ triggerId: 'webhook_echo',
335
+ input: {
336
+ value: 'payload-value'
337
+ },
338
+ type: 'demo.webhook',
339
+ output: {
340
+ echoed: 'payload-value'
341
+ }
342
+ });
343
+ expect(mapped.id).toBe('webhook-payload-value');
344
+
345
+ await unregisterSlateTriggerWebhook({
346
+ client,
347
+ triggerId: 'webhook_echo',
348
+ webhookBaseUrl: 'https://example.com/hooks/google-calendar',
349
+ registrationDetails: registration.registrationDetails
350
+ });
351
+
352
+ await expectSlateError(
353
+ () =>
354
+ client.invokeTool('fail', {
355
+ reason: 'intentional failure'
356
+ }),
357
+ { code: 'internal.unexpected', kind: 'internal', status: 500 }
358
+ );
359
+ });
360
+ });