Files
software-mansion__argent/packages/argent/test/installer-help.test.ts
Ignacy Łątka 5bc9c7be4c test: make four non-discriminating test sites fail on what they claim to pin (#1059)
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 &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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 -->
2026-09-09 12:38:37 +02:00

274 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
});
});