Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<string>,
console: Console.Console,
Expand Down Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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) {
Expand All @@ -122,12 +154,9 @@ describe("token permissions in command help", () => {
for (const row of group.rows) {
const shown: ReadonlyArray<string> =
"anyOf" in row
? row.anyOf.map((alternative: ReadonlyArray<string>) => alternative.join(" + "))
? row.anyOf.map((alternative) => alternative.join(" + "))
: "cases" in row
? row.cases.map(
(item: { for: string; allOf: ReadonlyArray<string> }) =>
`${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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading