Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changeset/many-fishes-raise.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ npx jscodeshift -t https://unpkg.com/@cloudflare/vitest-pool-workers/dist/codemo
- **`isolatedStorage` & `singleWorker`:** These have been removed in favour of a simpler isolation model that more closely matches Vitest. Storage isolation is now on a per test file basis, and you can make your test files share the same storage by using the Vitest flags `--max-workers=1 --no-isolate`
- **`import { env, SELF } from "cloudflare:test"`:** These have been removed in favour of `import { env, exports } from "cloudflare:workers"`. `exports.default.fetch()` has the same behaviour as `SELF.fetch()`, except that it doesn't expose Assets. To test your assets, write an integration test using [`startDevWorker()`](https://developers.cloudflare.com/workers/testing/unstable_startworker/)
- **`import { fetchMock } from "cloudflare:test"`:** This has been removed. Instead, [mock `globalThis.fetch`](https://github.com/cloudflare/workers-sdk/blob/main/fixtures/vitest-pool-workers-examples/request-mocking/test/imperative.test.ts) or use ecosystem libraries like [MSW (recommended)](https://mswjs.io/).
- **Vitest peer dependency:** `@cloudflare/vitest-pool-workers` now requires `vitest@^4.1.0`.
7 changes: 7 additions & 0 deletions .changeset/real-plants-slide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": patch
---

Fix autoconfig for Astro v6 projects to skip wrangler config generation

Astro 6+ generates its own wrangler configuration on build, so autoconfig now detects the Astro version and skips creating a `wrangler.jsonc` file for projects using Astro 6 or later. This prevents conflicts between the autoconfig-generated config and Astro's built-in config generation.
7 changes: 7 additions & 0 deletions .changeset/two-ants-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@cloudflare/vite-plugin": patch
---

Fix Sandbox SDK preview URL WebSocket routing

When using Sandbox SDK preview URLs, WebSocket requests using the `vite-hmr` protocol could be dropped before they reached the worker, causing HMR to fail. The plugin now forwards Sandbox WebSocket traffic and preserves the original request origin/host so worker proxy logic receives the correct URL.
17 changes: 17 additions & 0 deletions .changeset/workflows-instance-methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@cloudflare/workflows-shared": minor
"miniflare": minor
---

Workflow instances now support pause, resume, restart, and terminate in local dev.

```js
const instance = await env.MY_WORKFLOW.create({
id: "my-instance",
});

await instance.pause(); // pauses after the current step completes
await instance.resume(); // resumes from where it left off
await instance.restart(); // restarts the workflow from the beginning
await instance.terminate(); // terminates the workflow immediately
```
5 changes: 5 additions & 0 deletions .changeset/workflows-vitest-pool-waitforstatus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/vitest-pool-workers": patch
---

Workflows testing util `waitForStatus` now supports waiting for "terminated" and "paused" states.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { env, introspectWorkflow, SELF } from "cloudflare:test";
import { it } from "vitest";
import { describe, it } from "vitest";

const STATUS_COMPLETE = "complete";
const STEP_NAME = "AI content scan";
Expand Down Expand Up @@ -70,3 +70,96 @@ it("workflow batch should be able to reach the end and be successful", async ({
await introspector.dispose();
}
});

describe("workflow instance lifecycle methods", () => {
it("should terminate a workflow instance", async ({ expect }) => {
// CONFIG:
await using introspector = await introspectWorkflow(env.MODERATOR);
await introspector.modifyAll(async (m) => {
await m.disableSleeps();
await m.mockStepResult({ name: STEP_NAME }, { violationScore: 50 });
});

const res = await SELF.fetch("https://mock-worker.local/moderate");
const data = (await res.json()) as { id: string; details: unknown };

const instances = introspector.get();
expect(instances.length).toBe(1);
const instance = instances[0];

expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual({
violationScore: 50,
});

const handle = await env.MODERATOR.get(data.id);
await handle.terminate();

// ASSERTIONS:
await expect(instance.waitForStatus("terminated")).resolves.not.toThrow();

// DISPOSE: ensured by `await using`
});

it("should restart a workflow instance", async ({ expect }) => {
// CONFIG:
await using introspector = await introspectWorkflow(env.MODERATOR);
await introspector.modifyAll(async (m) => {
await m.disableSleeps();
await m.mockStepResult({ name: STEP_NAME }, { violationScore: 50 });
await m.mockEvent({
type: "moderation-decision",
payload: { moderatorAction: "approve" },
});
});

const res = await SELF.fetch("https://mock-worker.local/moderate");
const data = (await res.json()) as { id: string; details: unknown };

const instances = introspector.get();
expect(instances.length).toBe(1);
const instance = instances[0];

expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual({
violationScore: 50,
});

const handle = await env.MODERATOR.get(data.id);
await handle.restart();

// Mocks survive instace restart, so the restarted workflow re-runs
// with the same config
await expect(
instance.waitForStatus(STATUS_COMPLETE)
).resolves.not.toThrow();

// DISPOSE: ensured by `await using`
});

it("should pause a workflow instance", async ({ expect }) => {
// CONFIG:
await using introspector = await introspectWorkflow(env.MODERATOR);
await introspector.modifyAll(async (m) => {
await m.disableSleeps();
await m.mockStepResult({ name: STEP_NAME }, { violationScore: 50 });
});

const res = await SELF.fetch("https://mock-worker.local/moderate");
const data = (await res.json()) as { id: string; details: unknown };

const instances = introspector.get();
expect(instances.length).toBe(1);
const instance = instances[0];

expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual({
violationScore: 50,
});

const handle = await env.MODERATOR.get(data.id);
await handle.pause();

// ASSERTIONS:
await expect(instance.waitForStatus("paused")).resolves.not.toThrow();

// DISPOSE: ensured by `await using`
});
});
53 changes: 51 additions & 2 deletions fixtures/workflow-multiple/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,32 @@ export class Demo2 extends WorkflowEntrypoint<{}, Params> {
}
}

