From 434f865e9ee107fd4dc09b2ed1a63eedeb530382 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:18:58 +0530 Subject: [PATCH 1/4] refactor(cli): look up the delegated binary through `FileSystem` and `Config` --- .../src/command-internal/go-proxy.layer.ts | 116 +++++++++--------- apps/cli/src/command-internal/viper-env.ts | 2 +- 2 files changed, 57 insertions(+), 61 deletions(-) diff --git a/apps/cli/src/command-internal/go-proxy.layer.ts b/apps/cli/src/command-internal/go-proxy.layer.ts index 2bcd265e60..d9cf7320b5 100644 --- a/apps/cli/src/command-internal/go-proxy.layer.ts +++ b/apps/cli/src/command-internal/go-proxy.layer.ts @@ -1,9 +1,7 @@ -import { existsSync } from "node:fs"; import { createRequire } from "node:module"; import os from "node:os"; -import path from "node:path"; import process from "node:process"; -import { Config, Effect, FileSystem, Layer, Option, Path, Stream } from "effect"; +import { Config, Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect"; import * as ChildProcess from "effect/process/ChildProcess"; import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner"; import { resolveWorkdir } from "../config/command-settings.layer.ts"; @@ -15,8 +13,11 @@ import { GoProxyInvocation } from "./go-proxy-invocation.ts"; import { GoProxy } from "./go-proxy.service.ts"; import { readProjectLinkFile } from "./project-link.ts"; import { usesStackState } from "./stack-backend.ts"; +import { shellEnv } from "./viper-env.ts"; import { withProcessSpan } from "../shared/telemetry/spans.ts"; +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + const markDelegated = Effect.serviceOption(GoProxyInvocation).pipe( Effect.flatMap((invocation) => Option.isSome(invocation) ? invocation.value.markDelegated : Effect.void, @@ -43,12 +44,12 @@ const makeStackStateEnv = ( const { project } = link.value; return { SUPABASE_INTERNAL_STACK_STATE: "1", - SUPABASE_INTERNAL_LINKED_PROJECT: JSON.stringify({ + SUPABASE_INTERNAL_LINKED_PROJECT: yield* encodeJson({ ref: project.ref, name: project.name ?? "", organization_id: project.organization_id ?? "", organization_slug: project.organization_slug ?? "", - }), + }).pipe(Effect.orDie), }; }); @@ -73,10 +74,15 @@ export type BinaryResolution = | { readonly found: string } | { readonly notFound: ReadonlyArray }; -function resolveBinary(): BinaryResolution { +const resolveBinary = Effect.fnUntraced(function* ( + fs: FileSystem.FileSystem, + path: Path.Path, +): Effect.fn.Return { + const exists = (candidate: string) => + fs.exists(candidate).pipe(Effect.orElseSucceed(() => false)); const tried: string[] = []; - const envBin = process.env["SUPABASE_GO_BINARY"]; + const envBin = yield* shellEnv("SUPABASE_GO_BINARY"); if (envBin) return { found: envBin }; tried.push("$SUPABASE_GO_BINARY (unset)"); @@ -85,25 +91,27 @@ function resolveBinary(): BinaryResolution { // When running as a compiled standalone binary (exec'd by the base shim), process.execPath is // this binary's own path; look for supabase-go co-located next to it. const colocated = path.join(path.dirname(process.execPath), `supabase-go${ext}`); - if (existsSync(colocated)) return { found: colocated }; + if (yield* exists(colocated)) return { found: colocated }; tried.push(`${colocated} (not found alongside the shim)`); // When running from source, resolve via installed npm packages. - // Guard with existsSync — in dev the workspace stub packages exist but their bin/ is empty. + // Guard with exists — in dev the workspace stub packages exist but their bin/ is empty. const candidates = PLATFORM_CANDIDATES[process.platform]?.[os.arch()] ?? []; for (const suffix of candidates) { - try { - const pkgPath = path.dirname(require.resolve(`@supabase/cli-${suffix}/package.json`)); - const bin = path.join(pkgPath, "bin", `supabase-go${ext}`); - if (existsSync(bin)) return { found: bin }; - tried.push(`${bin} (npm package present, binary missing)`); - } catch { + const pkgJson = yield* Effect.try(() => + require.resolve(`@supabase/cli-${suffix}/package.json`), + ).pipe(Effect.option); + if (Option.isNone(pkgJson)) { tried.push(`@supabase/cli-${suffix} (npm package not installed)`); + continue; } + const bin = path.join(path.dirname(pkgJson.value), "bin", `supabase-go${ext}`); + if (yield* exists(bin)) return { found: bin }; + tried.push(`${bin} (npm package present, binary missing)`); } return { notFound: tried }; -} +}); /** * Builds a `curl | tar` install snippet for the host platform, using the version baked into this @@ -177,7 +185,7 @@ export function makeGoProxyLayer(opts?: { parentOwnsCapturedSuccessTail?: boolean; /** * Override binary resolution. Primarily a test seam so specs don't have to - * mutate `process.env.SUPABASE_GO_BINARY` or stub the filesystem: + * pin `SUPABASE_GO_BINARY` or stub the filesystem: * - `string` — treat as the resolved Go binary path. * - `{ notFound: [...] }` — simulate the not-found path; `.exec` will print * the diagnostic and fail with a non-zero exit code. @@ -202,7 +210,7 @@ export function makeGoProxyLayer(opts?: { const resolved: BinaryResolution = typeof opts?.binary === "string" ? { found: opts.binary } - : (opts?.binary ?? resolveBinary()); + : (opts?.binary ?? (yield* resolveBinary(fs, path))); const globalArgs = opts?.globalArgs ?? []; const projectWorkdir = Effect.gen(function* () { const envValue = yield* Config.option(Config.String("SUPABASE_WORKDIR")).pipe( @@ -227,12 +235,10 @@ export function makeGoProxyLayer(opts?: { yield* Effect.sync(() => { process.stderr.write(`${formatGoBinaryNotFoundError(resolved.notFound)}\n`); }); - return yield* Effect.fail( - new GoChildExitError({ - exitCode: 1, - message: "supabase-go binary not found", - }), - ); + return yield* new GoChildExitError({ + exitCode: 1, + message: "supabase-go binary not found", + }); } const binary = resolved.found; @@ -273,12 +279,10 @@ export function makeGoProxyLayer(opts?: { .pipe(Effect.orDie), ); if (exitCode !== 0) { - return yield* Effect.fail( - new GoChildExitError({ - exitCode, - message: `supabase-go exited with code ${exitCode} (see stderr for details)`, - }), - ); + return yield* new GoChildExitError({ + exitCode, + message: `supabase-go exited with code ${exitCode} (see stderr for details)`, + }); } yield* markDelegated; }), @@ -290,12 +294,10 @@ export function makeGoProxyLayer(opts?: { yield* Effect.sync(() => { process.stderr.write(`${formatGoBinaryNotFoundError(resolved.notFound)}\n`); }); - return yield* Effect.fail( - new GoChildExitError({ - exitCode: 1, - message: "supabase-go binary not found", - }), - ); + return yield* new GoChildExitError({ + exitCode: 1, + message: "supabase-go binary not found", + }); } const binary = resolved.found; yield* processControl.holdSignals(["SIGINT", "SIGTERM", "SIGHUP"]); @@ -322,36 +324,30 @@ export function makeGoProxyLayer(opts?: { { executable: binary, argCount: argv.length }, (traceEnv) => Effect.gen(function* () { - const handle = yield* spawner - .spawn( - ChildProcess.make(binary, argv, { - cwd: execOpts?.cwd ?? opts?.cwd, - env: { ...env, ...traceEnv }, - extendEnv: true, - stdin: execOpts?.stdin ?? "inherit", - stdout: "pipe", - stderr: "inherit", - detached: false, - }), - ) - .pipe(Effect.orDie); + const handle = yield* spawner.spawn( + ChildProcess.make(binary, argv, { + cwd: execOpts?.cwd ?? opts?.cwd, + env: { ...env, ...traceEnv }, + extendEnv: true, + stdin: execOpts?.stdin ?? "inherit", + stdout: "pipe", + stderr: "inherit", + detached: false, + }), + ); // Drain stdout fully before awaiting exit so a full pipe buffer can't // deadlock the child. - const captured = yield* Stream.mkString(Stream.decodeText(handle.stdout)).pipe( - Effect.orDie, - ); - const exitCode = yield* handle.exitCode.pipe(Effect.orDie); + const captured = yield* Stream.mkString(Stream.decodeText(handle.stdout)); + const exitCode = yield* handle.exitCode; return { captured, exitCode }; - }), + }).pipe(Effect.orDie), (result) => result.exitCode, ); if (exitCode !== 0) { - return yield* Effect.fail( - new GoChildExitError({ - exitCode, - message: `supabase-go exited with code ${exitCode} (see stderr for details)`, - }), - ); + return yield* new GoChildExitError({ + exitCode, + message: `supabase-go exited with code ${exitCode} (see stderr for details)`, + }); } if (opts?.parentOwnsCapturedSuccessTail !== true) { yield* markDelegated; diff --git a/apps/cli/src/command-internal/viper-env.ts b/apps/cli/src/command-internal/viper-env.ts index 47c095ccc2..96588f2866 100644 --- a/apps/cli/src/command-internal/viper-env.ts +++ b/apps/cli/src/command-internal/viper-env.ts @@ -16,7 +16,7 @@ function viperBool(raw: string | undefined): boolean { } /** Env-record providers never fail this read, so a `ConfigError` here is a defect. */ -const shellEnv = (name: string) => +export const shellEnv = (name: string) => Config.option(Config.String(name)).pipe(Effect.map(Option.getOrUndefined), Effect.orDie); /** Reads a single `SUPABASE_*` boolean env var through `Config` (see module doc). */ From 4b975f4a089760d7ea042810a43a69e7dae8089b Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:19:04 +0530 Subject: [PATCH 2/4] test(cli): run the delegated binary tests as effects --- .../go-proxy-target-env.integration.test.ts | 4 +- .../go-proxy.layer.unit.test.ts | 93 +++++++++++-------- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/apps/cli/src/command-internal/go-proxy-target-env.integration.test.ts b/apps/cli/src/command-internal/go-proxy-target-env.integration.test.ts index 91125ffe75..b01cc5afe8 100644 --- a/apps/cli/src/command-internal/go-proxy-target-env.integration.test.ts +++ b/apps/cli/src/command-internal/go-proxy-target-env.integration.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@effect/vitest"; import { BunServices } from "@effect/platform-bun"; -import { Effect, FileSystem, Layer, Option, Path } from "effect"; +import { Effect, FileSystem, Layer, Option, Path, Schema } from "effect"; import { useTempWorkdir, VALID_TOKEN } from "../../tests/helpers/command-mocks.ts"; import { isolatedHomeLayer } from "../../tests/helpers/command-mocks.ts"; @@ -79,7 +79,7 @@ const writeStackLink = Effect.fnUntraced(function* (workdir: string, poolerUrl?: yield* fs.makeDirectory(path.join(workdir, "supabase"), { recursive: true }); yield* fs.writeFileString( path.join(workdir, "supabase", ".link.json"), - JSON.stringify({ + yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ project: { ref: LINKED_REF }, active_branch: { ref: LINKED_REF, diff --git a/apps/cli/src/command-internal/go-proxy.layer.unit.test.ts b/apps/cli/src/command-internal/go-proxy.layer.unit.test.ts index 61bfd1cdad..a4bcfce155 100644 --- a/apps/cli/src/command-internal/go-proxy.layer.unit.test.ts +++ b/apps/cli/src/command-internal/go-proxy.layer.unit.test.ts @@ -9,6 +9,8 @@ import { FileSystem, Layer, Path, + PlatformError, + Schema, Sink, Stream, } from "effect"; @@ -100,7 +102,7 @@ function mockProcessControl() { ).pipe(Effect.asVoid), exit, setExitCode: () => Effect.void, - getExitCode: Effect.succeed(undefined), + getExitCode: Effect.undefined, }), ), }; @@ -131,7 +133,14 @@ function mockSpawner(exit: ExitBehavior, spawnedBeforeExit?: Deferred.Deferred { const TRIED = ["$SUPABASE_GO_BINARY (unset)"]; const PINNED_VERSION = "2.100.0"; - async function withMockedHost( + function withMockedHost( opts: { platform: NodeJS.Platform; arch: NodeJS.Architecture }, - fn: (mod: typeof import("./go-proxy.layer.ts")) => void | Promise, - ): Promise { - vi.resetModules(); - vi.doMock("../shared/cli/version.ts", () => ({ CLI_VERSION: PINNED_VERSION })); - const originalPlatform = process.platform; - const originalArch = process.arch; - Object.defineProperty(process, "platform", { value: opts.platform, configurable: true }); - Object.defineProperty(process, "arch", { value: opts.arch, configurable: true }); - try { - const mod = await import("./go-proxy.layer.ts"); - await fn(mod); - } finally { - Object.defineProperty(process, "platform", { - value: originalPlatform, - configurable: true, - }); - Object.defineProperty(process, "arch", { value: originalArch, configurable: true }); - vi.doUnmock("../cli/version.ts"); - vi.resetModules(); - } + fn: (mod: typeof import("./go-proxy.layer.ts")) => void, + ): Effect.Effect { + return Effect.acquireUseRelease( + Effect.sync(() => { + vi.resetModules(); + vi.doMock("../shared/cli/version.ts", () => ({ CLI_VERSION: PINNED_VERSION })); + const originalPlatform = process.platform; + const originalArch = process.arch; + Object.defineProperty(process, "platform", { value: opts.platform, configurable: true }); + Object.defineProperty(process, "arch", { value: opts.arch, configurable: true }); + return { originalPlatform, originalArch }; + }), + () => Effect.promise(() => import("./go-proxy.layer.ts")).pipe(Effect.map(fn)), + ({ originalPlatform, originalArch }) => + Effect.sync(() => { + Object.defineProperty(process, "platform", { + value: originalPlatform, + configurable: true, + }); + Object.defineProperty(process, "arch", { value: originalArch, configurable: true }); + vi.doUnmock("../cli/version.ts"); + vi.resetModules(); + }), + ); } - it("renders a copy-pasteable install snippet for linux x64", async () => { - await withMockedHost({ platform: "linux", arch: "x64" }, (mod) => { + it.effect("renders a copy-pasteable install snippet for linux x64", () => + withMockedHost({ platform: "linux", arch: "x64" }, (mod) => { const message = mod.formatGoBinaryNotFoundError(TRIED); expect(message).toContain( `https://github.com/supabase/cli/releases/download/v${PINNED_VERSION}/supabase_${PINNED_VERSION}_linux_amd64.tar.gz`, ); expect(message).toContain(`mkdir -p "$HOME/.local/share/supabase"`); expect(message).toContain(`export PATH="$HOME/.local/share/supabase:$PATH"`); - }); - }); + }), + ); - it("maps Node's win32 platform to the release asset's `windows` slug", async () => { - await withMockedHost({ platform: "win32", arch: "x64" }, (mod) => { + it.effect("maps Node's win32 platform to the release asset's `windows` slug", () => + withMockedHost({ platform: "win32", arch: "x64" }, (mod) => { const message = mod.formatGoBinaryNotFoundError(TRIED); expect(message).toContain( `https://github.com/supabase/cli/releases/download/v${PINNED_VERSION}/supabase_${PINNED_VERSION}_windows_amd64.tar.gz`, ); expect(message).not.toContain("win32"); - }); - }); + }), + ); - it("maps darwin arm64 to the matching release asset", async () => { - await withMockedHost({ platform: "darwin", arch: "arm64" }, (mod) => { + it.effect("maps darwin arm64 to the matching release asset", () => + withMockedHost({ platform: "darwin", arch: "arm64" }, (mod) => { expect(mod.formatGoBinaryNotFoundError(TRIED)).toContain( `supabase_${PINNED_VERSION}_darwin_arm64.tar.gz`, ); - }); - }); + }), + ); - it("omits the snippet on unsupported architectures (no release asset)", async () => { - await withMockedHost({ platform: "linux", arch: "ia32" }, (mod) => { + it.effect("omits the snippet on unsupported architectures (no release asset)", () => + withMockedHost({ platform: "linux", arch: "ia32" }, (mod) => { expect(mod.formatGoBinaryNotFoundError(TRIED)).not.toContain("curl -sL"); - }); - }); + }), + ); }); const proxyDeps = ( @@ -408,7 +421,7 @@ describe("makeGoProxyLayer", () => { ); const linked = { stackState: "1", - linked: JSON.stringify({ + linked: yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ ref: "abcdefghijklmnopqrst", name: "Acme", organization_id: "org_123", From 7ab289dff6880883b4aa5f438dcacc7b394e126e Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:19:08 +0530 Subject: [PATCH 3/4] refactor(cli): write the project link through schema json --- apps/cli/src/command-internal/project-link.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/command-internal/project-link.ts b/apps/cli/src/command-internal/project-link.ts index 77fb833b83..3817e92bf3 100644 --- a/apps/cli/src/command-internal/project-link.ts +++ b/apps/cli/src/command-internal/project-link.ts @@ -9,6 +9,8 @@ import { ErrorActionabilityId, } from "../shared/telemetry/error-actionability.ts"; +const encodePrettyJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown, { space: 2 })); + /** Document name under `/supabase`, local to one checkout. */ const PROJECT_LINK_FILENAME = ".link.json"; @@ -167,7 +169,8 @@ export const writeProjectLink = Effect.fn("ProjectLink.write")(function* ( yield* Effect.gen(function* () { yield* fs.makeDirectory(path.dirname(file), { recursive: true }); yield* ensureGitignore(fs, path, workdir); - yield* publish(fs, path, { target: file, content: `${JSON.stringify(link, null, 2)}\n` }); + const content = yield* encodePrettyJson(link).pipe(Effect.orDie); + yield* publish(fs, path, { target: file, content: `${content}\n` }); }).pipe( Effect.catchTags({ PlatformError: (error) => From 341ce310c8ccbbb8ce281feda59b2318ef5d0a9c Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:19:13 +0530 Subject: [PATCH 4/4] test(cli): run the permissions help tests as effects --- .../help-section.integration.test.ts | 47 ++++++-- .../help-section.tree.unit.test.ts | 104 +++++++++--------- 2 files changed, 92 insertions(+), 59 deletions(-) diff --git a/apps/cli/src/command-internal/command-permissions/help-section.integration.test.ts b/apps/cli/src/command-internal/command-permissions/help-section.integration.test.ts index 74c989c158..35f2bf4638 100644 --- a/apps/cli/src/command-internal/command-permissions/help-section.integration.test.ts +++ b/apps/cli/src/command-internal/command-permissions/help-section.integration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@effect/vitest"; -import { Console, Effect, Layer, Option } from "effect"; +import { Console, Effect, Layer, Option, Schema } from "effect"; import { CliOutput, Command } from "effect/cli"; import { emptyEnv, fakeConsole, mockOutput } from "../../../tests/helpers/mocks.ts"; @@ -17,6 +17,33 @@ import { YesFlag, } from "../global-flags.ts"; +/* The slice of a command's JSON help that the section assertions read. */ +const HelpJson = Schema.Struct({ + doc: Schema.Unknown, + permissions: Schema.Struct({ + schemes: Schema.Array( + Schema.Struct({ + groups: Schema.Array( + Schema.Struct({ + heading: Schema.String, + rows: Schema.Array( + Schema.Union([ + Schema.Struct({ anyOf: Schema.Array(Schema.Array(Schema.String)) }), + Schema.Struct({ + cases: Schema.Array( + Schema.Struct({ for: Schema.String, allOf: Schema.Array(Schema.String) }), + ), + }), + Schema.Struct({ note: Schema.String }), + ]), + ), + }), + ), + }), + ), + }), +}); + const layerFor = ( args: ReadonlyArray, console: Console.Console, @@ -77,9 +104,12 @@ describe("token permissions in command help", () => { Effect.gen(function* () { const text = yield* helpFor(["api-keys", "--help"]); const printed = yield* helpFor(["api-keys", "--help"], jsonCliOutputFormatter()); - const json = JSON.parse(printed.replace(/^log:/, "")); + const raw = yield* Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown))( + printed.replace(/^log:/, ""), + ); + const json = yield* Schema.decodeUnknownEffect(HelpJson)(raw); - expect(json.permissions).toEqual({ + expect(raw).toHaveProperty("permissions", { status: "mapped", schemes: [ { @@ -113,7 +143,9 @@ describe("token permissions in command help", () => { }, ], }); - expect(JSON.stringify(json.doc)).not.toContain("supabase/Command"); + expect( + yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(json.doc), + ).not.toContain("supabase/Command"); const block = text.slice(text.indexOf("PERMISSIONS")); for (const scheme of json.permissions.schemes) { @@ -122,12 +154,9 @@ describe("token permissions in command help", () => { for (const row of group.rows) { const shown: ReadonlyArray = "anyOf" in row - ? row.anyOf.map((alternative: ReadonlyArray) => alternative.join(" + ")) + ? row.anyOf.map((alternative) => alternative.join(" + ")) : "cases" in row - ? row.cases.map( - (item: { for: string; allOf: ReadonlyArray }) => - `${item.allOf.join(" + ")} for ${item.for}`, - ) + ? row.cases.map((item) => `${item.allOf.join(" + ")} for ${item.for}`) : [row.note]; for (const text of shown) expect(block).toContain(text); } diff --git a/apps/cli/src/command-internal/command-permissions/help-section.tree.unit.test.ts b/apps/cli/src/command-internal/command-permissions/help-section.tree.unit.test.ts index b48f858f26..a506950c1f 100644 --- a/apps/cli/src/command-internal/command-permissions/help-section.tree.unit.test.ts +++ b/apps/cli/src/command-internal/command-permissions/help-section.tree.unit.test.ts @@ -1,7 +1,7 @@ import { BunServices } from "@effect/platform-bun"; import { Context, Effect, Exit, Option } from "effect"; import type { Command } from "effect/cli"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "@effect/vitest"; import { commandTreeFor } from "../../../tests/helpers/command-flags.ts"; import type { PermissionVariant } from "../../../tests/helpers/permission-registry.ts"; @@ -69,39 +69,41 @@ describe("permission help section across the command tree", () => { ); }; - it("gives a flag a default only when parsing no arguments yields that value", async () => { - for (const [path, node] of leaves) { - const declared = Option.getOrUndefined(readPermissions(node.command)); - if (declared?.status !== "mapped") continue; - const params = paramsFor(path); - // Help rebuilds a parent group's flags without defaults, so only these carry one. - const known = new Map( - [...rootFlags, ...permissionFlagsOf(ownParams(node.command))].map((flag) => [ - flag.name, - flag, - ]), - ); - const tested = new Set( - declared.operations.flatMap((entry) => - (entry.when ?? []).flatMap((condition) => - condition.oneOf === undefined ? [] : [condition.flag], + it.effect("gives a flag a default only when parsing no arguments yields that value", () => + Effect.gen(function* () { + for (const [path, node] of leaves) { + const declared = Option.getOrUndefined(readPermissions(node.command)); + if (declared?.status !== "mapped") continue; + const params = paramsFor(path); + // Help rebuilds a parent group's flags without defaults, so only these carry one. + const known = new Map( + [...rootFlags, ...permissionFlagsOf(ownParams(node.command))].map((flag) => [ + flag.name, + flag, + ]), + ); + const tested = new Set( + declared.operations.flatMap((entry) => + (entry.when ?? []).flatMap((condition) => + condition.oneOf === undefined ? [] : [condition.flag], + ), ), - ), - ); - for (const flag of tested) { - const param = params.get(flag); - if (flag === undefined || param === undefined) continue; - const exit = await Effect.runPromiseExit( - param.parse({ flags: {}, arguments: [] }).pipe(Effect.provide(BunServices.layer)), ); - const parsed = Exit.isSuccess(exit) ? exit.value[1] : undefined; - expect( - known.get(flag)?.default, - `"${path}" --${flag} default differs from parsing no arguments`, - ).toBe(typeof parsed === "string" ? parsed : undefined); + for (const flag of tested) { + const param = params.get(flag); + if (flag === undefined || param === undefined) continue; + const exit = yield* param + .parse({ flags: {}, arguments: [] }) + .pipe(Effect.provide(BunServices.layer), Effect.exit); + const parsed = Exit.isSuccess(exit) ? exit.value[1] : undefined; + expect( + known.get(flag)?.default, + `"${path}" --${flag} default differs from parsing no arguments`, + ).toBe(typeof parsed === "string" ? parsed : undefined); + } } - } - }); + }), + ); it("never tests the presence of a text flag that has a default", () => { for (const [path, node] of leaves) { @@ -120,28 +122,30 @@ describe("permission help section across the command tree", () => { } }); - it("never turns on by default a switch a condition tests", async () => { - for (const [path, node] of leaves) { - const declared = Option.getOrUndefined(readPermissions(node.command)); - if (declared?.status !== "mapped") continue; - const params = paramsFor(path); - const tested = new Set( - declared.operations.flatMap((entry) => - (entry.when ?? []).flatMap((condition) => - condition.is === undefined ? [] : [condition.flag], + it.effect("never turns on by default a switch a condition tests", () => + Effect.gen(function* () { + for (const [path, node] of leaves) { + const declared = Option.getOrUndefined(readPermissions(node.command)); + if (declared?.status !== "mapped") continue; + const params = paramsFor(path); + const tested = new Set( + declared.operations.flatMap((entry) => + (entry.when ?? []).flatMap((condition) => + condition.is === undefined ? [] : [condition.flag], + ), ), - ), - ); - for (const flag of tested) { - const param = params.get(flag); - if (param === undefined || catalogFor(path).get(flag)?.hidden !== false) continue; - const [, value] = await Effect.runPromise( - param.parse({ flags: {}, arguments: [] }).pipe(Effect.provide(BunServices.layer)), ); - expect(value, `"${path}" --${flag} defaults to on`).not.toBe(true); + for (const flag of tested) { + const param = params.get(flag); + if (param === undefined || catalogFor(path).get(flag)?.hidden !== false) continue; + const [, value] = yield* param + .parse({ flags: {}, arguments: [] }) + .pipe(Effect.provide(BunServices.layer)); + expect(value, `"${path}" --${flag} defaults to on`).not.toBe(true); + } } - } - }); + }), + ); it("records every flag a command accepts when it declares its permissions", () => { for (const [path, node] of leaves) {