1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Test from "@/Test/Alchemy";
4import * as workers from "@distilled.cloud/cloudflare/workers";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import * as Schema from "effect/Schema";
10import * as HttpClient from "effect/http/HttpClient";
11import Stack from "./fixtures/cron/stack.ts";
12
13const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
14 providers: Cloudflare.providers(),
15 state: Cloudflare.state(),
16});
17
18const logLevel = Effect.provideService(
19 MinimumLogLevel,
20 process.env.DEBUG ? "Debug" : "Info",
21);
22
23const stack = beforeAll(deploy(Stack));
24afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
25
26test.provider(
27 "registers the event source's schedule with Cloudflare",
28 (scratch) =>
29 Effect.gen(function* () {
30 yield* scratch.destroy();
31 const { workerName, crons } = yield* stack;
32 const { accountId } = yield* yield* CloudflareEnvironment;
33 const { schedules } = yield* workers.getScriptSchedule({
34 accountId,
35 scriptName: workerName,
36 });
37 expect(crons).toEqual(["* * * * *"]);
38 expect(schedules.map(({ cron }) => cron)).toEqual(crons);
39 yield* scratch.destroy();
40 }).pipe(logLevel),
41 { tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"] },
42);
43
44// New schedules can take up to 15 minutes to propagate. Retain the fixture
45// with NO_DESTROY=1 before opting into wall-clock delivery. The local suite
46// covers native scheduled dispatch without waiting for cloud propagation.
47test.skipIf(
48 !!process.env.FAST || process.env.CLOUDFLARE_TEST_CRON_DELIVERY !== "1",
49)(
50 "deployed worker fires the scheduled handler on its cron trigger",
51 Effect.gen(function* () {
52 const { url, crons } = yield* stack;
53 expect(crons).toContain("* * * * *");
54
55 const client = yield* HttpClient.HttpClient;
56
57 // Reset also probes readiness of a fresh workers.dev URL.
58 yield* Effect.gen(function* () {
59 const res = yield* client.post(`${url}/reset`);
60 if (res.status !== 200) {
61 return yield* Effect.fail(new Error(`Worker not ready: ${res.status}`));
62 }
63 }).pipe(
64 Effect.retry({
65 schedule: Schedule.spaced("2 seconds"),
66 times: 8,
67 }),
68 );
69 const resetAt = yield* Effect.sync(() => Date.now());
70
71 const times = yield* Effect.gen(function* () {
72 const res = yield* client.get(`${url}/times`);
73 expect(res.status).toBe(200);
74 const body = yield* res.json.pipe(
75 Effect.flatMap(
76 Schema.decodeUnknownEffect(
77 Schema.Struct({ times: Schema.Array(Schema.Number) }),
78 ),
79 ),
80 );
81 return body.times.filter((time) => time >= resetAt);
82 }).pipe(
83 Effect.repeat({
84 schedule: Schedule.spaced("10 seconds"),
85 until: (recent) => recent.length > 0,
86 times: 9,
87 }),
88 );
89
90 expect(times.length).toBeGreaterThan(0);
91 for (const time of times) {
92 expect(time).toBeGreaterThanOrEqual(resetAt);
93 }
94 }).pipe(logLevel),
95 {
96 tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"],
97 timeout: 120_000,
98 },
99);