-
Notifications
You must be signed in to change notification settings - Fork 1k
[vitest-pool-workers] Workflows test support #10494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ff1f9a3
933c7d8
6943064
4de7468
5180325
9389ea2
72b316c
f592924
1c3b171
99f92a6
58dea1f
51ca66f
b0650f5
4f626c0
3565ce8
7e92776
83e6875
4e3167c
4f90105
d2f9c55
692ef3c
c727578
d7ededa
91505f6
6bc30cc
c4e4453
b42a6c3
c80393b
70e16ca
e46aa31
6e1f9b8
5d1670f
d3d6507
37c97a6
f229505
ea400d8
b37be40
801edc5
0f51b34
c96da01
040aca7
71aeb7a
606ef2a
5012fc4
1adf6bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "miniflare": patch | ||
| --- | ||
|
|
||
| Include workflow bining name in workflow plugin. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| "@cloudflare/vitest-pool-workers": minor | ||
| --- | ||
|
|
||
| Add Workflows test support to the `cloudflare:test` module. | ||
|
|
||
| The `cloudflare:test` module has two new APIs: | ||
|
|
||
| - `introspectWorkflowInstance` | ||
| - `introspectWorkflow` | ||
| which allow changing the behavior of one or multiple Workflow instances created during tests. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,9 @@ | ||
| # 🔁 workflows | ||
|
|
||
| Unit testing of workflows themselves is not possible yet - but you can still | ||
| trigger them in unit tests. | ||
| This Worker includes a ModeratorWorkflow that serves as a template for an automated content moderation process. | ||
| The testing suite uses workflow mocking to validate the logic of each step. | ||
|
|
||
| | Test | Overview | | ||
| | ----------------------------------------------- | ----------------------------------------------------------------------------------------- | | ||
| | [integration.test.ts](test/integration.test.ts) | Tests on the Worker's endpoints, ensuring that workflows are created and run correctly. | | ||
| | [unit.test.ts](test/unit.test.ts) | Tests on the internal logic of each workflow. It uses mocking to test steps in isolation. | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| interface Env { | ||
| TEST_WORKFLOW: Workflow; | ||
| declare namespace Cloudflare { | ||
| interface Env { | ||
| MODERATOR: Workflow; | ||
| } | ||
| } | ||
| interface Env extends Cloudflare.Env {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,89 @@ | ||
| import { | ||
| WorkerEntrypoint, | ||
| WorkflowEntrypoint, | ||
| WorkflowEvent, | ||
| WorkflowStep, | ||
| } from "cloudflare:workers"; | ||
|
|
||
| export class TestWorkflow extends WorkflowEntrypoint<Env> { | ||
| export class ModeratorWorkflow extends WorkflowEntrypoint<Env> { | ||
| async run(_event: Readonly<WorkflowEvent<unknown>>, step: WorkflowStep) { | ||
| console.log("ola"); | ||
| return "test-workflow"; | ||
| await step.sleep("sleep for a while", "10 seconds"); | ||
|
|
||
| // Get an initial analysis from an AI model | ||
| const aiResult = await step.do("AI content scan", async () => { | ||
| // Call to an workers-ai to scan the text content and return a violation score | ||
|
|
||
| // Simulated score: | ||
| const violationScore = Math.floor(Math.random() * 100); | ||
|
|
||
| return { violationScore: violationScore }; | ||
| }); | ||
|
|
||
| // Triage based on the AI score | ||
| if (aiResult.violationScore < 10) { | ||
| await step.do("auto approve content", async () => { | ||
| // API call to set app content status to "approved" | ||
| return { status: "auto_approved" }; | ||
| }); | ||
| return { status: "auto_approved" }; | ||
| } | ||
| if (aiResult.violationScore > 90) { | ||
| await step.do("auto reject content", async () => { | ||
| // API call to set app content status to "rejected" | ||
| return { status: "auto_rejected" }; | ||
| }); | ||
| return { status: "auto_rejected" }; | ||
| } | ||
|
|
||
| // If the score is ambiguous, require human review | ||
| type EventPayload = { | ||
| moderatorAction: string; | ||
| }; | ||
| const eventPayload = await step.waitForEvent<EventPayload>("human review", { | ||
| type: "moderation-decision", | ||
| timeout: "1 day", | ||
| }); | ||
|
|
||
| if (eventPayload) { | ||
| // The moderator responded in time. | ||
| const decision = eventPayload.payload.moderatorAction; // e.g., "approve" or "reject" | ||
| await step.do("apply moderator decision", async () => { | ||
| // API call to update content status based on the decision | ||
| return { status: "moderated", decision: decision }; | ||
| }); | ||
| return { status: "moderated", decision: decision }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export default class TestNamedEntrypoint extends WorkerEntrypoint<Env> { | ||
| async fetch(request: Request) { | ||
| const maybeId = new URL(request.url).searchParams.get("id"); | ||
| export default { | ||
| async fetch(request: Request, env: Env) { | ||
| const url = new URL(request.url); | ||
| const maybeId = url.searchParams.get("id"); | ||
| if (maybeId !== null) { | ||
| const instance = await this.env.TEST_WORKFLOW.get(maybeId); | ||
| const instance = await env.MODERATOR.get(maybeId); | ||
|
|
||
| return Response.json(await instance.status()); | ||
| } | ||
|
|
||
| const workflow = await this.env.TEST_WORKFLOW.create(); | ||
| if (url.pathname === "/moderate") { | ||
| const workflow = await env.MODERATOR.create(); | ||
| return Response.json({ | ||
| id: workflow.id, | ||
| details: await workflow.status(), | ||
| }); | ||
| } | ||
|
|
||
| return new Response(JSON.stringify({ id: workflow.id })); | ||
| } | ||
| } | ||
| if (url.pathname === "/moderate-batch") { | ||
| const workflows = await env.MODERATOR.createBatch([ | ||
| {}, | ||
| { id: "321" }, | ||
| {}, | ||
| ]); | ||
|
|
||
| const ids = workflows.map((workflow) => workflow.id); | ||
| return Response.json({ ids: ids }); | ||
| } | ||
|
|
||
| return new Response("Not found", { status: 404 }); | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,4 @@ | ||
| declare module "cloudflare:test" { | ||
| // Controls the type of `import("cloudflare:test").env` | ||
| interface ProvidedEnv extends Env { | ||
| TEST_WORKFLOW: Workflow; | ||
| } | ||
| interface ProvidedEnv extends Env {} | ||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { env, introspectWorkflow, SELF } from "cloudflare:test"; | ||
| import { expect, it } from "vitest"; | ||
|
|
||
| const STATUS_COMPLETE = "complete"; | ||
| const STEP_NAME = "AI content scan"; | ||
| const mockResult = { violationScore: 0 }; | ||
|
|
||
| // This example implicitly disposes the Workflow instance | ||
| it("workflow should be able to reach the end and be successful", async () => { | ||
| // CONFIG with `await using` to ensure Workflow instances cleanup: | ||
| await using introspector = await introspectWorkflow(env.MODERATOR); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. non-blocking: is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @edmundhung can we do this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decided on not worrying about this for now. Will be an improvement to be done later There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually prefer By reliable, I mean I've had tests hang/give weird errors when doing certain things in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL! Thank you for explaining |
||
| await introspector.modifyAll(async (m) => { | ||
| await m.disableSleeps(); | ||
| await m.mockStepResult({ name: STEP_NAME }, mockResult); | ||
| }); | ||
|
|
||
| await SELF.fetch(`https://mock-worker.local/moderate`); | ||
|
|
||
| const instances = introspector.get(); | ||
| expect(instances.length).toBe(1); | ||
|
|
||
| // ASSERTIONS: | ||
| const instance = instances[0]; | ||
| expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual( | ||
| mockResult | ||
| ); | ||
| await expect(instance.waitForStatus(STATUS_COMPLETE)).resolves.not.toThrow(); | ||
|
|
||
| // DISPOSE: ensured by `await using` | ||
| }); | ||
|
|
||
| // This example explicitly disposes the Workflow instances | ||
| it("workflow batch should be able to reach the end and be successful", async () => { | ||
| // CONFIG: | ||
| let introspector = await introspectWorkflow(env.MODERATOR); | ||
| try { | ||
| await introspector.modifyAll(async (m) => { | ||
| await m.disableSleeps(); | ||
| await m.mockStepResult({ name: STEP_NAME }, mockResult); | ||
| }); | ||
|
|
||
| await SELF.fetch(`https://mock-worker.local/moderate-batch`); | ||
|
|
||
| const instances = introspector.get(); | ||
| expect(instances.length).toBe(3); | ||
|
|
||
| // ASSERTIONS: | ||
| for (const instance of instances) { | ||
| expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual( | ||
| mockResult | ||
| ); | ||
| await expect( | ||
| instance.waitForStatus(STATUS_COMPLETE) | ||
| ).resolves.not.toThrow(); | ||
| } | ||
| } finally { | ||
| // DISPOSE: | ||
| // Workflow introspector should be disposed the end of each test, if no `await using` dyntax is used | ||
| // Also disposes all intercepted instances | ||
| await introspector.dispose(); | ||
| } | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.