Repository navigation
test(mutation): add a bounded Stryker harness for the merge-decision code - #572
Conversation
…code `npm run test:mutation` runs StrykerJS over src/utils/mergeGate.ts, src/utils/labelMatch.ts, src/labels/hold.ts and src/utils/duration.ts through vitest.mutation.config.mjs, which leaves out the bundle harness (it runs the committed dist/index.js, which no mutation of src/ can reach) and turns coverage off. Reports and the incremental file land in reports/mutation/, which is ignored along with .stryker-tmp. The Vitest config also carries a shim for stryker-mutator/stryker-js#6210: vitest-runner 10.0.0 selects a mutant's tests with a testNamePattern of space-joined names while Vitest 5 matches the " > "-joined chain, so without it every mutant inside a describe() is reported Survived with 0 tests run. First run: 151 mutants, 142 killed. The one real survivor, labelMatch.ts:35 (`at = found - part.length` let a middle part reuse the characters of the part before it), is pinned in labelMatch.test.ts; the eight remaining survivors are reviewed in docs/contributing.md. Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>
|
Important Held for human review by the hive's ACMM level gate. This PR was opened by the "quality" agent while Hive policy required a human checkpoint for that agent. Agent PRs are held at ACMM L3–L5; an Hive will keep the |
24dfbaf
ℹ️ Sentinel notice — informational; author is trusted (hive agent), merge is not blockedHive flagged this PR (author @hivecommons-hive[bot], head
Hive did not add the |
|
/kind cleanup 🐝 Hive Agent: — hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88 |
There was a problem hiding this comment.
Read the diff against the tree at merge-base (no src/ or dist/ change; config, tests, docs, lockfile only). This looks correct to me.
Verified:
- New test kills the real survivor. Traced
matchesLabelPattern(src/utils/labelMatch.ts:35) by hand: under the mutantat = found - part.length,'*ab*b*'vs'ab'givesindexOf('b', -2)→1→ true; the real code returns false. All four new assertions in__tests__/utils/labelMatch.test.ts:44-50distinguish the two. - Shim mechanism is real. Vitest 5 matches
testNamePatternagainst the" > "-joinedfullTestNamewhile@stryker-mutator/vitest-runner10.0.0 assigns a space-joined pattern (upstream stryker-mutator/stryker-js#6210); without the rewrite invitest.mutation.config.mjs:24-38every mutant inside adescribe()survives with 0 tests. TheObject.definePropertysetter intercepts Stryker's per-mutant assignment correctly. - Survivor table matches the source.
src/labels/hold.ts:26(required: true),:30(comment?.body),:73-74(empty-presentguard +core.debug), andsrc/utils/duration.ts:13(value === ''— on mutation,durationPart.exec('')yields null,consumedstays 0, result 0 either way) all read as the docs table claims. - Lockfile is clean. All 184 added
resolvedentries point at registry.npmjs.org; devDependencies only. vitest.mutation.config.mjsexcluding__tests__/bundle/**is sound: that harness executes the committeddist/index.js, which nosrc/mutant reaches.
Scope matches the body: Refs #554 is appropriate since this lands only the mutation-check box of the tracker. CI is green. No findings — leaving the merge decision to a maintainer since the PR carries hold.
— hive: agent=reviewer backend=copilot model=claude-fable-5 copilot=1.0.88
, #578-#583, #585) (#588) * test(cron): pin cronLgtm's merge count and its no-read short circuits cronLgtm resolves to the number of pull requests it merged or enqueued, and a pull request it skips before the shared merge path - gate failure or locked - costs no further request. Neither was asserted: the count is discarded by handleCronJob, and tide's own blockedReason also refuses a locked or gated PR, so the existing merge observers could not see the guards. A mutation run over src/cronJobs/lgtm.ts left 13 survivors; the nine behavioural ones (jobsDone++, return false, the !gate.ok and locked guards, { required: true } on github-token, state: 'open') are now killed by direct cronLgtm calls that watch GET /pulls/:number. The four left are log text. Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> * test(owners): pin the per-team group cache key, parseGroup anchor, decode, defaultBranch and isNotFound guards Running the bounded Stryker check against src/utils/owners.ts left 15 survivors at 100% line coverage. Thirteen are behavioural: with one groupCache key every @org/team entry resolves to the first team's members; parseGroup without its leading anchor reads alice@org/team as a group; a bare `options:` key, a blob without content, a payload without a repository, a string or null tree rejection and a path-less tree entry each hit a TypeError instead of the handled path; and repoHasOwners would answer a second repository from the first. Each is pinned with a direct call in a new file so the harness reports 98.49% on the module, leaving only the warning text held #564 asserts and a debug string. Closes #574 Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> * test(tide): pin mergeOnce's status guard, the 403-only fork diagnosis, sorted workflow list, queue-path retryIf, entry-less dequeue and isUnknown's state arm Each case fails when the guard it names is removed or inverted; line coverage already reached them all (Stryker on src/plugins/tide.ts: 477/517 killed before, 488/517 after). Refs #570 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> * test(auth): pin the repository-less org check, the gone-user comment skip, the non-RequestError and half-formed root OWNERS reads, and the OWNERS path in role errors A Stryker probe of src/utils/auth.ts (--mutate src/utils/auth.ts on the harness of #572) left 22 of 273 mutants alive. Eleven of them flip a guard the suite reaches but never distinguishes; this file pins each one: - checkOrgMember on a payload without a repository returns false by the early return, not by a TypeError caught as an unexpected error, so no warning annotation is emitted - checkIssueComments skips a comment whose user is null (e.user?.login) instead of aborting the scan - retrieveOwnersFile reports a string or null rejection of repos.getContent as the OWNERS lookup error rather than tripping on 'status' in e - a contents response with content but no encoding, or an encoding but no content, is rejected as an invalid OWNERS file rather than decoded - a root OWNERS role that is not a list is reported as 'OWNERS at OWNERS:' from both assertRootOwner and rootOwnersIncludes The seven remaining survivors are core.debug strings and the contents === '' short-circuit of rootOwnersIncludes, which is equivalent: parseOwners('OWNERS', '') yields empty lists and expandGroups of an empty list returns an empty set without a request. Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> * test(approve): pin the github-actions[bot] login-only skip, the uncoverable-file suggestion, the notifier whitespace and marker checks, the unlabeled other-label skip, the base-branch OWNERS probe and the required token A Stryker probe of src/plugins/approve.ts (--mutate src/plugins/approve.ts on the harness of #572, main@e3119ad) left 59 of 515 mutants alive. Twenty-six of them flip a guard the suite reaches but never distinguishes; this file pins each one in __tests__/plugins/approveGuards.test.ts: - approvalEvents skips a github-actions[bot] comment by its login alone, without relying on the Bot user type - suggestApprovers still names an approver when another changed file has no OWNERS entry at all, instead of dereferencing the missing per-file set - computeApproval logs the approver who covers none of the changed files and says nothing for a pull request without an author - renderNotifier's no-files body is exactly the verdict, the explanation and the marker - approveOnPullRequest returns on `unlabeled` of another label without a single api call, probes the OWNERS of the event's base branch rather than the default branch, and fails on a missing github-token input before reading anything - upsertNotifier recognises a stale notifier by a github-actions[bot] user without a Bot type, skips a comment whose user is gone, treats a notifier that differs only by surrounding whitespace as up to date, and never mistakes a bot comment without the literal marker for the notifier - approveOnReview on a payload without a review or a pull request fails on the missing pull request, not on the missing review Re-running the probe with this file kills all twenty-six; the 33 that remain are equivalent (log strings, optional chains on values already checked, switch defaults, an excluded.add after the covered files were removed). Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> * test(mergeQueue): pin errorMessage's null-rejection guards, the once-per-run warning's initial state and the [bot] suffix anchor Adds __tests__/utils/mergeQueueGuards.test.ts. Every line and branch of src/utils/mergeQueue.ts is already covered, but five of its mutants survive the Stryker harness: `npx stryker run --mutate src/utils/mergeQueue.ts` (main@e3119ad) kills 106 of 111 (score 95.50%). The survivors, each re-applied by hand at its reported span and confirmed to pass the existing mergeQueue.test.ts: - mergeQueue.ts:184 `e !== null` -> `true`, the whole `typeof e === 'object' && e !== null && 'errors' in e` conjunction -> `true`, and the final `&&` -> `||`: a null or undefined rejection of octokit.graphql would throw a TypeError out of errorMessage, so queueState would reject instead of warning and falling back to the REST merge, and enqueue would throw instead of returning `{ ok: false }`. - mergeQueue.ts:64 `let warnedUnavailable = false` -> `true`: every existing test calls resetMergeQueueWarnings() (directly or via setupActionsEnv), so the first real run of a job never warning went unnoticed. Pinned on a fresh module instance (vi.resetModules) that nothing has reset. - mergeQueue.ts:153 `/^github-actions$|\[bot\]$/i` -> `/^github-actions$|\[bot\]/i`: a login merely containing `[bot]` would count as an automation whose queue entry the gate may undo. With this file all five are killed (manual re-application of each mutant fails 1-3 of the new cases; 111/111 killed). No production code changes. Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> * test(lgtm): pin the refusal error log, the no-label cancel debug, the token guard, the missing-head error and isForbidden's non-object arms A Stryker probe of src/labels/lgtm.ts and src/plugins/lgtmBinding.ts (npx stryker run --mutate src/labels/lgtm.ts,src/plugins/lgtmBinding.ts on the harness of #572, main @ e3119ad, Stryker 10.0.0 / Vitest 5.0.4, 8 workers, 6 m 36 s) left 24 of 202 mutants alive (88.12 %). Fourteen flip a guard or drop a log line the suite reaches but never distinguishes from its mutation. Two new test files kill all fourteen; the re-run reports 192 killed / 10 survived (95.05 %). __tests__/label/lgtmGuards.test.ts (src/labels/lgtm.ts): - 29 `getInput('github-token', { required: true })` (2 mutants): the handler fails fast with "Input required and not supplied" and posts nothing - 140 `core.error(msg)` in refuse: the refusal is logged as an error with the same text it replies with - 96 the "could not find lgtm to remove" debug line of a cancel on an issue without the label, and that no DELETE follows __tests__/plugins/lgtmBindingGuards.test.ts (src/plugins/lgtmBinding.ts): - 171 `getInput('github-token', { required: true })` (2 mutants) in lgtmOnPullRequest - 166 `pull_request?.head?.sha` -> `head.sha`: a pull_request without a head throws the "missing pull request head" TypeError, not a property read failure - 173 the "lgtm: bind_to_commit is false" debug line - 182 isForbidden `typeof error === 'object' && error !== null && 'status' in error` (6 mutants: `true` x4, `||` x2): bindLgtm and isLgtmBound wrap a null, string or number rejection in their own error instead of throwing from `'status' in <primitive>` / `null.status` Reviewed survivors, not pinned (10): lgtm.ts 33, 34 (x2), 35, 65 and lgtmBinding.ts 162, 177, 178 are optional chains on fields every issue_comment / pull_request payload carries (comment.body, comment.user, issue.user, pull_request after the head check, sender once isBotUser is true); lgtmBinding.ts 66 `targetUrl === undefined` -> false spreads `target_url: undefined`, which JSON serialisation drops, so the request body is identical; lgtmBinding.ts 156 `label?.name ?? ''` -> any other string still fails the `!== 'lgtm'` comparison and returns early. Refs #554 Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> * test(trigger): pin the guards of /retest, /test, /ok-to-test and okToTestOnPullRequest that a Stryker probe left unasserted A Stryker probe of src/issueComment/trigger.ts (346 mutants) left 38 alive. Twenty-three of them flip a guard the suite reaches but never distinguishes from its mutation: the react() comment-id guard, the refusal error log, the gone-commenter and gone-author optional chains, the required github-token input, the .yaml and full-path workflow matches, the no-match argument echo, the ok-to-test label equality, the approved-and-labeled reaction, the status-only action_required candidate, the null-rejection statusOf guard, the synchronize/reopened action filter, the pull-less and head-less payloads and the null label entry. This file adds one test per guard; the probe re-run reports 331 killed / 15 survived (95.66 %). The 15 survivors are equivalent or unreachable and are listed in the pull request. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> * test: pin the milestone list separator, isBotUser's login arm, createComment's message and skipReadOnlyForkRun's payload guards A Stryker probe over the issue-comment, label and utility modules left nine behavioural survivors the unit suite reaches but never distinguishes from their mutations. Three new files pin them: - __tests__/issueCommentTest/milestoneAvailableList.test.ts: the not-found error joins the open milestones with ', ' (milestone.ts:66). - __tests__/utils/commentsGuards.test.ts: isBotUser accepts the github-actions[bot] login without a Bot type (comments.ts:67), createComment rewraps a refused post as 'could not add comment: ...' (comments.ts:34), and createCommentOnce honours a login-only bot marker. - __tests__/pullReqTest/readOnlyForkPayloads.test.ts: skipReadOnlyForkRun returns false rather than throwing on a payload without pull_request, head, repository or a string full_name (events.ts:23-25). Closes #584 Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> --------- Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> Co-authored-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Test Improvement
Adds the bounded, repeatable mutation check that #554's "Strengthen assertions and the test harness" section asks for ("Establish bounded, repeatable mutation checks for selected high-risk behavior, with reproducible commands and reviewed equivalent-mutant exclusions. A repository-wide mutation gate is not required.").
Files / cluster claimed:
stryker.config.mjs,vitest.mutation.config.mjs,package.json(test:mutationscript +@stryker-mutator/core,@stryker-mutator/vitest-runnerdevDependencies),package-lock.json,.gitignore,docs/contributing.md(new "Mutation checks" section appended after the phantom-branches section — disjoint from #556's table-row edit),__tests__/utils/labelMatch.test.ts(matchesLabelPatternmiddle-part overlap). Nosrc/ordist/change.npm run test:mutationruns StrykerJS over the merge-decision code:src/utils/mergeGate.ts,src/utils/labelMatch.ts,src/labels/hold.ts,src/utils/duration.ts.--mutate <file>probes any other file.coverageAnalysis: 'perTest', reports + incremental file inreports/mutation/(ignored). Nobreakthreshold: this is a local/nightly-style check, not a per-push gate, per Follow-up: strengthen test contracts, CI enforcement, release delivery, and quality triage #554.vitest.mutation.config.mjsleaves out__tests__/bundle/**(the harness runs the committeddist/index.js, which no mutation ofsrc/can reach) and turns coverage off.@stryker-mutator/vitest-runner10.0.0 selects a mutant's tests with atestNamePatternof space-joined names; Vitest 5 matches that pattern againstfullTestName, joined with" > "(node_modules/vitest/dist/task-utils.jscreateTaskName(names, separator = " > "), used byinterpretTaskModes). Without the shim every mutant inside adescribe()is reported Survived with 0 tests run — reproduced here: 123/151 survived,labelMatch.ts/mergeGate.ts0 killed. Reported upstream as 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 (fix PRs fix(vitest-runner): match Vitest 5 test name separator when filtering tests stryker-mutator/stryker-js#6214, fix(vitest-runner): match nested test names joined with " > " (vitest 5) stryker-mutator/stryker-js#6247 and fix(vitest-runner): use native test names for Vitest 5 selection stryker-mutator/stryker-js#6260 open, unreleased). AconfigureVitestplugin hook rewrites each space in the pattern Stryker assigns to(?: > | ). The docs say to drop the shim once a vitest-runner release carries the fix.First run (main @ e3119ad, Stryker 10.0.0, Vitest 5.0.4, Node 26)
mergeGate.tslabelMatch.tshold.tsduration.ts11 m 36 s on 2 workers. The one real survivor,
labelMatch.ts:35(at = found - part.length: a middle*-separated part could reuse characters of the part before it, so*ab*b*matchedab), is pinned by the newdoes not let a middle part reuse characters of the part before itcase. The eight remaining survivors are reviewed in the docs table (two equivalent induration.ts; six inhold.tswhose only observable is@actions/core's own error text, anissue_commentpayload GitHub never sends, or acore.debugstring).Not in this PR
A scheduled CI lane for this command would be a
.github/workflows/**change, which this agent's token cannot push; #554 says a repository-wide gate is not required, so none is proposed here.Validation
npm run test:mutation(twice: before the shim → 123 survived / 0 tests per runtime mutant; after → 142 killed; incremental re-run onlabelMatch.tsafter the new test → 47/47 killed)npx vitest run __tests__/utils/labelMatch.test.ts→ 24 passednpx eslint stryker.config.mjs vitest.mutation.config.mjs __tests__/utils/labelMatch.test.ts docs/contributing.md→ cleanpackage-lock.json: 169 added entries, 0 removed, 4@inquirer/*patch bumps pulled in by StrykerRelated Issue
Refs #554 — this lands the "bounded, repeatable mutation checks" box of the harness section; the tracker's CI-enforcement, release-delivery, remaining harness boxes (#569/#571 cover the unexpected-traffic ones) and routing sections stay open and are owned elsewhere.
Filed by quality agent (hold-gated mode). Human review required.
— hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88 requested_by=@mrbobbytables