Repository navigation
[quality] sweep.ts delayed-reviewers OWNERS guard (line 80 false arm) is covered by neither the unit nor the bundle suite #560
Description
Finding
src/cronJobs/sweep.ts guards the delayed-reviewers loop with branchHasOwners(octokit, context, pr.base.ref) before calling requestReviewersIfDue. The false arm of that guard — a pull request that is due for delayed blunderbuss but whose base branch has no OWNERS files — is exercised by neither suite:
- Unit,
npm run test:coverageonmain@e3119ad(167 files / 2330 tests, 99.92 % lines / 99.35 % branches):coverage/lcov.inforecordsSF:src/cronJobs/sweep.ts BRDA:80,4,1,0(zero hits).__tests__/cronJobTest/sweep.test.tshas asweep on a repository without OWNERS filesblock, but none of its cases configureblunderbuss.delay, so no pull request ever reaches the delayed loop there; thesweep with blunderbuss.delayblock serves an OWNERS file onmasterin every case. - End-to-end,
npm run test:coverage:e2eonmain@e3119ad(86 files / 528 tests, 100 % lines / 99.46 % branches):coverage-e2e/lcov.inforecords the sameBRDA:80,4,1,0; every scenario in__tests__/bundle/blunderbussDelay.test.tsroutesgit/trees/masterwith anOWNERSblob.
Both reports were produced locally from the same checkout at e3119ad; per docs/contributing.md the two runs are deliberately not merged (vite vs. tsc+webpack maps), so they are read separately above. The nearby zero-hit records at sweep.ts:85 (non-Error throw in the same loop) and :208 are claimed by #558 and #559 respectively and are not part of this issue.
Why it matters: this is the only path by which a repository that adopts blunderbuss.delay without OWNERS files is protected from the sweep attempting reviewer selection against an empty OWNERS set. A one-character mutant that drops the guard (|| true) is accepted by the whole current sweep.test.ts and by the whole current bundle suite — verified by applying it, rebuilding the coverage bundle, and running both.
Recommendation
Add one case per suite, in new files so the held PRs #558 (__tests__/cronJobTest/sweep.test.ts) and #559 / #555 (__tests__/bundle/blunderbussDelay.test.ts) are not touched:
- Unit:
__tests__/cronJobTest/sweepDelayWithoutOwners.test.ts—blunderbuss.delay: 1h, an emptymastertree, one pull request created and updated 70 min ago; assertsweepresolves with no candidates and no failures, the tree was read, and the timeline, reviews andrequested_reviewersendpoints were never called. - Bundle:
__tests__/bundle/blunderbussDelayWithoutOwners.test.ts— the same scenario throughdist/index.jsvia thescheduleevent; assert exit 0,sweep: 1 more checked for delayed reviewers, exactly onegit/trees/master?recursive=trueread, zero timeline/review reads, zero non-GET requests, and that the only non-config requests are the pull list and the tree read.
Both cases must fail against the || true mutant (the unit case via the unhandled-request guard, the bundle case via a could not read the timeline: HttpError: Not Found sweep failure).
Priority
- Impact: medium (guard protects the delayed blunderbuss path on OWNERS-less repositories; covered by neither suite → coverage-gap priority 1 under the hive evidence rules)
- Effort: low
Decision taken by hive: new test files rather than edits to the two files held PRs already occupy, to keep every open test PR rebase-free.
Filed by quality agent (hold-gated mode)
🐝 Hive Agent: quality | Instance: hosted-available-lke648397-260827-5q9t | SHA: e3119ad
— hive: agent=quality backend=copilot model=claude-fable-5.1
Activity
github-actions commentedon Oct 22, 2026
Please add a kind label with
/kind feature,/kind failing-testor/kind cleanup.hivecommons-hive commentedon Oct 22, 2026
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 4 closed as completed in the last 14 days) already cite
docs/contributing.md, 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] sweep: delayed-only pull listed without requested_reviewers/requested_teams keys is untested (sweep.ts:208)
Finding
requestReviewersIfDueinsrc/cronJobs/sweep.ts(line 208) guards the delayed-reviewers decision with(pr.requested_reviewers ?? []).length > 0 || (pr.requested_teams ?? []).length > 0, but no test lists a delayed-only pull request (updated before the normal lookback, inside theblunderbuss.delaydue window) whose list entry omits those keys. Both??fallback arms are the only remaining non-phantom branches insweep.tsthat neither suite reaches, once open PRs #558 (timeline-read failure, :85/:244) and #561 (OWNERS guard, :80) land.The existing
sweep.test.tscase treats a pull request listed without labels or requested_reviewers as unlabelled with no reviewers exercises the bare-listing shape only on the candidate path (sweepPullRequest), not on the delayed path.Evidence (revision
e3119ad, run locally)npm run test:coveragecoverage/lcov.info→SF:src/cronJobs/sweep.ts BRDA:208,27,1,0andBRDA:208,28,1,0npm run test:coverage:e2ecoverage-e2e/lcov.info→SF:src/cronJobs/sweep.ts BRDA:208,27,1,0andBRDA:208,28,1,0Per
docs/contributing.mdthe two coverage runs are deliberately not merged (vite vs. tsc+webpack maps), so they were analysed separately; both miss the same two arms. These are zero-hit arms, not the documented negative-count v8 phantoms (the awk recipe indocs/contributing.mdlists only close.ts:39, reopen.ts:36, approve.ts:504, auth.ts:220 as phantoms).Residuals deliberately not filed: the
e instanceof Error ? e.message : String(e)non-Error arms atsweep.ts:85andconfig.ts:618are unreachable through the code paths that feed them (parseDurationand octokit always throwErrors).Recommendation
Add one unit test and one bundle (e2e) test, in new files so they do not collide with #558's edits to
__tests__/cronJobTest/sweep.test.tsor #559's edits to__tests__/bundle/blunderbussDelay.test.ts:__tests__/cronJobTest/sweepDelayBareListing.test.ts: a delayed-only due pull served fromGET /pullswithrequested_reviewersandrequested_teamsremoved from the list item still gets reviewers requested (POST /pulls/1/requested_reviewerscalled,failures: []).__tests__/bundle/blunderbussDelayBareListing.test.ts: the same scenario againstdist/index.jsvia the fake GitHub API.Priority
Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:unknown— hive: agent=quality backend=copilot model=claude-fable-5.1
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 4 closed as completed in the last 14 days) already cite
docs/contributing.md, 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] normalizeBlunderbuss: the non-Error parseDuration catch arm (config.ts:618) is uncovered by both suites
Finding
normalizeBlunderbussinsrc/utils/config.tsrewraps aparseDurationfailure forblunderbuss.delayas(line 618 at
main@e3119ad). The non-Error arm of that ternary is the single remaining unit-uncovered branch inconfig.ts, and the same line is listed as uncovered by the bundle run.The sibling
sweep.lookbackarm (config.ts:677) already has the repository's treatment for this shape:__tests__/utils/configSweep.test.tsdrives the arm by spyingparseDurationto throw a string, and the arm carries a/* v8 ignore next -- non-Error arm unreachable from dist/index.js; … drives it */hint for the bundle run (same pattern as #549).blunderbuss.delayarrived in #530 without either.Coverage evidence
npm run test:coverageonmain@e3119ad(2330 tests, 99.92 % lines / 99.35 % branches) —config.ts100 / 99.61, uncovered618;coverage/lcov.inforecordsBRDA:618,76,1,0.npm run test:coverage:e2eonmain@e3119ad(528 bundle tests, 100 % lines / 99.46 % branches) —config.ts100 / 99.61, uncovered618.parseDurationonly ever throwsError, so the arm is unreachable fromdist/index.js.docs/contributing.md: their statement/branch maps come from different transforms and do not combine).Not one of the four documented phantom branches (
close.ts:39,reopen.ts:36,approve.ts:504,auth.ts:220): the record is a plain zero, not negative.Recommendation
Mirror the
sweep.lookbacktreatment:__tests__/utils/configBlunderbussDelay.test.tsthat spiesparseDuration(vi.mock('../../src/utils/duration', { spy: true })) to throw a string and assertsparseProwConfig('x', 'blunderbuss:\n delay: 1h\n')throwsx: clock skewv8 ignore nexthint on the non-Error alternate atconfig.ts:618naming that test, and repackdist/Decision taken by hive: follow the existing in-repo pattern for this arm shape (#549,
config.ts:677) rather than introduce a new one.Priority
config.ts; keeps both coverage floors honest)Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:e3119ad— hive: agent=quality backend=copilot model=claude-fable-5.1
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 4 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] sweep.ts:85 — the delayed-reviewers catch of sweep has no unit test and its String(e) arm no test at all
Finding
sweep(src/cronJobs/sweep.ts) runsbranchHasOwners+requestReviewersIfDuefor every delayed-only pull request inside atry/catchthat turns a throw into{ number, message:blunderbuss: ${e instanceof Error ? e.message : String(e)}}(line 85 atmain@e3119ad), so one failing pull request does not stop the loop and the sweep fails at the end with every failure listed.Evidence at
main@e3119ad, local runs:npm run test:coverage(2330 tests, 99.91 % lines / 99.35 % branches):coverage/lcov.infohasBRDA:85,5,0,0andBRDA:85,5,1,0. Neither arm of the catch is reached: no unit test makes a delayed pull request fail. (sweep.ts98.33 / 92.3, uncovered85,244.)npm run test:coverage:e2e(528 tests, 100 % lines / 99.46 % branches):coverage-e2e/lcov.infohasBRDA:85,5,0,1andBRDA:85,5,1,0. TheErrorarm is hit once by the bundle suite; theString(e)arm never.vitest.e2e-coverage.config.mjs).Held PRs #558, #561 and #562 each state that line 85 is out of their scope, so nothing open claims it.
Recommendation
Add a unit test file (new file, so it does not collide with #558's edits to
__tests__/cronJobTest/sweep.test.ts) with two due delayed-only pull requests on a base branch with OWNERS:sweeprejects withsweep: 1 pull request(s) failed: #1 (blunderbuss: <message>)and the other pull request still receives its reviewer request (Error arm; also pins that the loop continues past a failure);requestOwnersReviewersrejects once with a non-Error → the failure readsblunderbuss: <String(e)>(String(e) arm; samevi.mock(..., { spy: true })technique as__tests__/utils/configSweep.test.ts).The
String(e)arm stays unreachable from the bundle (octokit only ever rejects withRequestError); whether to v8-ignore it for the e2e run the way #537/#544/#549 did is a separate, production-file change and is not part of this issue.Priority
Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:e3119ad— hive: agent=quality backend=copilot model=claude-fable-5.1
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 4 closed as completed in the last 14 days) already cite
docs/contributing.md, 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] sweep: the ignore_drafts=false arm of delayed reviewers (sweep.ts:218) has no unit test
Finding
requestReviewersIfDueinsrc/cronJobs/sweep.tsbranches onblunderbuss.ignore_draftsat line 218. When it isfalse, a due draft is not skipped at line 204, and the sweep times the pull request fromcreated_atalone instead of reading its timeline for the lastready_for_review. The unit suite never takes that arm: everysweep with blunderbuss.delaytest in__tests__/cronJobTest/sweep.test.tsruns with the defaultignore_drafts: true.Evidence (both at
e3119ad, run locally):npm run test:coverage→coverage/lcov.infoBRDA:218,30,1,0(theignore_draftsfalse arm, 0 hits).npm run test:coverage:e2e→coverage-e2e/lcov.infoBRDA:218,30,1,1, driven by__tests__/bundle/blunderbussDelay.test.ts("uses creation time and requests on a due draft when ignore_drafts is false").The two runs are analyzed separately on purpose:
vitest.e2e-coverage.config.mjsdocuments that their branch maps come from different transforms and do not combine. So this arm is covered end-to-end but not by a unit test; the per-request behaviour (a draft is requested, the timeline is never read) is only pinned through the bundle.Not in scope:
close.ts:39,reopen.ts:36,approve.ts:504,auth.ts:220— these are the phantom negative-count branchesdocs/contributing.mddocuments (negativeBRDAhits in both runs), not misses.sweep.ts:80-85,:208,:244andconfig.ts:618are already claimed by open PRs #558, #561, #562, #563 and #565.Recommendation
Add a unit test, in its own file so it does not collide with #558's edits to the
sweep with blunderbuss.delayblock, that configuresblunderbuss: { delay: 1h, ignore_drafts: false }and asserts:issues/:number/timelineendpoint is never called;created_atgate at line 214 still applies).Priority
Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:unknown— hive: agent=quality backend=copilot model=claude-fable-5.1
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 4 closed as completed in the last 14 days) already cite
docs/contributing.md, 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] Mutation runner for the #554 harness box: Stryker 10 vitest-runner runs 0 tests per mutant under Vitest 5.0.4; config ready to land once upstream fixes selection
Finding
Tracker #554 (section P1: Strengthen assertions and the test harness) asks for "bounded, repeatable mutation checks for selected high-risk behavior, with reproducible commands and reviewed equivalent-mutant exclusions". The repository has no mutation runner (
grep -ri stryker .finds nothing outsidenode_modules), so every mutation check so far has been done by hand, one edit at a time, in PR descriptions (#557tide.ts:203, #558 and #562sweep.ts, #532).The obvious tool, StrykerJS with its Vitest runner, does not work against this repository today:
@stryker-mutator/vitest-runner10.0.0 (latest, 2026-08-14) is broken under the Vitest 5.0.4 this repository pins. Reproduced onmain@e3119ad, Node 26.12.0, afternpm install --save-dev --save-exact @stryker-mutator/core@10.0.0 @stryker-mutator/vitest-runner@10.0.0and the config below:npx stryker run --mutate src/plugins/tide.ts --checkers ""(defaults:related: true,coverageAnalysis: perTest)throwintide.ts:580survive although the clear-text report liststideOnPullRequest throws when the payload has no pull requestas the test "run" for it.vitest.related: false--mutate "src/plugins/tide.ts:578-581").related: false+--coverageAnalysis off(no per-test name filter at all)Two separate upstream defects are involved, both open at stryker-mutator/stryker-js:
describeon Vitest 5, because the runner joins suite and test names with a space while Vitest 5 matchestestNamePatternagainst names joined with>(vitest-runner: on Vitest 5 the per-test name filter matches nothing, so every covered mutant survives (testNamePattern now joins the chain with ' > ') stryker-mutator/stryker-js#6210, #6262, #6263, #6265; fix PRs #6214, #6220, #6247, #6260 — none merged as of 2026-10-22).ctx.start()on the runner's single reused Vitest instance executes nothing under Vitest 5.0.4 (the runner only clearsctx.state.filesMapbetween runs; I did not isolate why Vitest 5 then runs no files). This is the reason thecoverageAnalysis: offrow above also runs 0 tests; I did not find it filed upstream.Because every mutant "survives" with zero tests executed, a
test:mutationscript added now would report a 0 % score against a suite that in fact kills these mutants by hand (#557 shows thetide.ts:203mutant fails on one case). That is worse than no runner: it would teach readers to ignore the report. I therefore did not open a PR.Recommendation
Adopt StrykerJS once a
@stryker-mutator/vitest-runnerrelease carries the Vitest 5 selection fix (watch stryker-mutator/stryker-js#6260 and thevitest-runnerchangelog) and re-run the reproduction above; both failure modes must be gone (Ran N tests per mutantwith N > 0, and thetide.ts:580throw-removal mutant killed). The files to land are ready and need no further design — this is the exact text that was validated to instrument, dry-run and sandbox correctly here, only the per-mutant run is blocked:package.json: devDependencies@stryker-mutator/coreand@stryker-mutator/vitest-runner(exact-pinned to the fixed release), script"test:mutation": "stryker run"..gitignore: appendstryker.config.mjs(new):vitest.dir: '__tests__': the config'sinclude: ['__tests__/**/*.test.ts']is then resolved relative to that directory and the dry run finds no tests.docs/contributing.md: a### Mutation checkssubsection after Phantom "uncovered" branches recording the command, the four-file scope, the first measured baseline score per file, and the// Stryker disable next-lineconvention for reviewed equivalent mutants. (Added as a new subsection, not a row in the Testing table, so it does not collide with chore(coverage): raise the e2e branch floor to 98 after the pending coverage work landed #556's edit of that table.)thresholds.breakjust under the measured score and record the figure in the doc comment, mirroring howvitest.config.mjsdocuments its coverage floor.Decision taken by hive: wait for the upstream runner fix rather than (a) pinning a second Vitest 4 copy under an npm alias for the mutation run — the runner resolves
vitestfrom the project root, so this would need a separatenode_modules— or (b) writing a bespoke "apply these N hand mutants and expect a red test" script. Option (b) is what #557/#558/#562 already do in prose; it is reproducible but not bounded in the sense #554 asks for (it only re-checks mutants someone already thought of). If the upstream fix has not shipped by the time the P1 harness section of #554 is otherwise complete, (b) is the fallback and this issue should be re-scoped to it.No CI gate is proposed; #554 states a repository-wide mutation gate is not required, and
.github/workflows/changes are out of reach for this agent in any case.Priority
Related
Refs #554 — this is the focused child issue for its "bounded, repeatable mutation checks" box; the tracker stays open for its other sections.
Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:e3119ad— hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 3 closed as completed in the last 14 days) already cite
docs/contributing.md, 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: per-site v8-ignore markers on bundle-unreachable non-Error catch arms — one errorMessage() helper + ESLint selector
Finding
The repository keeps re-handling one class of coverage miss site by site: a
catchthat formats the error ase instanceof Error ? e.message : String(e)(or: e), whose non-Errorarm is unit-covered but never reachable fromdist/index.jsbecause octokit only rejects withRequestErrorand the local callee only throwsError. Each time the e2e floor invitest.e2e-coverage.config.mjsnotices one, the fix has been a per-site/* v8 ignore next -- non-Error arm unreachable from dist/index.js; <test> drives it */marker plus adist/rebuild. Instances so far:src/labels/lgtm.ts:68src/run.ts:42,src/utils/command.ts:25,src/utils/config.ts:678src/plugins/requireMatchingLabel.ts:169src/utils/config.ts:618(normalizeBlunderbuss)src/cronJobs/sweep.ts:85(delayed-reviewers catch)Evidence at
main@e3119ad(npm run test:coverage:e2e, 528 tests, 100 % lines / 99.46 % branches):coverage-e2e/lcov.inforeportsBRDA:85,5,1,0forsweep.tsandBRDA:618,76,1,0forconfig.ts— theString(e)arms with zero bundle hits. The unit run (npm run test:coverage, 2330 tests, 99.92 % / 99.35 %) is read separately asdocs/contributing.mdrequires.Sites of the same shape that have not yet been flagged and will be next:
src/cronJobs/labelSync.ts:107,src/cronJobs/sweep.ts:162,src/plugins/tide.ts:138. The repository also already has two private copies of the helper this proposal asks for:errorMessage()atsrc/plugins/approveReview.ts:229and the fallback line oferrorMessage()atsrc/utils/mergeQueue.ts:190.Three or more instances of one class means the gate is missing, so this issue proposes the gate instead of filing instance #6.
Recommendation
Decision requested: accept or decline the following gate.
One shared helper. Add
src/utils/errors.tsexportingerrorMessage(e: unknown): string(the body ofapproveReview.ts:229), with a single unit test in__tests__/utils/errors.test.tsdriving both arms. The one bundle-unreachableString(e)arm then lives in exactly one place and carries exactly onev8 ignoremarker; the per-site markers at the five sites above become plainerrorMessage(e)calls and are deleted.mergeQueue.ts'serrorMessagekeeps itserrors[]-array handling and falls through to the shared helper.Lint rule that stops the class from coming back. In
eslint.config.mjs, forsrc/**exceptsrc/utils/errors.ts, add:npm run lintalready runs in.github/workflows/test.yml(line 36), so no workflow file changes and the gate is enforced on every PR.src/, so it is outside what the quality lane may open — a human or the fix lane lands it)labelSync.ts:107,sweep.ts:162,tide.ts:138,sweep.ts:85) individually in the per-site-marker style of chore(coverage): v8-ignore requireMatchingLabel's bundle-unreachable non-Error arm at the rule loop's catch #549Risk: governance — this changes how contributors must write error handling in
src/, and it is a production-code refactor, which the quality lane is not permitted to PR. Decision taken by hive: proposed the simplest additive option (one helper, one lint selector, existing lint step) rather than a lower e2e branch floor or a blanketcoverage.ignoreClassMethods/path exclusion, both of which would hide real misses on the same lines.Priority
catchinsrc/otherwise costs a follow-up coverage PR plus adist/rebuild)Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:e3119ad— hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88