diff --git a/packages/stack/src/services/Mail.integration.test.ts b/packages/stack/src/services/Mail.integration.test.ts index 84a0ed4bde..ec60227b6c 100644 --- a/packages/stack/src/services/Mail.integration.test.ts +++ b/packages/stack/src/services/Mail.integration.test.ts @@ -1,6 +1,6 @@ import { NodeHttpClient, NodeServices } from "@effect/platform-node"; import { describe, expect, it } from "@effect/vitest"; -import { Effect, Exit, FileSystem, Layer, Path } from "effect"; +import { Data, Effect, Exit, FileSystem, Layer, Path, Ref, Stream } from "effect"; import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; import { makeStandaloneService } from "../../tests/standalone-service.ts"; import { makeServiceRecipe } from "./Catalog.ts"; @@ -59,6 +59,28 @@ const sendTestEmail = ( yield* HttpClientResponse.filterStatusOk(response); }); +class MailStallError extends Data.TaggedError("MailStallError")<{ readonly message: string }> {} + +/** Fails a stalled launch within the test timeout, naming its current phase and Mailpit's output. */ +const guardLaunch = + (duration: `${number} seconds`, phase: Ref.Ref, logs: Ref.Ref) => + (effect: Effect.Effect) => + effect.pipe( + Effect.timeoutOrElse({ + duration, + orElse: () => + Effect.all([Ref.get(phase), Ref.get(logs)]).pipe( + Effect.flatMap(([stalled, output]) => + Effect.fail( + new MailStallError({ + message: `Mailpit ${stalled} did not finish within ${duration}; output:\n${output || "(none)"}`, + }), + ), + ), + ), + }), + ); + describe("service catalog", () => { it.live("launches the real Mailpit recipe and serves its HTTP endpoint", () => Effect.scoped( @@ -70,16 +92,28 @@ describe("service catalog", () => { { service: "mail", pin: selection.mail, config: {} }, dockerOptions(root), ); + const logs = yield* Ref.make(""); + yield* Stream.fromSubscription(yield* recipe.logs).pipe( + Stream.runForEach(({ bytes }) => + Ref.update(logs, (text) => text + new TextDecoder().decode(bytes)), + ), + Effect.forkScoped({ startImmediately: true }), + ); const instance = yield* makeStandaloneService(recipe.definition, { id: "mail", config: recipe.creation, }); - yield* instance.start; - yield* instance.ready; - const endpoint = yield* recipe.endpoint("http"); - const response = yield* client.execute( - HttpClientRequest.get(`http://${httpHost(endpoint)}:${endpoint.port}/readyz`), - ); + const phase = yield* Ref.make("start"); + const response = yield* Effect.gen(function* () { + yield* instance.start; + yield* Ref.set(phase, "readiness"); + yield* instance.ready; + yield* Ref.set(phase, "/readyz request"); + const endpoint = yield* recipe.endpoint("http"); + return yield* client.execute( + HttpClientRequest.get(`http://${httpHost(endpoint)}:${endpoint.port}/readyz`), + ); + }).pipe(guardLaunch("25 seconds", phase, logs)); expect(response.status).toBe(200); yield* instance.stop; }),