Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,252 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { CLI_CODES } from "../errors/errors.js";
import { App, cli } from "./app.js";
function makeApp(writer: (s: string) => void = () => {}): App {
return new App({ name: "test-app", version: "1.2.3", writer });
}
describe("App.run()", { timeout: 5000 }, () => {
it("prints version with --version", { timeout: 5000 }, async () => {
const output: string[] = [];
const app = makeApp((s) => output.push(s));
await app.run(["--version"]);
expect(output.join("")).toContain("1.2.3");
});
it("prints help with --help", { timeout: 5000 }, async () => {
const output: string[] = [];
const app = makeApp((s) => output.push(s));
await app.run(["--help"]);
expect(output.join("")).toContain("Usage");
});
it("routes to registered command", { timeout: 5000 }, async () => {
const called: string[] = [];
const app = makeApp();
app.command("build").action(async () => {
called.push("build");
});
await app.run(["build"]);
expect(called).toEqual(["build"]);
});
it("throws CliError(ABORTED) when AbortSignal fires before execution", {
timeout: 5000,
}, async () => {
const controller = new AbortController();
controller.abort();
const app = makeApp();
app.action(async () => {});
await expect(
app.run([], { signal: controller.signal })
).rejects.toMatchObject({
code: CLI_CODES.APP.ABORTED,
});
});
it("calls onError with CliError on execution failure", {
timeout: 5000,
}, async () => {
const errors: Error[] = [];
const app = new App({
name: "app",
onError: (e) => errors.push(e),
});
app.action(async () => {
throw new Error("boom");
});
await app.run([]);
expect(errors.length).toBe(1);
});
it("OTel span does not throw (no-op tracer)", {
timeout: 5000,
}, async () => {
const app = makeApp();
app.action(async () => {});
await expect(app.run([])).resolves.not.toThrow();
});
it("rethrows error when no onError handler", {
timeout: 5000,
}, async () => {
const app = makeApp();
app.action(async () => {
throw new Error("unhandled");
});
await expect(app.run([])).rejects.toThrow("unhandled");
});
it("wraps error in CliError(RUN_FAILED) when passing to onError", {
timeout: 5000,
}, async () => {
const errors: Error[] = [];
const app = new App({
name: "app",
onError: (e) => errors.push(e),
});
app.action(async () => {
throw new Error("boom");
});
await app.run([]);
expect(errors[0]).toMatchObject({ code: CLI_CODES.APP.RUN_FAILED });
});
});
describe("App constructor edge cases", { timeout: 5000 }, () => {
it("uses process.stdout when no writer provided", {
timeout: 5000,
}, async () => {
const app = new App({ name: "test", version: "1.0.0" });
// Should not throw — uses default stdout writer
await app.run(["--version"]);
});
it("sets description when provided in config", { timeout: 5000 }, () => {
const app = new App({ name: "app", description: "My tool" });
expect(app.helpText()).toContain("My tool");
});
it("run() without argv defaults to process.argv.slice(2)", {
timeout: 5000,
}, async () => {
const app = new App({ name: "app", writer: () => {} });
app.action(async () => {});
// process.argv.slice(2) in test env is likely [] or test runner args — just shouldn't throw
await expect(app.run()).resolves.not.toThrow();
});
it("--version without version string does not print version", {
timeout: 5000,
}, async () => {
const output: string[] = [];
const errors: unknown[] = [];
const app = new App({
name: "app",
writer: (s) => output.push(s),
onError: (e) => errors.push(e),
});
app.action(async () => {});
await app.run(["--version"]);
expect(output).toHaveLength(0);
});
it("enableVersion:false passes --version to action", {
timeout: 5000,
}, async () => {
const output: string[] = [];
const errors: unknown[] = [];
const app = new App({
name: "app",
version: "1.0.0",
enableVersion: false,
writer: (s) => output.push(s),
onError: (e) => errors.push(e),
});
app.action(async () => {});
await app.run(["--version"]);
expect(output).toHaveLength(0);
});
it("enableHelp:false passes --help to action", {
timeout: 5000,
}, async () => {
const output: string[] = [];
const errors: unknown[] = [];
const app = new App({
name: "app",
enableHelp: false,
writer: (s) => output.push(s),
onError: (e) => errors.push(e),
});
app.action(async () => {});
await app.run(["--help"]);
expect(output).toHaveLength(0);
});
it("onError receives error from failed action", {
timeout: 5000,
}, async () => {
const errors: Error[] = [];
const app = new App({ name: "app", onError: (e) => errors.push(e) });
app.action(async () => {
throw new Error("action error");
});
await app.run([]);
expect(errors.length).toBe(1);
expect(errors[0]).toMatchObject({ code: CLI_CODES.APP.RUN_FAILED });
});
it("onError wraps non-Error throw as CliError", {
timeout: 5000,
}, async () => {
const errors: Error[] = [];
const app = new App({ name: "app", onError: (e) => errors.push(e) });
(app as unknown as { _root: { _action: unknown } })._root._action =
async () => {
throw "string error";
};
await app.run([]);
expect(errors[0]).toMatchObject({ code: CLI_CODES.APP.RUN_FAILED });
});
});
describe("App builder methods", { timeout: 5000 }, () => {
it("description() sets description on root command", {
timeout: 5000,
}, () => {
const app = makeApp();
app.description("My tool");
expect(app.helpText()).toContain("My tool");
});
it("option() adds global option", { timeout: 5000 }, async () => {
let captured: unknown;
const app = makeApp();
app.option("--dry-run, -d", "dry run mode");
app.action(async (ctx) => {
captured = ctx.options["dryRun"];
});
await app.run(["--dry-run"]);
expect(captured).toBe(true);
});
it("argument() adds positional argument", { timeout: 5000 }, async () => {
let captured: unknown;
const app = makeApp();
app.argument("<file>", "file to process");
app.action(async (ctx) => {
captured = ctx.args["file"];
});
await app.run(["myfile.txt"]);
expect(captured).toBe("myfile.txt");
});
it("example() shows up in help text", { timeout: 5000 }, () => {
const output: string[] = [];
const app = new App({ name: "app", writer: (s) => output.push(s) });
app.example("app build --verbose");
app.run(["--help"]);
return new Promise<void>((r) =>
setTimeout(() => {
expect(output.join("")).toContain("app build --verbose");
r();
}, 50)
);
});
});
describe("cli() factory", { timeout: 5000 }, () => {
it("creates App with name and version", { timeout: 5000 }, () => {
const app = cli("myapp", "2.0.0");
expect(app.name).toBe("myapp");
expect(app.version).toBe("2.0.0");
});
it("creates App with name only", { timeout: 5000 }, () => {
const app = cli("myapp");
expect(app.name).toBe("myapp");
expect(app.version).toBeUndefined();
});
});