mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
5bc9c7be4c
Four tests could not fail on the thing they name: an assertion implied by the one below it, an assertion satisfied by the fixture's own filename, a clearance poll with nothing to clear, and `it.each` rows whose rendered name described only argv[0] - two of them stating the opposite of what the row asserts. Each site now fails when its claim is broken, proven by mutation. Tests only, no source change, so no docs update. Fixes #943 Fixes #942 Fixes #841 Fixes #861 <details> <summary>#943 — capability: toolId/platform were substrings of the path assertion</summary> `toContain("demo-tool")` + `toContain("android")` are both substrings of `tools/demo-tool/platforms/android.ts`, asserted on the next line. Replaced by the prose sentence, which names both outside the path. Mutation - drop the sentence naming the tool and the platform: ```diff - `Tool '${opts.toolId}' is not yet implemented on ${opts.platform}. ` + + `Tool is not yet implemented. ` + ``` | | result | |---|---| | before | `Tests 7 passed (7)` - green | | after | `AssertionError: expected 'Tool is not yet implemented. The cros…' to contain 'Tool \'demo-tool\' is not yet impleme…'` | The sibling "works without a hint" assertions are load-bearing and untouched. </details> <details> <summary>#942 — native-profiler: the wording assertion matched inside missing_cpu.xml</summary> `/missing|not found|unreadable/i` matched the fixture filename, so any product copy passed. The wording now has to land before the backticked path (a negated character class cannot cross into it), which no path can satisfy. Mutation - reword the ENOENT copy: ```diff - if (code === "ENOENT") return `not found at \`${filePath}\``; + if (code === "ENOENT") return `absent at \`${filePath}\``; ``` | | result | |---|---| | before | `Tests 1 passed (1)` - green (also green with the state word deleted outright) | | after | `AssertionError: expected '# iOS Instruments Analysis…' to match /-\s*\*\*cpu\*\*:[^…]*(?:missing\|not found\|unreadable)/i` | </details> <details> <summary>#841 — vega-cli: the output-cap reaps had nothing to observe</summary> `expect(await waitForClear(SENTINEL_OVERFLOW)).toBe(0)` reads 0 just as readily for a launcher that never spawned a worker. The cap trips on the first write, so there was no window to see one in: the fake now spawns its sentinel worker **before** the flood and holds the flood - and so the reap - until the test writes a gate file, having seen the worker. Both the stdout and the stderr twin. Gated rather than timed, so the observation is decided, not raced. Cost is ~+110ms and ~+50ms on the two tests (165/177ms to 278/226ms). Mutation - the flood branches' workers stop carrying the sentinel, reap path untouched: ```diff - const worker = spawn("sleep", [secs], { stdio: "ignore" }); + const worker = spawn("sleep", ["600.0"], { stdio: "ignore" }); ... - spawnSync("sleep", [secs]); + spawnSync("sleep", ["600.0"]); ``` | | result | |---|---| | before | `Tests 12 passed (12)` - green, and `pgrep -f '^sleep 600\.0$'` empty (the reap works; nothing observed it) | | after | both overflow tests `AssertionError: expected +0 to be 1` | Cross-check that the pair pins the production reap: with `reapOnce()` made a no-op in `vega-cli.ts`, both fail `expected 2 to be +0`. **Flake runs: 30 consecutive passes** (2 x 15, before and after the comment pass), `12 passed (12)` every time. </details> <details> <summary>#861 — four it.each rows named only argv[0] (plus a fifth site)</summary> A single `%j` against a multi-element row renders only the first element. Each row is now one argv, nested, so `%j` names all of it. `packages/argent-cli/test/tools-help.test.ts` ``` - prints usage for "--help" without contacting the tool-server <- duplicate - prints usage for "--json" without contacting the tool-server <- false: a bare --json prints no usage and does hit the server + prints usage for ["--help","--json"] without contacting the tool-server + prints usage for ["--json","--help"] without contacting the tool-server ``` `packages/argent/test/installer-help.test.ts` ``` - treats "--foo" as a help request <- rendered twice; a bare --foo is the opposite + treats ["--foo","--help"] as a help request + treats ["--foo","help"] as a help request - does not treat "--metro-port" as a help request + does not treat ["--metro-port","8082"] as a help request ``` Swept the class across every `it.each` array-literal table in the repo (154 sites). One more reproduces it - `packages/argent-cli/test/telemetry-command.test.ts`, fixed here: ``` - prints usage for status and writes nothing <- rendered twice - prints usage for disable and writes nothing <- false: a bare `disable` writes config + prints usage for ["status","--help"] and writes nothing + prints usage for ["status","-h"] and writes nothing + prints usage for ["disable","-h"] and writes nothing ``` Left alone: ~64 sites with a discriminating label column (`_label` first, house style) and 16 whose first column is a real discriminator - names unique and truthful in both groups. `providers-command.test.ts:440` drops a third "shape" column from its name but stays unique and true, so it is a legibility nit, not this defect. </details> <details> <summary>Checks run</summary> - `npm run format`, `npm run lint` (the latter needs `npm ci` inside `packages/docs` locally) - `npx tsc --noEmit -p packages/{tool-server,argent-cli,argent}/tsconfig.test.json` - `packages/tool-server`: `npx vitest run --shard=1/6` .. `6/6` - 6163 passed, 1 skipped - `npx vitest run --root packages/argent-cli` (619), `--root packages/argent` (80) - `vega-cli-timeout.test.ts` x30 consecutive, zero flakes </details> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Improved test parameterization so command-line help cases display complete argument lists accurately. * Strengthened error-message and profiler warning assertions to verify exact wording and placement. * Improved subprocess timeout and output-limit tests by synchronizing worker startup and cleanup checks. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
274 lines
11 KiB
TypeScript
274 lines
11 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
||
import {
|
||
INSTALLER_COMMAND_META,
|
||
INSTALLER_COMMANDS,
|
||
installerHelpRequested,
|
||
isInstallerCommand,
|
||
printInstallerHelp,
|
||
} from "../src/installer-help.js";
|
||
|
||
// Guards the fix for #451: `argent <installer-cmd> --help` must short-circuit
|
||
// to help instead of forwarding `--help` to the side-effecting installer
|
||
// functions (which don't handle it and would run the real command — e.g.
|
||
// `uninstall --help` opening the destructive removal prompt).
|
||
|
||
describe("installerHelpRequested", () => {
|
||
it("is true for every installer subcommand with --help", () => {
|
||
for (const command of INSTALLER_COMMANDS) {
|
||
expect(installerHelpRequested(command, ["--help"])).toBe(true);
|
||
}
|
||
});
|
||
|
||
it("is true for the -h short flag", () => {
|
||
expect(installerHelpRequested("uninstall", ["-h"])).toBe(true);
|
||
expect(installerHelpRequested("remove", ["-h"])).toBe(true);
|
||
});
|
||
|
||
it("is true when the help flag trails other arguments", () => {
|
||
expect(installerHelpRequested("init", ["--scope", "project", "--help"])).toBe(true);
|
||
expect(installerHelpRequested("update", ["--foo", "-h"])).toBe(true);
|
||
});
|
||
|
||
// On destructive commands the match is deliberately lenient: a fat-fingered
|
||
// help request must never fall through to the real uninstall (which, with no
|
||
// `--yes`, opens the "Remove argent configuration…" confirm with Yes
|
||
// pre-selected).
|
||
it("is true for --help=<value> (a plausible fat-finger)", () => {
|
||
expect(installerHelpRequested("uninstall", ["--help=foo"])).toBe(true);
|
||
expect(installerHelpRequested("init", ["--help="])).toBe(true);
|
||
});
|
||
|
||
it("is true for case-insensitive help flags", () => {
|
||
expect(installerHelpRequested("uninstall", ["--HELP"])).toBe(true);
|
||
expect(installerHelpRequested("remove", ["-H"])).toBe(true);
|
||
expect(installerHelpRequested("update", ["--Help=x"])).toBe(true);
|
||
});
|
||
|
||
it("is true for single-dash and smart-dash help flags", () => {
|
||
// `-help` is the Go-style spelling; `—help`/`–help` are what smart-dash
|
||
// editors rewrite `--help` into when it is pasted through them.
|
||
expect(installerHelpRequested("uninstall", ["-help"])).toBe(true);
|
||
expect(installerHelpRequested("uninstall", ["—help"])).toBe(true);
|
||
expect(installerHelpRequested("init", ["–help"])).toBe(true);
|
||
expect(installerHelpRequested("update", ["—help=x"])).toBe(true);
|
||
});
|
||
|
||
it("is true for the bareword `help` as the first argument", () => {
|
||
expect(installerHelpRequested("uninstall", ["help"])).toBe(true);
|
||
expect(installerHelpRequested("init", ["HELP"])).toBe(true);
|
||
});
|
||
|
||
it("is true for a bareword `help` after a non-value flag (the --yes footgun)", () => {
|
||
// `uninstall --yes help`: `--yes` takes no value, so `help` is a help
|
||
// request. Before this was honoured, the argv fell through to the REAL
|
||
// uninstall with `--yes` set — which skips the confirmation prompt and
|
||
// prunes the workspace config with no help and no prompt.
|
||
expect(installerHelpRequested("uninstall", ["--yes", "help"])).toBe(true);
|
||
expect(installerHelpRequested("uninstall", ["-y", "help"])).toBe(true);
|
||
expect(installerHelpRequested("update", ["--no-telemetry", "help"])).toBe(true);
|
||
// uninstall parses no value-taking flags at all, so even `--from help`
|
||
// (a flag it does not know) reads as a help request there.
|
||
expect(installerHelpRequested("uninstall", ["--from", "help"])).toBe(true);
|
||
});
|
||
|
||
it("does not treat a bareword `help` after a value-taking flag as help", () => {
|
||
// `--from help` names a package/tarball literally called `help`, and
|
||
// `--version help` / `--project-root help` are those flags' values; none
|
||
// may be swallowed as a help request.
|
||
expect(installerHelpRequested("init", ["--from", "help"])).toBe(false);
|
||
expect(installerHelpRequested("install", ["--from", "help"])).toBe(false);
|
||
expect(installerHelpRequested("update", ["--version", "help"])).toBe(false);
|
||
expect(installerHelpRequested("update", ["--project-root", "help"])).toBe(false);
|
||
});
|
||
|
||
it("is false for a near-miss flag that only starts with --help", () => {
|
||
// `--helpme` is not a help request (no `=`, not exactly `--help`).
|
||
expect(installerHelpRequested("uninstall", ["--helpme"])).toBe(false);
|
||
// `/help` is not a recognised spelling either — it falls through to the
|
||
// command, where the interactive confirmation still guards uninstall.
|
||
expect(installerHelpRequested("uninstall", ["/help"])).toBe(false);
|
||
});
|
||
|
||
it("is false for an installer subcommand without a help flag", () => {
|
||
expect(installerHelpRequested("init", [])).toBe(false);
|
||
expect(installerHelpRequested("uninstall", ["--yes"])).toBe(false);
|
||
expect(installerHelpRequested("update", ["--check"])).toBe(false);
|
||
});
|
||
|
||
it("is false for non-installer commands even with --help (they handle it themselves)", () => {
|
||
// `argent run <tool> --help` must still print the tool's own flags, and
|
||
// likewise for tools/server/etc. — never intercepted here.
|
||
expect(installerHelpRequested("run", ["--help"])).toBe(false);
|
||
expect(installerHelpRequested("run", ["gesture-tap", "--help"])).toBe(false);
|
||
expect(installerHelpRequested("tools", ["--help"])).toBe(false);
|
||
expect(installerHelpRequested("server", ["-h"])).toBe(false);
|
||
// A non-installer command must not be intercepted via the bareword either.
|
||
expect(installerHelpRequested("run", ["help"])).toBe(false);
|
||
});
|
||
|
||
it("is false for an undefined command", () => {
|
||
expect(installerHelpRequested(undefined, ["--help"])).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("isInstallerCommand", () => {
|
||
it("recognizes exactly the installer command set", () => {
|
||
expect([...INSTALLER_COMMANDS].sort()).toEqual(
|
||
["init", "install", "mcp", "remove", "uninstall", "update"].sort()
|
||
);
|
||
for (const command of INSTALLER_COMMANDS) {
|
||
expect(isInstallerCommand(command)).toBe(true);
|
||
}
|
||
});
|
||
|
||
it("rejects non-installer and undefined commands", () => {
|
||
expect(isInstallerCommand("run")).toBe(false);
|
||
expect(isInstallerCommand(undefined)).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("printInstallerHelp", () => {
|
||
let logSpy: ReturnType<typeof vi.spyOn>;
|
||
|
||
function render(command: (typeof INSTALLER_COMMANDS)[number]): string {
|
||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||
printInstallerHelp(command);
|
||
return logSpy.mock.calls.map((call: unknown[]) => String(call[0])).join("\n");
|
||
}
|
||
|
||
afterEach(() => {
|
||
logSpy?.mockRestore();
|
||
});
|
||
|
||
it("prints the usage line and summary for every installer command", () => {
|
||
for (const command of INSTALLER_COMMANDS) {
|
||
const out = render(command);
|
||
const meta = INSTALLER_COMMAND_META[command];
|
||
expect(out).toContain(`Usage: ${meta.usage}`);
|
||
expect(out).toContain(meta.summary);
|
||
expect(out).toContain("Run `argent --help` for the full list of commands.");
|
||
}
|
||
});
|
||
|
||
it("lists the real flags each command accepts", () => {
|
||
// The point of `--help` is to tell the user which flags exist.
|
||
// (installer-flags-sync.test.ts asserts these lists against the parsers.)
|
||
const init = render("init");
|
||
expect(init).toContain("Options:");
|
||
expect(init).toContain("--yes, -y");
|
||
expect(init).toContain("--no-telemetry");
|
||
expect(init).toContain("--from <path>");
|
||
expect(init).toContain("--global");
|
||
expect(init).toContain("--local");
|
||
|
||
const update = render("update");
|
||
expect(update).toContain("--version <version>");
|
||
expect(update).toContain("--no-telemetry");
|
||
expect(update).toContain("--global");
|
||
expect(update).toContain("--local");
|
||
|
||
const uninstall = render("uninstall");
|
||
expect(uninstall).toContain("Options:");
|
||
expect(uninstall).toContain("--yes, -y");
|
||
expect(uninstall).toContain("--global");
|
||
expect(uninstall).toContain("--local");
|
||
});
|
||
|
||
it("points aliases at their target command instead of duplicating options", () => {
|
||
const install = render("install");
|
||
expect(install).toContain("Run `argent init --help` to see its options.");
|
||
expect(install).not.toContain("Options:");
|
||
|
||
const remove = render("remove");
|
||
expect(remove).toContain("Run `argent uninstall --help` to see its options.");
|
||
expect(remove).not.toContain("Options:");
|
||
});
|
||
|
||
it("only writes to stdout (no wizard / prompt / network)", () => {
|
||
// The whole contract of the help path is that it is side-effect-free. This
|
||
// asserts the function writes to console.log and returns nothing — not how
|
||
// many calls it batches the output into, which is an implementation
|
||
// detail. The end-to-end guarantee (config left untouched) is covered in
|
||
// cli-dispatch.test.ts.
|
||
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||
const result = printInstallerHelp("uninstall");
|
||
expect(result).toBeUndefined();
|
||
expect(spy).toHaveBeenCalled();
|
||
spy.mockRestore();
|
||
});
|
||
});
|
||
|
||
describe("INSTALLER_COMMAND_META", () => {
|
||
it("covers exactly the installer command set", () => {
|
||
expect(Object.keys(INSTALLER_COMMAND_META).sort()).toEqual([...INSTALLER_COMMANDS].sort());
|
||
});
|
||
|
||
it("summaries have no trailing period (they are rendered inline in the top-level table)", () => {
|
||
for (const command of INSTALLER_COMMANDS) {
|
||
expect(INSTALLER_COMMAND_META[command].summary.endsWith(".")).toBe(false);
|
||
}
|
||
});
|
||
|
||
it("aliases carry no own options and non-aliases carry at least one", () => {
|
||
for (const command of INSTALLER_COMMANDS) {
|
||
const meta = INSTALLER_COMMAND_META[command];
|
||
if (meta.aliasOf) {
|
||
expect(meta.options).toHaveLength(0);
|
||
expect(isInstallerCommand(meta.aliasOf)).toBe(true);
|
||
} else {
|
||
expect(meta.options.length).toBeGreaterThan(0);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("mcp is intercepted like the installers", () => {
|
||
// `argent mcp --help` used to start the stdio server, which then blocked
|
||
// reading JSON-RPC from stdin — the reason this command joined the set.
|
||
// One argv per row, nested so `%j` names all of it: un-nested, the single placeholder
|
||
// takes only argv[0], so the last two rows both read `treats "--foo"` — the opposite of
|
||
// what they assert.
|
||
it.each([
|
||
[["--help"]],
|
||
[["-h"]],
|
||
[["-H"]],
|
||
[["--HELP"]],
|
||
[["-help"]],
|
||
[["—help"]],
|
||
[["–help"]],
|
||
[["--help=x"]],
|
||
[["help"]],
|
||
[["HELP"]],
|
||
[["--foo", "--help"]],
|
||
[["--foo", "help"]],
|
||
])("treats %j as a help request", (rest) => {
|
||
expect(installerHelpRequested("mcp", rest)).toBe(true);
|
||
});
|
||
|
||
it("leaves a real server launch alone", () => {
|
||
// Every generated editor config invokes a bare `argent mcp`.
|
||
expect(installerHelpRequested("mcp", [])).toBe(false);
|
||
});
|
||
|
||
it.each([[["--helpme"]], [["/help"]], [["--metro-port", "8082"]]])(
|
||
"does not treat %j as a help request",
|
||
(rest) => {
|
||
expect(installerHelpRequested("mcp", rest)).toBe(false);
|
||
}
|
||
);
|
||
|
||
it("documents itself without inviting the reader to run it blind", () => {
|
||
const logs: string[] = [];
|
||
const spy = vi.spyOn(console, "log").mockImplementation((m) => void logs.push(String(m)));
|
||
printInstallerHelp("mcp");
|
||
spy.mockRestore();
|
||
const out = logs.join("\n");
|
||
|
||
expect(out).toContain("Usage: argent mcp");
|
||
expect(out).toContain("JSON-RPC");
|
||
// The whole point: say why running it by hand looks like a hang.
|
||
expect(out).toContain("hung");
|
||
expect(out).toContain("--help, -h");
|
||
});
|
||
});
|