Initial Commit
This commit is contained in:
252
packages/devtools/tooling/cli/src/app/app.test.ts
Normal file
252
packages/devtools/tooling/cli/src/app/app.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user