mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-11 21:01:16 +02:00
219 lines
7.1 KiB
TypeScript
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);
|
|
}),
|
|
);
|
|
});
|