workflow 4.1.0-beta.62 → 4.1.0-beta.63

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.
@@ -1,4 +1,4 @@
1
- export type { Event, StartOptions, WorkflowReadableStreamOptions, WorkflowRun, } from '@workflow/core/runtime';
1
+ export type { Event, StartOptions, StopSleepOptions, StopSleepResult, WorkflowReadableStreamOptions, WorkflowRun, } from '@workflow/core/runtime';
2
2
  export declare class Run {
3
3
  constructor();
4
4
  }
@@ -1 +1 @@
1
- {"version":3,"file":"api-workflow.d.ts","sourceRoot":"","sources":["../src/api-workflow.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,KAAK,EACL,YAAY,EACZ,6BAA6B,EAC7B,WAAW,GACZ,MAAM,wBAAwB,CAAC;AAQhC,qBAAa,GAAG;;CAIf;AACD,eAAO,MAAM,MAAM,aAA+B,CAAC;AACnD,eAAO,MAAM,cAAc,aAAuC,CAAC;AACnE,eAAO,MAAM,UAAU,aAAmC,CAAC;AAC3D,eAAO,MAAM,aAAa,aAAsC,CAAC;AACjE,eAAO,MAAM,OAAO,aAAgC,CAAC;AACrD,eAAO,MAAM,KAAK,aAA8B,CAAC"}
1
+ {"version":3,"file":"api-workflow.d.ts","sourceRoot":"","sources":["../src/api-workflow.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,KAAK,EACL,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,6BAA6B,EAC7B,WAAW,GACZ,MAAM,wBAAwB,CAAC;AAQhC,qBAAa,GAAG;;CAIf;AACD,eAAO,MAAM,MAAM,aAA+B,CAAC;AACnD,eAAO,MAAM,cAAc,aAAuC,CAAC;AACnE,eAAO,MAAM,UAAU,aAAmC,CAAC;AAC3D,eAAO,MAAM,aAAa,aAAsC,CAAC;AACjE,eAAO,MAAM,OAAO,aAAgC,CAAC;AACrD,eAAO,MAAM,KAAK,aAA8B,CAAC"}
@@ -12,4 +12,4 @@ export const resumeHook = () => workflowStub('resumeHook');
12
12
  export const resumeWebhook = () => workflowStub('resumeWebhook');
13
13
  export const runStep = () => workflowStub('runStep');
14
14
  export const start = () => workflowStub('start');
15
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLXdvcmtmbG93LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS13b3JrZmxvdy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFPQSxNQUFNLFlBQVksR0FBRyxDQUFDLElBQVksRUFBRSxFQUFFO0lBQ3BDLE1BQU0sSUFBSSxLQUFLLENBQ2IsZ0VBQWdFLElBQUksMkZBQTJGLENBQ2hLLENBQUM7QUFDSixDQUFDLENBQUM7QUFFRixNQUFNLE9BQU8sR0FBRztJQUNkO1FBQ0UsWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ3RCLENBQUM7Q0FDRjtBQUNELE1BQU0sQ0FBQyxNQUFNLE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsUUFBUSxDQUFDLENBQUM7QUFDbkQsTUFBTSxDQUFDLE1BQU0sY0FBYyxHQUFHLEdBQUcsRUFBRSxDQUFDLFlBQVksQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0FBQ25FLE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsWUFBWSxDQUFDLENBQUM7QUFDM0QsTUFBTSxDQUFDLE1BQU0sYUFBYSxHQUFHLEdBQUcsRUFBRSxDQUFDLFlBQVksQ0FBQyxlQUFlLENBQUMsQ0FBQztBQUNqRSxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsR0FBRyxFQUFFLENBQUMsWUFBWSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0FBQ3JELE1BQU0sQ0FBQyxNQUFNLEtBQUssR0FBRyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsT0FBTyxDQUFDLENBQUMifQ==
15
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLXdvcmtmbG93LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS13b3JrZmxvdy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFTQSxNQUFNLFlBQVksR0FBRyxDQUFDLElBQVksRUFBRSxFQUFFO0lBQ3BDLE1BQU0sSUFBSSxLQUFLLENBQ2IsZ0VBQWdFLElBQUksMkZBQTJGLENBQ2hLLENBQUM7QUFDSixDQUFDLENBQUM7QUFFRixNQUFNLE9BQU8sR0FBRztJQUNkO1FBQ0UsWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ3RCLENBQUM7Q0FDRjtBQUNELE1BQU0sQ0FBQyxNQUFNLE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsUUFBUSxDQUFDLENBQUM7QUFDbkQsTUFBTSxDQUFDLE1BQU0sY0FBYyxHQUFHLEdBQUcsRUFBRSxDQUFDLFlBQVksQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0FBQ25FLE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsWUFBWSxDQUFDLENBQUM7QUFDM0QsTUFBTSxDQUFDLE1BQU0sYUFBYSxHQUFHLEdBQUcsRUFBRSxDQUFDLFlBQVksQ0FBQyxlQUFlLENBQUMsQ0FBQztBQUNqRSxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsR0FBRyxFQUFFLENBQUMsWUFBWSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0FBQ3JELE1BQU0sQ0FBQyxNQUFNLEtBQUssR0FBRyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsT0FBTyxDQUFDLENBQUMifQ==
package/dist/api.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { type Event, getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, type StartOptions, start, type WorkflowReadableStreamOptions, type WorkflowRun, } from '@workflow/core/runtime';
1
+ export { type Event, getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, type StartOptions, start, type StopSleepOptions, type StopSleepResult, type WorkflowReadableStreamOptions, type WorkflowRun, } from '@workflow/core/runtime';
2
2
  //# sourceMappingURL=api.d.ts.map
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,KAAK,EACV,cAAc,EACd,MAAM,EACN,GAAG,EACH,UAAU,EACV,aAAa,EACb,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,EACL,KAAK,6BAA6B,EAClC,KAAK,WAAW,GACjB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,KAAK,EACV,cAAc,EACd,MAAM,EACN,GAAG,EACH,UAAU,EACV,aAAa,EACb,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,6BAA6B,EAClC,KAAK,WAAW,GACjB,MAAM,wBAAwB,CAAC"}
package/dist/api.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, start, } from '@workflow/core/runtime';
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBRUwsY0FBYyxFQUNkLE1BQU0sRUFDTixHQUFHLEVBQ0gsVUFBVSxFQUNWLGFBQWEsRUFDYixPQUFPLEVBRVAsS0FBSyxHQUdOLE1BQU0sd0JBQXdCLENBQUMifQ==
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBRUwsY0FBYyxFQUNkLE1BQU0sRUFDTixHQUFHLEVBQ0gsVUFBVSxFQUNWLGFBQWEsRUFDYixPQUFPLEVBRVAsS0FBSyxHQUtOLE1BQU0sd0JBQXdCLENBQUMifQ==
@@ -47,6 +47,22 @@ import type { WorkflowReadableStreamOptions } from "workflow/api";
47
47
  export default WorkflowReadableStreamOptions;`}
48
48
  />
49
49
 
50
+ #### StopSleepOptions
51
+
52
+ <TSDoc
53
+ definition={`
54
+ import type { StopSleepOptions } from "workflow/api";
55
+ export default StopSleepOptions;`}
56
+ />
57
+
58
+ #### StopSleepResult
59
+
60
+ <TSDoc
61
+ definition={`
62
+ import type { StopSleepResult } from "workflow/api";
63
+ export default StopSleepResult;`}
64
+ />
65
+
50
66
  ## Examples
51
67
 
52
68
  ### Basic Status Check
@@ -78,6 +94,33 @@ export async function GET(req: Request) {
78
94
  }
79
95
  ```
80
96
 
97
+ ### Wake Up a Sleeping Workflow
98
+
99
+ Interrupt pending `sleep()` calls to resume a workflow early. This is useful for testing workflows or building custom UIs that let users skip wait periods:
100
+
101
+ ```typescript lineNumbers
102
+ import { getRun } from "workflow/api";
103
+
104
+ export async function POST(req: Request) {
105
+ const { runId } = await req.json();
106
+ const run = getRun(runId);
107
+
108
+ // Wake up all pending sleep calls
109
+ const { stoppedCount } = await run.wakeUp(); // [!code highlight]
110
+
111
+ return Response.json({ stoppedCount });
112
+ }
113
+ ```
114
+
115
+ You can also target specific sleep calls by correlation ID:
116
+
117
+ ```typescript lineNumbers
118
+ const run = getRun("my-run-id"); // @setup
119
+ const { stoppedCount } = await run.wakeUp({
120
+ correlationIds: ["wait_abc123"],
121
+ });
122
+ ```
123
+
81
124
  ## Related Functions
82
125
 
83
126
  - [`start()`](/docs/api-reference/workflow-api/start) - Start a new workflow and get its run ID.
@@ -49,6 +49,8 @@ Simply deploy your application:
49
49
  vercel deploy
50
50
  ```
51
51
 
52
+ <FluidComputeCallout />
53
+
52
54
  <Callout>
53
55
  Learn more about the [Vercel World](/worlds/vercel) and its capabilities.
54
56
  </Callout>
@@ -22,6 +22,8 @@ Deploy your application to Vercel:
22
22
  vercel deploy
23
23
  ```
24
24
 
25
+ <FluidComputeCallout />
26
+
25
27
  That's it. Vercel automatically:
26
28
 
27
29
  - Selects the Vercel World backend
@@ -122,6 +122,8 @@ Keep in mind that calling a step function outside of a workflow function will no
122
122
 
123
123
  Workflow functions have the ability to automatically suspend while they wait on asynchronous work. While suspended, the workflow's state is stored via the [event log](/docs/how-it-works/event-sourcing) and no compute resources are used until the workflow resumes execution.
124
124
 
125
+ <FluidComputeCallout />
126
+
125
127
  There are multiple ways a workflow can suspend:
126
128
 
127
129
  - Waiting on a step function: the workflow yields while the step runs in the step runtime.
@@ -225,6 +225,8 @@ npx workflow inspect runs
225
225
 
226
226
  Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
227
227
 
228
+ <FluidComputeCallout />
229
+
228
230
  To deploy your Astro project to Vercel, ensure that the [Astro Vercel adapter](https://docs.astro.build/en/guides/integrations-guide/vercel) is configured:
229
231
 
230
232
  ```bash
@@ -258,6 +258,8 @@ npx workflow inspect runs
258
258
 
259
259
  Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
260
260
 
261
+ <FluidComputeCallout />
262
+
261
263
  Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
262
264
 
263
265
  ## Next Steps
@@ -245,6 +245,8 @@ npx workflow inspect runs # add '--web' for an interactive Web based UI
245
245
 
246
246
  Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
247
247
 
248
+ <FluidComputeCallout />
249
+
248
250
  Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
249
251
 
250
252
  ## Next Steps
@@ -240,6 +240,8 @@ npx workflow inspect runs
240
240
 
241
241
  Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
242
242
 
243
+ <FluidComputeCallout />
244
+
243
245
  Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
244
246
 
245
247
  ## Next Steps
@@ -327,6 +327,8 @@ WorkflowModule.forRoot({
327
327
 
328
328
  Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
329
329
 
330
+ <FluidComputeCallout />
331
+
330
332
  Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
331
333
 
332
334
  ## Next Steps
@@ -260,6 +260,8 @@ npx workflow inspect runs
260
260
 
261
261
  Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and need no special configuration.
262
262
 
263
+ <FluidComputeCallout />
264
+
263
265
  Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
264
266
 
265
267
  ## Troubleshooting
@@ -224,6 +224,8 @@ npx workflow inspect runs
224
224
 
225
225
  Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
226
226
 
227
+ <FluidComputeCallout />
228
+
227
229
  Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
228
230
 
229
231
  ## Next Steps
@@ -225,6 +225,8 @@ npx workflow inspect runs
225
225
 
226
226
  Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
227
227
 
228
+ <FluidComputeCallout />
229
+
228
230
  Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
229
231
 
230
232
  ## Next Steps
@@ -224,6 +224,8 @@ npx workflow inspect runs
224
224
 
225
225
  Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
226
226
 
227
+ <FluidComputeCallout />
228
+
227
229
  Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
228
230
 
229
231
  ## Next Steps
@@ -230,6 +230,8 @@ npx workflow inspect runs
230
230
 
231
231
  Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
232
232
 
233
+ <FluidComputeCallout />
234
+
233
235
  Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
234
236
 
235
237
  ## Next Steps
package/docs/meta.json CHANGED
@@ -7,6 +7,7 @@
7
7
  "how-it-works",
8
8
  "observability",
9
9
  "ai",
10
+ "testing",
10
11
  "deploying",
11
12
  "errors",
12
13
  "api-reference"
@@ -0,0 +1,368 @@
1
+ ---
2
+ title: Testing
3
+ description: Unit test individual steps and integration test entire workflows using Vitest and the Vite plugin.
4
+ type: conceptual
5
+ summary: Learn how to unit test steps and integration test workflows using Vitest.
6
+ prerequisites:
7
+ - /docs/foundations/workflows-and-steps
8
+ - /docs/getting-started/vite
9
+ related:
10
+ - /docs/foundations/hooks
11
+ - /docs/api-reference/workflow-api/start
12
+ - /docs/api-reference/workflow-api/resume-hook
13
+ - /docs/api-reference/workflow-api/get-run
14
+ - /docs/observability
15
+ ---
16
+
17
+ Testing is a critical part of building reliable workflows. Because steps are just functions annotated with directives, they can be unit tested like any other JavaScript function. Workflow DevKit also provides a [Vite plugin](/docs/getting-started/vite) that integrates with [Vitest](https://vitest.dev), enabling full integration tests against a real workflow runtime.
18
+
19
+ This guide covers two approaches:
20
+
21
+ 1. **Unit testing** - Test individual steps as plain functions, without the workflow runtime.
22
+ 2. **Integration testing** - Test entire workflows against a real workflow setup using the Vite plugin. Required for workflows that use [hooks](/docs/foundations/hooks), webhooks, [`sleep()`](/docs/api-reference/workflow/sleep), or retries.
23
+
24
+ ## Unit Testing Steps
25
+
26
+ Without the workflow compiler, the `"use step"` directive is a no-op. Your step functions run as regular JavaScript functions, making them straightforward to unit test with no special configuration.
27
+
28
+ ### Example Steps
29
+
30
+ Given a workflow file with step functions like this:
31
+
32
+ ```typescript title="workflows/user-signup.ts" lineNumbers
33
+ import { sleep } from "workflow";
34
+
35
+ export async function handleUserSignup(email: string) {
36
+ "use workflow";
37
+
38
+ const user = await createUser(email);
39
+ await sendWelcomeEmail(user);
40
+
41
+ await sleep("5d");
42
+ await sendOnboardingEmail(user);
43
+
44
+ return { userId: user.id, status: "onboarded" };
45
+ }
46
+
47
+ export async function createUser(email: string) {
48
+ "use step"; // [!code highlight]
49
+ return { id: crypto.randomUUID(), email };
50
+ }
51
+
52
+ export async function sendWelcomeEmail(user: { id: string; email: string }) {
53
+ "use step"; // [!code highlight]
54
+ // Send email logic
55
+ }
56
+
57
+ export async function sendOnboardingEmail(user: { id: string; email: string }) {
58
+ "use step"; // [!code highlight]
59
+ // Send email logic
60
+ }
61
+ ```
62
+
63
+ ### Writing Unit Tests for Steps
64
+
65
+ You can import and test step functions directly with Vitest. No special configuration or workflow plugin is needed:
66
+
67
+ ```typescript title="workflows/user-signup.test.ts" lineNumbers
68
+ import { describe, it, expect } from "vitest";
69
+ import { createUser, sendWelcomeEmail } from "./user-signup"; // [!code highlight]
70
+
71
+ describe("createUser step", () => {
72
+ it("should create a user with the given email", async () => {
73
+ const user = await createUser("test@example.com");
74
+
75
+ expect(user.email).toBe("test@example.com");
76
+ expect(user.id).toBeDefined();
77
+ });
78
+ });
79
+
80
+ describe("sendWelcomeEmail step", () => {
81
+ it("should send a welcome email without throwing", async () => {
82
+ const user = { id: "user-1", email: "test@example.com" };
83
+ await expect(sendWelcomeEmail(user)).resolves.not.toThrow();
84
+ });
85
+ });
86
+ ```
87
+
88
+ This approach is ideal for verifying the business logic inside individual steps in isolation.
89
+
90
+ <Callout type="info">
91
+ Unit testing works well for individual steps. A simple workflow that only calls steps can also be unit tested this way, since `"use workflow"` is similarly a no-op without the compiler. However, any workflow that uses runtime features like [`sleep()`](/docs/api-reference/workflow/sleep), [hooks](/docs/foundations/hooks), or [webhooks](/docs/foundations/hooks#understanding-webhooks) cannot be unit tested directly because those APIs require the workflow runtime. Use [integration testing](#integration-testing-with-the-vite-plugin) for testing entire workflows, especially those that depend on workflow-only features.
92
+ </Callout>
93
+
94
+ ## Integration Testing with the Vite Plugin
95
+
96
+ For workflows that rely on runtime features like [hooks](/docs/foundations/hooks), [webhooks](/docs/foundations/hooks#understanding-webhooks), [`sleep()`](/docs/api-reference/workflow/sleep), or error retries, you need to test against a real workflow setup. The `workflow/vite` plugin integrates directly with Vitest, compiling your `"use workflow"` and `"use step"` directives so the full workflow runtime is active during tests.
97
+
98
+ ### Vitest Configuration
99
+
100
+ Create a separate Vitest config for integration tests that includes the `workflow()` plugin and a `globalSetup` script:
101
+
102
+ ```typescript title="vitest.integration.config.ts" lineNumbers
103
+ import { defineConfig } from "vitest/config";
104
+ import { workflow } from "workflow/vite"; // [!code highlight]
105
+
106
+ export default defineConfig({
107
+ plugins: [workflow()], // [!code highlight]
108
+ test: {
109
+ include: ["**/*.integration.test.ts"],
110
+ testTimeout: 60_000, // Workflows may take longer than default timeout
111
+ globalSetup: "./vitest.integration.setup.ts", // [!code highlight]
112
+ },
113
+ });
114
+ ```
115
+
116
+ <Callout type="info">
117
+ Use a separate Vitest configuration and a distinct file naming convention (e.g. `*.integration.test.ts`) to keep unit tests and integration tests separate. Unit tests run with a standard Vitest config without the workflow plugin, while integration tests use the config above.
118
+ </Callout>
119
+
120
+ ### Global Setup Script
121
+
122
+ Integration tests need a running server to execute workflow steps. The `globalSetup` script starts a [Nitro](https://v3.nitro.build) server as a sidecar process before tests run, and tears it down afterwards:
123
+
124
+ ```typescript title="vitest.integration.setup.ts" lineNumbers
125
+ import { spawn } from "node:child_process";
126
+ import { setTimeout as delay } from "node:timers/promises";
127
+ import type { ChildProcess } from "node:child_process";
128
+
129
+ let server: ChildProcess | null = null;
130
+ const PORT = "4000";
131
+
132
+ export async function setup() { // [!code highlight]
133
+ console.log("Starting server for workflow execution...");
134
+
135
+ server = spawn("npx", ["nitro", "dev", "--port", PORT], {
136
+ stdio: "pipe",
137
+ detached: false,
138
+ env: process.env,
139
+ });
140
+
141
+ // Wait for the server to be ready
142
+ const ready = await new Promise<boolean>((resolve) => {
143
+ const timeout = setTimeout(() => resolve(false), 15_000);
144
+
145
+ server?.stdout?.on("data", (data) => {
146
+ const output = data.toString();
147
+ console.log("[server]", output);
148
+ if (output.includes("listening") || output.includes("ready")) {
149
+ clearTimeout(timeout);
150
+ resolve(true);
151
+ }
152
+ });
153
+
154
+ server?.stderr?.on("data", (data) => {
155
+ console.error("[server]", data.toString());
156
+ });
157
+
158
+ server?.on("error", (error) => {
159
+ console.error("Failed to start server:", error);
160
+ clearTimeout(timeout);
161
+ resolve(false);
162
+ });
163
+ });
164
+
165
+ if (!ready) {
166
+ throw new Error("Server failed to start within 15 seconds");
167
+ }
168
+
169
+ await delay(2_000); // Allow full initialization
170
+
171
+ // Point the workflow runtime at the local server
172
+ process.env.WORKFLOW_LOCAL_BASE_URL = `http://localhost:${PORT}`; // [!code highlight]
173
+ process.env.WORKFLOW_LOCAL_DATA_DIR = "./.workflow-data"; // [!code highlight]
174
+
175
+ console.log("Server ready for workflow execution");
176
+ }
177
+
178
+ export async function teardown() { // [!code highlight]
179
+ if (server) {
180
+ console.log("Stopping server...");
181
+ server.kill("SIGTERM");
182
+ await delay(1_000);
183
+ if (!server.killed) {
184
+ server.kill("SIGKILL");
185
+ }
186
+ }
187
+ }
188
+ ```
189
+
190
+ The setup script sets two environment variables that the workflow runtime reads:
191
+
192
+ - `WORKFLOW_LOCAL_BASE_URL` tells the runtime where to send step execution requests
193
+ - `WORKFLOW_LOCAL_DATA_DIR` tells the runtime where to persist workflow state locally
194
+
195
+ <Callout type="info">
196
+ You can use any server framework that supports the workflow runtime. The example above uses [Nitro](https://v3.nitro.build), but you could also use a [Next.js](https://nextjs.org), [Hono](https://hono.dev), or any other supported server.
197
+ </Callout>
198
+
199
+ ### Running Integration Tests
200
+
201
+ Add a script to your `package.json`:
202
+
203
+ ```json title="package.json"
204
+ {
205
+ "scripts": {
206
+ "test": "vitest",
207
+ "test:integration": "vitest --config vitest.integration.config.ts"
208
+ }
209
+ }
210
+ ```
211
+
212
+ ### Testing Hooks and Waits
213
+
214
+ The real power of integration testing comes when testing workflow-only features. Hooks and waits can be resumed programmatically using the [`workflow/api`](/docs/api-reference/workflow-api) functions, making it straightforward to simulate external events in your tests.
215
+
216
+ Given a workflow that waits for approval via a hook, then sleeps before publishing:
217
+
218
+ ```typescript title="workflows/approval.ts" lineNumbers
219
+ import { createHook, sleep } from "workflow";
220
+
221
+ export async function approvalWorkflow(documentId: string) {
222
+ "use workflow";
223
+
224
+ const prepared = await prepareDocument(documentId);
225
+
226
+ using hook = createHook<{ approved: boolean; reviewer: string }>({ // [!code highlight]
227
+ token: `approval:${documentId}`, // [!code highlight]
228
+ }); // [!code highlight]
229
+
230
+ const decision = await hook; // [!code highlight]
231
+
232
+ if (decision.approved) {
233
+ // Wait 24 hours before publishing (e.g. grace period for retractions)
234
+ await sleep("24h"); // [!code highlight]
235
+ await publishDocument(prepared);
236
+ return { status: "published", reviewer: decision.reviewer };
237
+ }
238
+
239
+ return { status: "rejected", reviewer: decision.reviewer };
240
+ }
241
+
242
+ async function prepareDocument(documentId: string) {
243
+ "use step";
244
+ return { id: documentId, content: "..." };
245
+ }
246
+
247
+ async function publishDocument(doc: { id: string; content: string }) {
248
+ "use step";
249
+ console.log(`Publishing document ${doc.id}`);
250
+ }
251
+ ```
252
+
253
+ You can write an integration test that starts the workflow, resumes the hook, and uses [`wakeUp()`](/docs/api-reference/workflow-api/get-run) to skip the sleep — so your tests don't have to wait for the full duration:
254
+
255
+ ```typescript title="workflows/approval.integration.test.ts" lineNumbers
256
+ import { describe, it, expect } from "vitest";
257
+ import { setTimeout as delay } from "node:timers/promises";
258
+ import { start, getRun, resumeHook } from "workflow/api"; // [!code highlight]
259
+ import { approvalWorkflow } from "./approval";
260
+
261
+ describe("approvalWorkflow", () => {
262
+ it("should publish when approved", async () => {
263
+ const run = await start(approvalWorkflow, ["doc-123"]); // [!code highlight]
264
+
265
+ // Resume the hook programmatically, simulating an external approval
266
+ await resumeHook("approval:doc-123", { // [!code highlight]
267
+ approved: true, // [!code highlight]
268
+ reviewer: "alice", // [!code highlight]
269
+ }); // [!code highlight]
270
+
271
+ // Wait for the workflow to replay and reach the sleep() call
272
+ await delay(5_000); // [!code highlight]
273
+
274
+ // Skip the 24-hour sleep so the test completes immediately
275
+ await getRun(run.runId).wakeUp(); // [!code highlight]
276
+
277
+ const result = await run.returnValue;
278
+ expect(result).toEqual({
279
+ status: "published",
280
+ reviewer: "alice",
281
+ });
282
+ });
283
+
284
+ it("should reject when not approved", async () => {
285
+ const run = await start(approvalWorkflow, ["doc-456"]);
286
+
287
+ await resumeHook("approval:doc-456", {
288
+ approved: false,
289
+ reviewer: "bob",
290
+ });
291
+
292
+ // No wakeUp() needed here — the rejected path has no sleep
293
+ const result = await run.returnValue;
294
+ expect(result).toEqual({
295
+ status: "rejected",
296
+ reviewer: "bob",
297
+ });
298
+ });
299
+ });
300
+ ```
301
+
302
+ <Callout type="info">
303
+ [`start()`](/docs/api-reference/workflow-api/start), [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook), and [`getRun().wakeUp()`](/docs/api-reference/workflow-api/get-run) are the key API functions for integration testing. Use `start()` to trigger a workflow, `resumeHook()` to simulate external events, and `wakeUp()` to skip `sleep()` calls so tests run instantly. See the [API Reference](/docs/api-reference/workflow-api) for the full list of available functions.
304
+ </Callout>
305
+
306
+ ## Debugging Test Runs
307
+
308
+ When integration tests fail, the [Workflow DevKit CLI and Web UI](/docs/observability) can help you inspect what happened. Because integration tests persist workflow state to `WORKFLOW_LOCAL_DATA_DIR`, you can use the same observability tools you would use in development.
309
+
310
+ Launch the Web UI to visually explore your test workflow runs:
311
+
312
+ ```bash
313
+ npx workflow web
314
+ ```
315
+
316
+ Or use the CLI to inspect runs in the terminal:
317
+
318
+ ```bash
319
+ # List recent workflow runs
320
+ npx workflow inspect runs
321
+
322
+ # Inspect a specific run
323
+ npx workflow inspect runs <run-id>
324
+ ```
325
+
326
+ The Web UI shows each step, its inputs and outputs, retry attempts, hook state, and timing. This is especially useful for diagnosing issues with hooks that were not resumed, steps that failed unexpectedly, or workflows that timed out.
327
+
328
+ ![Workflow DevKit Web UI](/o11y-ui.png)
329
+
330
+ <Callout type="info">
331
+ See the [Observability](/docs/observability) docs for the full set of CLI commands and Web UI features.
332
+ </Callout>
333
+
334
+ ## Best Practices
335
+
336
+ ### Separate Unit and Integration Tests
337
+
338
+ Keep two test configurations:
339
+
340
+ - **Unit tests** - Standard Vitest config, no workflow plugin. Fast, no infrastructure required.
341
+ - **Integration tests** - Vitest config with `workflow()` plugin. Tests the full workflow lifecycle including hooks, sleeps, and retries.
342
+
343
+ ### Use Custom Hook Tokens for Deterministic Testing
344
+
345
+ When testing workflows with hooks, use [custom tokens](/docs/foundations/hooks#custom-tokens-for-deterministic-hooks) based on predictable values (like document IDs or test identifiers). This makes it easy to resume the correct hook in your test code.
346
+
347
+ ### Set Appropriate Timeouts
348
+
349
+ Workflows may take longer to execute than typical unit tests, especially when they involve multiple steps or retries. Set a generous `testTimeout` in your integration test config.
350
+
351
+ ### Test Error and Retry Scenarios
352
+
353
+ Integration tests are the right place to verify that your workflows handle errors correctly, including retryable errors, fatal errors, and timeout scenarios.
354
+
355
+ ## Further Reading
356
+
357
+ - [Hooks & Webhooks](/docs/foundations/hooks) - Pausing and resuming workflows with external data
358
+ - [`start()` API Reference](/docs/api-reference/workflow-api/start) - Start workflows programmatically
359
+ - [`resumeHook()` API Reference](/docs/api-reference/workflow-api/resume-hook) - Resume hooks with data
360
+ - [`getRun()` API Reference](/docs/api-reference/workflow-api/get-run) - Check workflow run status and wake up sleeping runs
361
+ - [Vite Integration](/docs/getting-started/vite) - Set up the Vite plugin
362
+ - [Observability](/docs/observability) - Inspect and debug workflow runs with the CLI and Web UI
363
+
364
+ ---
365
+
366
+ <Callout type="info">
367
+ This guide was inspired by the testing approach described in Mux's article [*Launching durable AI workflows for video with @mux/ai*](https://www.mux.com/blog/launching-durable-ai-workflows-for-video-with-mux-ai#testing), which demonstrates how Mux uses the `workflow/vite` plugin with Vitest to integration test their durable AI video workflows built on Workflow DevKit.
368
+ </Callout>
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Testing",
3
+ "pages": ["testing"]
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow",
3
- "version": "4.1.0-beta.62",
3
+ "version": "4.1.0-beta.63",
4
4
  "description": "Workflow DevKit - Build durable, resilient, and observable workflows",
5
5
  "main": "dist/typescript-plugin.cjs",
6
6
  "type": "module",
@@ -52,17 +52,17 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "ms": "2.1.3",
55
- "@workflow/astro": "4.0.0-beta.36",
56
- "@workflow/cli": "4.1.0-beta.62",
57
- "@workflow/core": "4.1.0-beta.62",
55
+ "@workflow/astro": "4.0.0-beta.37",
56
+ "@workflow/cli": "4.1.0-beta.63",
57
+ "@workflow/core": "4.1.0-beta.63",
58
58
  "@workflow/errors": "4.1.0-beta.17",
59
59
  "@workflow/typescript-plugin": "4.0.1-beta.5",
60
- "@workflow/next": "4.0.1-beta.58",
61
- "@workflow/nest": "0.0.0-beta.11",
62
- "@workflow/nitro": "4.0.1-beta.57",
63
- "@workflow/nuxt": "4.0.1-beta.46",
64
- "@workflow/sveltekit": "4.0.0-beta.51",
65
- "@workflow/rollup": "4.0.0-beta.19"
60
+ "@workflow/next": "4.0.1-beta.59",
61
+ "@workflow/nest": "0.0.0-beta.12",
62
+ "@workflow/nitro": "4.0.1-beta.58",
63
+ "@workflow/nuxt": "4.0.1-beta.47",
64
+ "@workflow/sveltekit": "4.0.0-beta.52",
65
+ "@workflow/rollup": "4.0.0-beta.20"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/ms": "2.1.0",