Skip to content

test(owners): pin the fail-closed OWNERS group read when a later members page cannot be fetched (owners.ts:153-162) - #564

Closed
hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
quality/test-owners-group-pagination-failure
Closed

hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
quality/test-owners-group-pagination-failure

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Test Improvement

Adds __tests__/utils/ownersGroupPagination.test.ts, which reads an OWNERS @codertocat/maintainers group through the real octokit.paginate against msw Link headers (the existing group tests in __tests__/utils/owners.test.ts stub paginate; __tests__/bundle/ownersGroups.test.ts serves a single page). It pins expandGroups (src/utils/owners.ts:150-165) and the /approve outcome through assertAuthorizedByOwnersOrMembership:

  • the paginator follows Link to page two, so a member listed only there is an approver (and both pages are requested with per_page=100);
  • a page-one member is an approver once every page is read, with no warning;
  • when page two answers 500, the whole group contributes nobody: the page-one member is denied (dave is not an approver for any changed file), exactly one warning is emitted (OWNERS: could not read the members of @codertocat/maintainers: HttpError: Server Error), and both pages were requested — i.e. the read fails closed rather than trusting the members fetched so far;
  • that denial does not fall back to organization membership (no GET /orgs/{org}/members/{login} is made; the unhandled-request guard would reject one).

Evidence at main e3119ad: npm run test:coverage (99.92 / 99.35 / 100 / 99.91) and npm run test:coverage:e2e (100 / 99.46 / 100 / 100) both already hit owners.ts:160-162, so this is a behavioural contract, not a line-coverage change — per #554's "judge tests by behavior" section.

Related Issue

Refs #554 — ticks only the "OWNERS pagination failure: fail a subsequent membership page and assert the warning and fail-closed behavior" box of the P2 cross-request section. Decision taken by hive: the scenario is pinned at the unit layer with a real Octokit rather than in __tests__/bundle/, because the bundle variant needs the fakeGithub response-header support that #555 adds and that file is occupied ground until #555 merges. The tracker's other boxes (paginated OWNERS/timeline in #555, boundaries in #558, successive sweeps in #559, scheduling outage recovery, and the P1 CI/release/harness sections) stay open; the issue states a single test PR must not close it.

Disjoint from the open hold-gated PRs: touches only the new file; no change to fakeGithub.ts, ownersGroups.test.ts, blunderbussDelay*.test.ts, sweep*.test.ts, tide.test.ts, auth.ts, or config.ts.

Tests

  • npx vitest run __tests__/utils/ownersGroupPagination.test.ts — 4 passed
  • npx eslint __tests__/utils/ownersGroupPagination.test.ts — clean

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

— hive: agent=quality backend=copilot model=claude-fable-5.1 requested_by=@mrbobbytables

…ers page cannot be fetched (owners.ts:153-162)

Let the real octokit paginator walk a team's Link headers instead of the
stubbed paginate the existing group tests use. A page-one member is an
approver once every page is read; when page two returns 500 the group
contributes nobody, the run warns once naming the team, and the page-one
member is denied without a fallback to organization membership.

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 22, 2026 07:47
@hivecommons-hive hivecommons-hive Bot added the hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 22, 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

Copy link
Copy Markdown
Contributor Author

/kind cleanup


🐝 Hive Agent: quality | Instance: hosted-available-lke648397-260827-5q9t | SHA: 96cfcd8

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

@mrbobbytables

Copy link
Copy Markdown
Member

Consolidated into #589 (commit cherry-picked unchanged, author and DCO sign-off preserved).

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 added a commit that referenced this pull request Oct 26, 2026
…solidates #555, #558, #559, #561, #562, #564-#567) (#589)

* test: cover paginated OWNERS and review timelines

Exercise approval authorization, automatic reviewer selection, and delayed reviewer timing across page boundaries.

Hive-Run: #554

Hive-Plan: P2 cross-request contracts

Hive-Spec: issue-554#P2-cross-request-contracts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: mrbobbytables <mrbobbytables@users.noreply.github.com>

* test(sweep): pin the inclusive due-window bounds and the timeline-read failure of delayed reviewers

Add four cases to the 'sweep with blunderbuss.delay' block of
__tests__/cronJobTest/sweep.test.ts, exercising requestReviewersIfDue's
window checks at the exact instants and 1 ms past them, for created_at
and for the last ready_for_review alike, and the failure path when the
timeline read itself fails.

