@tailor-platform/sdk 1.45.2 → 1.47.0

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/{actor-BmxQeMFP.d.mts → actor-DhXSqWTW.d.mts} +2 -2
  3. package/dist/application-CN9Htzup.mjs +4 -0
  4. package/dist/{application-B4zVVNRS.mjs → application-TasSqBTD.mjs} +22 -41
  5. package/dist/application-TasSqBTD.mjs.map +1 -0
  6. package/dist/cli/index.mjs +101 -61
  7. package/dist/cli/index.mjs.map +1 -1
  8. package/dist/cli/lib.d.mts +13 -13
  9. package/dist/cli/lib.mjs +4 -4
  10. package/dist/cli/lib.mjs.map +1 -1
  11. package/dist/{client-BwXkoiMq.mjs → client-COfsXV69.mjs} +31 -120
  12. package/dist/client-COfsXV69.mjs.map +1 -0
  13. package/dist/{client-DTaArWQr.mjs → client-DYSkSLRr.mjs} +1 -1
  14. package/dist/configure/index.d.mts +4 -4
  15. package/dist/configure/index.mjs +4 -43
  16. package/dist/configure/index.mjs.map +1 -1
  17. package/dist/{crash-report-BUHzuzDn.mjs → crashreport-B8lVOx0U.mjs} +1 -1
  18. package/dist/{crash-report-CtYCva4d.mjs → crashreport-CKJwnWsX.mjs} +9 -9
  19. package/dist/crashreport-CKJwnWsX.mjs.map +1 -0
  20. package/dist/{index-DV-5OIEv.d.mts → index-BRvNi5q9.d.mts} +2 -2
  21. package/dist/{index-BBvPd9Uv.d.mts → index-BXyS7xKC.d.mts} +2 -2
  22. package/dist/{index-Dxe6alSZ.d.mts → index-BbOTbZFf.d.mts} +2 -2
  23. package/dist/{index-DUKJPEwq.d.mts → index-BoU_52Du.d.mts} +6 -6
  24. package/dist/{index-B5_4Tzm2.d.mts → index-iy-hNfGp.d.mts} +2 -2
  25. package/dist/{interceptor-CrcDfLPq.mjs → interceptor-CBsqEWDK.mjs} +1 -1
  26. package/dist/{interceptor-CrcDfLPq.mjs.map → interceptor-CBsqEWDK.mjs.map} +1 -1
  27. package/dist/mock-BP-9O5On.mjs +796 -0
  28. package/dist/mock-BP-9O5On.mjs.map +1 -0
  29. package/dist/plugin/builtin/enum-constants/index.d.mts +1 -1
  30. package/dist/plugin/builtin/file-utils/index.d.mts +1 -1
  31. package/dist/plugin/builtin/kysely-type/index.d.mts +1 -1
  32. package/dist/plugin/builtin/seed/index.d.mts +1 -1
  33. package/dist/plugin/index.d.mts +2 -2
  34. package/dist/{repl-editor-BlT2dFtm.mjs → repl-editor-CZpLlOBj.mjs} +1 -1
  35. package/dist/{repl-editor-BlT2dFtm.mjs.map → repl-editor-CZpLlOBj.mjs.map} +1 -1
  36. package/dist/{runtime-D97Ydu2S.mjs → runtime-DDYL2Zf1.mjs} +148 -70
  37. package/dist/runtime-DDYL2Zf1.mjs.map +1 -0
  38. package/dist/{service-CCgw66c6.mjs → service-obEU5gSM.mjs} +1 -1
  39. package/dist/{service-CCgw66c6.mjs.map → service-obEU5gSM.mjs.map} +1 -1
  40. package/dist/{tailor-db-field-Hx9OqPWY.d.mts → tailor-db-field-Bn8ZC5lK.d.mts} +1 -1
  41. package/dist/{schema-DBq6hr6h.mjs → tailordb-Bg9-TZj1.mjs} +42 -2
  42. package/dist/tailordb-Bg9-TZj1.mjs.map +1 -0
  43. package/dist/telemetry-21afNV9_.mjs +4 -0
  44. package/dist/{telemetry-DXitz4RH.mjs → telemetry-DcL8Fsm_.mjs} +1 -1
  45. package/dist/{telemetry-DXitz4RH.mjs.map → telemetry-DcL8Fsm_.mjs.map} +1 -1
  46. package/dist/utils/test/index.d.mts +13 -4
  47. package/dist/utils/test/index.mjs +12 -3
  48. package/dist/utils/test/index.mjs.map +1 -1
  49. package/dist/vitest/environment.d.mts +12 -0
  50. package/dist/vitest/environment.mjs +44 -0
  51. package/dist/vitest/environment.mjs.map +1 -0
  52. package/dist/vitest/index.d.mts +345 -0
  53. package/dist/vitest/index.mjs +350 -0
  54. package/dist/vitest/index.mjs.map +1 -0
  55. package/dist/vitest/setup.d.mts +64 -0
  56. package/dist/vitest/setup.mjs +141 -0
  57. package/dist/vitest/setup.mjs.map +1 -0
  58. package/dist/{workflow.generated-DFljpJh7.d.mts → workflow.generated-i7PK4fg-.d.mts} +2 -2
  59. package/docs/cli/application.md +19 -17
  60. package/docs/cli/crashreport.md +119 -0
  61. package/docs/cli/executor.md +9 -9
  62. package/docs/cli/function.md +5 -5
  63. package/docs/cli/setup.md +1 -0
  64. package/docs/cli/tailordb.md +1 -1
  65. package/docs/cli/workflow.md +8 -8
  66. package/docs/cli-reference.md +8 -8
  67. package/docs/quickstart.md +2 -2
  68. package/docs/services/auth.md +2 -2
  69. package/docs/services/secret.md +4 -4
  70. package/docs/services/tailordb-migration.md +10 -10
  71. package/docs/services/tailordb.md +44 -13
  72. package/docs/services/workflow.md +1 -1
  73. package/docs/testing.md +530 -243
  74. package/package.json +32 -6
  75. package/dist/application-B4zVVNRS.mjs.map +0 -1
  76. package/dist/application-BIzicxMA.mjs +0 -4
  77. package/dist/client-BwXkoiMq.mjs.map +0 -1
  78. package/dist/crash-report-CtYCva4d.mjs.map +0 -1
  79. package/dist/runtime-D97Ydu2S.mjs.map +0 -1
  80. package/dist/schema-DBq6hr6h.mjs.map +0 -1
  81. package/dist/telemetry-BvI1EgMG.mjs +0 -4
  82. package/docs/cli/crash-report.md +0 -118
