Skip to content

Commit 2f52d14

Browse files
[vitest] [world-local] Fix local-world data recovery isolation (#1895)
1 parent cd50618 commit 2f52d14

19 files changed

Lines changed: 804 additions & 54 deletions

File tree

.changeset/moody-rivers-play.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
Tighten cleanup in `dev.test.ts` `should include steps discovered from workflow imports` so the deferred builder drops the discovered step from the manifest before the next test file runs. Avoids a Windows-only race where the generated step route retains an import to a deleted source file and breaks every subsequent step request.

.changeset/swift-cobras-repair.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@workflow/vitest": patch
3+
"@workflow/world-local": patch
4+
---
5+
6+
Fix local-world recovery isolation in Vitest and support custom test directories

docs/content/docs/api-reference/vitest/index.mdx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,30 @@ export default defineConfig({
2222
});
2323
```
2424

25+
Pass a [`WorkflowTestOptions`](#workflowtestoptions) object when your project uses a non-standard layout — for example, a monorepo where `workflows/` does not live at the Vitest config's directory, or when the default `.workflow-data` / `.workflow-vitest` output locations need to move. The plugin forwards these paths to `buildWorkflowTests()` and `setupWorkflowTests()` through Vitest's per-project provided context, so each Vitest workspace project stays isolated.
26+
27+
{/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
28+
29+
```typescript
30+
import { defineConfig } from "vitest/config";
31+
import { workflow } from "@workflow/vitest";
32+
33+
export default defineConfig({
34+
plugins: [
35+
workflow({
36+
cwd: "./apps/api",
37+
rootDir: "./apps/api/test-artifacts",
38+
}),
39+
],
40+
});
41+
```
42+
43+
**Parameters:**
44+
45+
| Parameter | Type | Description |
46+
| --- | --- | --- |
47+
| `options?` | `WorkflowTestOptions` | Optional configuration |
48+
2549
**Returns:** `Plugin[]`
2650

2751
## Setup Functions
@@ -83,7 +107,10 @@ Tears down the workflow test world. Clears the global world and closes the Local
83107

84108
| Option | Type | Default | Description |
85109
| --- | --- | --- | --- |
86-
| `cwd` | `string` | `process.cwd()` | The working directory of the project (where `workflows/` lives) |
110+
| `cwd` | `string` | `process.cwd()` | The working directory of the project (where `workflows/` lives). Relative paths resolve against `process.cwd()`. |
111+
| `rootDir` | `string` | same as `cwd` | Root directory used for default test artifacts. When set, `dataDir` and `outDir` default to `<rootDir>/.workflow-data` and `<rootDir>/.workflow-vitest`. Relative paths resolve against `cwd`. |
112+
| `dataDir` | `string` | `<rootDir>/.workflow-data` | Directory for workflow runtime data written by the test world. Relative paths resolve against `cwd`. |
113+
| `outDir` | `string` | `<rootDir>/.workflow-vitest` | Directory for generated workflow and step bundles. Relative paths resolve against `cwd`. |
87114

88115
## Test Helpers
89116

packages/core/e2e/dev.test.ts

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,20 @@ export function createDevTests(config?: DevTestConfig) {
150150
});
151151

152152
afterEach(async () => {
153+
// Restore file contents before deleting any files. If a deletion races
154+
// ahead of an api-file restore, the dev server briefly sees an import
155+
// pointing at a missing module and fails compilation. On Windows that
156+
// failure can stick — Turbopack leaves stale imports in the generated
157+
// step route bundle — and every subsequent step request returns 500.
158+
const toRestore = restoreFiles.filter((item) => item.content !== '');
159+
const toDelete = restoreFiles.filter((item) => item.content === '');
153160
await Promise.all(
154-
restoreFiles.map(async (item) => {
155-
if (item.content === '') {
156-
await fs.unlink(item.path);
157-
} else {
158-
await fs.writeFile(item.path, item.content);
159-
}
160-
})
161+
toRestore.map((item) => fs.writeFile(item.path, item.content))
161162
);
163+
if (toDelete.length > 0) {
164+
await prewarm();
165+
}
166+
await Promise.all(toDelete.map((item) => fs.unlink(item.path)));
162167
await prewarm();
163168
restoreFiles.length = 0;
164169
});
@@ -251,12 +256,27 @@ export async function ${marker}() {
251256
);
252257
restoreFiles.push({ path: importedStepFile, content });
253258

259+
const apiFile = path.join(appPath, finalConfig.apiFilePath);
260+
const apiFileContent = await fs.readFile(apiFile, 'utf8');
261+
254262
await pollUntil({
255263
description:
256264
'manifest.json to include imported step hot-reload marker',
257265
timeoutMs: 50_000,
258266
check: async () => {
259-
await triggerWorkflowRun('importedStepOnlyWorkflow');
267+
try {
268+
await triggerWorkflowRun('importedStepOnlyWorkflow');
269+
} catch (error) {
270+
// Turbopack on Windows occasionally caches a stale resolver
271+
// failure (e.g. `Could not parse module
272+
// '@workflow/core/dist/runtime/start.js'`) after an HMR
273+
// cascade and returns 500 to every request until something
274+
// invalidates its cache. Rewriting the api file is enough to
275+
// force a fresh resolve on the next request, so we treat the
276+
// 500 as transient and keep polling instead of bailing out.
277+
await fs.writeFile(apiFile, apiFileContent);
278+
throw error;
279+
}
260280
const manifestFunctionNames = await readManifestStepFunctionNames();
261281
expect(manifestFunctionNames).toContain(marker);
262282
},
@@ -311,7 +331,7 @@ ${apiFileContent}`
311331

312332
test.skipIf(!usesDeferredBuilder)(
313333
'should include steps discovered from workflow imports',
314-
{ timeout: 30_000 },
334+
{ timeout: 60_000 },
315335
async () => {
316336
const workflowFile = path.join(
317337
appPath,
@@ -369,6 +389,39 @@ ${apiFileContent}`
369389
);
370390
},
371391
});
392+
393+
// Tear down in-test (rather than relying on afterEach) so we can wait
394+
// for the deferred builder to drop the discovered step from the
395+
// manifest before the next test file runs. The generated step route
396+
// bundle holds a literal `import '../workflows/discovered-via-workflow-step.ts'`
397+
// that is only pruned when the deferred builder rebuilds and
398+
// `filterExistingFiles` excludes the now-missing source. On Windows
399+
// the rebuild can lag behind the deletion, leaving the bundle
400+
// unable to compile and breaking every step request in subsequent
401+
// tests (which all share the same dev server).
402+
await fs.writeFile(apiFile, apiFileContent);
403+
await fs.unlink(workflowFile);
404+
await fs.unlink(stepFile);
405+
for (const trackedPath of [apiFile, workflowFile, stepFile]) {
406+
const idx = restoreFiles.findIndex(
407+
(item) => item.path === trackedPath
408+
);
409+
if (idx !== -1) {
410+
restoreFiles.splice(idx, 1);
411+
}
412+
}
413+
await pollUntil({
414+
description:
415+
'manifest.json to drop discoveredViaWorkflowStep after cleanup',
416+
timeoutMs: 25_000,
417+
check: async () => {
418+
await fetchWithTimeout('/api/chat');
419+
const manifestFunctionNames = await readManifestStepFunctionNames();
420+
expect(manifestFunctionNames).not.toContain(
421+
'discoveredViaWorkflowStep'
422+
);
423+
},
424+
});
372425
}
373426
);
374427

