1import * as AWS from "@/AWS";
2import { Cluster } from "@/AWS/ECS/Cluster.ts";
3import { every } from "@/AWS/ECS/Schedule.ts";
4import { Task } from "@/AWS/ECS/Task.ts";
5import * as Test from "@/Test/Alchemy";
6import * as scheduler from "@distilled.cloud/aws/scheduler";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { getDefaultVpcNetwork } from "../DefaultVpc.ts";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// `AWS.ECS.every` synthesizes an EventBridge Scheduler schedule plus the
14// invoke role required to `ecs:RunTask` the target definition. Deploy a
15// cluster + external busybox task + schedule, verify the schedule wiring
16// out-of-band via the Scheduler API (expression, ECS target, network
17// config, role), then destroy and verify the schedule is gone.
18test.provider(
19 "every schedules an ECS RunTask target",
20 (stack) =>
21 Effect.gen(function* () {
22 yield* stack.destroy();
23
24 const { subnetIds } = yield* getDefaultVpcNetwork;
25 const subnets = subnetIds.slice(0, 2);
26
27 const { scheduleName, taskDefinitionArn, clusterArn } =
28 yield* stack.deploy(
29 Effect.gen(function* () {
30 const cluster = yield* Cluster("EveryCluster", {
31 clusterName: "alchemy-test-ecs-every",
32 });
33 const task = yield* Task("EveryTask", {
34 image: "busybox:stable",
35 command: ["sh", "-c", "echo alchemy-ecs-every"],
36 cpu: 256,
37 memory: 512,
38 taskName: "alchemy-test-ecs-every",
39 });
40 // The id doubles as the schedule's physical name.
41 const schedule = yield* every(
42 "alchemy-test-ecs-every-heartbeat",
43 "cron(0 3 * * ? *)",
44 { cluster, task, subnets },
45 );
46 return {
47 scheduleName: schedule.scheduleName,
48 taskDefinitionArn: task.taskDefinitionArn,
49 clusterArn: cluster.clusterArn,
50 };
51 }),
52 );
53
54 // Observe the schedule out-of-band: the cron expression passes
55 // through untouched, and the ECS target carries the task definition,
56 // the cluster as the target ARN, the awsvpc network config, and the
57 // synthesized invoke role.
58 const described = yield* scheduler.getSchedule({
59 Name: scheduleName,
60 });
61 expect(described.ScheduleExpression).toBe("cron(0 3 * * ? *)");
62 expect(described.Target?.Arn).toBe(clusterArn);
63 expect(described.Target?.EcsParameters?.TaskDefinitionArn).toBe(
64 taskDefinitionArn,
65 );
66 expect(described.Target?.EcsParameters?.LaunchType).toBe("FARGATE");
67 expect(
68 described.Target?.EcsParameters?.NetworkConfiguration
69 ?.awsvpcConfiguration?.Subnets,
70 ).toEqual(subnets);
71 expect(described.Target?.RoleArn).toContain(":role/");
72
73 yield* stack.destroy();
74
75 const gone = yield* scheduler
76 .getSchedule({ Name: scheduleName })
77 .pipe(
78 Effect.catchTag("ResourceNotFoundException", () =>
79 Effect.succeed(undefined),
80 ),
81 );
82 expect(gone).toBeUndefined();
83 }),
84 {
85 tags: [
86 "provider:aws",
87 "provider:aws:ec2",
88 "provider:aws:ecs",
89 "provider:aws:scheduler",
90 "live",
91 ],
92 timeout: 240_000,
93 },
94);