File size: 5,587 Bytes
eb3f11e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | import { Command } from "commander";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createDeferred } from "../../../test/helpers/promise.js";
import { configureCommandFromSectionsArg } from "../../commands/configure.commands.js";
import { defaultRuntime, ExitError } from "../../runtime.js";
import { registerConfigCli } from "../config-cli.js";
import { registerConfigureCommand } from "./register.configure.js";
const mocks = vi.hoisted(() => ({
interactive: vi.fn(),
runWizard: vi.fn(),
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
}));
vi.mock("../../commands/configure.wizard.js", () => ({ runConfigureWizard: mocks.runWizard }));
vi.mock("../terminal-interactivity.js", () => ({ isTerminalInteractive: mocks.interactive }));
vi.mock("../../runtime.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../runtime.js")>()),
defaultRuntime: mocks.runtime,
}));
const routes = ["configure", "config"] as const;
type Route = (typeof routes)[number];
async function parse(route: Route, options: string[] = []) {
const program = new Command();
if (route === "configure") {
registerConfigureCommand(program);
} else {
registerConfigCli(program);
}
return await program.parseAsync([route, ...options], { from: "user" });
}
describe("registered configure dispatch", { concurrent: false }, () => {
beforeEach(() => {
vi.resetAllMocks();
mocks.interactive.mockReturnValue(true);
mocks.runWizard.mockResolvedValue(undefined);
mocks.runtime.exit.mockImplementation((code: number) => {
throw new ExitError(code);
});
});
it.each(routes)("%s omits sections for the full chooser", async (route) => {
await parse(route);
expect(mocks.runWizard).toHaveBeenCalledExactlyOnceWith(
{ command: "configure" },
defaultRuntime,
);
expect(mocks.runWizard.mock.calls[0]?.[1]).toBe(defaultRuntime);
expect(mocks.runtime.exit).not.toHaveBeenCalled();
});
it.each(routes)("%s preserves repeated, trimmed section values", async (route) => {
await parse(route, ["--section", " channels ", "--section", "health", "--section", "channels"]);
expect(mocks.runWizard).toHaveBeenCalledExactlyOnceWith(
{ command: "configure", sections: ["channels", "health", "channels"] },
defaultRuntime,
);
expect(mocks.runWizard.mock.calls[0]?.[1]).toBe(defaultRuntime);
});
it.each(routes)("%s rejects a blank section before testing the terminal", async (route) => {
mocks.interactive.mockReturnValue(false);
await expect(parse(route, ["--section", " "])).rejects.toMatchObject({ code: 1 });
expect(mocks.runtime.error).toHaveBeenCalledOnce();
expect(mocks.runtime.error.mock.calls[0]?.[0]).toContain('Invalid --section: "".');
expect(mocks.interactive).not.toHaveBeenCalled();
expect(mocks.runWizard).not.toHaveBeenCalled();
});
it.each(routes)("%s rejects an unknown section without widening scope", async (route) => {
await expect(parse(route, ["--section", "not-a-section"])).rejects.toMatchObject({ code: 1 });
expect(mocks.runtime.error.mock.calls[0]?.[0]).toContain("Invalid --section: not-a-section.");
expect(mocks.runWizard).not.toHaveBeenCalled();
});
it.each(routes)("%s refuses a non-interactive terminal", async (route) => {
mocks.interactive.mockReturnValue(false);
await expect(parse(route, ["--section", "channels"])).rejects.toMatchObject({ code: 1 });
expect(mocks.runtime.error).toHaveBeenCalledOnce();
expect(mocks.runtime.error.mock.calls[0]?.[0]).toContain(
"requires an interactive terminal (TTY)",
);
expect(mocks.runWizard).not.toHaveBeenCalled();
});
it.each(routes)("%s waits for the admitted wizard to settle", async (route) => {
const entered = createDeferred();
const released = createDeferred();
mocks.runWizard.mockImplementationOnce(async () => {
entered.resolve(undefined);
await released.promise;
});
let settled = false;
const parsing = parse(route, ["--section", "channels"]);
void parsing.catch(entered.reject);
const observed = parsing.then(
() => {
settled = true;
},
() => {
settled = true;
},
);
try {
await entered.promise;
expect(settled).toBe(false);
} finally {
released.resolve(undefined);
await observed;
}
await parsing;
expect(settled).toBe(true);
expect(mocks.runWizard).toHaveBeenCalledOnce();
});
it.each(routes)("%s preserves its registered failure contract", async (route) => {
const failure = new Error("fixture wizard failure");
mocks.runWizard.mockRejectedValueOnce(failure);
if (route === "configure") {
await expect(parse(route)).rejects.toMatchObject({ code: 1 });
expect(mocks.runtime.error).toHaveBeenCalledWith("fixture wizard failure");
} else {
await expect(parse(route)).rejects.toBe(failure);
expect(mocks.runtime.error).not.toHaveBeenCalled();
expect(mocks.runtime.exit).not.toHaveBeenCalled();
}
expect(mocks.runWizard).toHaveBeenCalledOnce();
});
it("passes a caller-provided runtime by identity", async () => {
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
await configureCommandFromSectionsArg(["channels"], runtime, { interactive: true });
expect(mocks.runWizard).toHaveBeenCalledExactlyOnceWith(
{ command: "configure", sections: ["channels"] },
runtime,
);
expect(mocks.runWizard.mock.calls[0]?.[1]).toBe(runtime);
});
});
|