Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
38a16da
Drop the unreachable visited-set guard in the bundle AST counter
claude Jul 26, 2026
acc6651
Close mutation gaps in the Biome, copy-check and cleanup scripts
claude Jul 26, 2026
76e290b
Check that markup between 'click' and 'here' still reads as one phrase
claude Jul 26, 2026
d40fed1
WIP: test the compact test reporter's parsing, estimate and summary
claude Jul 26, 2026
44f1e31
WIP: more reporter tests; simplify progress total and flag checks
claude Jul 26, 2026
b4aa4e4
Take the compact test reporter's mutation score to 100%
claude Jul 26, 2026
a1940de
Sort imports
claude Jul 26, 2026
0bcf042
Assert the exact Markdown mutation report, and share the console capture
claude Jul 26, 2026
5e126db
Take Codex's notes: no narrating comment, no test-only export
claude Jul 26, 2026
cd3c0d6
Take the mutation summary's own report to 100%
claude Jul 26, 2026
2138940
Close small mutation gaps in the script helpers, and split the summar…
claude Jul 26, 2026
83c1291
Drop imports the summary test split left behind
claude Jul 26, 2026
f7d0733
Wait for the second lock attempt before checking it is blocked
claude Jul 26, 2026
4609a8f
Pull the PR queue report's arguments and GitHub calls into testable m…
claude Jul 26, 2026
db7b6ac
Close the mutation gaps in the process helpers too
claude Jul 26, 2026
61be209
Merge remote-tracking branch 'origin/main' into claude/mutation-check…
claude Jul 26, 2026
139bde7
Import the shared result type where the rename left it missing
claude Jul 26, 2026
feb5dda
Keep an empty command a rejected promise, not a thrown error
claude Jul 26, 2026
0ec970c
Move the PR queue report itself into a module the tests can run
claude Jul 26, 2026
ef938eb
Share the queue reply fixture between the two PR queue tests
claude Jul 26, 2026
fe1947c
Use the # alias for the colours import
claude Jul 26, 2026
9452595
Cover the PR queue's own test helper and its waiting
claude Jul 26, 2026
8cdf7ba
Merge remote-tracking branch 'origin/main' into claude/mutation-check…
claude Jul 26, 2026
00ea69a
Space the two TODO sections apart
claude Jul 26, 2026
50ab350
Take CodeRabbit's three notes
claude Jul 26, 2026
6c20fb2
Call the shared output helper by its own name
claude Jul 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -1540,6 +1540,20 @@ Starting points: `findMisplacedTests` in `scripts/unit-tests-report-imports.ts`
(the `test.imports.includes(appEntry)` guard and the `subjects.length !== 1`
guard), and `SHARED_SETUP_FILES` in `scripts/test-subjects.ts`.

## Split the compact test reporter (from PR #1944 review)

Codex flagged `scripts/compact-test-reporter.ts` at 561 lines, above the ~400
target. It holds five separate jobs: reading the `deno test` arguments and
guessing how many tests will run, parsing TAP failure reports, drawing the
progress bar, printing the run summary, and running the child process.

The tests for it already sit in `test/scripts/compact-test-reporter/` as
`estimate`, `diagnostics`, `progress`, `reporter` and `summary`, so the source
can be split into a folder of the same names and each pair stays mirrored (which
is what the mutation gate wants). Out of scope for #1944, whose job was closing
the mutation gaps rather than moving the file around; the file scores 100% as it
stands, so the split can be a pure move.

---

## Mutation gaps in `src/shared/crypto/encryption.ts`
Expand Down
6 changes: 1 addition & 5 deletions scripts/bench/bundle-composition/javascript-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@ import { parseSync } from "npm:oxc-parser@0.132.0";
import { map } from "#fp";