package/docs/testing.md CHANGED
@@ -1,47 +1,341 @@
1
1
  # Testing Guide
2
2
 
3
- This guide covers testing patterns for Tailor Platform SDK applications using [Vitest](https://vitest.dev/).
3
+ Tailor Platform SDK applications are tested with [Vitest](https://vitest.dev/) at two layers:
4
4
 
5
- For a complete working example with full test code, use the `testing` template:
5
+ | Layer | What it exercises | Deployment required |
6
+ | ---------- | ---------------------------------------------------- | ------------------- |
7
+ | Unit tests | Resolver / workflow job / executor TypeScript source | No |
8
+ | E2E tests | Deployed GraphQL API, TailorDB, and workflows | Yes |
6
9
 
7
- ```bash
8
- npm create @tailor-platform/sdk -- --template testing <your-project-name>
10
+ Lean on unit tests for the day-to-day feedback loop — they run fast and exercise business logic against real SDK types with no deployment in the loop. Reach for E2E tests to confirm integration against a live platform, where mocked globals can drift from the real GraphQL, TailorDB, and workflow runtime.
11
+
12
+ Unit-test entrypoints exposed by the SDK:
13
+
14
+ - `resolver.body({ input, user, env })` — invoke a resolver
15
+ - `workflowJob.body(input, { env })` / `workflowJob.trigger(input)` — invoke or chain a workflow job
16
+ - `executor.operation.body(args)` — invoke a function-kind executor
17
+
18
+ Helpers under `@tailor-platform/sdk/test`:
19
+
20
+ - `unauthenticatedTailorUser` — default `user` value for resolver contexts
21
+ - `WORKFLOW_TEST_ENV_KEY` — env key consumed by `.trigger()` when run locally
22
+
23
+ Platform API mocks under `@tailor-platform/sdk/vitest` (auto-injected by the [`tailor-runtime` Vitest environment](#runtime-environment-emulation-beta) below):
24
+
25
+ - `tailordbMock` — TailorDB query stubs and call recording
26
+ - `workflowMock` — `tailor.workflow` job / wait / resolve mocks
27
+ - `secretmanagerMock`, `authconnectionMock`, `idpMock`, `fileMock`, `iconvMock` — corresponding platform API mocks
28
+
29
+ For tighter alignment with the production runtime — Node.js module blocking, Web-only globals, and platform API mocks — pair the resolver helpers with the [`tailor-runtime` Vitest environment](#runtime-environment-emulation-beta) below.
30
+
31
+ Three starter templates demonstrate the patterns below in a working project:
32
+
33
+ - `npm create @tailor-platform/sdk -- --template resolver <name>` — resolvers, TailorDB mocking, and DI
34
+ - `npm create @tailor-platform/sdk -- --template executor <name>` — executors with extracted DB helpers
35
+ - `npm create @tailor-platform/sdk -- --template workflow <name>` — workflow jobs, wait points, and an E2E suite
36
+
37
+ ## Runtime Environment Emulation (Beta)
38
+
39
+ The Tailor Platform function runtime only provides Web Standard APIs. Node.js built-in modules like `node:crypto` and globals like `Buffer` are not available. The `tailor-runtime` Vitest environment catches these incompatibilities locally before deployment.
40
+
41
+ ### Setup
42
+
43
+ ```typescript
44
+ // vitest.config.ts
45
+ import { defineConfig } from "vitest/config";
46
+ import { tailorRuntime } from "@tailor-platform/sdk/vitest";
47
+
48
+ export default defineConfig({
49
+ plugins: [tailorRuntime()],
50
+ test: {
51
+ environment: "tailor-runtime",
52
+ },
53
+ });
9
54
  ```
10
55
 
56
+ `tailorRuntime()` provides:
57
+
58
+ 1. **Node.js module blocking** — `import { randomBytes } from "node:crypto"` in production code throws an error with a suggestion for the Web Standard API alternative (`globalThis.crypto`). Test files (`*.test.ts`, `*.spec.ts`) are exempt.
59
+ 2. **Node.js globals removal** — Only globals available in the platform runtime are kept (whitelist). `Buffer`, `global`, `setImmediate`, `__dirname`, `__filename`, `performance`, and others are removed.
60
+ 3. **Platform API mocks** — `globalThis.tailordb`, `globalThis.tailor`, `TailorErrors`, `TailorErrorMessage`, `TailorDBFileError` are auto-injected with mock control objects for response configuration and call recording.
61
+
62
+ ### TailorDB Mock
63
+
64
+ The environment auto-injects a mock `tailordb.Client`. Use `tailordbMock` to configure responses and assert on executed queries:
65
+
66
+ ```typescript
67
+ import { tailordbMock } from "@tailor-platform/sdk/vitest";
68
+
69
+ beforeEach(() => {
70
+ tailordbMock.reset();
71
+ });
72
+
73
+ test("resolver queries the database", async () => {
74
+ // Order-based: stage rows for each upcoming query in one call
75
+ tailordbMock.enqueueResults(
76
+ [], // BEGIN (empty result)
77
+ [{ age: 30 }], // SELECT (one row)
78
+ [], // COMMIT
79
+ );
80
+
81
+ const result = await resolver.body({ input: { email: "test@example.com" } });
82
+
83
+ expect(result).toEqual({ oldAge: 30, newAge: 31 });
84
+ expect(tailordbMock.executedQueries).toHaveLength(3);
85
+ expect(tailordbMock.createdClients).toMatchObject([{ namespace: "tailordb" }]);
86
+ });
87
+ ```
88
+
89
+ Three response modes:
90
+
91
+ - **`enqueueResult(...rows)`** — Order-based, single query. Arguments are the row objects returned by the next `queryObject` call (`enqueueResult()` for empty, `enqueueResult({ id: "1" })` for one row, `enqueueResult({ a: 1 }, { a: 2 })` for multiple rows). Consumed in FIFO order.
92
+ - **`enqueueResults(...rowsArrays)`** — Order-based, multiple queries. Each argument is a rows array for one upcoming query. Equivalent to calling `enqueueResult` for each entry but easier to read for transactional sequences.
93
+ - **`setQueryResolver((query, params) => rows)`** — Content-based fallback. Called when the queue is empty.
94
+
95
+ ```typescript
96
+ test("content-based mock", async () => {
97
+ tailordbMock.setQueryResolver((query) => {
98
+ if (query.includes("SELECT")) return [{ id: "1", name: "test" }];
99
+ return [];
100
+ });
101
+
102
+ const result = await resolver.body({ input: { userId: "1" } });
103
+
104
+ expect(tailordbMock.executedQueries[0].query).toContain("SELECT");
105
+ });
106
+ ```
107
+
108
+ ### Workflow Mock
109
+
110
+ The environment auto-injects `tailor.workflow.triggerJobFunction`. Use `workflowMock` to configure job responses:
111
+
112
+ ```typescript
113
+ import { workflowMock } from "@tailor-platform/sdk/vitest";
114
+
115
+ beforeEach(() => {
116
+ workflowMock.reset();
117
+ });
118
+
119
+ test("workflow triggers jobs", async () => {
120
+ workflowMock.setJobHandler((jobName, args) => {
121
+ if (jobName === "validate-order") return { valid: true };
122
+ if (jobName === "process-payment") return { txnId: "txn-1" };
123
+ return null;
124
+ });
125
+
126
+ const result = await main({ input: { orderId: "o-1" } });
127
+
128
+ expect(workflowMock.triggeredJobs).toEqual([
129
+ { jobName: "validate-order", args: { orderId: "o-1" } },
130
+ { jobName: "process-payment", args: { orderId: "o-1" } },
131
+ ]);
132
+ });
133
+ ```
134
+
135
+ `workflowMock` also supports order-based responses:
136
+
137
+ ```typescript
138
+ // Single response for the next triggerJobFunction call
139
+ workflowMock.enqueueResult({ valid: true });
140
+
141
+ // Multiple responses for subsequent calls (FIFO)
142
+ workflowMock.enqueueResults({ valid: true }, { txnId: "txn-1" });
143
+ ```
144
+
145
+ ### SecretManager Mock
146
+
147
+ ```typescript
148
+ import { secretmanagerMock } from "@tailor-platform/sdk/vitest";
149
+
150
+ beforeEach(() => secretmanagerMock.reset());
151
+
152
+ test("reads secrets from vault", async () => {
153
+ secretmanagerMock.setSecrets({
154
+ "my-vault": { API_KEY: "sk-123", DB_PASS: "secret" },
155
+ });
156
+
157
+ const key = await tailor.secretmanager.getSecret("my-vault", "API_KEY");
158
+ expect(key).toBe("sk-123");
159
+ expect(secretmanagerMock.calls).toEqual([
160
+ { method: "getSecret", vault: "my-vault", name: "API_KEY" },
161
+ ]);
162
+ });
163
+ ```
164
+
165
+ ### AuthConnection Mock
166
+
167
+ ```typescript
168
+ import { authconnectionMock } from "@tailor-platform/sdk/vitest";
169
+
170
+ beforeEach(() => authconnectionMock.reset());
171
+
172
+ test("returns configured token", async () => {
173
+ authconnectionMock.setTokens({
174
+ google: { access_token: "ya29.xxx", expires_in: 3600 },
175
+ });
176
+
177
+ const token = await tailor.authconnection.getConnectionToken("google");
178
+ expect(token.access_token).toBe("ya29.xxx");
179
+ });
180
+ ```
181
+
182
+ When no token is configured for a connection, it returns `{ access_token: "mock-token" }`.
183
+
184
+ ### IDP Mock
185
+
186
+ ```typescript
187
+ import { idpMock } from "@tailor-platform/sdk/vitest";
188
+
189
+ beforeEach(() => idpMock.reset());
190
+
191
+ test("resolver-based", async () => {
192
+ idpMock.setResolver((method, args) => {
193
+ if (method === "user") return { id: "u-1", name: "alice", disabled: false };
194
+ return null; // falls back to defaults
195
+ });
196
+
197
+ const client = new tailor.idp.Client({ namespace: "my-ns" });
198
+ const user = await client.user("u-1");
199
+ expect(user.name).toBe("alice");
200
+ });
201
+
202
+ test("queue-based", async () => {
203
+ idpMock.enqueueResult({ id: "u-1", name: "alice", disabled: false });
204
+
205
+ const client = new tailor.idp.Client({ namespace: "my-ns" });
206
+ const user = await client.user("u-1");
207
+ expect(user.name).toBe("alice");
208
+ expect(idpMock.calls).toMatchObject([{ method: "user", namespace: "my-ns" }]);
209
+ });
210
+ ```
211
+
212
+ ### File Mock
213
+
214
+ ```typescript
215
+ import { fileMock } from "@tailor-platform/sdk/vitest";
216
+
217
+ beforeEach(() => fileMock.reset());
218
+
219
+ test("mock file download", async () => {
220
+ fileMock.enqueueResult({
221
+ data: new Uint8Array([1, 2, 3]),
222
+ metadata: { contentType: "image/png", fileSize: 3, sha256sum: "abc", lastUploadedAt: "" },
223
+ });
224
+
225
+ const result = await tailordb.file.download("ns", "Doc", "attachment", "r-1");
226
+ expect(result.data).toEqual(new Uint8Array([1, 2, 3]));
227
+ expect(fileMock.calls).toMatchObject([{ method: "download", recordId: "r-1" }]);
228
+ });
229
+ ```
230
+
231
+ ### Iconv Mock
232
+
233
+ ```typescript
234
+ import { iconvMock } from "@tailor-platform/sdk/vitest";
235
+
236
+ beforeEach(() => iconvMock.reset());
237
+
238
+ test("mock encoding conversion", () => {
239
+ iconvMock.setResolver((method, args) => {
240
+ if (method === "decode") return "decoded-text";
241
+ return null; // falls back to default empty string
242
+ });
243
+
244
+ const result = tailor.iconv.decode(new Uint8Array([0x48, 0x69]), "UTF-8");
245
+ expect(result).toBe("decoded-text");
246
+ expect(iconvMock.calls).toMatchObject([{ method: "decode" }]);
247
+ });
248
+ ```
249
+
250
+ ### Loading Secrets from Config
251
+
252
+ Pass a config path to load `defineSecretManager()` values into the mock:
253
+
254
+ ```typescript
255
+ export default defineConfig({
256
+ plugins: [tailorRuntime({ config: "./tailor.config.ts" })],
257
+ test: { environment: "tailor-runtime" },
258
+ });
259
+ ```
260
+
261
+ This makes `tailor.secretmanager.getSecret("vault", "key")` return the values defined in your config. You can still override with `secretmanagerMock.setSecrets()` in individual tests.
262
+
263
+ ### Per-Project Configuration
264
+
265
+ Apply the runtime environment only to unit tests while keeping other test projects (e.g. e2e) in the default Node.js environment:
266
+
267
+ ```typescript
268
+ export default defineConfig({
269
+ plugins: [tailorRuntime()],
270
+ test: {
271
+ projects: [
272
+ // `extends: true` is required so each project inherits the root-level
273
+ // `tailorRuntime()` plugin (transform hook + injected setup file).
274
+ // Without it, only the environment name rewrite applies — node:* import
275
+ // blocking and per-test global cleanup will silently not run.
276
+ {
277
+ extends: true,
278
+ test: {
279
+ name: "unit",
280
+ environment: "tailor-runtime",
281
+ include: ["src/**/*.test.ts"],
282
+ },
283
+ },
284
+ {
285
+ extends: true,
286
+ test: {
287
+ name: "e2e",
288
+ include: ["e2e/**/*.test.ts"],
289
+ globalSetup: "e2e/globalSetup.ts",
290
+ },
291
+ },
292
+ ],
293
+ },
294
+ });
295
+ ```
296
+
297
+ ### Known Limitations
298
+
299
+ - **`process` and `require`** are not removed or blocked. Vitest's internal runner depends on them extensively. On the real platform runtime, they do not exist.
300
+
11
301
  ## Unit Tests
12
302
 
13
- Unit tests verify resolver and workflow logic locally without requiring deployment.
303
+ Unit tests call `.body()` (or `.trigger()`) directly on a resolver, workflow job, or executor and stub any platform-provided globals they touch.
304
+
305
+ ### Testing Resolvers
14
306
 
15
- ### Simple Resolver Testing
307
+ #### Simple resolver
16
308
 
17
- Test resolvers by directly calling `resolver.body()` with mock inputs.
309
+ For pure logic with no external dependencies, invoke `.body()` directly:
18
310
 
19
311
  ```typescript
20
- import { unauthenticatedTailorUser } from "@tailor-platform/sdk";
312
+ import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test";
313
+ import { describe, expect, test } from "vitest";
21
314
  import resolver from "../src/resolver/add";
22
315
 
23
316
  describe("add resolver", () => {
24
- test("basic functionality", async () => {
317
+ test("adds two numbers", async () => {
25
318
  const result = await resolver.body({
26
319
  input: { left: 1, right: 2 },
27
320
  user: unauthenticatedTailorUser,
321
+ env: {},
28
322
  });
29
323
  expect(result).toBe(3);
30
324
  });
31
325
  });
32
326
  ```
33
327
 
34
- **Key points:**
328
+ **Use when:** calculations, data transformations, anything that does not hit the database.
35
329
 
36
- - Use `unauthenticatedTailorUser` for testing logic that doesn't depend on user context
37
- - **Best for:** Calculations, data transformations without database dependencies
330
+ #### Mocking the TailorDB client
38
331
 
39
- ### Mock TailorDB Client
332
+ Stub the global `tailordb.Client` and queue raw query results in order. Best for resolvers that issue a short, predictable query sequence:
40
333
 
41
- Mock the global `tailordb.Client` using `vi.stubGlobal()` to simulate database operations and control responses for each query.
334
+ > If you are running with the [`tailor-runtime` Vitest environment](#runtime-environment-emulation-beta), `tailordb.Client` is auto-injected drive it with `tailordbMock` instead of `vi.stubGlobal()`.
42
335
 
43
336
  ```typescript
44
- import { unauthenticatedTailorUser } from "@tailor-platform/sdk";
337
+ import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test";
338
+ import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from "vitest";
45
339
  import resolver from "../src/resolver/incrementUserAge";
46
340
 
47
341
  describe("incrementUserAge resolver", () => {
@@ -58,25 +352,20 @@ describe("incrementUserAge resolver", () => {
58
352
  ),
59
353
  });
60
354
  });
355
+ afterAll(() => vi.unstubAllGlobals());
356
+ afterEach(() => mockQueryObject.mockReset());
61
357
 
62
- afterAll(() => {
63
- vi.unstubAllGlobals();
64
- });
65
-
66
- afterEach(() => {
67
- mockQueryObject.mockReset();
68
- });
69
-
70
- test("basic functionality", async () => {
71
- // Mock database responses for each query in sequence
72
- mockQueryObject.mockResolvedValueOnce({}); // Begin transaction
73
- mockQueryObject.mockResolvedValueOnce({ rows: [{ age: 30 }] }); // Select
74
- mockQueryObject.mockResolvedValueOnce({}); // Update
75
- mockQueryObject.mockResolvedValueOnce({}); // Commit
358
+ test("increments age inside a transaction", async () => {
359
+ // BEGIN → SELECT → UPDATE → COMMIT
360
+ mockQueryObject.mockResolvedValueOnce({});
361
+ mockQueryObject.mockResolvedValueOnce({ rows: [{ age: 30 }] });
362
+ mockQueryObject.mockResolvedValueOnce({});
363
+ mockQueryObject.mockResolvedValueOnce({});
76
364
 
77
365
  const result = await resolver.body({
78
366
  input: { email: "test@example.com" },
79
367
  user: unauthenticatedTailorUser,
368
+ env: {},
80
369
  });
81
370
 
82
371
  expect(result).toEqual({ oldAge: 30, newAge: 31 });
@@ -85,114 +374,79 @@ describe("incrementUserAge resolver", () => {
85
374
  });
86
375
  ```
87
376
 
88
- **Key points:**
377
+ **Use when:** the business logic runs a few fixed queries and you want to assert the exact call sequence.
89
378
 
90
- - Control exact database responses (query results, errors)
91
- - Verify database interaction flow (transactions, queries)
92
- - Test transaction rollback scenarios
93
- - **Best for:** Business logic with simple database operations
379
+ #### Extracting DB operations (dependency injection)
94
380
 
95
- ### Dependency Injection Pattern
96
-
97
- Extract database operations into a `DbOperations` interface, allowing business logic to be tested independently from Kysely implementation.
98
-
99
- First, structure your resolver to accept database operations:
381
+ Once the logic gets more involved, mocking raw SQL calls becomes brittle. Push database access behind a `DbOperations` interface and test the pure function that uses it:
100
382
 
101
383
  ```typescript
102
- import { createResolver, t } from "@tailor-platform/sdk";
103
- import { getDB } from "generated/tailordb";
384
+ // src/resolver/decrementUserAge.ts
385
+ import type { Selectable } from "@tailor-platform/sdk/kysely";
386
+ import type { Namespace } from "../generated/db";
104
387
 
105
388
  export interface DbOperations {
106
- transaction: (fn: (ops: DbOperations) => Promise<unknown>) => Promise<void>;
107
- getUser: (email: string, forUpdate: boolean) => Promise<{ email: string; age: number }>;
108
- updateUser: (user: { email: string; age: number }) => Promise<void>;
389
+ transaction: <T>(fn: (ops: DbOperations) => Promise<T>) => Promise<T>;
390
+ getUser: (email: string, forUpdate: boolean) => Promise<Selectable<Namespace["main-db"]["User"]>>;
391
+ updateUser: (user: Selectable<Namespace["main-db"]["User"]>) => Promise<void>;
109
392
  }
110
393
 
111
- export async function decrementUserAge(
112
- email: string,
113
- dbOperations: DbOperations,
114
- ): Promise<{ oldAge: number; newAge: number }> {
115
- let oldAge: number;
116
- let newAge: number;
117
-
118
- await dbOperations.transaction(async (ops) => {
394
+ export async function decrementUserAge(email: string, db: DbOperations) {
395
+ return await db.transaction(async (ops) => {
119
396
  const user = await ops.getUser(email, true);
120
- oldAge = user.age;
121
- newAge = user.age - 1;
397
+ const oldAge = user.age;
398
+ const newAge = user.age - 1;
122
399
  await ops.updateUser({ ...user, age: newAge });
400
+ return { oldAge, newAge };
123
401
  });
124
-
125
- return { oldAge, newAge };
126
402
  }
127
-
128
- export default createResolver({
129
- name: "decrementUserAge",
130
- operation: "mutation",
131
- input: { email: t.string() },
132
- body: async (context) => {
133
- const db = getDB("tailordb");
134
- const dbOperations = createDbOperations(db);
135
- return await decrementUserAge(context.input.email, dbOperations);
136
- },
137
- output: t.object({ oldAge: t.number(), newAge: t.number() }),
138
- });
139
403
  ```
140
404
 
141
- Then test by mocking the interface:
405
+ The `resolver` template wires this into `createResolver` by implementing a `createDbOperations` helper (backed by Kysely) and passing it to `decrementUserAge`. See `src/resolver/updateUser.ts` in the template for the full file.
142
406
 
143
407
  ```typescript
144
- import { DbOperations, decrementUserAge } from "../src/resolver/decrementUserAge";
145
-
146
- describe("decrementUserAge resolver", () => {
147
- test("basic functionality", async () => {
148
- // Mock DbOperations implementation
149
- const dbOperations = {
150
- transaction: vi.fn(
151
- async (fn: (ops: DbOperations) => Promise<unknown>) => await fn(dbOperations),
152
- ),
408
+ // src/resolver/decrementUserAge.test.ts
409
+ import { describe, expect, test, vi } from "vitest";
410
+ import { type DbOperations, decrementUserAge } from "./decrementUserAge";
411
+
412
+ describe("decrementUserAge", () => {
413
+ test("decrements age", async () => {
414
+ const db = {
415
+ transaction: vi.fn(async (fn: (ops: DbOperations) => Promise<unknown>) => await fn(db)),
153
416
  getUser: vi.fn().mockResolvedValue({ email: "test@example.com", age: 30 }),
154
417
  updateUser: vi.fn(),
155
418
  } as DbOperations;
156
419
 
157
- const result = await decrementUserAge("test@example.com", dbOperations);
420
+ const result = await decrementUserAge("test@example.com", db);
158
421
 
159
422
  expect(result).toEqual({ oldAge: 30, newAge: 29 });
160
- expect(dbOperations.getUser).toHaveBeenCalledExactlyOnceWith("test@example.com", true);
161
- expect(dbOperations.updateUser).toHaveBeenCalledExactlyOnceWith(
162
- expect.objectContaining({ age: 29 }),
163
- );
423
+ expect(db.getUser).toHaveBeenCalledExactlyOnceWith("test@example.com", true);
424
+ expect(db.updateUser).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ age: 29 }));
164
425
  });
165
426
  });
166
427
  ```
167
428
 
168
- **Key points:**
429
+ **Use when:** multi-step business logic. The tests survive query rewrites because they assert high-level intent, not SQL shape.
169
430
 
170
- - Test business logic independently from Kysely implementation details
171
- - Mock high-level operations instead of low-level SQL queries
172
- - **Best for:** Complex business logic with multiple database operations
431
+ #### Resolvers that resume a workflow
173
432
 
174
- ### Testing Resolvers that Call `.resolve()`
175
-
176
- Use `setupWaitPointMock` to mock `tailor.workflow.resolve` when testing resolvers that resume a suspended workflow execution.
433
+ Resolvers that call `waitPoint.resolve(...)` delegate to `tailor.workflow.resolve` at runtime. With the `tailor-runtime` environment active, use `workflowMock.setResolveHandler` to drive the user-supplied callback and inspect `workflowMock.resolveCalls`:
177
434
 
178
435
  ```typescript
179
- import { afterEach } from "vitest";
180
- import { setupWaitPointMock, unauthenticatedTailorUser } from "@tailor-platform/sdk/test";
181
- import resolver from "./resolvers/resolveApproval";
182
-
183
- const TailorGlobal = globalThis as { tailor?: { workflow?: Record<string, unknown> } };
436
+ import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test";
437
+ import { workflowMock } from "@tailor-platform/sdk/vitest";
438
+ import { beforeEach, describe, expect, test } from "vitest";
439
+ import resolver from "./resolveApproval";
184
440
 
185
441
  describe("resolveApproval resolver", () => {
186
- afterEach(() => {
187
- delete TailorGlobal.tailor;
442
+ beforeEach(() => {
443
+ workflowMock.reset();
188
444
  });
189
445
 
190
- test("resolves approval", async () => {
191
- const { resolveCalls } = setupWaitPointMock({
192
- onResolve: (_execId, _key, callback) => {
193
- const result = callback({ message: "Please approve", orderId: "order-1" });
194
- expect(result).toEqual({ approved: true });
195
- },
446
+ test("resolves approval with approved=true", async () => {
447
+ workflowMock.setResolveHandler((_executionId, _key, callback) => {
448
+ const result = callback({ message: "Please approve order order-1", orderId: "order-1" });
449
+ expect(result).toEqual({ approved: true });
196
450
  });
197
451
 
198
452
  const result = await resolver.body({
@@ -202,163 +456,195 @@ describe("resolveApproval resolver", () => {
202
456
  });
203
457
 
204
458
  expect(result).toEqual({ resolved: true });
205
- expect(resolveCalls).toHaveLength(1);
206
- expect(resolveCalls[0]).toEqual({ executionId: "exec-1", key: "approval" });
459
+ expect(workflowMock.resolveCalls).toEqual([{ executionId: "exec-1", key: "approval" }]);
207
460
  });
208
461
  });
209
462
  ```
210
463
 
211
- **Key points:**
464
+ `setResolveHandler` receives `(executionId, key, callback)` and decides whether to invoke the callback — that's how you assert the value returned to the suspended job.
212
465
 
213
- - `onResolve` lets you verify the callback behavior in resolvers that call `.resolve()`
214
- - Clean up mocks in `afterEach` by deleting `TailorGlobal.tailor`
215
- - **Best for:** Resolvers that resume suspended workflow executions
466
+ ### Testing Executors
216
467
 
217
- ### Workflow Job Unit Tests
468
+ Function-kind executors expose their handler as `executor.operation.body(args)`. The shape of `args` is determined by the trigger — for example, `recordCreatedTrigger({ type: user })` produces `{ newRecord }` typed against the type's output. GraphQL, webhook, and workflow operation kinds are declarative and don't expose a user-authored body to test.
218
469
 
219
- Test individual workflow job logic locally without deploying. Call `.body()` directly:
470
+ The `executor` template extracts shared DB access into a helper (`shared.ts`) and tests the helper directly against a mocked `tailordb.Client` (same TailorDB-mocking pattern as the resolver section). Executor handlers themselves stay thin and can be tested by spying on the helper:
220
471
 
221
472
  ```typescript
222
- import workflow, { addNumbers, calculate } from "./workflows/calculation";
473
+ import { describe, expect, test, vi } from "vitest";
474
+ import onUserCreated from "./onUserCreated";
475
+ import * as shared from "./shared";
476
+
477
+ describe("onUserCreated executor", () => {
478
+ test("creates an audit log with the new user's name and email", async () => {
479
+ const createAuditLog = vi.spyOn(shared, "createAuditLog").mockResolvedValue(undefined);
480
+
481
+ if (onUserCreated.operation.kind !== "function") {
482
+ throw new Error("expected function operation");
483
+ }
484
+ await onUserCreated.operation.body({
485
+ newRecord: {
486
+ id: "user-1",
487
+ name: "Alice",
488
+ email: "alice@example.com",
489
+ role: "ADMIN",
490
+ createdAt: "2025-01-01T00:00:00Z",
491
+ updatedAt: "2025-01-01T00:00:00Z",
492
+ },
493
+ });
223
494
 
224
- describe("workflow jobs", () => {
225
- test("addNumbers.body() adds two numbers", () => {
226
- const result = addNumbers.body({ a: 2, b: 3 }, { env: {} });
227
- expect(result).toBe(5);
495
+ expect(createAuditLog).toHaveBeenCalledExactlyOnceWith({
496
+ action: "USER_CREATED",
497
+ entityType: "User",
498
+ entityId: "user-1",
499
+ message: "Admin user created: Alice (alice@example.com)",
500
+ });
228
501
  });
229
502
  });
230
503
  ```
231
504
 
232
- ### Mocking Dependent Jobs
505
+ To exercise the full chain (executor → helper → TailorDB), drop the spy and stub the global `tailordb.Client` instead, exactly as shown for resolvers.
233
506
 
234
- For jobs that trigger other jobs, mock the dependencies using `vi.spyOn()`:
507
+ ### Testing Workflow Jobs
235
508
 
236
- ```typescript
237
- import { afterEach, vi } from "vitest";
238
- import workflow, { addNumbers, calculate, multiplyNumbers } from "./workflows/calculation";
509
+ Workflow jobs expose the same `.body()` entrypoint as resolvers, plus `.trigger()` for calling them from another job or a test.
239
510
 
240
- describe("workflow with dependencies", () => {
241
- afterEach(() => {
242
- vi.restoreAllMocks();
243
- });
511
+ #### Simple job
244
512
 
245
- test("calculate.body() with mocked dependent jobs", async () => {
246
- // Mock the trigger methods for dependent jobs
247
- vi.spyOn(addNumbers, "trigger").mockResolvedValue(5);
248
- vi.spyOn(multiplyNumbers, "trigger").mockResolvedValue(10);
513
+ Call `.body()` with the input and a stub `{ env: {} }`:
249
514
 
250
- const result = await calculate.body({ a: 2, b: 3 }, { env: {} });
515
+ ```typescript
516
+ import { describe, expect, test } from "vitest";
517
+ import { validateOrder } from "./order-fulfillment";
251
518
 
252
- expect(addNumbers.trigger).toHaveBeenCalledWith({ a: 2, b: 3 });
253
- expect(result).toBe(10);
519
+ describe("validateOrder", () => {
520
+ test("accepts a valid order", () => {
521
+ const result = validateOrder.body({ orderId: "order-1", amount: 100 }, { env: {} });
522
+ expect(result).toEqual({ valid: true, orderId: "order-1" });
523
+ });
524
+
525
+ test("rejects a non-positive amount", () => {
526
+ expect(() => validateOrder.body({ orderId: "order-1", amount: 0 }, { env: {} })).toThrow(
527
+ "Order amount must be positive",
528
+ );
254
529
  });
255
530
  });
256
531
  ```
257
532
 
258
- **Note:** To execute dependent jobs without mocking, and they require `env`, use `vi.stubEnv(WORKFLOW_TEST_ENV_KEY, ...)` and call `.trigger()` directly as shown in the integration test section below.
259
-
260
- ### Workflow Integration Tests with `.trigger()`
533
+ #### Jobs that trigger other jobs
261
534
 
262
- Test the full workflow execution locally using `workflow.mainJob.trigger()`:
535
+ Spy on each dependent job's `.trigger()` to replace it with a deterministic result:
263
536
 
264
537
  ```typescript
265
- import { WORKFLOW_TEST_ENV_KEY } from "@tailor-platform/sdk/test";
266
- import { afterEach, vi } from "vitest";
267
- import workflow from "./workflows/calculation";
268
-
269
- describe("workflow integration", () => {
270
- afterEach(() => {
271
- vi.unstubAllEnvs();
272
- });
273
-
274
- test("workflow.mainJob.trigger() executes all jobs", async () => {
275
- // Set environment variables for the workflow
276
- vi.stubEnv(WORKFLOW_TEST_ENV_KEY, JSON.stringify({ NODE_ENV: "test" }));
538
+ import { afterEach, describe, expect, test, vi } from "vitest";
539
+ import { fulfillOrder, processPayment, sendConfirmation, validateOrder } from "./order-fulfillment";
540
+
541
+ describe("fulfillOrder", () => {
542
+ afterEach(() => vi.restoreAllMocks());
543
+
544
+ test("chains validate → pay → confirm", async () => {
545
+ vi.spyOn(validateOrder, "trigger").mockResolvedValue({ valid: true, orderId: "order-1" });
546
+ vi.spyOn(processPayment, "trigger").mockResolvedValue({
547
+ transactionId: "txn-order-1",
548
+ amount: 100,
549
+ status: "completed",
550
+ });
551
+ vi.spyOn(sendConfirmation, "trigger").mockResolvedValue({
552
+ orderId: "order-1",
553
+ transactionId: "txn-order-1",
554
+ confirmed: true,
555
+ });
277
556
 
278
- // No mocking - all jobs execute their actual body functions
279
- const result = await workflow.mainJob.trigger({ a: 3, b: 4 });
557
+ const result = await fulfillOrder.body({ orderId: "order-1", amount: 100 }, { env: {} });
280
558
 
281
- expect(result).toBe(21); // (3 + 4) * 3 = 21
559
+ expect(validateOrder.trigger).toHaveBeenCalledWith({ orderId: "order-1", amount: 100 });
560
+ expect(result).toMatchObject({ confirmed: true, paymentStatus: "completed" });
282
561
  });
283
562
  });
284
563
  ```
285
564
 
286
- **Key points:**
565
+ **Use when:** you want to isolate the orchestrating job from its dependencies.
287
566
 
288
- - Use `.body()` for unit testing individual job logic
289
- - Use `vi.spyOn(job, "trigger").mockResolvedValue(...)` to mock dependent jobs when they don't need `env`
290
- - If dependent jobs require `env`, use `vi.stubEnv(WORKFLOW_TEST_ENV_KEY, ...)` and call `.trigger()` instead of mocking
291
- - Use `workflow.mainJob.trigger()` to execute the full workflow chain and get the result
292
- - **Best for:** Testing workflow orchestration and job dependencies
567
+ #### Jobs that wait on approval
293
568
 
294
- ### Testing Jobs with Wait Points
295
-
296
- Use `setupWaitPointMock` to mock `tailor.workflow.wait` when testing jobs that suspend on wait points:
569
+ `.wait()` calls delegate to `tailor.workflow.wait`. With the `tailor-runtime` environment active, use `workflowMock.setWaitHandler` to drive each branch and inspect `workflowMock.waitCalls`:
297
570
 
298
571
  ```typescript
299
- import { afterEach, vi } from "vitest";
300
- import { setupWaitPointMock } from "@tailor-platform/sdk/test";
301
- import { processWithApproval } from "./workflows/approval";
302
-
303
- const TailorGlobal = globalThis as { tailor?: { workflow?: Record<string, unknown> } };
572
+ import { workflowMock } from "@tailor-platform/sdk/vitest";
573
+ import { beforeEach, describe, expect, test } from "vitest";
574
+ import { processWithApproval } from "./approval";
304
575
 
305
- describe("approval workflow", () => {
306
- afterEach(() => {
307
- delete TailorGlobal.tailor;
576
+ describe("processWithApproval", () => {
577
+ beforeEach(() => {
578
+ workflowMock.reset();
308
579
  });
309
580
 
310
- test("approved flow returns approved status", async () => {
311
- const { waitCalls } = setupWaitPointMock({
312
- onWait: (_key, _payload) => ({ approved: true }),
313
- });
581
+ test("returns approved status when .wait() resolves positively", async () => {
582
+ workflowMock.setWaitHandler({ approved: true });
314
583
 
315
584
  const result = await processWithApproval.body({ orderId: "order-1" }, { env: {} });
316
585
 
317
586
  expect(result).toEqual({ orderId: "order-1", status: "approved" });
318
- expect(waitCalls).toHaveLength(1);
319
- expect(waitCalls[0]).toEqual({
587
+ expect(workflowMock.waitCalls[0]).toEqual({
320
588
  key: "approval",
321
589
  payload: { message: "Please approve order order-1", orderId: "order-1" },
322
590
  });
323
591
  });
324
592
 
325
- test("rejected flow returns rejected status", async () => {
326
- setupWaitPointMock({
327
- onWait: () => ({ approved: false }),
328
- });
593
+ test("returns rejected status when .wait() resolves negatively", async () => {
594
+ workflowMock.setWaitHandler({ approved: false });
329
595
 
330
596
  const result = await processWithApproval.body({ orderId: "order-2" }, { env: {} });
331
597
 
332
- expect(result).toEqual({ orderId: "order-2", status: "rejected" });
598
+ expect(result.status).toBe("rejected");
333
599
  });
334
600
  });
335
601
  ```
336
602
 
337
- **Key points:**
603
+ `setWaitHandler` accepts a static value (returned from every `.wait()` call) or a function `(key, payload) => unknown` to compute one per call. `waitCalls` captures the `key` and `payload` passed in.
604
+
605
+ #### Running a full workflow locally
606
+
607
+ To exercise the full chain without any mocking, call `workflow.mainJob.trigger()`. Dependent jobs run their real `.body()` functions. Set `WORKFLOW_TEST_ENV_KEY` first so triggered jobs see the workflow env:
608
+
609
+ ```typescript
610
+ import { WORKFLOW_TEST_ENV_KEY } from "@tailor-platform/sdk/test";
611
+ import { afterEach, describe, expect, test, vi } from "vitest";
612
+ import workflow from "./order-fulfillment";
613
+
614
+ describe("order-fulfillment workflow", () => {
615
+ afterEach(() => vi.unstubAllEnvs());
616
+
617
+ test("mainJob.trigger() executes all jobs", async () => {
618
+ vi.stubEnv(WORKFLOW_TEST_ENV_KEY, JSON.stringify({}));
619
+
620
+ const result = await workflow.mainJob.trigger({ orderId: "order-3", amount: 300 });
621
+
622
+ expect(result).toMatchObject({ confirmed: true, paymentStatus: "completed" });
623
+ });
624
+ });
625
+ ```
338
626
 
339
- - `onWait` controls what `.wait()` returns use it to test different branches (approved/rejected)
340
- - Clean up mocks in `afterEach` by deleting `TailorGlobal.tailor`
341
- - **Best for:** Jobs that suspend on wait points for human-in-the-loop approval
627
+ **Use when:** you want to verify orchestration end to end without the cost of a real deployment.
342
628
 
343
629
  ## End-to-End (E2E) Tests
344
630
 
345
- E2E tests verify your application works correctly when deployed to Tailor Platform. They test the full stack including GraphQL API, database operations, and authentication.
631
+ E2E tests run against a deployed Tailor Platform application. They exercise the full stack GraphQL, TailorDB, auth, workflows — end to end.
346
632
 
347
- ### Setting Up E2E Tests
633
+ The `workflow` template ships a complete `e2e/` directory (`globalSetup.ts`, `workflow.test.ts`, `resolver.test.ts`) that you can copy.
348
634
 
349
- The examples below use `graphql-request` as a lightweight GraphQL client.
635
+ ### Install a GraphQL client
350
636
 
351
637
  ```bash
352
638
  pnpm add -D graphql-request
353
639
  ```
354
640
 
355
- **1. Global Setup**
641
+ ### Global setup
356
642
 
357
- Create a global setup file that retrieves deployment information before running tests:
643
+ Resolve the deployed URL and a machine-user token, and expose them to tests via `inject`:
358
644
 
359
645
  ```typescript
360
646
  // e2e/globalSetup.ts
361
- import { machineUserToken, show } from "@tailor-platform/sdk/cli";
647
+ import { getMachineUserToken, show } from "@tailor-platform/sdk/cli";
362
648
  import type { TestProject } from "vitest/node";
363
649
 
364
650
  declare module "vitest" {
@@ -370,17 +656,13 @@ declare module "vitest" {
370
656
 
371
657
  export async function setup(project: TestProject) {
372
658
  const app = await show();
373
- const tokens = await machineUserToken({
374
- name: "admin",
375
- });
659
+ const tokens = await getMachineUserToken({ name: "admin" });
376
660
  project.provide("url", app.url);
377
661
  project.provide("token", tokens.accessToken);
378
662
  }
379
663
  ```
380
664
 
381
- **2. Test Files**
382
-
383
- Create tests that use injected credentials to send real queries to your deployed application:
665
+ ### Resolver E2E test
384
666
 
385
667
  ```typescript
386
668
  // e2e/resolver.test.ts
@@ -389,75 +671,80 @@ import { gql, GraphQLClient } from "graphql-request";
389
671
  import { describe, expect, inject, test } from "vitest";
390
672
 
391
673
  function createGraphQLClient() {
392
- const endpoint = new URL("/query", inject("url")).href;
393
- return new GraphQLClient(endpoint, {
394
- headers: {
395
- Authorization: `Bearer ${inject("token")}`,
396
- },
674
+ return new GraphQLClient(new URL("/query", inject("url")).href, {
675
+ headers: { Authorization: `Bearer ${inject("token")}` },
397
676
  errorPolicy: "all",
398
677
  });
399
678
  }
400
679
 
401
- describe("resolver", () => {
402
- const graphQLClient = createGraphQLClient();
403
-
404
- describe("incrementUserAge", () => {
405
- const uuid = randomUUID();
680
+ describe("incrementUserAge", () => {
681
+ const client = createGraphQLClient();
682
+ const email = `alice-${randomUUID()}@example.com`;
406
683
 
407
- test("prepare data", async () => {
408
- const query = gql`
409
- mutation {
410
- createUser(input: {
411
- name: "alice"
412
- email: "alice-${uuid}@example.com"
413
- age: 30
414
- }) {
684
+ test("prepares the user", async () => {
685
+ const res = await client.rawRequest(
686
+ gql`
687
+ mutation ($input: UserCreateInput!) {
688
+ createUser(input: $input) {
415
689
  id
416
690
  }
417
691
  }
418
- `;
419
- const result = await graphQLClient.rawRequest(query);
420
- expect(result.errors).toBeUndefined();
421
- });
692
+ `,
693
+ { input: { name: "alice", email, age: 30 } },
694
+ );
695
+ expect(res.errors).toBeUndefined();
696
+ });
422
697
 
423
- test("basic functionality", async () => {
424
- const query = gql`
425
- mutation {
426
- incrementUserAge(email: "alice-${uuid}@example.com") {
698
+ test("increments the user's age", async () => {
699
+ const res = await client.rawRequest(
700
+ gql`
701
+ mutation ($email: String!) {
702
+ incrementUserAge(email: $email) {
427
703
  oldAge
428
704
  newAge
429
705
  }
430
706
  }
431
- `;
432
- const result = await graphQLClient.rawRequest(query);
433
- expect(result.errors).toBeUndefined();
434
- expect(result.data).toEqual({
435
- incrementUserAge: { oldAge: 30, newAge: 31 },
436
- });
437
- });
707
+ `,
708
+ { email },
709
+ );
710
+ expect(res.errors).toBeUndefined();
711
+ expect(res.data).toEqual({ incrementUserAge: { oldAge: 30, newAge: 31 } });
438
712
  });
439
713
  });
440
714
  ```
441
715
 
442
- **3. Vitest Configuration**
716
+ ### Workflow E2E test
443
717
 
444
- Configure Vitest to use the global setup:
718
+ Use `startWorkflow` from the CLI helpers. It starts the workflow on the deployed platform and returns an `executionId` plus a `wait()` that blocks until the run completes:
445
719
 
446
720
  ```typescript
447
- import { defineConfig } from "vitest/config";
721
+ // e2e/workflow.test.ts
722
+ import { randomUUID } from "node:crypto";
723
+ import { startWorkflow } from "@tailor-platform/sdk/cli";
724
+ import { describe, expect, test } from "vitest";
725
+ import config from "../tailor.config";
726
+ import userProfileSync from "../src/workflow/sync-profile";
727
+
728
+ describe("user-profile-sync workflow", () => {
729
+ test("executes end to end", { timeout: 180_000 }, async () => {
730
+ const { executionId, wait } = await startWorkflow({
731
+ workflow: userProfileSync,
732
+ authInvoker: config.auth.invoker("admin"),
733
+ arg: {
734
+ name: "workflow-test",
735
+ email: `wf-${randomUUID()}@example.com`,
736
+ age: 25,
737
+ },
738
+ });
739
+ console.log(`execution id: ${executionId}`);
448
740
 
449
- export default defineConfig({
450
- test: {
451
- include: ["e2e/**/*.test.ts"],
452
- globalSetup: ["e2e/globalSetup.ts"],
453
- },
741
+ const result = await wait();
742
+ expect(result).toMatchObject({
743
+ workflowName: "user-profile-sync",
744
+ status: "SUCCESS",
745
+ });
746
+ });
454
747
  });
455
748
  ```
456
749
 
457
- **Key points:**
458
-
459
- - Tests run against actual deployed application
460
- - `inject("url")` and `inject("token")` provide deployment credentials automatically
461
- - Machine user authentication enables API access without manual token management
462
- - Verify database persistence and API contracts
463
- - **Best for:** Integration testing, end-to-end API validation
750
+ **Use when:** verifying actual deployments, auth flows, schema migrations, and anything that depends on runtime platform behavior you cannot mock.