Skip to content

test(trigger): pin the guards of /retest, /test, /ok-to-test and okToTestOnPullRequest that a Stryker probe left unasserted - #583

Closed
hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
quality/test-trigger-guards
Closed

hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
quality/test-trigger-guards

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Test Improvement

A Stryker probe of src/issueComment/trigger.ts (npx stryker run --mutate src/issueComment/trigger.ts on #572's harness, main @ e3119ad, Stryker 10.0.0 / Vitest 5.0.4, 8 workers, 7 m 40 s) left 38 of 346 mutants alive (89.02 %). Twenty-three flip a guard the suite reaches but never distinguishes from its mutation. This PR adds one test file that kills all twenty-three; re-running the probe with it reports 331 killed / 15 survived (95.66 %).

Files / cluster claimed: __tests__/issueCommentTest/triggerGuards.test.ts (new). No src/, dist/ or existing-test change. Disjoint from every held PR: #571 edits __tests__/issueCommentTest/trigger.test.ts (the harness-wide unhandled-request assertion) but not this file, and this file already serves every request it provokes (zero msw unhandled frames), so it stays green under #571's rule. Follows the pattern of #575 (ownersGuards), #578 (tideGuards), #579 (authGuards), #580 (approveGuards), #581 (mergeQueueGuards) and #582 (lgtmGuards).

Mutants pinned (span → test)

trigger.ts span Mutation Test
292:7 / 292:32 if (commentId === undefined) { return } → false / {} a reaction is attempted against an undefined comment id and only its failure is caught /retest › a comment without an id attempts no reaction request and logs no warning
304:3 core.error(msg) removed a refusal is commented but never annotated /retest › a refusal is logged as an error annotation with the full message
204:29-65 comment?.user?.login → comment?.user.login a comment whose user is gone throws a TypeError instead of being refused /retest › a comment whose user is gone is refused with a comment and makes no Actions call
202:60 / 202:72 and 191:60 / 191:72 getInput('github-token', { required: true }) → {} / false an empty token is used for unauthenticated calls instead of failing fast /retest › fails before any request when the github-token input is missing; okToTestOnPullRequest › fails before any request …
90:148 args.join(' ') → join('') the no-match comment runs the arguments together /test › the no-match comment echoes several arguments separated by spaces
276:30 `${arg}.yaml` → ` ` a .yaml workflow is never matched by basename /test › matches a workflow stored as .yaml by its basename
276:58 path === file → false the full workflow path never matches /test › matches a workflow by its full path
121:47 label.toLowerCase() === okToTestLabel → true any label counts as ok-to-test already present, so the label is never added /ok-to-test › adds ok-to-test to a pull request that carries only unrelated labels
126:7 approved === 0 → true a labeled PR with runs approved gets the "no runs waiting" comment instead of the rocket /ok-to-test › with the label already present and runs approved reacts instead of commenting
215:59-93 issue?.user?.login → issue?.user.login a PR whose author is gone throws instead of proceeding /ok-to-test › on a pull request whose author is gone approves and reacts for a member
146:20 / 146:35 run.status === 'action_required' → false / '' a run reporting action_required by status alone is not approved /ok-to-test › approves a run that reports action_required by status alone
323:10 / 323:39 typeof error === 'object' && error !== null && 'status' in error → true / error !== null → true a null rejection throws a TypeError instead of the named run error /ok-to-test › a null approve rejection fails with its string form
175:7 / 175:58 action !== 'synchronize' && action !== 'reopened' → false / {} a labeled PR is approved on opened, labeled, closed, edited okToTestOnPullRequest › makes no Actions call on %s even when labeled
180:27 pull?.labels → pull.labels a payload without a pull request throws instead of returning quietly okToTestOnPullRequest › a synchronize payload without a pull request returns quietly
186:24-39 pull?.head?.sha → pull?.head.sha a head-less PR throws a bare TypeError instead of the missing-head error okToTestOnPullRequest › a labeled pull request without a head object throws the missing-head error
181:62 label?.name → label.name a null entry in labels throws instead of being skipped okToTestOnPullRequest › skips a null label entry and still finds ok-to-test

Reviewed survivors, not pinned (15)

  • 40:107, 272:20, 181:77 — the ?? '' fallback literal; no comparison can match "stryker was here!".
  • 40:37, 44:43 — run.status === 'completed' in the failed/successful filters; GitHub reports conclusion: null until a run completes, so a fixture that kills these would not represent a real run (the test(label-sync): pin the case-insensitive name and color folds, the null-description fold, the dry-run counts and per_page #531/test(pullRequestOwners): omit draft/state/labels for real, pin the rename dedupe and the repository-keyed pull and tip memos #533 lesson).
  • 76:40, 204:29-58, 291:41 — context.payload.comment?.… when the payload has no comment at all; handleIssueComment dispatches on the comment body, so no comment never reaches these lines.
  • 210:7, 215:59-86 — context.payload.issue?.… when there is no issue; the issue-number check at line 206 throws first.
  • 186:24-34, 187:34 — pull?.head, pull?.number when pull is undefined; line 180 already returns on a missing pull request.
  • 220:66 — 'reviewers' → '': assertPullRequestOwner/assertRootOwner branch only on role === 'approvers', so any other role string is the reviewers arm.
  • 241:29 — current === undefined → false: differs only for a run whose name is undefined (not null) while GITHUB_WORKFLOW is unset; GitHub serves a string or null.
  • 255:7 — count++ → count--: only rerun === 0 is ever observed and -N is never 0 when a run was re-run.

I have not edited docs/contributing.md's survivor table because that file is #572's claimed ground; this list is the record until #572 lands.

Validation

  • npx vitest run __tests__/issueCommentTest/triggerGuards.test.ts → 20 passed, 0 msw unhandled requests
  • npx eslint __tests__/issueCommentTest/triggerGuards.test.ts → clean
  • npm run test:coverage → 168 files / 2350 passed; 99.92 % stmts / 99.35 % branches / 100 % funcs / 99.91 % lines (unchanged — this PR adds assertion strength, not line coverage)
  • Stryker on trigger.ts before: 308 killed / 38 survived (89.02 %); after: 331 / 15 (95.66 %)

Related Issue

Refs #554 — ticks a slice of the "Strengthen assertions and the test harness" section (reviewed mutation survivors on the /retest, /test, /ok-to-test trigger module); the tracker's CI-enforcement, release-delivery 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

…TestOnPullRequest 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>
@hivecommons-hive
hivecommons-hive Bot requested a review from jpmcb as a code owner October 25, 2026 23:37
@hivecommons-hive hivecommons-hive Bot added the hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 25, 2026
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

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 outreach PR that targets a repository this hive does not manage is held at every level because it publishes project-facing communication into someone else's project.

Hive will keep the hold label until a human removes it. Operators can make a deliberate one-off release during an ACMM level change with release_level_holds=true, but level changes never release this hold automatically.

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test-only PR (one new file, no src//dist changes) — looks correct to me.

Verified against the tree at head c1c6c47:

  • Ran npx vitest run __tests__/issueCommentTest/triggerGuards.test.ts on the branch: 20/20 pass, no msw unhandled requests — matches the Validation section.
  • Each test's comment names a real guard in src/issueComment/trigger.ts and I confirmed the guards exist where claimed: the commentId === undefined return in react (trigger.ts:292), core.error(msg) in refuse (trigger.ts:304), comment?.user?.login in prepare (trigger.ts:204), the action !== 'synchronize' && action !== 'reopened' gate (trigger.ts:175), label?.name null-entry skip (trigger.ts:181), and the typeof error === 'object' && error !== null && 'status' in error arm of statusOf (trigger.ts:323).
  • All helpers the file imports (failOnUnhandledRequest, ObserveRequest, noOrgOrRepoConfigExcept, defaultBranchTree, mergeQueueGraphql, repoHasLabels, prHandlers) exist in __tests__/testUtils.ts / __tests__/utils/ownersFixtures.ts on main — no dependency on any held PR, and the file is disjoint from #571's trigger.test.ts as the body claims.
  • The surviving-mutant rationale in the body is consistent with the code (e.g. the line-180 early return does make pull?.head/pull?.number unreachable with pull undefined).

Refs #554 (non-closing) is appropriate: this ticks one slice of a multi-section tracker. All checks are green; carries hold.

— hive: agent=reviewer backend=copilot model=claude-fable-5 copilot=1.0.88

@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

/kind cleanup


🐝 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

@github-actions github-actions Bot added kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt. and removed needs-kind labels Oct 26, 2026
@mrbobbytables

Copy link
Copy Markdown
Member

Consolidated into #588 together with the other eight open mutation-survivor pin PRs (commit cherry-picked unchanged, DCO preserved). Closing in favour of that PR.

mrbobbytables added a commit that referenced this pull request Oct 26, 2026
, #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>
@mrbobbytables
mrbobbytables deleted the quality/test-trigger-guards branch October 26, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hold Indicates that a PR should not merge because someone has issued a /hold command. kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant