Skip to content

Commit 64d6808

Browse files
committed
test(cli): cover the tested/failed/refused split on a mixed verify/test run
packages/cli/src/commands/verify.ts computes tested = results.length - refused.length, and derives both the human summary and the --json ok field from failed, which excludes refused rules. Every existing test runs exactly one rule per invocation, so neither arithmetic operation was ever exercised: with one rule, results.length and results.length - refused.length agree, and a refused rule leaking into failed is invisible without a genuine, non-refused failure to compare it against. Adds two tests to verify.test.ts that spawn the built CLI's `test` command (refused only comes from testOneRule on a runtime rule denied --dangerously-run-scripts; verify never refuses, so it can't exercise this).
1 parent e9a6ebc commit 64d6808

1 file changed

Lines changed: 185 additions & 1 deletion

File tree

‎packages/cli/test/verify.test.ts‎

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import { execFile } from "node:child_process";
12
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
2-
import { join } from "node:path";
33
import { tmpdir } from "node:os";
4+
import { join, resolve } from "node:path";
5+
import { promisify } from "node:util";
6+
47
import { describe, expect, it, beforeEach, afterEach } from "vitest";
58
import { stringify } from "yaml";
69

710
import { verifyRule, getSchemaPayload } from "../src/rules/verify";
11+
import { cliRejectionToResult } from "./support/spawn-cli";
12+
13+
const execFileAsync = promisify(execFile);
814

