From 760aa265f16a7c12bd3ecf4085a05ca97a5ef2d5 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Thu, 1 Oct 2026 16:54:26 +0200 Subject: [PATCH 1/2] test(stack): bound integration concurrency to the Docker engine Most stack integration files start real containers, so the Docker engine is the throughput bottleneck. Four workers on a typical engine allocation (or a CI runner shared with the CLI integration project) left per-test throughput unchanged while stretching each test several times toward its timeout. Two workers keep wall time roughly flat and restore the margin. Also take setup work out of timed test bodies: AuthStorage shares one owned database per file and runs Auth and Storage as independent cases instead of chaining four cold starts, and the native cleanup test prepares its PostgreSQL artifact in the shared cache before the test instead of downloading it into a fresh cache inside the 30 s budget. --- ...dRunner.native-cleanup.integration.test.ts | 26 +++-- .../services/AuthStorage.integration.test.ts | 99 ++++++++++++------- packages/stack/vitest.config.ts | 5 +- 3 files changed, 85 insertions(+), 45 deletions(-) diff --git a/packages/stack/src/host/CommandRunner.native-cleanup.integration.test.ts b/packages/stack/src/host/CommandRunner.native-cleanup.integration.test.ts index 240d72a977..3d134bb541 100644 --- a/packages/stack/src/host/CommandRunner.native-cleanup.integration.test.ts +++ b/packages/stack/src/host/CommandRunner.native-cleanup.integration.test.ts @@ -1,20 +1,32 @@ import { NodeHttpClient, NodeServices } from "@effect/platform-node"; -import { expect, it } from "@effect/vitest"; -import { Context, Effect, FileSystem, Layer, Path, Stream } from "effect"; +import { beforeAll, expect, it } from "@effect/vitest"; +import { Context, Effect, FileSystem, Layer, Stream } from "effect"; import { systemError } from "effect/PlatformError"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; +import { tmpdir } from "node:os"; +import { postgresVersion, prepareNativeArtifact } from "../Artifacts.ts"; import { postgres } from "../Commands.ts"; import * as CommandRunner from "./CommandRunner.ts"; +const cacheRoot = `${tmpdir()}/supabase-stack-artifacts`; +const layer = Layer.merge(NodeServices.layer, NodeHttpClient.layerNodeHttp); + +beforeAll( + () => + prepareNativeArtifact({ service: "database", version: postgresVersion("17") }, cacheRoot).pipe( + Effect.provide(layer), + Effect.runPromise, + ), + 120_000, +); + it.live.skipIf(process.platform === "win32")( "retries failed native workload cleanup when the stack runner is cleaned up", () => Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "native-runner-cleanup-" }); - const cacheRoot = path.join(root, "cache"); let isRunningCalls = 0; const delegate = yield* ChildProcessSpawner.ChildProcessSpawner; const spawner = ChildProcessSpawner.make((command) => @@ -52,13 +64,13 @@ it.live.skipIf(process.platform === "win32")( }); }), ); - const layer = CommandRunner.layer({ + const runnerLayer = CommandRunner.layer({ stackId: "native-cleanup-test", root, cacheRoot, runtime: "native", }).pipe(Layer.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner))); - const runner = Context.get(yield* Layer.build(layer), CommandRunner.Service); + const runner = Context.get(yield* Layer.build(runnerLayer), CommandRunner.Service); const result = yield* runner .run({ command: { @@ -80,6 +92,6 @@ it.live.skipIf(process.platform === "win32")( expect(isRunningCalls).toBe(3); yield* runner.cleanup; expect(isRunningCalls).toBe(4); - }).pipe(Effect.provide(Layer.merge(NodeServices.layer, NodeHttpClient.layerNodeHttp))), + }).pipe(Effect.provide(layer)), ), ); diff --git a/packages/stack/src/services/AuthStorage.integration.test.ts b/packages/stack/src/services/AuthStorage.integration.test.ts index 437e7975e8..2ae316c0d9 100644 --- a/packages/stack/src/services/AuthStorage.integration.test.ts +++ b/packages/stack/src/services/AuthStorage.integration.test.ts @@ -1,56 +1,73 @@ import { NodeHttpClient, NodeServices } from "@effect/platform-node"; -import { describe, expect, it } from "@effect/vitest"; -import { Effect, FileSystem, Layer, Redacted, Schema } from "effect"; +import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest"; +import { Effect, Exit, FileSystem, Layer, Redacted, Schema, Scope } from "effect"; import { HttpClient, HttpClientRequest } from "effect/unstable/http"; import { SignJWT } from "jose"; -import { makeService } from "../Service.ts"; +import { makeService, type ServiceInstance } from "../Service.ts"; import { makeServiceRecipe } from "./Catalog.ts"; import { makeDockerHttpRelay, makeDockerTcpRelay } from "../../tests/docker-relay.ts"; import { makeDockerDatabaseRoot } from "../../tests/docker-fixture.ts"; -const options = (root: string) => ({ +const dockerOptions = (root: string) => ({ stackId: "catalog-auth-storage", instanceId: "instance", root, cacheRoot: `${root}/cache`, - runtime: "native" as const, -}); - -const dockerOptions = (root: string) => ({ - ...options(root), runtime: "docker" as const, }); +const secret = "catalog-auth-storage-secret-with-at-least-32-chars"; + +let scope: Scope.Closeable; +let root: string; +let database: ServiceInstance; +let databaseUrl: string; + describe("service catalog", () => { + beforeAll(() => { + scope = Scope.makeUnsafe(); + return Effect.gen(function* () { + const databaseRoot = yield* makeDockerDatabaseRoot("catalog-auth-storage-"); + const databaseRecipe = yield* makeServiceRecipe( + { + service: "database", + config: { + version: "17", + databasePassword: Redacted.make("postgres"), + jwtSecret: Redacted.make(secret), + jwtExpiry: 3600, + }, + }, + dockerOptions(databaseRoot), + ); + const databaseService = yield* makeService(databaseRecipe.definition, { + id: "database", + config: databaseRecipe.creation, + }); + yield* databaseService.start; + yield* databaseService.ready; + const databaseRelay = yield* makeDockerTcpRelay(databaseRecipe.endpoint("sql")); + root = databaseRoot; + database = databaseService; + databaseUrl = `postgresql://supabase_admin:postgres@${databaseRelay.host}:${databaseRelay.port}/postgres`; + }).pipe( + Effect.provide(Layer.merge(NodeServices.layer, NodeHttpClient.layerNodeHttp)), + Scope.provide(scope), + Effect.runPromise, + ); + }, 120_000); + + afterAll( + () => database.stop.pipe(Effect.andThen(Scope.close(scope, Exit.void)), Effect.runPromise), + 60_000, + ); + it.live( - "migrates Auth and serves Storage against the owned database", + "migrates Auth and signs in a signed-up user against the owned database", () => Effect.scoped( Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; const client = yield* HttpClient.HttpClient; - const root = yield* makeDockerDatabaseRoot("catalog-auth-storage-"); - const secret = "catalog-auth-storage-secret-with-at-least-32-chars"; - const databaseRecipe = yield* makeServiceRecipe( - { - service: "database", - config: { - version: "17", - databasePassword: Redacted.make("postgres"), - jwtSecret: Redacted.make(secret), - jwtExpiry: 3600, - }, - }, - dockerOptions(root), - ); - const database = yield* makeService(databaseRecipe.definition, { - id: "database", - config: databaseRecipe.creation, - }); - yield* database.start; - yield* database.ready; - const databaseRelay = yield* makeDockerTcpRelay(databaseRecipe.endpoint("sql")); - const databaseUrl = `postgresql://supabase_admin:postgres@${databaseRelay.host}:${databaseRelay.port}/postgres`; const authRecipe = yield* makeServiceRecipe( { service: "auth", @@ -92,7 +109,19 @@ describe("service catalog", () => { Schema.fromJsonString(Schema.Struct({ access_token: Schema.String })), )(loginBody); expect(loginToken.access_token.length).toBeGreaterThan(0); + yield* auth.stop; + }), + ).pipe(Effect.provide(Layer.merge(NodeServices.layer, NodeHttpClient.layerNodeHttp))), + { timeout: 120_000 }, + ); + it.live( + "serves Storage objects and imgproxy transforms against the owned database", + () => + Effect.scoped( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const client = yield* HttpClient.HttpClient; const storageRoot = `${root}/storage`; yield* fs.makeDirectory(storageRoot, { recursive: true }); const imgproxyRecipe = yield* makeServiceRecipe( @@ -183,10 +212,8 @@ describe("service catalog", () => { ); expect(transformedImage.status).toBe(200); expect(transformedImage.headers["content-type"]).toContain("image/"); - yield* imgproxy.stop; yield* storage.stop; - yield* auth.stop; - yield* database.stop; + yield* imgproxy.stop; }), ).pipe(Effect.provide(Layer.merge(NodeServices.layer, NodeHttpClient.layerNodeHttp))), { timeout: 120_000 }, diff --git a/packages/stack/vitest.config.ts b/packages/stack/vitest.config.ts index 88e8c7d186..54b17a76c0 100644 --- a/packages/stack/vitest.config.ts +++ b/packages/stack/vitest.config.ts @@ -24,8 +24,9 @@ export default defineConfig({ include: ["**/*.integration.test.ts"], hookTimeout: 120_000, testTimeout: 30_000, - // Integration workers start real service processes and containers. - maxWorkers: 4, + // Most files start real containers, so the Docker engine bounds throughput; more + // workers only stretch each test's duration toward its timeout. + maxWorkers: 2, sequence: { groupOrder: 1 }, }, }, From c7e5204bfe15d2367a92ffcd130098fc01465c11 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Fri, 2 Oct 2026 13:41:35 +0200 Subject: [PATCH 2/2] test(stack): always close the shared AuthStorage fixture scope afterAll now closes the scope unconditionally via Effect.ensuring and only stops the database when beforeAll actually created it, so a setup failure before assignment or a stop failure no longer leaks the temp dir, Docker volume, container, or TCP relay registered on the scope. --- .../stack/src/services/AuthStorage.integration.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/stack/src/services/AuthStorage.integration.test.ts b/packages/stack/src/services/AuthStorage.integration.test.ts index 2ae316c0d9..2bd4feddcd 100644 --- a/packages/stack/src/services/AuthStorage.integration.test.ts +++ b/packages/stack/src/services/AuthStorage.integration.test.ts @@ -20,7 +20,7 @@ const secret = "catalog-auth-storage-secret-with-at-least-32-chars"; let scope: Scope.Closeable; let root: string; -let database: ServiceInstance; +let database: ServiceInstance | undefined; let databaseUrl: string; describe("service catalog", () => { @@ -58,7 +58,11 @@ describe("service catalog", () => { }, 120_000); afterAll( - () => database.stop.pipe(Effect.andThen(Scope.close(scope, Exit.void)), Effect.runPromise), + () => + Effect.suspend(() => (database === undefined ? Effect.void : database.stop)).pipe( + Effect.ensuring(Scope.close(scope, Exit.void)), + Effect.runPromise, + ), 60_000, );