Merge commit '3c60637c1a27da8ba66888de518d58d5707801f2' as 'repos/effect-smol'
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import { Config, ConfigProvider, Effect, FileSystem, Layer, Option, Path, Ref, Stdio } from "effect"
|
||||
import { TestConsole } from "effect/testing"
|
||||
import { Argument, CliError, Flag, Prompt } from "effect/unstable/cli"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process"
|
||||
import * as MockTerminal from "./services/MockTerminal.ts"
|
||||
|
||||
const ConsoleLayer = TestConsole.layer
|
||||
const FileSystemLayer = FileSystem.layerNoop({})
|
||||
const PathLayer = Path.layer
|
||||
const TerminalLayer = MockTerminal.layer
|
||||
const StdioLayer = Stdio.layerTest({})
|
||||
const ChildProcessSpawnerLayer = Layer.succeed(
|
||||
ChildProcessSpawner.ChildProcessSpawner,
|
||||
ChildProcessSpawner.make(() => Effect.die("Not implemented"))
|
||||
)
|
||||
|
||||
const TestLayer = Layer.mergeAll(
|
||||
ConsoleLayer,
|
||||
FileSystemLayer,
|
||||
PathLayer,
|
||||
TerminalLayer,
|
||||
StdioLayer,
|
||||
ChildProcessSpawnerLayer
|
||||
)
|
||||
|
||||
describe("Param", () => {
|
||||
describe("optional", () => {
|
||||
it.effect("returns none when an optional boolean flag is omitted", () =>
|
||||
Effect.gen(function*() {
|
||||
const flag = Flag.boolean("verbose").pipe(Flag.optional)
|
||||
|
||||
const [, value] = yield* flag.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
|
||||
assert.deepStrictEqual(value, Option.none())
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("returns some when an optional boolean flag is provided", () =>
|
||||
Effect.gen(function*() {
|
||||
const flag = Flag.boolean("verbose").pipe(Flag.optional)
|
||||
|
||||
const [, value] = yield* flag.parse({
|
||||
flags: { verbose: ["false"] },
|
||||
arguments: []
|
||||
})
|
||||
|
||||
assert.deepStrictEqual(value, Option.some(false))
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
})
|
||||
|
||||
describe("withFallbackPrompt", () => {
|
||||
it.effect("prompts for missing flag values and preserves remaining args", () =>
|
||||
Effect.gen(function*() {
|
||||
const prompt = Prompt.text({ message: "Name" })
|
||||
const flag = Flag.string("name").pipe(Flag.withFallbackPrompt(prompt))
|
||||
|
||||
yield* MockTerminal.inputText("Chandra")
|
||||
yield* MockTerminal.inputKey("enter")
|
||||
|
||||
const [remaining, value] = yield* flag.parse({
|
||||
flags: {},
|
||||
arguments: ["tail"]
|
||||
})
|
||||
|
||||
assert.strictEqual(value, "Chandra")
|
||||
assert.deepStrictEqual(remaining, ["tail"])
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("does not prompt when flag value is provided", () =>
|
||||
Effect.gen(function*() {
|
||||
const prompt = Prompt.text({ message: "Name" })
|
||||
const flag = Flag.string("name").pipe(Flag.withFallbackPrompt(prompt))
|
||||
|
||||
const [, value] = yield* flag.parse({
|
||||
flags: { name: ["Ava"] },
|
||||
arguments: []
|
||||
})
|
||||
|
||||
assert.strictEqual(value, "Ava")
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("prompts for missing arguments", () =>
|
||||
Effect.gen(function*() {
|
||||
const prompt = Prompt.text({ message: "File" })
|
||||
const argument = Argument.string("file").pipe(Argument.withFallbackPrompt(prompt))
|
||||
|
||||
yield* MockTerminal.inputText("notes.txt")
|
||||
yield* MockTerminal.inputKey("enter")
|
||||
|
||||
const [remaining, value] = yield* argument.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
|
||||
assert.strictEqual(value, "notes.txt")
|
||||
assert.deepStrictEqual(remaining, [])
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("builds flag fallback prompts from effects lazily", () =>
|
||||
Effect.gen(function*() {
|
||||
const calls = yield* Ref.make(0)
|
||||
const prompt = Effect.gen(function*() {
|
||||
yield* Ref.update(calls, (n) => n + 1)
|
||||
return Prompt.text({ message: "Name from effect" })
|
||||
})
|
||||
|
||||
const flag = Flag.string("name").pipe(Flag.withFallbackPrompt(prompt))
|
||||
|
||||
const [, provided] = yield* flag.parse({
|
||||
flags: { name: ["Ava"] },
|
||||
arguments: []
|
||||
})
|
||||
assert.strictEqual(provided, "Ava")
|
||||
assert.strictEqual(yield* Ref.get(calls), 0)
|
||||
|
||||
yield* MockTerminal.inputText("Mia")
|
||||
yield* MockTerminal.inputKey("enter")
|
||||
|
||||
const [remaining, prompted] = yield* flag.parse({
|
||||
flags: {},
|
||||
arguments: ["tail"]
|
||||
})
|
||||
|
||||
assert.strictEqual(prompted, "Mia")
|
||||
assert.deepStrictEqual(remaining, ["tail"])
|
||||
assert.strictEqual(yield* Ref.get(calls), 1)
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("builds argument fallback prompts from effects lazily", () =>
|
||||
Effect.gen(function*() {
|
||||
const calls = yield* Ref.make(0)
|
||||
const prompt = Effect.gen(function*() {
|
||||
yield* Ref.update(calls, (n) => n + 1)
|
||||
return Prompt.text({ message: "File from effect" })
|
||||
})
|
||||
|
||||
const argument = Argument.string("file").pipe(Argument.withFallbackPrompt(prompt))
|
||||
|
||||
const [providedRemaining, provided] = yield* argument.parse({
|
||||
flags: {},
|
||||
arguments: ["notes.txt", "tail"]
|
||||
})
|
||||
assert.strictEqual(provided, "notes.txt")
|
||||
assert.deepStrictEqual(providedRemaining, ["tail"])
|
||||
assert.strictEqual(yield* Ref.get(calls), 0)
|
||||
|
||||
yield* MockTerminal.inputText("draft.md")
|
||||
yield* MockTerminal.inputKey("enter")
|
||||
|
||||
const [promptedRemaining, prompted] = yield* argument.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
|
||||
assert.strictEqual(prompted, "draft.md")
|
||||
assert.deepStrictEqual(promptedRemaining, [])
|
||||
assert.strictEqual(yield* Ref.get(calls), 1)
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("propagates errors from effectful fallback prompt creation", () =>
|
||||
Effect.gen(function*() {
|
||||
const failure = new CliError.UserError({
|
||||
cause: "failed to build prompt"
|
||||
})
|
||||
const prompt = Effect.fail(failure)
|
||||
|
||||
const flag = Flag.string("name").pipe(Flag.withFallbackPrompt(prompt))
|
||||
|
||||
const error = yield* Effect.flip(
|
||||
flag.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
)
|
||||
|
||||
assert.strictEqual(error, failure)
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("prefers defaults over fallback prompts", () =>
|
||||
Effect.gen(function*() {
|
||||
const prompt = Prompt.text({ message: "Name" })
|
||||
const flag = Flag.string("name").pipe(
|
||||
Flag.withDefault("guest"),
|
||||
Flag.withFallbackPrompt(prompt)
|
||||
)
|
||||
|
||||
const [, value] = yield* flag.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
|
||||
assert.strictEqual(value, "guest")
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("does not prompt for invalid flag values", () =>
|
||||
Effect.gen(function*() {
|
||||
const prompt = Prompt.text({ message: "Count" })
|
||||
const flag = Flag.integer("count").pipe(Flag.withFallbackPrompt(prompt))
|
||||
|
||||
const error = yield* Effect.flip(
|
||||
flag.parse({
|
||||
flags: { count: ["nope"] },
|
||||
arguments: []
|
||||
})
|
||||
)
|
||||
|
||||
assert.instanceOf(error, CliError.InvalidValue)
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("does not prompt for invalid argument values", () =>
|
||||
Effect.gen(function*() {
|
||||
const prompt = Prompt.text({ message: "Count" })
|
||||
const argument = Argument.integer("count").pipe(Argument.withFallbackPrompt(prompt))
|
||||
|
||||
const error = yield* Effect.flip(
|
||||
argument.parse({
|
||||
flags: {},
|
||||
arguments: ["nope"]
|
||||
})
|
||||
)
|
||||
|
||||
assert.instanceOf(error, CliError.InvalidValue)
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("does not prompt for missing boolean flags", () =>
|
||||
Effect.gen(function*() {
|
||||
const prompt = Prompt.text({ message: "Verbose" })
|
||||
const flag = Flag.boolean("verbose").pipe(Flag.withFallbackPrompt(prompt))
|
||||
|
||||
const [, value] = yield* flag.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
|
||||
assert.strictEqual(value, false)
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("returns MissingOption when prompt is cancelled", () =>
|
||||
Effect.gen(function*() {
|
||||
const prompt = Prompt.text({ message: "Name" })
|
||||
const flag = Flag.string("name").pipe(Flag.withFallbackPrompt(prompt))
|
||||
|
||||
yield* MockTerminal.inputKey("c", { ctrl: true })
|
||||
|
||||
const error = yield* Effect.flip(
|
||||
flag.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
)
|
||||
|
||||
assert.instanceOf(error, CliError.MissingOption)
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
|
||||
it.effect("returns MissingArgument when argument prompt is cancelled", () =>
|
||||
Effect.gen(function*() {
|
||||
const prompt = Prompt.text({ message: "File" })
|
||||
const argument = Argument.string("file").pipe(Argument.withFallbackPrompt(prompt))
|
||||
|
||||
yield* MockTerminal.inputKey("c", { ctrl: true })
|
||||
|
||||
const error = yield* Effect.flip(
|
||||
argument.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
)
|
||||
|
||||
assert.instanceOf(error, CliError.MissingArgument)
|
||||
}).pipe(Effect.provide(TestLayer)))
|
||||
})
|
||||
|
||||
describe("withFallbackConfig", () => {
|
||||
it.effect("uses ConfigProvider when a flag is missing", () => {
|
||||
const provider = ConfigProvider.fromEnv({
|
||||
env: {
|
||||
NAME: "Ava"
|
||||
}
|
||||
})
|
||||
|
||||
return Effect.gen(function*() {
|
||||
const flag = Flag.string("name").pipe(
|
||||
Flag.withFallbackConfig(Config.string("NAME"))
|
||||
)
|
||||
|
||||
const [, value] = yield* flag.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
|
||||
assert.strictEqual(value, "Ava")
|
||||
}).pipe(
|
||||
Effect.provideService(ConfigProvider.ConfigProvider, provider),
|
||||
Effect.provide(TestLayer)
|
||||
)
|
||||
})
|
||||
|
||||
it.effect("uses flag values before reading config fallbacks", () => {
|
||||
const provider = ConfigProvider.fromEnv({
|
||||
env: {
|
||||
NAME: "Ava"
|
||||
}
|
||||
})
|
||||
|
||||
return Effect.gen(function*() {
|
||||
const flag = Flag.string("name").pipe(
|
||||
Flag.withFallbackConfig(Config.string("NAME"))
|
||||
)
|
||||
|
||||
const [, value] = yield* flag.parse({
|
||||
flags: { name: ["Maya"] },
|
||||
arguments: []
|
||||
})
|
||||
|
||||
assert.strictEqual(value, "Maya")
|
||||
}).pipe(
|
||||
Effect.provideService(ConfigProvider.ConfigProvider, provider),
|
||||
Effect.provide(TestLayer)
|
||||
)
|
||||
})
|
||||
|
||||
it.effect("uses ConfigProvider when an argument is missing", () => {
|
||||
const provider = ConfigProvider.fromEnv({
|
||||
env: {
|
||||
REPOSITORY: "repo"
|
||||
}
|
||||
})
|
||||
|
||||
return Effect.gen(function*() {
|
||||
const argument = Argument.string("repository").pipe(
|
||||
Argument.withFallbackConfig(Config.string("REPOSITORY"))
|
||||
)
|
||||
|
||||
const [, value] = yield* argument.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
|
||||
assert.strictEqual(value, "repo")
|
||||
}).pipe(
|
||||
Effect.provideService(ConfigProvider.ConfigProvider, provider),
|
||||
Effect.provide(TestLayer)
|
||||
)
|
||||
})
|
||||
|
||||
it.effect("returns MissingOption when config is missing", () => {
|
||||
const provider = ConfigProvider.fromEnv({ env: {} })
|
||||
|
||||
return Effect.gen(function*() {
|
||||
const flag = Flag.string("name").pipe(
|
||||
Flag.withFallbackConfig(Config.string("NAME"))
|
||||
)
|
||||
|
||||
const error = yield* Effect.flip(
|
||||
flag.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
)
|
||||
|
||||
assert.instanceOf(error, CliError.MissingOption)
|
||||
}).pipe(
|
||||
Effect.provideService(ConfigProvider.ConfigProvider, provider),
|
||||
Effect.provide(TestLayer)
|
||||
)
|
||||
})
|
||||
|
||||
it.effect("returns InvalidValue when config fails to parse", () => {
|
||||
const provider = ConfigProvider.fromEnv({
|
||||
env: {
|
||||
COUNT: "nope"
|
||||
}
|
||||
})
|
||||
|
||||
return Effect.gen(function*() {
|
||||
const flag = Flag.integer("count").pipe(
|
||||
Flag.withFallbackConfig(Config.int("COUNT"))
|
||||
)
|
||||
|
||||
const error = yield* Effect.flip(
|
||||
flag.parse({
|
||||
flags: {},
|
||||
arguments: []
|
||||
})
|
||||
)
|
||||
|
||||
assert.instanceOf(error, CliError.InvalidValue)
|
||||
}).pipe(
|
||||
Effect.provideService(ConfigProvider.ConfigProvider, provider),
|
||||
Effect.provide(TestLayer)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user