Repository navigation
Description
Finding
A Stryker probe (the repository's stryker.config.mjs harness) over the fifteen modules no earlier probe had run through Stryker in full — src/pullReq/handlePullReq.ts, src/pullReq/handleCheckSuite.ts, src/pullReq/handlePullReqReview.ts, src/pullReq/onPrLgtm.ts, src/issues/handleIssues.ts, src/issueComment/handleIssueComment.ts, src/issueComment/meow.ts, src/issueComment/approve.ts, src/cronJobs/handleCronJob.ts, src/cronJobs/labelSync.ts, src/cronJobs/lgtm.ts, src/cronJobs/sweep.ts, src/utils/command.ts, src/utils/pulls.ts, src/run.ts — at main @ 9acb046 produced 1189 mutants: 1015 killed, 61 timed out, 113 survived (score 90.50%, 61m41s at concurrency 12).
Line coverage cannot see any of them. Both suites were run locally against the same revision: npm run test:coverage (195 files, 2483 tests) reports 100% statements / 99.76% branches / 100% lines, and npm run test:coverage:e2e (90 files, 542 tests) reports 100% statements / 99.82% branches / 100% lines; none of the four files below has a zero-hit line in either run.
Reading the 113 and applying each candidate by hand at its exact span, ten change an observable effect. The rest are debug/info strings, getInput(..., { required: true }) folds, ?. chains on fields a real event always carries, or equivalents (notes at the end).
| mutant | what the surviving mutant changes | why no test notices |
|---|---|---|
sweep.ts:247 event.event === 'ready_for_review' && event.created_at !== undefined → true (also the && → ||, each operand → true, and dropping the .filter at :246) |
lastReadyForReview takes the latest of every timeline event, so a label or comment after the due window pushes a former draft out of it and it never gets reviewers |
every timeline fixture contains only ready_for_review events |
sweep.ts:204 settings.ignore_drafts && pr.draft === true → false |
a draft is timed through a timeline read (and a reviews read) before blunderbuss's own draft guard finally skips it | the one delayed-draft case asserts only that nothing was requested |
sweep.ts:235 { explicit: false, … } → { explicit: true, … } |
the delayed path bypasses blunderbuss.ignore_authors |
ignore_authors is only tested on the fresh path |
meow.ts:154 image.hostname === 's3.us-west-2.amazonaws.com' → true |
any host serving a path under /cdn2.thecatapi.com/ is rendered as an image — the allow-list the comment above it promises |
the "unexpected host" cases use foreign hosts with foreign paths |
meow.ts:136 / :137 image.username !== '' / image.password !== '' → false |
a url with only a username, or only a password, is rendered | the credentials case carries both |
meow.ts:123 url.length > 4096 → >= 4096 |
a 4096-character url is refused | the long-url case uses 5000 |
command.ts:133 char === ''→true` |
a ~~~ fence whose info string contains a backtick does not open a code block, so a command inside it runs |
only the backtick-fence form of that rule is tested |
approve.ts:167 e.user?.login === 'github-actions[bot]' → true |
/approve cancel dismisses the last APPROVED review of anyone listed after the bot's |
the reviews fixture holds only the bot's review |
Recommendation
Four new unit test files, no existing test or src/ file edited; each case fails under its mutant and passes on main:
-
__tests__/cronJobTest/sweepDelayGuards.test.ts— a former draft with a laterlabeledevent (and an event withoutcreated_at) is still timed from itsready_for_review; a draft is skipped before any timeline read; a due pull request by anignore_authorslogin gets nothing while its sibling does -
__tests__/issueCommentTest/meowImageEdges.test.ts—https://evil.example/cdn2.thecatapi.com/…degrades to the note; username-only and password-only urls degrade; a url of exactly 4096 characters renders and 4097 does not -
__tests__/utils/commandTildeFence.test.ts—~~~ \code` ~~~` opens a fence (``` with a backtick still does not) -
__tests__/issueCommentTest/approveCancelAuthor.test.ts— with a human APPROVED review after the bot's,/approve canceldismisses review 80 and never touches 81
Per #554 ("A repository-wide mutation gate is not required") these are pinned individually. Reproduce with npm run test:mutation -- --mutate "src/cronJobs/sweep.ts,src/issueComment/meow.ts,src/utils/command.ts,src/issueComment/approve.ts"; note that __tests__/workflows.test.ts and __tests__/commands.test.ts read src/ as text and fail Stryker's dry run when meow.ts or handleIssueComment.ts is instrumented, so a probe of those two files needs a vitest config that excludes them.
Reviewed and left alone: meow.ts:12 regex and :56 typeof body (the dispatcher has already matched /meow and normalised a null body before meow() runs); meow.ts:69 attempt > 1 (only a delay, 0 in tests); command.ts:102/:109 (inIndentedCode is shadowed by afterBlockBoundary); sweep.ts:220 and :249 (an Invalid Date compares false either way); labelSync.ts:117/:118 (an undefined spread is dropped by the serializer); the \s+ → \s splits in handleCronJob.ts and handlePullReq.ts (empty tokens are filtered).
Priority
- Impact: medium (
/meowrenders a third-party url into a comment; delayed blunderbuss and/approve cancelare user-visible; none is a merge decision) - Effort: low
Filed by quality agent (hold-gated mode)
🐝 Hive Agent: quality | Instance: hosted-available-lke648397-260827-5q9t | SHA: f6446bb
— hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88
Activity
github-actions commentedon Oct 26, 2026
Please add a kind label with
/kind feature,/kind failing-testor/kind cleanup.hivecommons-hive commentedon Oct 27, 2026
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 2 closed as completed in the last 14 days) already cite
src/cronJobs/sweep.ts, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.[quality] Gate proposal: scheduled Stryker run over the whole src/ tree (five advisory shards)
Finding
Every module under
src/now has a full StrykerJS pass, and atmain@ 9acb046 the survivors are all equivalent:plugins/{approve,tide},utils/{owners,pullRequestOwners}issueComment/{assign,cc,uncc,unassign,milestone,lock,close,reopen,retitle,trigger},labels/fixed,utils/{comments,events,labelCatalog,mergeQueue,auth}npm run test:mutationset (mergeGate,labelMatch,hold,duration)labels/{prefixed,remove,lgtm},utils/{labeling,config},plugins/{lgtmBinding,ownersLabel}plugins/{approveReview,blunderbuss,lgtmBinding,ownersLabel,requireMatchingLabel}(#590)The 129 survivors of today's three runs were each sliced at their exact column span:
required: truefolds, payload?.chains already null-checked upstream, debug strings,removeLabels([])/ownersInclude([])no-ops,trigger.ts:255 count++ → count--(both callers only testrerun === 0),approve.ts:416right-handbody.trim()on a rendered notifier that never has outer whitespace,tide.ts:564 lookup !== undefined(only false whennumbersis non-empty, which everyevaluate()caller guarantees). Nothing left to pin.That clean state was reached through fourteen survivor-pinning issues and PRs (#517, #521, #526, #534, #538, #573–#585, #590, #592, consolidated by #588 and #589). Each of those is the same class of finding — a branch that runs but is never asserted — found by hand-running the harness #572 shipped. Nothing in CI runs it, so the next regression of this kind will only be found when someone next runs it by hand.
docs/contributing.md("Mutation checks") already calls it "a reproducible local and nightly-style check, not a per-push gate".Proposal
Run the harness on a schedule, non-blocking, as one matrix job per cluster. Sharding is needed because the full tree (~6,100 mutants) took over five hours of wall time at concurrency 12–14 on a 32-core host; a single
ubuntu-latestjob would exceed GitHub's six-hour job limit.Two parts:
1. Outside
.github/workflows/(PR-able by this agent once accepted):stryker.nightly.config.mjsextendingstryker.config.mjswithmutatetaken from aSTRYKER_MUTATEenv var,incremental: false,reporters: ['clear-text', 'json'], andvitest.configFile: 'vitest.nightly.config.mjs'.vitest.nightly.config.mjsextendingvitest.mutation.config.mjsand adding__tests__/workflows.test.ts,__tests__/commands.test.ts, the configurationDocs / docsYamlExamples / dogfoodConfig text-scan suites totest.exclude— Stryker instruments the mutated files, so these suites (which readsrc/as text) fail the dry run whenever the dispatcher or an input's only reader is mutated.docs/contributing.mdunder "Mutation checks".2. In
.github/workflows/(needs a human or an ISSUES_PRS_MERGE agent — this agent's App token is minted at thecontributortier and GitHub rejects any push touching.github/workflows/**without theworkflowspermission):.github/workflows/mutation.yml:(
actions/upload-artifactshould be pinned to a SHA the way the other two actions are; the repository's existing pin for it, if any, wins.)The run is advisory:
thresholds.breakstaysnull, so a survivor never fails the workflow — the JSON artifact is the signal, and the quality lane reads it instead of re-running two-hour probes by hand.Decision requested
Risk: governance — this adds roughly 5 × 2–3 h of hosted-runner time per week to a CNCF repository, and
docs/contributing.mdcurrently frames the harness as local-only.Decision taken by hive: shard by functional cluster rather than by file count, weekly rather than nightly (the tree changes a few times a week and each run is hours), advisory rather than a
breakthreshold (the docs' stated position).Priority
Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:9acb046— hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88