packages/vitest/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"scripts": {
2727
"build": "tsc",
2828
"clean": "tsc --build --clean && rm -rf dist ||:",
29-
"dev": "tsc --watch"
29+
"dev": "tsc --watch",
30+
"test": "vitest run src"
3031
},
3132
"dependencies": {
3233
"@workflow/builders": "workspace:*",
@@ -38,7 +39,8 @@
3839
"devDependencies": {
3940
"@types/node": "catalog:",
4041
"@workflow/tsconfig": "workspace:*",
41-
"vite": "7.3.2"
42+
"vite": "7.3.2",
43+
"vitest": "catalog:"
4244
},
4345
"peerDependencies": {
4446
"vite": ">=6.0.0",
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
import type { TestProject } from 'vitest/node';
12
import { buildWorkflowTests } from './index.js';
3+
import {
4+
readProvidedWorkflowTestOptions,
5+
WORKFLOW_VITEST_OPTIONS_KEY,
6+
} from './options.js';
27

3-
export async function setup() {
4-
await buildWorkflowTests();
8+
export async function setup(project: TestProject) {
9+
await buildWorkflowTests(
10+
readProvidedWorkflowTestOptions(
11+
project.config.provide?.[WORKFLOW_VITEST_OPTIONS_KEY]
12+
)
13+
);
514
}

0 commit comments

Comments
 (0)