1import * as AWS from "@/AWS";
2import { ServerHost } from "@/Server/Process.ts";
3import * as Telemetry from "@/Telemetry.ts";
4import * as Config from "effect/Config";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as Ref from "effect/Ref";
8import * as Schedule from "effect/Schedule";
9import { HttpServerRequest } from "effect/http/HttpServerRequest";
10import * as HttpServerResponse from "effect/http/HttpServerResponse";
11
12/**
13 * End-to-end fixture for `AWS.ECS.Task`: a long-running server.
14 *
15 * - `yield* ServerHost` + `host.run(...)` registers a background loop that
16 * increments a counter once a second (this is the pattern from issue #706).
17 * - the returned `{ fetch }` handler is served over HTTP by the container's Bun
18 * HTTP server. `/ticks` reports the counter so the test can prove the
19 * background loop is actually running inside the deployed Fargate task.
20 */
21export default class TestTask extends AWS.ECS.Task<TestTask>()(
22 "EcsE2ETask",
23 {
24 main: import.meta.filename,
25 cpu: 256,
26 memory: 512,
27 port: 3000,
28 // Build/run on ARM64 so the image built on an Apple Silicon host matches
29 // the Fargate runtime architecture (Graviton).
30 runtimePlatform: {
31 cpuArchitecture: "ARM64",
32 operatingSystemFamily: "LINUX",
33 },
34 // Docker Hub's `oven/bun` image; the public.ecr.aws default mirror
35 // aggressively rate-limits anonymous pulls (429) during local builds.
36 image: "oven/bun:1",
37 },
38 Effect.gen(function* () {
39 const host = yield* ServerHost;
40 const ticks = yield* Ref.make(0);
41
42 // Long-running background loop (the `host.run` pattern from #706).
43 yield* host.run(
44 Ref.update(ticks, (n) => n + 1).pipe(
45 Effect.repeat(Schedule.spaced("1 second")),
46 Effect.asVoid,
47 ),
48 );
49
50 return {
51 fetch: Effect.gen(function* () {
52 const request = yield* HttpServerRequest;
53 const url = new URL(request.url, "http://task");
54 if (url.pathname === "/health") {
55 return yield* HttpServerResponse.json({ ok: true });
56 }
57 if (url.pathname === "/ticks") {
58 return yield* HttpServerResponse.json({
59 ticks: yield* Ref.get(ticks),
60 });
61 }
62 // Telemetry probe: each hit produces one root span, one child span,
63 // and one log record. The smoke test drives two rapid hits and
64 // asserts both land in a SINGLE batched traces payload — process
65 // runtimes batch on intervals; they must not flush per request.
66 if (url.pathname === "/work") {
67 const tag = url.searchParams.get("tag") ?? "none";
68 yield* Effect.log(`ecs-work-log:${tag}`).pipe(
69 Effect.withSpan(`work:${tag}`),
70 );
71 return yield* HttpServerResponse.json({
72 marker: `ecs-did-work:${tag}`,
73 });
74 }
75 return HttpServerResponse.text("hello from ecs task");
76 }),
77 };
78 }).pipe(
79 // Process-lifetime telemetry via the binding layer: the collector url
80 // resolves from the deployer's COLLECTOR_URL config at deploy time and
81 // binds onto the task; exporters build once into the process root scope.
82 Effect.provide(
83 Layer.unwrap(
84 Effect.gen(function* () {
85 const url = yield* Config.String("COLLECTOR_URL");
86 return Telemetry.layerOtlp({ url, serviceName: "otel-ecs-e2e" });
87 }),
88 ),
89 ),
90 ),
91) {}