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 -->
146 lines
5.6 KiB
TypeScript
146 lines
5.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import * as fs from "node:fs";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { telemetry } from "../src/telemetry.js";
|
|
import { getConsentState, _resetConsentCacheForTest } from "@argent/telemetry";
|
|
|
|
// Real-filesystem test of `argent telemetry enable|disable [--scope]`: a
|
|
// sandboxed HOME (global scope) and a tmp project root (`.git` marker), so the
|
|
// restrictive project/global merge is exercised through the real command.
|
|
// Only the network client is stubbed — no events must leave the test.
|
|
|
|
vi.mock("@argent/telemetry", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("@argent/telemetry")>();
|
|
return {
|
|
...actual,
|
|
init: vi.fn(),
|
|
shutdown: vi.fn(async () => undefined),
|
|
};
|
|
});
|
|
|
|
class ExitError extends Error {
|
|
constructor(public code: number) {
|
|
super(`process.exit(${code})`);
|
|
}
|
|
}
|
|
|
|
let homeDir: string;
|
|
let projectDir: string;
|
|
let originalHome: string | undefined;
|
|
let originalUserProfile: string | undefined;
|
|
let originalCwd: string;
|
|
let logSpy: ReturnType<typeof vi.spyOn>;
|
|
let errSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
homeDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "argent-tel-home-")));
|
|
projectDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "argent-tel-proj-")));
|
|
fs.mkdirSync(path.join(projectDir, ".git"));
|
|
originalHome = process.env.HOME;
|
|
originalUserProfile = process.env.USERPROFILE;
|
|
originalCwd = process.cwd();
|
|
process.env.HOME = homeDir;
|
|
process.env.USERPROFILE = homeDir;
|
|
process.chdir(projectDir);
|
|
_resetConsentCacheForTest();
|
|
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
|
|
throw new ExitError(code ?? 0);
|
|
}) as never);
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(originalCwd);
|
|
if (originalHome === undefined) delete process.env.HOME;
|
|
else process.env.HOME = originalHome;
|
|
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
else process.env.USERPROFILE = originalUserProfile;
|
|
vi.restoreAllMocks();
|
|
_resetConsentCacheForTest();
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function readJson(file: string): Record<string, unknown> | null {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
const globalConfig = () => readJson(path.join(homeDir, ".argent", "config.json"));
|
|
const projectConfig = () => readJson(path.join(projectDir, ".argent", "config.json"));
|
|
const output = () => logSpy.mock.calls.map((c: unknown[]) => String(c[0])).join("\n");
|
|
const cleanEnv = { DO_NOT_TRACK: undefined, ARGENT_TELEMETRY: undefined };
|
|
|
|
describe("argent telemetry — scopes", () => {
|
|
it("disable defaults to the global scope", async () => {
|
|
await telemetry(["disable"]);
|
|
expect(globalConfig()).toEqual({ telemetry: { enabled: false } });
|
|
expect(projectConfig()).toBeNull();
|
|
expect(output()).toContain("global scope");
|
|
});
|
|
|
|
it("disable --scope project writes the committed project file only", async () => {
|
|
await telemetry(["disable", "--scope", "project"]);
|
|
expect(projectConfig()).toEqual({ telemetry: { enabled: false } });
|
|
expect(globalConfig()).toBeNull();
|
|
const state = getConsentState(cleanEnv);
|
|
expect(state.enabled).toBe(false);
|
|
expect(state.source.detail).toBe("config.json (project)");
|
|
});
|
|
|
|
it("enable --scope=project alone cannot override a global opt-out (restrictive)", async () => {
|
|
await telemetry(["disable"]);
|
|
await telemetry(["enable", "--scope=project"]);
|
|
expect(projectConfig()).toEqual({ telemetry: { enabled: true } });
|
|
expect(getConsentState(cleanEnv).enabled).toBe(false);
|
|
expect(output()).toContain("stays disabled");
|
|
});
|
|
|
|
it("a global enable cannot override a project opt-out", async () => {
|
|
await telemetry(["disable", "--scope", "project"]);
|
|
await telemetry(["enable"]);
|
|
expect(globalConfig()).toEqual({ telemetry: { enabled: true } });
|
|
expect(getConsentState(cleanEnv).enabled).toBe(false);
|
|
});
|
|
|
|
it("status names the document that opted out", async () => {
|
|
await telemetry(["disable", "--scope", "project"]);
|
|
logSpy.mockClear();
|
|
await telemetry(["status"]);
|
|
expect(output()).toContain("state: disabled");
|
|
expect(output()).toContain("config.json (project)");
|
|
});
|
|
|
|
it("rejects an unknown scope and an unknown argument", async () => {
|
|
await expect(telemetry(["disable", "--scope", "team"])).rejects.toThrow("process.exit(2)");
|
|
await expect(telemetry(["enable", "--force"])).rejects.toThrow("process.exit(2)");
|
|
expect(errSpy).toHaveBeenCalled();
|
|
expect(globalConfig()).toBeNull();
|
|
expect(projectConfig()).toBeNull();
|
|
});
|
|
|
|
// `argent <command> --help` is what the top-level help tells the user to run,
|
|
// and every other subcommand honours it after its own subcommand too.
|
|
// One argv per row, nested so `%j` names all of it: un-nested, the single placeholder
|
|
// takes only argv[0], which renders the two `status` rows identically and reads the rest
|
|
// as claims about a bare `enable` / `disable`.
|
|
it.each([
|
|
[["--help"]],
|
|
[["-h"]],
|
|
[["status", "--help"]],
|
|
[["status", "-h"]],
|
|
[["enable", "--help"]],
|
|
[["disable", "-h"]],
|
|
])("prints usage for %j and writes nothing", async (args) => {
|
|
await telemetry(args);
|
|
expect(output()).toContain("argent telemetry status");
|
|
expect(errSpy).not.toHaveBeenCalled();
|
|
expect(globalConfig()).toBeNull();
|
|
expect(projectConfig()).toBeNull();
|
|
});
|
|
});
|