export class Demo3 extends WorkflowEntrypoint<{}, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const result = await step.do("First step", async function () {
return {
output: "First step result",
};
});

await step.waitForEvent("wait for signal", {
type: "continue",
});

const result2 = await step.do("Second step", async function () {
return {
output: "workflow3",
};
});

return "i'm workflow3";
}
}

type Env = {
WORKFLOW: Workflow;
WORKFLOW2: Workflow;
WORKFLOW3: Workflow;
};

export default class extends WorkerEntrypoint<Env> {
Expand All @@ -71,8 +94,15 @@ export default class extends WorkerEntrypoint<Env> {
if (url.pathname === "/favicon.ico") {
return new Response(null, { status: 404 });
}
let workflowToUse =
workflowName == "2" ? this.env.WORKFLOW2 : this.env.WORKFLOW;

let workflowToUse: Workflow;
if (workflowName === "3") {
workflowToUse = this.env.WORKFLOW3;
} else if (workflowName === "2") {
workflowToUse = this.env.WORKFLOW2;
} else {
workflowToUse = this.env.WORKFLOW;
}

let handle: WorkflowInstance;
if (url.pathname === "/create") {
Expand All @@ -81,6 +111,25 @@ export default class extends WorkerEntrypoint<Env> {
} else {
handle = await workflowToUse.create({ id });
}
} else if (url.pathname === "/pause") {
handle = await workflowToUse.get(id);
await handle.pause();
} else if (url.pathname === "/resume") {
handle = await workflowToUse.get(id);
await handle.resume();
} else if (url.pathname === "/restart") {
handle = await workflowToUse.get(id);
await handle.restart();
} else if (url.pathname === "/terminate") {
handle = await workflowToUse.get(id);
await handle.terminate();
} else if (url.pathname === "/sendEvent") {
handle = await workflowToUse.get(id);
await handle.sendEvent({
type: "continue",
payload: await req.json(),
});
return Response.json({ ok: true });
} else {
handle = await workflowToUse.get(id);
}
Expand Down
Loading
Loading