From 14650493890e998e6561ec3f535b3140d95420f4 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:52:31 +0530 Subject: [PATCH 1/2] test(cli): cover the remaining test files with effect lint --- .oxlintrc.effect.json | 8 - .../config-validate.parity.unit.test.ts | 31 +- ...iner-inputs.precedence.integration.test.ts | 5 +- .../local-db-running.integration.test.ts | 408 +++++++++--------- ...b-config.toml-read.precedence.unit.test.ts | 5 +- .../hosted-consent-coverage.unit.test.ts | 26 +- ...tack-config.precedence.integration.test.ts | 5 +- ...tart.config-precedence.integration.test.ts | 5 +- .../tests/helpers/compute-target-selection.ts | 2 +- apps/cli/tests/helpers/config-goldens.ts | 26 +- .../config-values-layer.integration.test.ts | 9 +- apps/cli/tests/helpers/db-target-selection.ts | 78 ++-- apps/cli/tests/helpers/hosted-consent.ts | 4 +- .../tests/helpers/hosted-target-selection.ts | 12 +- 14 files changed, 316 insertions(+), 308 deletions(-) diff --git a/.oxlintrc.effect.json b/.oxlintrc.effect.json index 81a7e16ba2..d3209edd72 100644 --- a/.oxlintrc.effect.json +++ b/.oxlintrc.effect.json @@ -19,15 +19,7 @@ "apps/cli/src/shared/cli/bin.ts", "apps/cli/src/shared/compute/stacks/**", "apps/cli/src/shared/feedback/database.types.ts", - "apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts", "apps/cli/src/command-internal/db-bootstrap/local-db-running.ts", - "apps/cli/src/command-internal/config-validate.parity.unit.test.ts", - "apps/cli/src/command-internal/hosted-consent/hosted-consent-coverage.unit.test.ts", - "apps/cli/tests/helpers/config-goldens.ts", - "apps/cli/tests/helpers/config-values-layer.integration.test.ts", - "apps/cli/tests/helpers/db-target-selection.ts", - "apps/cli/tests/helpers/hosted-consent.ts", - "apps/cli/tests/helpers/hosted-target-selection.ts", "packages/api/src/generated/contracts.ts" ] } diff --git a/apps/cli/src/command-internal/config-validate.parity.unit.test.ts b/apps/cli/src/command-internal/config-validate.parity.unit.test.ts index ec202899df..fd2c7a9846 100644 --- a/apps/cli/src/command-internal/config-validate.parity.unit.test.ts +++ b/apps/cli/src/command-internal/config-validate.parity.unit.test.ts @@ -1,7 +1,4 @@ import { unusedStackServices } from "../../tests/helpers/unused-stack.ts"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; import { CliConfigSchema, type CliConfig } from "@supabase/config"; @@ -32,12 +29,20 @@ const stringifyJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); * the `baseConfig`/`WORKDIR` pattern from `local-config-values.unit.test.ts`. */ -function withConfig(content: string) { - const dir = mkdtempSync(join(tmpdir(), "config-validate-parity-")); - mkdirSync(join(dir, "supabase"), { recursive: true }); - writeFileSync(join(dir, "supabase", "config.toml"), content); - return dir; -} +const withConfig = (content: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectory({ prefix: "config-validate-parity-" }); + yield* fs.makeDirectory(path.join(dir, "supabase"), { recursive: true }); + yield* fs.writeFileString(path.join(dir, "supabase", "config.toml"), content); + return dir; + }).pipe(Effect.provide(BunServices.layer)); + +const removeConfig = (dir: string) => + FileSystem.FileSystem.use((fs) => fs.remove(dir, { recursive: true, force: true })).pipe( + Effect.provide(BunServices.layer), + ); const readD = (workdir: string) => Effect.gen(function* () { @@ -49,13 +54,13 @@ const readD = (workdir: string) => /** Drives D's real pipeline and asserts the failure message contains `message`. */ function failsWithD(tomlLines: ReadonlyArray, message: string) { return Effect.gen(function* () { - const dir = withConfig(tomlLines.join("\n")); + const dir = yield* withConfig(tomlLines.join("\n")); const exit = yield* readD(dir).pipe(Effect.exit); expect(Exit.isFailure(exit), `D: expected failure containing: ${message}`).toBe(true); if (Exit.isFailure(exit)) { expect(stringifyJson(exit.cause)).toContain(message); } - rmSync(dir, { recursive: true, force: true }); + yield* removeConfig(dir); }); } @@ -338,7 +343,7 @@ describe("shared api + auth validation branches, cross-caller parity (S vs L)", /** Drives S's real pipeline (`resolveStorageCredentials`, local branch) to failure. */ const failsWithS = (overrides: Record, message: string) => Effect.gen(function* () { - const dir = withConfig(stringifyToml({ project_id: "test", ...overrides })); + const dir = yield* withConfig(stringifyToml({ project_id: "test", ...overrides })); const exit = yield* resolveStorageCredentials({ projectRef: "" }).pipe( Effect.provide( Layer.mergeAll( @@ -360,7 +365,7 @@ describe("shared api + auth validation branches, cross-caller parity (S vs L)", if (Exit.isFailure(exit)) { expect(stringifyJson(exit.cause)).toContain(message); } - rmSync(dir, { recursive: true, force: true }); + yield* removeConfig(dir); }); // Ambient SUPABASE_API_* / SUPABASE_AUTH_* values would override the config diff --git a/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.precedence.integration.test.ts b/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.precedence.integration.test.ts index dd4c7fbc50..3cb27f822b 100644 --- a/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.precedence.integration.test.ts +++ b/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.precedence.integration.test.ts @@ -192,10 +192,9 @@ describe("local container inputs precedence goldens", () => { projectEnvValues: configEnvOnly(setup.projectEnvValues ?? {}), }, }; + const goldenText = yield* goldenJson(golden, { [workdir]: "" }); yield* Effect.promise(() => - ctx - .expect(goldenJson(golden, { [workdir]: "" })) - .toMatchFileSnapshot(`${GOLDEN_DIR}/${fixture.golden}.json`), + ctx.expect(goldenText).toMatchFileSnapshot(`${GOLDEN_DIR}/${fixture.golden}.json`), ); }).pipe(Effect.provide(BunServices.layer)), ); diff --git a/apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts b/apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts index cd67069fe1..c04f53fada 100644 --- a/apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts +++ b/apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts @@ -1,11 +1,7 @@ -import { mkdtempSync, rmSync } from "node:fs"; -import http from "node:http"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { BunServices } from "@effect/platform-bun"; +import { BunHttpServer, BunServices, BunSocketServer } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { ConfigProvider, Effect, FileSystem, Layer, Option, Path } from "effect"; +import { ConfigProvider, Effect, FileSystem, Layer, Option, Path, Schedule, Stream } from "effect"; +import { HttpServerRequest, HttpServerResponse } from "effect/http"; import { ChildProcessSpawner } from "effect/process"; import { configValuesLayer } from "../../../tests/helpers/config-values-layer.ts"; @@ -44,68 +40,85 @@ function withDockerHost( endpoint: string, effect: Effect.Effect, ): Effect.Effect { - return Effect.suspend(() => { - const previous = process.env["DOCKER_HOST"]; - process.env["DOCKER_HOST"] = endpoint; - return effect.pipe( - Effect.provideService( - ConfigProvider.ConfigProvider, - ConfigProvider.fromEnv({ preserveEmptyStrings: true }), - ), - Effect.ensuring( - Effect.sync(() => { - if (previous === undefined) delete process.env["DOCKER_HOST"]; - else process.env["DOCKER_HOST"] = previous; - }), - ), - ); - }); + return effect.pipe( + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnv({ env: { DOCKER_HOST: endpoint }, preserveEmptyStrings: true }), + ), + ); } const ENGINE_IDENTITY_HEADERS = { "api-version": "1.55" }; -const makeEngineServer = (respond: (req: http.IncomingMessage, res: http.ServerResponse) => void) => - Effect.acquireRelease( - Effect.callback<{ - readonly socketPath: string; - readonly requestedUrls: Array; - readonly server: http.Server; - readonly dir: string; - }>((resume) => { - let settled = false; - const dir = mkdtempSync(join(tmpdir(), "ldbeng-")); - const socketPath = join(dir, "d.sock"); - const requestedUrls: Array = []; - const server = http.createServer((req, res) => { - requestedUrls.push(req.url ?? ""); - respond(req, res); - }); - server.once("error", (cause) => { - if (settled) return; - settled = true; - rmSync(dir, { recursive: true, force: true }); - resume(Effect.die(cause)); - }); - server.listen(socketPath, () => { - if (settled) return; - settled = true; - resume(Effect.succeed({ socketPath, requestedUrls, server, dir })); - }); - return Effect.sync(() => { - settled = true; - server.close(); - rmSync(dir, { recursive: true, force: true }); - }); - }), - ({ dir, server }) => - Effect.callback((resume) => { - server.closeAllConnections?.(); - server.close(() => { - rmSync(dir, { recursive: true, force: true }); - resume(Effect.void); - }); +const engineJson = (status: number, body: string) => + HttpServerResponse.text(body, { + status, + headers: ENGINE_IDENTITY_HEADERS, + contentType: "application/json", + }); + +const makeEngineServer = ( + respond: ( + request: HttpServerRequest.HttpServerRequest, + ) => Effect.Effect, +) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "ldbeng-" }); + const socketPath = path.join(dir, "d.sock"); + const requestedUrls: Array = []; + const server = yield* BunHttpServer.make({ unix: socketPath }); + yield* server.serve( + Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest; + requestedUrls.push(request.url); + return yield* respond(request); }), - ); + ); + return { socketPath, requestedUrls }; + }); + +/* + * A raw-socket peer for wire shapes an HTTP server will not produce: it reads + * the request head, writes `reply` verbatim, then closes the connection. With + * no `reply` it holds the connection open without answering. + */ +const makeRawEngineServer = (reply: string | undefined) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "ldbraw-" }); + const socketPath = path.join(dir, "d.sock"); + const server = yield* BunSocketServer.make({ path: socketPath }); + yield* server + .run((socket) => + Effect.scoped( + Effect.gen(function* () { + const reader = yield* socket.reader; + const writer = yield* socket.writer; + const decoder = new TextDecoder(); + let head = ""; + while (!head.includes("\r\n\r\n")) { + for (const chunk of yield* reader.pull) { + head += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true }); + } + } + if (reply === undefined) return yield* Effect.never; + yield* writer.write(reply); + }), + ), + ) + .pipe(Effect.forkScoped); + return { socketPath }; + }); + +const missingSocketEndpoint = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "ldbgone-" }); + return `unix://${path.join(dir, "never-created.sock")}`; +}); const localDockerEngineTestLayer = localDockerEngineLayer.pipe( Layer.provide(BunServices.layer), @@ -123,10 +136,14 @@ describe("LocalDockerEngine (direct Engine-API transport)", () => { it.live("answers present for an Engine 200 inspect payload on the documented route", () => Effect.scoped( Effect.gen(function* () { - const engine = yield* makeEngineServer((_req, res) => { - res.writeHead(200, { "content-type": "application/json", ...ENGINE_IDENTITY_HEADERS }); - res.end(JSON.stringify({ Id: "abc123", State: { Running: true, Status: "running" } })); - }); + const engine = yield* makeEngineServer(() => + Effect.succeed( + engineJson( + 200, + JSON.stringify({ Id: "abc123", State: { Running: true, Status: "running" } }), + ), + ), + ); const answer = yield* withDockerHost( `unix://${engine.socketPath}`, engineInspectContainer("supabase_db_engine-probe"), @@ -134,7 +151,7 @@ describe("LocalDockerEngine (direct Engine-API transport)", () => { expect(answer).toEqual(Option.some({ exists: true, running: true, status: "running" })); expect(engine.requestedUrls).toEqual(["/containers/supabase_db_engine-probe/json"]); }), - ), + ).pipe(Effect.provide(BunServices.layer)), ); it.live.each([ @@ -148,56 +165,62 @@ describe("LocalDockerEngine (direct Engine-API transport)", () => { ([, state, running, status]) => Effect.scoped( Effect.gen(function* () { - const engine = yield* makeEngineServer((_req, res) => { - res.writeHead(200, { - "content-type": "application/json", - ...ENGINE_IDENTITY_HEADERS, - }); - res.end( - JSON.stringify({ Id: "abc123", ...(state === undefined ? {} : { State: state }) }), - ); - }); + const engine = yield* makeEngineServer(() => + Effect.succeed( + engineJson( + 200, + JSON.stringify({ + Id: "abc123", + ...(state === undefined ? {} : { State: state }), + }), + ), + ), + ); const answer = yield* withDockerHost( `unix://${engine.socketPath}`, engineInspectContainer("supabase_db_engine-probe"), ); expect(answer).toEqual(Option.some({ exists: true, running, status })); }), - ), + ).pipe(Effect.provide(BunServices.layer)), ); it.live("answers absent for an Engine 404", () => Effect.scoped( Effect.gen(function* () { - const engine = yield* makeEngineServer((_req, res) => { - res.writeHead(404, { "content-type": "application/json", ...ENGINE_IDENTITY_HEADERS }); - res.end(JSON.stringify({ message: "No such container" })); - }); + const engine = yield* makeEngineServer(() => + Effect.succeed(engineJson(404, JSON.stringify({ message: "No such container" }))), + ); const answer = yield* withDockerHost( `unix://${engine.socketPath}`, engineInspectContainer("supabase_db_engine-probe"), ); expect(answer).toEqual(Option.some({ exists: false })); }), - ), + ).pipe(Effect.provide(BunServices.layer)), ); it.live("gives no answer for a responder that does not identify as a Docker engine", () => Effect.scoped( Effect.gen(function* () { - const engine = yield* makeEngineServer((req, res) => { - if (req.url?.includes("present") === true) { - res.writeHead(200, { "content-type": "application/json" }); - res.end(JSON.stringify({ Id: "not-an-engine" })); - return; + const engine = yield* makeEngineServer((request) => { + if (request.url.includes("present")) { + return Effect.succeed( + HttpServerResponse.text(JSON.stringify({ Id: "not-an-engine" }), { + status: 200, + contentType: "application/json", + }), + ); } - if (req.url?.includes("server-header") === true) { - res.writeHead(404, { server: "Docker/28.5.2 (linux)" }); - res.end(JSON.stringify({ message: "No such container" })); - return; + if (request.url.includes("server-header")) { + return Effect.succeed( + HttpServerResponse.text(JSON.stringify({ message: "No such container" }), { + status: 404, + headers: { server: "Docker/28.5.2 (linux)" }, + }), + ); } - res.writeHead(404); - res.end(); + return Effect.succeed(HttpServerResponse.empty({ status: 404 })); }); const present = yield* withDockerHost( `unix://${engine.socketPath}`, @@ -215,18 +238,20 @@ describe("LocalDockerEngine (direct Engine-API transport)", () => { expect(absent).toEqual(Option.none()); expect(viaServerHeader).toEqual(Option.some({ exists: false })); }), - ), + ).pipe(Effect.provide(BunServices.layer)), ); it.live("gives no answer for an empty or malformed Engine 200 body", () => Effect.scoped( Effect.gen(function* () { - const engine = yield* makeEngineServer((req, res) => { - res.writeHead(200, { "content-type": "application/json", ...ENGINE_IDENTITY_HEADERS }); - res.end( - req.url?.includes("empty") === true ? "" : JSON.stringify(["not", "an", "object"]), - ); - }); + const engine = yield* makeEngineServer((request) => + Effect.succeed( + engineJson( + 200, + request.url.includes("empty") ? "" : JSON.stringify(["not", "an", "object"]), + ), + ), + ); const empty = yield* withDockerHost( `unix://${engine.socketPath}`, engineInspectContainer("empty"), @@ -238,7 +263,7 @@ describe("LocalDockerEngine (direct Engine-API transport)", () => { expect(empty).toEqual(Option.none()); expect(malformed).toEqual(Option.none()); }), - ), + ).pipe(Effect.provide(BunServices.layer)), ); it.live( @@ -246,20 +271,16 @@ describe("LocalDockerEngine (direct Engine-API transport)", () => { () => Effect.scoped( Effect.gen(function* () { - const engine = yield* makeEngineServer((_req, res) => { - res.writeHead(500, { - "content-type": "application/json", - ...ENGINE_IDENTITY_HEADERS, - }); - res.end(JSON.stringify({ message: "layer store corrupted" })); - }); + const engine = yield* makeEngineServer(() => + Effect.succeed(engineJson(500, JSON.stringify({ message: "layer store corrupted" }))), + ); const answer = yield* withDockerHost( `unix://${engine.socketPath}`, engineInspectContainer("supabase_db_engine-probe"), ); expect(answer).toEqual(Option.none()); }), - ), + ).pipe(Effect.provide(BunServices.layer)), ); it.live( @@ -267,52 +288,46 @@ describe("LocalDockerEngine (direct Engine-API transport)", () => { () => Effect.scoped( Effect.gen(function* () { - const engine = yield* makeEngineServer(() => {}); + const engine = yield* makeRawEngineServer(undefined); const answer = yield* withDockerHost( `unix://${engine.socketPath}`, engineInspectContainer("supabase_db_engine-probe"), ); expect(answer).toEqual(Option.none()); }), - ), + ).pipe(Effect.provide(BunServices.layer)), 15_000, ); it.live("gives no answer for an oversized body", () => Effect.scoped( Effect.gen(function* () { - const engine = yield* makeEngineServer((_req, res) => { - res.writeHead(200, { "content-type": "application/json", ...ENGINE_IDENTITY_HEADERS }); - res.end(`{"Id":"${"a".repeat(80 * 1024)}"}`); - }); + const engine = yield* makeEngineServer(() => + Effect.succeed(engineJson(200, `{"Id":"${"a".repeat(80 * 1024)}"}`)), + ); const answer = yield* withDockerHost( `unix://${engine.socketPath}`, engineInspectContainer("supabase_db_engine-probe"), ); expect(answer).toEqual(Option.none()); }), - ), + ).pipe(Effect.provide(BunServices.layer)), ); it.live("gives no answer when the peer resets after the headers", () => Effect.scoped( Effect.gen(function* () { - const engine = yield* makeEngineServer((_req, res) => { - res.writeHead(200, { - "content-type": "application/json", - "content-length": "1000", - ...ENGINE_IDENTITY_HEADERS, - }); - res.write('{"Id":'); - res.socket?.destroy(); - }); + const engine = yield* makeRawEngineServer( + "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: 1000\r\n" + + 'api-version: 1.55\r\n\r\n{"Id":', + ); const answer = yield* withDockerHost( `unix://${engine.socketPath}`, engineInspectContainer("supabase_db_engine-probe"), ); expect(answer).toEqual(Option.none()); }), - ), + ).pipe(Effect.provide(BunServices.layer)), ); it.live("traces the request, the decline, and the fallback through DebugLogger", () => { @@ -333,10 +348,9 @@ describe("LocalDockerEngine (direct Engine-API transport)", () => { }).pipe(Effect.provide(localDockerEngineTestLayer.pipe(Layer.provide(recorder)))); return Effect.scoped( Effect.gen(function* () { - const engine = yield* makeEngineServer((_req, res) => { - res.writeHead(404, { "content-type": "application/json", ...ENGINE_IDENTITY_HEADERS }); - res.end(JSON.stringify({ message: "No such container" })); - }); + const engine = yield* makeEngineServer(() => + Effect.succeed(engineJson(404, JSON.stringify({ message: "No such container" }))), + ); yield* withDockerHost(`unix://${engine.socketPath}`, traced); expect( lines.some( @@ -360,17 +374,12 @@ describe("LocalDockerEngine (direct Engine-API transport)", () => { yield* withDockerHost("deploy:hunter4@10.0.0.5:2376", traced); expect(lines.some((l) => l.includes("not directly addressable (no scheme)"))).toBe(true); expect(lines.some((l) => l.includes("hunter4") || l.includes("deploy"))).toBe(false); - const missingDir = mkdtempSync(join(tmpdir(), "ldbgone-")); - yield* withDockerHost(`unix://${join(missingDir, "never-created.sock")}`, traced).pipe( - Effect.ensuring( - Effect.sync(() => rmSync(missingDir, { recursive: true, force: true })), - ), - ); + yield* withDockerHost(yield* missingSocketEndpoint, traced); expect( lines.some((l) => l.startsWith("debug:") && l.includes("no definitive Engine answer")), ).toBe(true); }), - ); + ).pipe(Effect.provide(BunServices.layer)); }); it.live( @@ -378,41 +387,41 @@ describe("LocalDockerEngine (direct Engine-API transport)", () => { () => Effect.scoped( Effect.gen(function* () { - const engine = yield* makeEngineServer((_req, res) => { - res.writeHead(200, { - "content-type": "application/json", - ...ENGINE_IDENTITY_HEADERS, - }); - const drip = setInterval(() => { - res.write("a"); - }, 500); - res.on("close", () => { - clearInterval(drip); - }); - }); + const drip = new TextEncoder().encode("a"); + const engine = yield* makeEngineServer(() => + Effect.succeed( + HttpServerResponse.stream( + Stream.fromSchedule(Schedule.spaced("500 millis")).pipe(Stream.as(drip)), + { + status: 200, + headers: ENGINE_IDENTITY_HEADERS, + contentType: "application/json", + }, + ), + ), + ); const answer = yield* withDockerHost( `unix://${engine.socketPath}`, engineInspectContainer("supabase_db_engine-probe"), ); expect(answer).toEqual(Option.none()); }), - ), + ).pipe(Effect.provide(BunServices.layer)), 15_000, ); }); - it.live("gives no answer when the local socket cannot be dialed", () => { - const missingDir = mkdtempSync(join(tmpdir(), "ldbgone-")); - return withDockerHost( - `unix://${join(missingDir, "never-created.sock")}`, - engineInspectContainer("supabase_db_engine-probe"), - ).pipe( - Effect.ensuring(Effect.sync(() => rmSync(missingDir, { recursive: true, force: true }))), - Effect.map((answer) => { + it.live("gives no answer when the local socket cannot be dialed", () => + Effect.scoped( + Effect.gen(function* () { + const answer = yield* withDockerHost( + yield* missingSocketEndpoint, + engineInspectContainer("supabase_db_engine-probe"), + ); expect(answer).toEqual(Option.none()); }), - ); - }); + ).pipe(Effect.provide(BunServices.layer)), + ); it.live("gives no answer for an endpoint the transport cannot address (ssh)", () => withDockerHost( @@ -433,22 +442,24 @@ describe("isLocalDbRunning", () => { toml: 'project_id = "engine-probe"\n', }, ) => - Effect.gen(function* () { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workdir = mkdtempSync(join(tmpdir(), "ldbrun-")); - if (options.toml !== undefined) { - yield* fs.makeDirectory(path.join(workdir, "supabase"), { recursive: true }); - yield* fs.writeFileString(path.join(workdir, "supabase", "config.toml"), options.toml); - } - return yield* isLocalDbRunning(spawner, fs, path, workdir).pipe( - Effect.ensuring(Effect.sync(() => rmSync(workdir, { recursive: true, force: true }))), - ); - }).pipe( - Effect.provide(configValuesLayer({ output: mockOutput().layer, env: options.env })), - Effect.provide(spawnerLayer), - Effect.provide(BunServices.layer), + Effect.scoped( + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const workdir = yield* fs.makeTempDirectoryScoped({ prefix: "ldbrun-" }); + if (options.toml !== undefined) { + yield* fs.makeDirectory(path.join(workdir, "supabase"), { recursive: true }); + yield* fs.writeFileString(path.join(workdir, "supabase", "config.toml"), options.toml); + } + return yield* isLocalDbRunning(spawner, fs, path, workdir); + }), + ).pipe( + Effect.provide( + configValuesLayer({ output: mockOutput().layer, env: options.env }).pipe( + Layer.provideMerge(Layer.mergeAll(BunServices.layer, spawnerLayer)), + ), + ), ); it.live("probes the container named by config.toml's project_id", () => { @@ -570,37 +581,38 @@ describe("isLocalDbRunning", () => { exitCode: 1, stderr: ["Error response from daemon: No such container: supabase_db_engine-probe"], })); - const missingDir = mkdtempSync(join(tmpdir(), "ldbgone-")); - return withDockerHost( - `unix://${join(missingDir, "never-created.sock")}`, - probe(mock.layer).pipe(Effect.provide(localDockerEngineTestLayer)), - ).pipe( - Effect.ensuring(Effect.sync(() => rmSync(missingDir, { recursive: true, force: true }))), - Effect.map((running) => { + return Effect.scoped( + Effect.gen(function* () { + const running = yield* withDockerHost( + yield* missingSocketEndpoint, + probe(mock.layer).pipe(Effect.provide(localDockerEngineTestLayer)), + ); expect(running).toBe(false); expect(mock.spawned.map((s) => s.args)).toEqual([ ["container", "inspect", "supabase_db_engine-probe"], ]); }), - ); + ).pipe(Effect.provide(BunServices.layer)); }, ); }); describe("localDbState", () => { const stateOf = (spawnerLayer: Layer.Layer) => - Effect.gen(function* () { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workdir = mkdtempSync(join(tmpdir(), "ldbstate-")); - return yield* localDbState(spawner, fs, path, workdir, "engine-probe").pipe( - Effect.ensuring(Effect.sync(() => rmSync(workdir, { recursive: true, force: true }))), - ); - }).pipe( - Effect.provide(configValuesLayer({ output: mockOutput().layer })), - Effect.provide(spawnerLayer), - Effect.provide(BunServices.layer), + Effect.scoped( + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const workdir = yield* fs.makeTempDirectoryScoped({ prefix: "ldbstate-" }); + return yield* localDbState(spawner, fs, path, workdir, "engine-probe"); + }), + ).pipe( + Effect.provide( + configValuesLayer({ output: mockOutput().layer }).pipe( + Layer.provideMerge(Layer.mergeAll(BunServices.layer, spawnerLayer)), + ), + ), Effect.provideService(LocalDockerEngine, { inspectContainer: () => Effect.succeed(Option.none()), }), diff --git a/apps/cli/src/command-internal/db-config.toml-read.precedence.unit.test.ts b/apps/cli/src/command-internal/db-config.toml-read.precedence.unit.test.ts index 253d01fd99..d75c0cd9ae 100644 --- a/apps/cli/src/command-internal/db-config.toml-read.precedence.unit.test.ts +++ b/apps/cli/src/command-internal/db-config.toml-read.precedence.unit.test.ts @@ -231,10 +231,9 @@ describe("db toml reader precedence goldens", () => { .expect(Result.isSuccess(outcome) ? outcome.success.projectId : undefined) .toBe(fixture.expectedProjectId); } + const goldenText = yield* goldenJson(snapshot, { [workdir]: "" }); yield* Effect.promise(() => - ctx - .expect(goldenJson(snapshot, { [workdir]: "" })) - .toMatchFileSnapshot(`${GOLDEN_DIR}/${fixture.golden}.json`), + ctx.expect(goldenText).toMatchFileSnapshot(`${GOLDEN_DIR}/${fixture.golden}.json`), ); }).pipe(Effect.provide(BunServices.layer)), ); diff --git a/apps/cli/src/command-internal/hosted-consent/hosted-consent-coverage.unit.test.ts b/apps/cli/src/command-internal/hosted-consent/hosted-consent-coverage.unit.test.ts index 6866152680..6193c832b2 100644 --- a/apps/cli/src/command-internal/hosted-consent/hosted-consent-coverage.unit.test.ts +++ b/apps/cli/src/command-internal/hosted-consent/hosted-consent-coverage.unit.test.ts @@ -1,7 +1,6 @@ -import { readFileSync, readdirSync } from "node:fs"; -import { join, sep } from "node:path"; import { fileURLToPath } from "node:url"; -import { Option } from "effect"; +import { BunServices } from "@effect/platform-bun"; +import { Effect, FileSystem, Option, Path } from "effect"; import { describe, expect, it } from "vitest"; import { commandTreeFor } from "../../../tests/helpers/command-flags.ts"; @@ -31,15 +30,24 @@ const consentLeafPaths: ReadonlySet = new Set(consentDefaultByLeaf.keys( const LITERAL_CALL = /\bdescribeHostedConsent\(\s*"([^"]+)"/g; const ANY_CALL = /\bdescribeHostedConsent\(/g; -const testFiles = readdirSync(srcDir, { recursive: true, encoding: "utf8" }) - .filter((file) => file.endsWith(".test.ts")) - .map((file) => file.split(sep).join("/")); +const testSources = await Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const files = yield* fs.readDirectory(srcDir, { recursive: true }); + return yield* Effect.forEach( + files.filter((file) => file.endsWith(".test.ts")), + (file) => + Effect.map(fs.readFileString(path.join(srcDir, file)), (source) => ({ + file: file.split(path.sep).join("/"), + source, + })), + ); +}).pipe(Effect.provide(BunServices.layer), Effect.runPromise); const callsByLeaf = new Map>(); const nonLiteralCallSites: Array = []; -for (const file of testFiles) { - const source = readFileSync(join(srcDir, file), "utf8"); +for (const { file, source } of testSources) { const literalCalls = [...source.matchAll(LITERAL_CALL)]; for (const [, leaf] of literalCalls) { callsByLeaf.set(leaf!, [...(callsByLeaf.get(leaf!) ?? []), file]); @@ -52,7 +60,7 @@ for (const file of testFiles) { describe("hosted consent coverage", () => { it("scans real test files and finds consenting leaves in the command tree", () => { - expect(testFiles.length).toBeGreaterThan(0); + expect(testSources.length).toBeGreaterThan(0); expect(consentLeafPaths.size).toBeGreaterThan(0); }); diff --git a/apps/cli/src/command-internal/stack-config.precedence.integration.test.ts b/apps/cli/src/command-internal/stack-config.precedence.integration.test.ts index ee5b8bbd84..bdaf084706 100644 --- a/apps/cli/src/command-internal/stack-config.precedence.integration.test.ts +++ b/apps/cli/src/command-internal/stack-config.precedence.integration.test.ts @@ -184,10 +184,9 @@ describe("stack config precedence goldens", () => { source: config.source, projectEnvValues: configEnvOnly(config.projectEnvValues), }; + const goldenText = yield* goldenJson(golden, { [workdir]: "" }); yield* Effect.promise(() => - ctx - .expect(goldenJson(golden, { [workdir]: "" })) - .toMatchFileSnapshot(`${GOLDEN_DIR}/${fixture.golden}.json`), + ctx.expect(goldenText).toMatchFileSnapshot(`${GOLDEN_DIR}/${fixture.golden}.json`), ); }).pipe(Effect.provide(BunServices.layer)), ); diff --git a/apps/cli/src/commands/start/start.config-precedence.integration.test.ts b/apps/cli/src/commands/start/start.config-precedence.integration.test.ts index bad2b0c28b..87e5dbd3cd 100644 --- a/apps/cli/src/commands/start/start.config-precedence.integration.test.ts +++ b/apps/cli/src/commands/start/start.config-precedence.integration.test.ts @@ -376,10 +376,9 @@ describe("start container env precedence goldens", () => { ), ), }; + const goldenText = yield* goldenJson(golden, { [workdir]: "" }); yield* Effect.promise(() => - ctx - .expect(goldenJson(golden, { [workdir]: "" })) - .toMatchFileSnapshot(`${GOLDEN_DIR}/${fixture.golden}.json`), + ctx.expect(goldenText).toMatchFileSnapshot(`${GOLDEN_DIR}/${fixture.golden}.json`), ); }).pipe(Effect.provide(BunServices.layer)), ); diff --git a/apps/cli/tests/helpers/compute-target-selection.ts b/apps/cli/tests/helpers/compute-target-selection.ts index 96e75b125d..96e7d59e30 100644 --- a/apps/cli/tests/helpers/compute-target-selection.ts +++ b/apps/cli/tests/helpers/compute-target-selection.ts @@ -22,7 +22,7 @@ export interface ComputeTargetSelectionScenario { readonly routes: (ref: string) => ComputeHttpRoutes; readonly run: ( flags: TargetSelectionFlags, - ) => Effect.Effect; + ) => Effect.Effect; readonly activeFlags?: ActiveFlags; } diff --git a/apps/cli/tests/helpers/config-goldens.ts b/apps/cli/tests/helpers/config-goldens.ts index af393c509e..e85c5d6742 100644 --- a/apps/cli/tests/helpers/config-goldens.ts +++ b/apps/cli/tests/helpers/config-goldens.ts @@ -1,6 +1,5 @@ -import { realpathSync } from "node:fs"; import { afterEach, vi } from "vitest"; -import { Option, Redacted } from "effect"; +import { Effect, FileSystem, Option, Redacted, Schema } from "effect"; import { PINNED_ENV_PREFIXES } from "./config-env-pins.ts"; @@ -51,28 +50,25 @@ function normalize(value: unknown): unknown { return value; } -function resolvedPath(candidate: string): string { - try { - return realpathSync(candidate); - } catch { - return candidate; - } -} +const encodeGolden = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown, { space: 2 })); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); /** * Stable golden serialization: sorted keys, `Option`/`undefined` collapsed to value/`null`, `Set` * as sorted array, functions dropped, and each `redactions` key (e.g. a temp workdir) replaced * by its placeholder. */ -export function goldenJson( +export const goldenJson = Effect.fnUntraced(function* ( value: unknown, redactions: Readonly> = {}, -): string { - let text = `${JSON.stringify(normalize(value), null, 2)}\n`; +) { + const fs = yield* FileSystem.FileSystem; + let text = `${yield* encodeGolden(normalize(value))}\n`; for (const [needle, placeholder] of Object.entries(redactions)) { - for (const variant of new Set([needle, resolvedPath(needle)])) { - text = text.replaceAll(JSON.stringify(variant).slice(1, -1), placeholder); + const resolved = yield* fs.realPath(needle).pipe(Effect.orElseSucceed(() => needle)); + for (const variant of new Set([needle, resolved])) { + text = text.replaceAll((yield* encodeJson(variant)).slice(1, -1), placeholder); } } return text; -} +}); diff --git a/apps/cli/tests/helpers/config-values-layer.integration.test.ts b/apps/cli/tests/helpers/config-values-layer.integration.test.ts index 235b427568..da2ae03ca9 100644 --- a/apps/cli/tests/helpers/config-values-layer.integration.test.ts +++ b/apps/cli/tests/helpers/config-values-layer.integration.test.ts @@ -1,5 +1,5 @@ import { BunServices } from "@effect/platform-bun"; -import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "@effect/vitest"; import { Effect, FileSystem, Option } from "effect"; import { CliConfigKeys } from "../../src/config/cli-config-keys.ts"; @@ -11,14 +11,11 @@ import { mockOutput } from "./mocks.ts"; const STRAY_PORT = "11111"; const AMBIENT_NAME = "SUPABASE_DB_PORT"; -let previous: string | undefined; beforeEach(() => { - previous = process.env[AMBIENT_NAME]; - process.env[AMBIENT_NAME] = STRAY_PORT; + vi.stubEnv(AMBIENT_NAME, STRAY_PORT); }); afterEach(() => { - if (previous === undefined) delete process.env[AMBIENT_NAME]; - else process.env[AMBIENT_NAME] = previous; + vi.unstubAllEnvs(); }); const readDbPort = Effect.gen(function* () { diff --git a/apps/cli/tests/helpers/db-target-selection.ts b/apps/cli/tests/helpers/db-target-selection.ts index 0a61de2571..4f2313d56c 100644 --- a/apps/cli/tests/helpers/db-target-selection.ts +++ b/apps/cli/tests/helpers/db-target-selection.ts @@ -1,9 +1,6 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { Effect, Exit, Layer, Option, Predicate, type Scope } from "effect"; +import { Effect, Exit, FileSystem, Layer, Option, Path, Predicate, type Scope } from "effect"; import { CliOutput, Command } from "effect/cli"; import { legacyProjectStateLayer } from "../../src/shared/config/legacy-project-state.layer.ts"; @@ -90,9 +87,9 @@ export const storageTargetOf = ( }; interface DbTargetAttempt { - readonly layer: Layer.Layer; + readonly layer: Layer.Layer; readonly out: ReturnType; - readonly run: Effect.Effect; + readonly run: Effect.Effect; /** The target the leaf connected or sent requests to; `undefined` until it reached one. */ readonly target: () => ObservedTarget | undefined; readonly cachedRef?: () => string | undefined; @@ -128,7 +125,7 @@ interface DbTargetSelectionScenario { readonly workdir: string; readonly args: ReadonlyArray; readonly target: TargetFlagValuesWithDbUrl; - }) => DbTargetAttempt | Effect.Effect, unknown, BunServices.BunServices>; + }) => DbTargetAttempt | Effect.Effect, Error, BunServices.BunServices>; /** Wraps each row, for a leaf whose tests pin ambient environment variables around their body. */ readonly around?: (row: Effect.Effect) => Effect.Effect; /** The flags besides `--remote` that the run sets and the leaf's permission map branches on. */ @@ -137,11 +134,14 @@ interface DbTargetSelectionScenario { readonly optOut?: Partial>; } -const linkCheckout = (workdir: string) => { - const temp = join(workdir, "supabase", ".temp"); - mkdirSync(temp, { recursive: true }); - writeFileSync(join(temp, "project-ref"), VALID_REF); -}; +const linkCheckout = (workdir: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const temp = path.join(workdir, "supabase", ".temp"); + yield* fs.makeDirectory(temp, { recursive: true }); + yield* fs.writeFileString(path.join(temp, "project-ref"), VALID_REF); + }).pipe(Effect.provide(BunServices.layer)); const expectCachedRef = (ctx: DbTargetAttempt, ref: string | undefined) => { if (ctx.cachedRef !== undefined) expect(ctx.cachedRef()).toBe(ref); @@ -170,34 +170,36 @@ export function describeDbTargetSelection( readonly linked?: true; readonly check: ( ctx: DbTargetAttempt, - ) => Effect.Effect; + ) => Effect.Effect; }, ) => { if (optedOut(row)) return; - it.effect(name, () => { - const workdir = scenario.workdir(); - if (spec.linked === true) linkCheckout(workdir); - const attempted = scenario.attempt({ - workdir, - args: [...words, ...spec.args], - target: spec.target, - }); - const row = Effect.flatMap( - Effect.isEffect(attempted) - ? Effect.provide(attempted, BunServices.layer) - : Effect.succeed(attempted), - (ctx) => - spec - .check(ctx) - .pipe( - Effect.provide( - Layer.mergeAll(ctx.layer, commandTargetLayer.pipe(Layer.provide(ctx.out.layer))), + it.effect(name, () => + Effect.gen(function* () { + const workdir = scenario.workdir(); + if (spec.linked === true) yield* linkCheckout(workdir); + const attempted = scenario.attempt({ + workdir, + args: [...words, ...spec.args], + target: spec.target, + }); + const row = Effect.flatMap( + Effect.isEffect(attempted) + ? Effect.provide(attempted, BunServices.layer) + : Effect.succeed(attempted), + (ctx) => + spec + .check(ctx) + .pipe( + Effect.provide( + Layer.mergeAll(ctx.layer, commandTargetLayer.pipe(Layer.provide(ctx.out.layer))), + ), + Effect.scoped, ), - Effect.scoped, - ), - ); - return scenario.around === undefined ? row : scenario.around(row); - }); + ); + yield* scenario.around === undefined ? row : scenario.around(row); + }), + ); }; describe(`target selection: ${path}`, () => { @@ -287,7 +289,7 @@ export function describeDbTargetSelection( linked: true, check: (ctx) => Effect.gen(function* () { - const error = yield* Effect.flip(ctx.run); + const error = yield* ctx.run.pipe(Effect.asVoid, Effect.flip, Effect.orDie); expect(error).toMatchObject({ _tag: "LinkBranchNotFoundError", message: expect.stringContaining("Available branches: feature-x"), @@ -375,7 +377,7 @@ export function describeDbTargetSelection( target: { ...BARE_REMOTE, local: true }, check: (ctx) => Effect.gen(function* () { - const error = yield* Effect.flip(ctx.run); + const error = yield* ctx.run.pipe(Effect.asVoid, Effect.flip, Effect.orDie); expect(error).toMatchObject({ _tag: "TargetSelectorConflictError" }); expect(ctx.target()).toBeUndefined(); expectCachedRef(ctx, undefined); diff --git a/apps/cli/tests/helpers/hosted-consent.ts b/apps/cli/tests/helpers/hosted-consent.ts index fab4317728..a3a6997799 100644 --- a/apps/cli/tests/helpers/hosted-consent.ts +++ b/apps/cli/tests/helpers/hosted-consent.ts @@ -42,7 +42,7 @@ interface HostedConsentEnv { } interface HostedConsentInvocation { - readonly run: Effect.Effect; + readonly run: Effect.Effect; /** Persistent writes so far; defaults to the non-GET requests the mocked API recorded. */ readonly mutations?: () => number; } @@ -53,7 +53,7 @@ export interface HostedConsentScenario { /** The leaf asks for a typed confirmation instead of a yes/no prompt. */ readonly typed?: boolean; readonly api?: MockCommandPlatformApiOpts; - readonly setup?: (workdir: string) => Effect.Effect; + readonly setup?: (workdir: string) => Effect.Effect; /** Builds the handler call; runs once per case, so per-case state can live in its closure. */ readonly invoke: (env: HostedConsentEnv) => HostedConsentInvocation; } diff --git a/apps/cli/tests/helpers/hosted-target-selection.ts b/apps/cli/tests/helpers/hosted-target-selection.ts index a5cde75e32..b1cede7880 100644 --- a/apps/cli/tests/helpers/hosted-target-selection.ts +++ b/apps/cli/tests/helpers/hosted-target-selection.ts @@ -37,7 +37,7 @@ interface RecordedCall { } interface TargetSelectionHarness { - readonly layer: Layer.Layer; + readonly layer: Layer.Layer; readonly requests: () => ReadonlyArray; readonly warnings: () => ReadonlyArray; /** The ref the leaf cached as its target. */ @@ -65,10 +65,10 @@ export interface TargetSelectionMatrix { readonly workdir: string; }) => TargetSelectionHarness; /** Seeds the workdir before every run. */ - readonly prepare?: (workdir: string) => Effect.Effect; + readonly prepare?: (workdir: string) => Effect.Effect; /** Marks the workdir as linked to {@link VALID_REF}, the parent a branch name resolves under. */ - readonly linkParent: (workdir: string) => Effect.Effect; - readonly run: (flags: TargetSelectionFlags) => Effect.Effect; + readonly linkParent: (workdir: string) => Effect.Effect; + readonly run: (flags: TargetSelectionFlags) => Effect.Effect; } // `snippets list` names its project in a `project_ref` query parameter, not in the path. @@ -171,7 +171,7 @@ export function describeTargetSelectionMatrix( remote: Option.some({ _tag: "Branch", name: "feature-x" }), projectRef: Option.none(), }) - .pipe(Effect.flip); + .pipe(Effect.asVoid, Effect.flip, Effect.orDie); expect(error).toMatchObject({ _tag: "TargetNeedsProjectError", @@ -246,7 +246,7 @@ export interface HostedTargetSelectionScenario { ) => ReturnType; readonly run: ( flags: TargetSelectionFlags, - ) => Effect.Effect; + ) => Effect.Effect; readonly activeFlags?: ActiveFlags; } From df7d4de352aeec03568023dc2d980f99b1d6b941 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:34:59 +0530 Subject: [PATCH 2/2] nit --- .../config-validate.parity.unit.test.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/cli/src/command-internal/config-validate.parity.unit.test.ts b/apps/cli/src/command-internal/config-validate.parity.unit.test.ts index fd2c7a9846..662b9e3d9b 100644 --- a/apps/cli/src/command-internal/config-validate.parity.unit.test.ts +++ b/apps/cli/src/command-internal/config-validate.parity.unit.test.ts @@ -33,17 +33,12 @@ const withConfig = (content: string) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const dir = yield* fs.makeTempDirectory({ prefix: "config-validate-parity-" }); + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "config-validate-parity-" }); yield* fs.makeDirectory(path.join(dir, "supabase"), { recursive: true }); yield* fs.writeFileString(path.join(dir, "supabase", "config.toml"), content); return dir; }).pipe(Effect.provide(BunServices.layer)); -const removeConfig = (dir: string) => - FileSystem.FileSystem.use((fs) => fs.remove(dir, { recursive: true, force: true })).pipe( - Effect.provide(BunServices.layer), - ); - const readD = (workdir: string) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -60,8 +55,7 @@ function failsWithD(tomlLines: ReadonlyArray, message: string) { if (Exit.isFailure(exit)) { expect(stringifyJson(exit.cause)).toContain(message); } - yield* removeConfig(dir); - }); + }).pipe(Effect.scoped); } const decodeConfig = Schema.decodeUnknownSync(CliConfigSchema); @@ -365,8 +359,7 @@ describe("shared api + auth validation branches, cross-caller parity (S vs L)", if (Exit.isFailure(exit)) { expect(stringifyJson(exit.cause)).toContain(message); } - yield* removeConfig(dir); - }); + }).pipe(Effect.scoped); // Ambient SUPABASE_API_* / SUPABASE_AUTH_* values would override the config // under test, so pin every participating key to unset for the duration of