t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/Cli/exitCodes.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

219 lines
7.1 KiB
TypeScript

import { PlatformServices } from "@/Util/PlatformServices.ts";
import { describe, expect, it } from "alchemy-test";
import { nodePath, nodeSupportsDevMode } from "../nodeProbe.ts";
import * as Effect from "effect/Effect";
import * as Stream from "effect/Stream";
import * as FileSystem from "effect/FileSystem";
import * as ChildProcess from "effect/process/ChildProcess";
import { fileURLToPath } from "node:url";
const CLI = fileURLToPath(new URL("../../bin/cli.js", import.meta.url));
/**
* Run the CLI with no TTY on any stdio and return its exit code. Each
* invocation is a real `bun bin/cli.js` child with `ALCHEMY_HOME` pointed at
* a throwaway directory, so these pin the exit-code contract scripts and
* agents rely on — 0 only when the command completed — without touching the
* real `~/.alchemy`.
*/
const exitCodeOf = (
args: ReadonlyArray<string>,
env: Record<string, string> = {},
runtime = "bun",
) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const home = yield* fs.makeTempDirectoryScoped({
prefix: "alchemy-exit-codes-",
});
const handle = yield* ChildProcess.make(runtime, [CLI, ...args], {
env: { ALCHEMY_HOME: home, ...env },
extendEnv: true,
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
killSignal: "SIGTERM",
forceKillAfter: "1 second",
});
return yield* handle.exitCode;
}).pipe(Effect.scoped, Effect.provide(PlatformServices));
/** Like {@link exitCodeOf}, but from an empty project directory with stderr captured. */
const runInEmptyProject = (args: ReadonlyArray<string>, runtime = "bun") =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const home = yield* fs.makeTempDirectoryScoped({
prefix: "alchemy-exit-codes-",
});
const project = yield* fs.makeTempDirectoryScoped({
prefix: "alchemy-empty-project-",
});
const handle = yield* ChildProcess.make(runtime, [CLI, ...args], {
cwd: project,
env: { ALCHEMY_HOME: home },
extendEnv: true,
stdin: "ignore",
stdout: "ignore",
stderr: "pipe",
killSignal: "SIGTERM",
forceKillAfter: "1 second",
});
const [stderr, exitCode] = yield* Effect.all(
[handle.stderr.pipe(Stream.decodeText, Stream.mkString), handle.exitCode],
{ concurrency: 2 },
);
return { stderr, exitCode };
}).pipe(Effect.scoped, Effect.provide(PlatformServices));
describe("CLI exit codes", { tags: ["unit", "local"] }, () => {
it.live("dev without a stack entrypoint reports it and exits 1", () =>
Effect.gen(function* () {
const { stderr, exitCode } = yield* runInEmptyProject(["dev"]);
expect(exitCode).toBe(1);
expect(stderr).toContain(
"Stack entrypoint 'alchemy.run.ts' does not exist",
);
expect(stderr).not.toContain("PlatformError");
expect(stderr).not.toContain("at Effect.fn");
}),
);
it.live.skipIf(!nodeSupportsDevMode)(
"dev without a stack entrypoint reports it and exits 1 under node",
() =>
Effect.gen(function* () {
const { stderr, exitCode } = yield* runInEmptyProject(
["dev"],
nodePath!,
);
expect(exitCode).toBe(1);
expect(stderr).toContain(
"Stack entrypoint 'alchemy.run.ts' does not exist",
);
expect(stderr).not.toContain("PlatformError");
}),
);
it.live("plan accepts --adopt like deploy --dry-run", () =>
Effect.gen(function* () {
const { stderr, exitCode } = yield* runInEmptyProject([
"plan",
"--adopt",
]);
expect(exitCode).toBe(1);
expect(stderr).not.toContain("Unrecognized flag");
expect(stderr).toContain(
"Stack entrypoint 'alchemy.run.ts' does not exist",
);
}),
);
it.live("bare `profile` without a terminal prints help and exits 1", () =>
Effect.gen(function* () {
expect(yield* exitCodeOf(["profile"])).toBe(1);
}),
);
it.live("bare `state` without a terminal prints help and exits 1", () =>
Effect.gen(function* () {
expect(yield* exitCodeOf(["state"])).toBe(1);
}),
);
it.live("--no-input is accepted and forces a plain, working run", () =>
Effect.gen(function* () {
expect(yield* exitCodeOf(["--no-input", "profile", "list"])).toBe(0);
}),
);
it.live("--help exits 0", () =>
Effect.gen(function* () {
expect(yield* exitCodeOf(["--help"])).toBe(0);
}),
);
// Pins buildless node dev: the launcher must run the checkout's .ts/.tsx
// source under plain node (type stripping + the register-dev-mode hooks)
// — --help renders the TSX help view through the full terminal runtime.
// Stderr is captured: this is the one test in the file whose failure
// mode is environmental, so "expected 9 to be 0" alone is useless.
it.live.skipIf(!nodeSupportsDevMode)(
"--help exits 0 under node from source, no build required",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const home = yield* fs.makeTempDirectoryScoped({
prefix: "alchemy-exit-codes-",
});
const handle = yield* ChildProcess.make(nodePath!, [CLI, "--help"], {
// The test runner is bun and marks every child's env as
// bun-invoked; blank the markers so the launcher takes its
// node path like a real `node bin/cli.js` invocation.
env: {
ALCHEMY_HOME: home,
npm_execpath: "",
npm_config_user_agent: "",
},
extendEnv: true,
stdin: "ignore",
stdout: "ignore",
stderr: "pipe",
killSignal: "SIGTERM",
forceKillAfter: "1 second",
});
const [stderr, exitCode] = yield* Effect.all(
[
handle.stderr.pipe(
Stream.decodeText,
Stream.runCollect,
Effect.map((chunks) => chunks.join("")),
),
handle.exitCode,
],
{ concurrency: 2 },
);
expect({
exitCode,
stderr: exitCode === 0 ? "" : stderr,
}).toEqual({ exitCode: 0, stderr: "" });
}).pipe(Effect.scoped, Effect.provide(PlatformServices)),
);
it.live("provider check-env exits 1 when a required var is missing", () =>
Effect.gen(function* () {
expect(
yield* exitCodeOf(["provider", "check-env", "--provider", "neon"], {
NEON_API_KEY: "",
}),
).toBe(1);
}),
);
it.live("provider check-env exits 0 when the contract is satisfied", () =>
Effect.gen(function* () {
expect(
yield* exitCodeOf(["provider", "check-env", "--provider", "neon"], {
NEON_API_KEY: "napi_test_key",
}),
).toBe(0);
}),
);
it.live("provider check-env accepts an explicit profile", () =>
Effect.gen(function* () {
expect(
yield* exitCodeOf(
[
"provider",
"check-env",
"--profile",
"default",
"--provider",
"neon",
],
{ NEON_API_KEY: "napi_test_key" },
),
).toBe(0);
}),
);
});