Each boundary case fails on a one-character mutant of sweep.ts that the
existing suite accepted: '>' to '>=' on the created_at early return,
'<' to '<=' or '>' to '>=' on the shifted-window check. The failure case
covers the catch in lastReadyForReview (previously unit-uncovered) and
pins that the error is reported as a blunderbuss failure of that pull
request while the other pull requests still get their reviewers.

Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>

* test(sweep): pin that a second delayed sweep leaves the first run's requested reviewers alone

The delayed-blunderbuss bundle suite modelled a pull that already had
reviewers only as a static fixture. This drives dist/index.js through two
successive sweeps against a fake whose pull list reflects the reviewer
request the first sweep made, and asserts the second run neither re-reads
the timeline or reviews nor requests reviewers again.

Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>

* test(sweep): pin the delayed-reviewers OWNERS guard on a base branch without OWNERS files

Both suites left the false arm of the branchHasOwners guard ahead of
requestReviewersIfDue in src/cronJobs/sweep.ts with zero hits: every
delayed-blunderbuss case served an OWNERS file on master. Add one unit case
and one dist/index.js scenario, each in its own file, that configure
blunderbuss.delay, serve an empty master tree and a due pull request, and
assert the tree is probed once while the timeline, reviews and
requested_reviewers endpoints are never touched. Both fail when the guard is
replaced by `|| true`.

Fixes #560

Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>

* test(sweep): pin the reviewer-key fallbacks of delayed blunderbuss on a bare pull listing (sweep.ts:208)

requestReviewersIfDue reads requested_reviewers ?? [] and
requested_teams ?? [] off the pull list entry, but no unit or bundle test
listed a delayed-only pull request without those keys, leaving both
fallback arms unreached in coverage/ and coverage-e2e/. Serve a due pull
with the keys removed on both harnesses and assert reviewers are still
requested.

Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>

* test(owners): pin the fail-closed OWNERS group read when a later members page cannot be fetched (owners.ts:153-162)

Let the real octokit paginator walk a team's Link headers instead of the
stubbed paginate the existing group tests use. A page-one member is an
approver once every page is read; when page two returns 500 the group
contributes nobody, the run warns once naming the team, and the page-one
member is denied without a fallback to organization membership.

Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>

* test(sweep): pin the delayed-reviewers catch of sweep in its Error and String(e) arms (sweep.ts:85)

Adds __tests__/cronJobTest/sweepDelayFailures.test.ts: two due delayed-only
pull requests on a base branch with OWNERS, where one fails. A 500 on the
reviews read is recorded as that pull request's `blunderbuss:` failure while
the other still gets its reviewer request; a non-Error rejection of
requestOwnersReviewers is stringified the same way.

Before: coverage/lcov.info BRDA:85,5,0,0 / BRDA:85,5,1,0 (neither arm hit by
the unit suite). After: both 1; sweep.ts branches 92.3 -> 94.87 %.

Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>

* test(sweep): pin the ignore_drafts=false arm of delayed reviewers (sweep.ts:218)

With blunderbuss.delay set and ignore_drafts: false, requestReviewersIfDue
must not skip drafts and must time every pull request from created_at
alone, never reading the timeline. The unit suite only ever ran the
delayed sweep with the default ignore_drafts: true, so that arm was
reached by the bundle suite alone. Pins it in its own file so it does not
collide with the pending edits to the delay block of sweep.test.ts.

Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>

* test(sweep): pin the no-catch-up recovery of delayed reviewers after missed schedule runs

After the schedule misses runs for longer than sweep.lookback, a pull
request whose review clock expired before now - delay - lookback is
neither listed, read nor requested by the delayed-reviewers step, and a
longer sweep.lookback is what recovers it. Pinned in a unit suite
(__tests__/cronJobTest/sweepDelayOutage.test.ts) and against
dist/index.js (__tests__/bundle/blunderbussDelayOutage.test.ts); the
blunderbuss.delay paragraph of docs/configuration.md now records the
expectation.

Refs #554

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>

* test(harness): fail tide and sweep unit tests on unserved requests

Completes the #571 wiring that was deferred while #557 and #558 occupied
these files: both servers now answer the merge-queue read with
noMergeQueue and assert at the end of each test that no request went
unserved; sweep's OWNERS scenarios serve the base-branch tip instead of
running through the 'could not read the tip' fallback.

Refs #570

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Bob Killen <bkillen@linuxfoundation.org>

---------

Signed-off-by: mrbobbytables <mrbobbytables@users.noreply.github.com>
Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>
Signed-off-by: Bob Killen <bkillen@linuxfoundation.org>
Co-authored-by: mrbobbytables <mrbobbytables@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>
@mrbobbytables
mrbobbytables deleted the quality/test-owners-group-pagination-failure 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