const countAstNodes = (root: object): number => {
const visited = new WeakSet<object>();
let count = 0;
const visit = (value: unknown): void => {
if (value === null || typeof value !== "object" || visited.has(value)) {
return;
}
visited.add(value);
if (value === null || typeof value !== "object") return;
if ("type" in value) count += 1;
map(visit)(Object.values(value));
};
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
* `deno task precommit`, or on its own with `deno task check:copy`.
*/

import { runCopyCheck } from "./check-copy/run.ts";
import { CATALOG_DIR, runCopyCheck } from "./check-copy/run.ts";

Deno.exit(runCopyCheck("src/locales/en", console.log, console.error));
Deno.exit(runCopyCheck(CATALOG_DIR, console.log, console.error));
2 changes: 2 additions & 0 deletions scripts/check-copy/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import { type CopyEntry, findIssues, formatIssue } from "./rules.ts";

export const CATALOG_DIR = "src/locales/en";

/** Read every translatable string from a locale folder's JSON files. */
export const readCatalog = (dir: string): CopyEntry[] => {
const entries: CopyEntry[] = [];
Expand Down
25 changes: 10 additions & 15 deletions scripts/compact-test-reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const PROGRESS_WIDTH = 24;
const TEST_RESULT_RE = /^\s*(not\s+)?ok\s+\d+(?:\s+-\s+(.*))?$/;
const PLAN_RE = /^\s*(\d+)\.\.(\d+)(?:\s+#.*)?$/;
const STEP_FAILURE_RE = /^\d+\s+test\s+steps?\s+failed\.$/;
const REPORTER_FLAGS = new Set(["--reporter"]);
/** Deno test flags that take a separate value, which is never a file path. */
const FILE_ARG_VALUE_FLAGS = new Set([
"--cert",
"--config",
Expand Down Expand Up @@ -238,9 +238,9 @@ const locationFromStack = (
): Location | undefined => {
const matches = message.match(/file:\/\/[^\s)]+:\d+:\d+/g) ?? [];
for (const match of matches) {
// The pattern ends in :line:column, so both colons are always there.
const columnSplit = match.lastIndexOf(":");
const lineSplit = match.lastIndexOf(":", columnSplit - 1);
if (lineSplit === -1 || columnSplit === -1) continue;

const url = match.slice(0, lineSplit);
let file: string;
Expand All @@ -263,12 +263,7 @@ const locationFromStack = (
};

export const hasReporterArg = (args: string[]): boolean =>
args.some(
(arg, index) =>
REPORTER_FLAGS.has(arg) ||
arg.startsWith("--reporter=") ||
args[index - 1] === "--reporter",
);
args.some((arg) => arg === "--reporter" || arg.startsWith("--reporter="));

const collectFileArgs = (args: string[]): string[] => {
const files: string[] = [];
Expand Down Expand Up @@ -336,7 +331,7 @@ export const estimateTapEventCount = async (

export class CompactTapReporter {
#cwd: string;
#estimatedTotal?: number | undefined;
#estimatedTotal: number;
Comment thread
stefan-burke marked this conversation as resolved.
#hideProgress: boolean;
#stdout: (line: string) => void;
#stderr: (line: string) => void;
Expand All @@ -349,7 +344,7 @@ export class CompactTapReporter {

constructor(options: CompactTapReporterOptions) {
this.#cwd = options.cwd;
this.#estimatedTotal = options.estimatedTotal;
this.#estimatedTotal = options.estimatedTotal ?? 0;
this.#hideProgress = options.hideProgress ?? false;
this.#stdout = options.stdout ?? console.log;
this.#stderr = options.stderr ?? console.error;
Expand Down Expand Up @@ -464,19 +459,19 @@ export class CompactTapReporter {
}

#growEstimatedTotal(total: number): void {
if (!Number.isFinite(total) || total <= 0) return;
this.#estimatedTotal = Math.max(this.#estimatedTotal ?? 0, total);
if (total <= 0) return;
this.#estimatedTotal = Math.max(this.#estimatedTotal, total);
}

/** A bar and a count; the total grows whenever the run outruns it. */
#progress(): string {
if (this.#hideProgress) return "";

// A result line is what asks for progress, so at least one test is done
// and growing the total by it always leaves a total of one or more.
const done = this.#passed + this.#failed;
this.#growEstimatedTotal(done);
const total = this.#estimatedTotal;
if (!total) {
return `[${String(done).padStart(4, " ")} done]`;
}

const shownDone = Math.min(done, total);
const fill = Math.min(
Expand Down
46 changes: 46 additions & 0 deletions scripts/mutation/equivalent-mutants.txt
Original file line number Diff line number Diff line change
Expand Up @@ -928,3 +928,49 @@ scripts/mutation/test-map.ts:119:46 && → || # ownedTest gives every integra
# SumUp client: values that are never falsy apart from null.
src/shared/sumup.ts:215:18 ?? → || # withClient resolves to the callback's `true` or to null on failure, so the only non-null value is truthy and ?? false and || false agree
src/shared/sumup.ts:254:37 ?? → || # settings.sumup.keyMode is typed "test" | "live" | null, so it has no falsy non-null value and ?? "unknown" and || "unknown" agree

# Bundle AST bench: the parser's filename argument is a diagnostic label only.
scripts/bench/bundle-composition/javascript-ast.ts:17:28 bundle.js → "" # oxc uses the name only to label diagnostics and to pick a language from its extension; "bundle.js" and "" both select the default JavaScript parser, and the only parser output this module reads is errors[0].message (which never contains the name) and the node count

# Script cleanup: the second length check can never see a single error.
scripts/cleanup.ts:7:23 1 → 0 # the line above already throws when there is exactly one error, so this check only ever runs with 0 or 2+ errors, where > 1 and > 0 agree

# Compact test reporter: fallbacks for values the regexes always provide, and
# comparisons an earlier line has already ruled out.
scripts/compact-test-reporter.ts:85:33 ?? → || # /^\s*/ matches every string, so the optional chain always yields a length and the fallback never runs
scripts/compact-test-reporter.ts:85:37 0 → 1 # same line: the fallback is unreachable because /^\s*/ always matches
scripts/compact-test-reporter.ts:91:62 0 → 1 # this arm runs only when every line is blank, and slicing 0 or 1 characters off blank lines leaves the same text once the join is trimmed
scripts/compact-test-reporter.ts:131:35 ?? → || # match[2] exists whenever the line matched, and its trimmed value is a string, so ?? "" and || "" agree
scripts/compact-test-reporter.ts:131:39 → "mutated" # unreachable fallback: match[2] is always present on a matched line
scripts/compact-test-reporter.ts:132:33 ?? → || # match[1] is the indent capture, always present on a matched line
scripts/compact-test-reporter.ts:132:37 → "mutated" # unreachable fallback for the same reason
scripts/compact-test-reporter.ts:169:58 ?? → || # atIndex came from findIndex, so lines[atIndex] is always a string
scripts/compact-test-reporter.ts:169:62 → "mutated" # unreachable fallback for the same reason
scripts/compact-test-reporter.ts:175:31 ?? → || # match[1] is one of file/line/column on a matched line, never missing
scripts/compact-test-reporter.ts:175:35 → "mutated" # unreachable fallback; an unknown key is ignored by assignAtField either way
scripts/compact-test-reporter.ts:175:63 ?? → || # match[2] is always present on a matched line
scripts/compact-test-reporter.ts:175:67 → "mutated" # unreachable fallback for the same reason
scripts/compact-test-reporter.ts:203:40 ?? → || # parseYamlTapDiagnostic returns an object or undefined, so its only falsy value is the undefined the fallback handles
scripts/compact-test-reporter.ts:208:43 ?? → || # diagnostic.message is a string or undefined, and the empty string it can be is the fallback itself
scripts/compact-test-reporter.ts:208:47 → "mutated" # neither the empty string nor "mutated" matches the step-failure pattern, so the outcome is the same
scripts/compact-test-reporter.ts:239:61 ?? → || # String.match with a global pattern returns an array or null, and an array is always truthy
scripts/compact-test-reporter.ts:323:60 → "mutated" # the fallback text for an unreadable file is only counted for test declarations, and neither the empty string nor "mutated" holds one
scripts/compact-test-reporter.ts:347:50 ?? → || # an estimated total of 0 falls back to 0, so ?? and || pick the same number
scripts/compact-test-reporter.ts:347:54 0 → 1 # the total is immediately grown to the number of finished tests, which is 1 or more whenever it is read
scripts/compact-test-reporter.ts:348:46 ?? → || # hideProgress is a boolean or undefined, and false falls back to false either way
scripts/compact-test-reporter.ts:349:34 ?? → || # the stdout option is a function when present, and functions are always truthy
scripts/compact-test-reporter.ts:350:34 ?? → || # the stderr option is a function when present, and functions are always truthy
scripts/compact-test-reporter.ts:384:21 --- → "" # a line that trims to nothing has already returned above, so comparing the trimmed line with "" is never true
scripts/compact-test-reporter.ts:384:42 ... → "" # same line, same reason
scripts/compact-test-reporter.ts:443:44 ?? → || # locationFromStack returns a location object or undefined, and objects are always truthy

# Mutation summary: grouped results are arrays or absent, never falsy-but-set.
scripts/mutation/summary.ts:65:69 ?? → || # Object.groupBy gives each status a non-empty array or nothing at all, and an array is always truthy
scripts/mutation/summary.ts:92:33 ?? → || # the survivors group is an array when present, so ?? [] and || [] pick the same list

# Lock file: the handle exists only to carry the advisory lock.
scripts/lock-file.ts:4:41 true → false # nothing ever reads or writes through this handle, so opening it write-only still takes and releases the same lock

# Process helpers: the timer handle before the timer exists.
scripts/process.ts:60:17 0 → 1 # the promise body runs straight away and overwrites this handle, so its starting value is never the one cleared
scripts/process.ts:62:12 = → += # the handle starts at 0, so adding to it and replacing it store the same timer id
Loading