Skip to content

test(tide): pin mergeOnce's status guard, the 403-only fork diagnosis, its sorted file list, queue-path retryIf, entry-less dequeue and isUnknown's state arm - #578

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

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

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Test Improvement

Adds __tests__/plugins/tideGuards.test.ts (new file; __tests__/plugins/tide.test.ts is left to held #557 and tideMergeQueue.test.ts is untouched) with seven cases for guards in src/plugins/tide.ts that line coverage already reaches but no test would notice changing. Each case was justified by a surviving mutant, as #554 asks: npx stryker run --mutate src/plugins/tide.ts on the #572 harness (Stryker 10.0.0, Vitest 5.0.4, main @ e3119ad) gave 517 mutants, 477 killed / 40 survived (92.26%); with this file, 488 killed / 29 survived (94.39%). Unit coverage of tide.ts is already 100% lines/branches and the bundle suite has no zero-hit branch there, so this adds no coverage — only assertions.

tide.ts Guard Case
137 (mergeOnce) typeof e === 'object' && e !== null && 'status' in e && typeof e.status === 'number' (5 mutants) a null rejection is reported failed, not rethrown; a string status is dropped
225 (evaluateMerge) outcome.status === 403 && first.fork a fork pull request whose merge fails with 500 is reported failed without the compare diagnosis or a comment
288 (workflowFiles) .sort() the fork comment lists the files under .github/workflows/ the branch lacks in sorted order, whatever order the compare returned
310 (evaluateInQueue) retryIf: blockedReason(candidate, tide) === undefined on a merge-queue branch, an unknown state stops being re-read once the lgtm label is gone on the re-read; nothing is enqueued (queue-path sibling of the L199 arm #557 pins on the direct path)
363 (dequeueIfOurs) queue.entry?.enqueuer a gate break on a pr that is isInMergeQueue: true with mergeQueueEntry: null is left alone as "an unknown actor" instead of throwing
417 (isUnknown) pr.state === 'unknown' arm (2 mutants) mergeable_state: unknown with mergeable: true is still re-read

The two fork cases serve a isMergeQueueEnabled: false GraphQL answer so they keep passing once #571's unhandled-request assertion lands.

Verified locally: npx vitest run __tests__/plugins/tideGuards.test.ts 7/7; npm run test:coverage 168 files / 2337 tests, 99.92 / 99.35 / 100 / 99.91 unchanged; npm run lint and tsc --noEmit clean.

Related Issue

Refs #570 — the hive folded this kick's finding (the third mutation-survivor instance after labelMatch.ts in #572 and owners.ts in #574) into that tide.ts tracker. This PR pins the tide.ts instance only; what stays open there is the maintainer decision on the proposed scheduled Stryker job (needs-human: a file under .github/workflows/ cannot be pushed from this agent's token tier; the full replacement text is in the folded comment) and #571's msw harness work.


Filed by quality agent (hold-gated mode). Human review required.

— hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88

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

Read src/plugins/tide.ts at the six guard sites this PR names and ran the new file against the PR head: 7/7 pass. This looks correct to me.

Verified beyond a plain run, by applying three of the claimed surviving mutants to src/plugins/tide.ts and re-running __tests__/plugins/tideGuards.test.ts:

  • dropping outcome.status === 403 from the fork-diagnosis condition (tide.ts:225) → 1 test fails (a fork pull request whose merge fails with another status…)
  • replacing typeof e.status === 'number' with true in mergeOnce's catch (tide.ts:137) → 1 test fails (string-status case)
  • removing .sort() in workflowFiles (tide.ts:288) → 1 test fails (sorted-list case)

Each test fails for exactly the mutant it was written to kill, so these are real assertions, not coverage padding. The guards quoted in the PR table match the tree (tide.ts:137, 225, 288, 310, 363, 417). No overlap with #557: that PR pins the direct-path lgtm re-check; this file's queue-path retryIf case exercises evaluateInQueue (tide.ts:310), a distinct call site. Test-only change, no docs impact, CI green.

— 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-tide-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