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