915
/**
1016
* A rule that fires on `eval(...)`, plus one test file holding exactly the
@@ -744,3 +750,181 @@ describe("getSchemaPayload", () => {
744750
).toBe(true);
745751
});
746752
});
753+
754+
/**
755+
* `packages/cli/src/commands/verify.ts`'s `tested`/`failed`/`refused` split —
756+
* taskless/cli#284.
757+
*
758+
* `tested = results.length - refused.length`, and both the human summary and
759+
* the `--json` `ok` field are derived from `failed`, which itself excludes
760+
* anything `isRefused`. Every other test in this repo runs a single rule per
761+
* invocation, which cannot exercise either arithmetic operation: with one
762+
* rule, `results.length - refused.length` and `results.length` agree, and a
763+
* refused rule "leaking" into `failed` is invisible unless something else in
764+
* the run is a genuine, non-refused failure to compare it against.
765+
*
766+
* These run `taskless test` (not `verify`) because `refused` is only ever set
767+
* by `testOneRule` on a runtime rule denied `--dangerously-run-scripts` — see
768+
* `packages/cli/src/rules/inspect.ts`. `verify` never produces a refusal, so
769+
* it cannot exercise this split at all.
770+
*/
771+
const verifyMixedRunBinPath = resolve(import.meta.dirname, "../dist/index.js");
772+
773+
async function runVerifyMixedRunCli(args: string[]) {
774+
try {
775+
const { stdout, stderr } = await execFileAsync("node", [
776+
verifyMixedRunBinPath,
777+
...args,
778+
]);
779+
return { stdout, stderr, exitCode: 0 };
780+
} catch (error) {
781+
return cliRejectionToResult(error, [verifyMixedRunBinPath, ...args]);
782+
}
783+
}
784+
785+
describe("test: the tested/failed/refused split on a mixed run (#284)", () => {
786+
let cwd: string;
787+
788+
/** An sg rule whose own fixtures either all pass or one deliberately fails. */
789+
async function sgRule(
790+
id: string,
791+
options: { passes: boolean }
792+
): Promise<void> {
793+
const directory = join(cwd, ".taskless", "rules", "sg", id);
794+
await mkdir(join(directory, ".tests"), { recursive: true });
795+
await writeFile(
796+
join(directory, `${id}.yml`),
797+
stringify({
798+
id,
799+
language: "TypeScript",
800+
severity: "error",
801+
message: `no ${id}`,
802+
rule: { pattern: "console.log($ARG)" },
803+
})
804+
);
805+
// A "valid" fixture that actually fires the rule is what makes `ast-grep
806+
// test` — and therefore `taskless test` — report this rule as failed
807+
// rather than refused or passed.
808+
const valid = options.passes
809+
? ["const a = 1;"]
810+
: ["console.log('this should fail');"];
811+
await writeFile(
812+
join(directory, ".tests", `${id}-test.yml`),
813+
stringify({ id, valid, invalid: ["console.log('correct');"] })
814+
);
815+
}
816+
817+
/**
818+
* A runtime rule that verifies cleanly but whose fixtures are always
819+
* REFUSED, because nothing here ever passes `--dangerously-run-scripts`.
820+
* Shape mirrors `runtime-check.test.ts`'s `RUNTIME_CAPTURE`/`RUNTIME_CHECK`.
821+
*/
822+
async function refusedRuntimeRule(id: string): Promise<void> {
823+
const directory = join(cwd, ".taskless", "rules", "runtime", id);
824+
await mkdir(join(directory, "captures"), { recursive: true });
825+
await writeFile(
826+
join(directory, "captures", "logs.yml"),
827+
stringify({
828+
id: "logs-abc12345",
829+
language: "typescript",
830+
rule: { pattern: "console.log($A)" },
831+
metadata: {
832+
taskless: {
833+
version: 1,
834+
kind: "runtime",
835+
name: "logs",
836+
check: "check.ts",
837+
match: "anchor",
838+
},
839+
},
840+
})
841+
);
842+
await writeFile(
843+
join(directory, "check.ts"),
844+
"export default async function (root, matches) {\n" +
845+
' return matches.map((m) => ({ file: m.file, line: m.line, message: "runtime " + m.rule, severity: "warning" }));\n' +
846+
"}\n"
847+
);
848+
}
849+
850+
beforeEach(async () => {
851+
cwd = await mkdtemp(join(tmpdir(), "tskl-verify-mixed-"));
852+
await runVerifyMixedRunCli(["init", "--no-interactive", "-d", cwd]);
853+
});
854+
855+
afterEach(async () => {
856+
await rm(cwd, { recursive: true, force: true });
857+
});
858+
859+
it("keeps tested, failed, and refused as three independent counts", async () => {
860+
// One rule of each outcome: a pass, a genuine failure, and a refusal.
861+
// `results.length` is 3; `tested` must be 2 (3 minus the 1 refused); and
862+
// `failed` must be 1 (the genuine failure only, not the refusal too).
863+
// With all three numbers different, no pair of them can be swapped for
864+
// another and still match what this test asserts.
865+
await sgRule("pass-rule", { passes: true });
866+
await sgRule("fail-rule", { passes: false });
867+
await refusedRuntimeRule("rt-rule");
868+
869+
const jsonResult = await runVerifyMixedRunCli([
870+
"test",
871+
"-d",
872+
cwd,
873+
"--json",
874+
]);
875+
const report = JSON.parse(jsonResult.stdout) as {
876+
ok: boolean;
877+
rules: {
878+
engine: string;
879+
ruleId: string;
880+
ok: boolean;
881+
refused?: string;
882+
}[];
883+
};
884+
// The `ok` field is derived from `failed`, and must stay false because of
885+
// the one genuine failure — a mutation that let the refusal leak into
886+
// `failed` would not be visible here (it is already false), which is why
887+
// the second test below isolates that case with no genuine failure.
888+
expect(report.ok).toBe(false);
889+
expect(jsonResult.exitCode).not.toBe(0);
890+
expect(report.rules.find((rule) => rule.ruleId === "pass-rule")?.ok).toBe(
891+
true
892+
);
893+
expect(report.rules.find((rule) => rule.ruleId === "fail-rule")?.ok).toBe(
894+
false
895+
);
896+
const runtimeRule = report.rules.find((rule) => rule.ruleId === "rt-rule");
897+
expect(runtimeRule?.refused).toBeDefined();
898+
899+
// The human summary is the only place `tested` is rendered at all — it is
900+
// not in the `--json` envelope — so this is the only way to pin its value.
901+
const humanResult = await runVerifyMixedRunCli(["test", "-d", cwd]);
902+
expect(humanResult.exitCode).not.toBe(0);
903+
expect(humanResult.stdout).toContain("1 of 2 rule(s) failed.");
904+
expect(humanResult.stdout).toContain("1 rule(s) did not run.");
905+
});
906+
907+
it("does not let a refused rule count as a failure", async () => {
908+
// No genuine failure at all: one passing rule and one refused rule. If a
909+
// refused rule were counted in `failed`, `ok` would read false and the
910+
// human summary would read "of 1 rule(s) failed" instead of "tested".
911+
await sgRule("pass-rule", { passes: true });
912+
await refusedRuntimeRule("rt-rule");
913+
914+
const jsonResult = await runVerifyMixedRunCli([
915+
"test",
916+
"-d",
917+
cwd,
918+
"--json",
919+
]);
920+
const report = JSON.parse(jsonResult.stdout) as { ok: boolean };
921+
expect(report.ok).toBe(true);
922+
expect(jsonResult.exitCode).toBe(0);
923+
924+
const humanResult = await runVerifyMixedRunCli(["test", "-d", cwd]);
925+
expect(humanResult.exitCode).toBe(0);
926+
expect(humanResult.stdout).toContain("1 rule(s) tested.");
927+
expect(humanResult.stdout).toContain("1 rule(s) did not run.");
928+
expect(humanResult.stdout).not.toContain("failed");
929+
});
930+
});

0 commit comments

Comments
 (0)