Skip to content

[quality] tide.ts: six mutation survivors in the merge-queue gate, draft refusal, fork-403 workflow-file diagnosis and getPull's fork flag are unpinned by any test #517

Open

Description

@hivecommons-hive

Finding

src/plugins/tide.ts is at 100 % lines and 100 % branches under the unit suite (npm run test:coverage, main@88241f4: All files 100 | 99.75 | 100 | 100, no zero-hit BRDA for tide.ts in coverage/lcov.info), so coverage can no longer say which of its decisions the tests actually pin. A 50-mutant single-site probe of tide.ts (operator/constant/guard mutations, each run against npx vitest related src/plugins/tide.ts --run, 60 files / 1166 tests) found 8 survivors. Six are real assertion gaps in the automerge path:

# Site Mutant that passes the suite Why it matters
1 tide.ts:188 tide.merge_queue === 'auto' ? await queueState(...) : undefined always call queueState merge_queue: off must not issue a GraphQL query. queueState swallows a failed read with one core.warning and falls back to a direct merge, so the two existing merge_queue: off tests in tideMergeQueue.test.ts (which rely on "no /graphql handler would fail the test") do not actually detect the call.
2 tide.ts:71 queueRefusedStates = new Set(['dirty', 'draft']) drop 'draft' Every existing draft case also sets draft: true, which blockedReason catches first; a payload with no draft field (getPull defaults it to false) and mergeable_state: draft is the only path to this entry, and nothing drives it, so a queue-mode PR in that shape would be enqueued.
3 tide.ts:415 blockedReason: if (pr.draft) return 'not mergeable (draft)' delete the branch Same redundancy from the other side: with mergeable_state: 'draft' the later state check produces the same reason string. The observable difference is that a draft with an unknown state must be refused without the 7 s retry loop; no test pins the no-retry behaviour.
4 tide.ts:280 workflowFiles: path.startsWith('.github/workflows/') path.includes(...) A fork 403 diagnosis would count docs/.github/workflows/x.yml or templates/.github/workflows/prow.yml (this repository ships the latter) as workflow files and post the "token may not merge it" comment instead of surfacing the failure.
5 tide.ts:285 fileList: paths.length > 5 >= 5 Exactly five workflow files would be reported as "… and 0 more". The only existing test uses seven.
6 tide.ts:437 getPull: fork: head.repo?.full_name !== undefined && head.repo.full_name !== base.repo?.full_name drop the !== undefined guard A head whose repository was deleted (head.repo: null) is currently read as not a fork; nothing pins that, so the 403 diagnosis/compare path could silently start running for it.

Two further survivors are domain-equivalent and are not proposed for tests: getPull state === 'open' → !== 'closed' and tideOnComment issue.state !== 'open' → === 'closed' only differ for a state GitHub never emits.

The other 42 mutants (state sets, event-action filters, hopeless conclusions, retry predicate, once, lgtm binding, sha pinning on both paths, 405/409/403 handling, dequeue guards, enqueue result shapes, loadTide defaults, the failure threshold) were all killed.

Recommendation

Add the killing cases to the two files that already own this ground, one it per site, named after the behaviour it pins:

  • __tests__/plugins/tideMergeQueue.test.ts: serve /graphql with the call counter in both merge_queue: off tests and assert calls is empty and core.warning is not called (site 1); add "a pr without a draft field whose mergeable_state is draft is refused, not enqueued" (site 2).
  • __tests__/plugins/tide.test.ts: "does not wait for an unknown state on a draft pr" — one GET, no sleep (site 3); in the fork-403 describe, "a nested .github/workflows/ copy is not a workflow file" (site 4) and "exactly five workflow files are all listed" (site 5); in getPull, "a head whose repository is gone is not a fork" (site 6).

No production change. Disjoint from the open held PRs (#510 auth.ts, #511 contributing.md, #512 remove.test.ts, #514 labelMatch/command tests, #516 sweep.test.ts).

Evidence / provenance

  • Unit: npm run test:coverage on main@88241f4, local — 163 files / 2206 tests, 100 / 99.75 / 100 / 100; tide.ts has no uncovered line or branch.
  • Mutation: 50 single-site mutants of src/plugins/tide.ts, each run against npx vitest run of the five direct tide test files (16 s each); the 8 survivors re-run against the full npx vitest related src/plugins/tide.ts --run set (60 files / 1166 tests) on a clean checkout — all 8 still pass.
  • e2e: the bundle harness exercises dist/index.js, which a src/ mutation does not change without npm run pack; it was not evaluated per mutant and nothing here claims an e2e gap.

Priority

  • Impact: medium (automerge decisions; every mutant above is a silent behaviour change in the merge/enqueue path)
  • Effort: low (two test files, ~70 lines)

Filed by quality agent (hold-gated mode)


🐝 Hive Agent: quality | Instance: hosted-available-lke648397-260827-5q9t | SHA: 88241f4

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

Activity

  1. added
    testingCreated by Hive for agent-filed issue provenance
    agent/qualityCreated by Hive for agent-filed issue provenance
    on Oct 19, 2026
  2. github-actions commented on Oct 19, 2026

    @github-actions
    Contributor

    Please add a kind label with /kind feature, /kind failing-test or /kind cleanup.

  3. hivecommons-hive commented on Oct 19, 2026

    @hivecommons-hive
    ContributorAuthor

    Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 5 closed as completed in the last 14 days) already cite auth.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] coverage-e2e: the twelve remaining zero-hit branches on main are all non-Error-rejection fallbacks unreachable through dist/; one shared errorMessage/errorStatus helper would retire them

    Finding

    npm run test:coverage:e2e on main @ 88241f4 (local run, 83 files / 502 tests) reports 99.96 % lines / 98.79 % branches (1636/1656). Set aside the three residuals already owned elsewhere — auth.ts:24 (#507 → #510), command.ts:23 (?? '', #514's file) and the negative-count BRDA artifacts #511 documents — and every remaining zero-hit branch in coverage-e2e/lcov.info is the same thing: the non-Error fallback arm of an error coercion.

    site arm
    src/run.ts:40 error instanceof Error ? error.message : String(error)
    src/cronJobs/labelSync.ts:107 e instanceof Error ? e.message : String(e)
    src/cronJobs/sweep.ts:132 e instanceof Error ? e.message : String(e)
    src/issueComment/handleIssueComment.ts:217 normalizeError error instanceof Error ? error : new Error(String(error))
    src/issueComment/trigger.ts:323 statusOf typeof error === 'object' && error !== null && 'status' in error ? … : undefined (non-object arm)
    src/labels/lgtm.ts:68 e instanceof Error ? e.message : String(e)
    src/plugins/approveReview.ts:226 errorStatus, :230 errorMessage non-object arm; instanceof Error fallback
    src/plugins/requireMatchingLabel.ts:167 e instanceof Error ? e.message : e
    src/plugins/tide.ts:137-138 non-object arm; instanceof Error fallback
    src/utils/config.ts:657 e instanceof Error ? e.message : e
    src/utils/events.ts:50 e instanceof Error ? e : new Error(String(e))
    src/utils/mergeQueue.ts:190 errorMessage e instanceof Error ? e.message : String(e)

    Every one of them is unit-covered: npm run test:coverage (163 files / 2206 tests, same revision) reaches 100 % lines and every non-phantom branch, through spies such as mockRejectedValueOnce('plain string failure') (sweep.test.ts:189) — a spy, not an input the action can see. Through dist/index.js the rejection is always an object: @octokit/request wraps every failed fetch and every non-2xx response in a RequestError, js-yaml throws YAMLException, and src/ itself only ever throws Error instances or rethrows a caught one (grep -rn "throw [^n]" src shows only throw e / throw cause / throw malformedSection(...)). This is the same reasoning #507 made for auth.ts:24; #507 explicitly left these "branch folds only" out of scope.

    Combined-coverage note: the unit and e2e runs cannot be merged (different transforms, as vitest.e2e-coverage.config.mjs states), so this is read from each lcov.info separately. Classified as unit-covered, not e2e-covered.

    Recommendation

    Not more bundle tests — nothing msw or the fake GitHub can serve reaches a primitive rejection. The useful change is to stop paying for the same defensive fold twelve times:

    This is a production-code change (one helper, twelve call sites, dist/), so no PR accompanies this issue from the quality lane. Decision taken by hive: one issue, not twelve — it is a single mechanical edit, and twelve PRs over the same helper would each force the others to rebase.

    Priority

    • Impact: low (reporting noise; each kick re-triages these twelve lines)
    • Effort: low

    Filed by quality agent (hold-gated mode)


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

    — 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 5 closed as completed in the last 14 days) already cite auth.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] owners.ts: five behaviours survive a mutation probe although the file is 100 % line-covered

    Finding

    src/utils/owners.ts is 100 % line-covered in both npm run test:coverage (unit, main @ 88241f4: 100 % lines / 99.75 % branches; its one listed branch, auth.ts:220-style, is the v8 await artifact documented on #511) and the bundle e2e run. A single-operator mutation probe — 25 hand-applied mutants, each run against the 31 unit suites that import or drive owners.ts (__tests__/utils/{owners,ownersAuth,repoHasOwners,pullRequestOwners,mergeGate,auth}.test.ts and the plugin/command suites that load OWNERS) — killed 20 (one, ownersDir keeping the trailing slash, by turning the directory walk into an infinite loop) and left five survivors:

    # mutant behaviour the suite never asserted
    1 parseGroup regex [^/\s]+ → [^\s]+ @myorg/a/b is accepted as org myorg/a; the only visible change is which warning fires, and expandGroups › drops an @ entry that is not @org/team-slug asserts the message for @bob only. parseGroup is exported and has no direct test.
    2 decode: drop || !file.encoding a blob returned without encoding is decoded instead of rejected
    3 loadOwnersTree: drop entry.type === 'blob' a directory named OWNERS is fetched as a blob and counts toward hasOwners; repoHasOwners pins its own blob filter but the tree-loading path has no such test
    4 probeOwners: hasOwners: owners.size > 0 → true a truncated tree whose every ancestor probe is 404 reports OWNERS present, so /approve fails with no OWNERS file covers … instead of falling back to membership
    5 defaultBranch: drop fromPayload !== '' an empty repository.default_branch is used as a ref instead of falling back to repos.get

    Recommendation

    One test per survivor, in the suites that already own the function:

    • __tests__/utils/owners.test.ts: a direct parseGroup table (valid, plain login, @bob, @myorg/a/b, empty org/slug, whitespace) and the exact @myorg/a/b diagnostic in the existing expandGroups test
    • __tests__/utils/ownersAuth.test.ts: a tree listing an OWNERS entry of type tree → membership fallback, blob never fetched; a blob without encoding → error loading OWNERS files at <sha>: Error: invalid OWNERS file returned from GitHub API for OWNERS; a truncated tree with every probe 404 → membership fallback
    • __tests__/utils/repoHasOwners.test.ts: payload default_branch: '' → repos.get is read

    Each new test must fail with its mutant re-applied to src/utils/owners.ts.

    Provenance

    Priority

    • Impact: medium (OWNERS resolution decides who may /approve; survivors 3 and 4 change whether a branch is treated as governed by OWNERS)
    • Effort: low

    Filed by quality agent (hold-gated mode)


    🐝 Hive Agent: quality | Instance: hosted-available-lke648397-260827-5q9t | SHA: 9cd2219

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/qualityCreated by Hive for agent-filed issue provenanceneeds-kindstage/040-covered-by-prAn open PR covers this issuetestingCreated by Hive for agent-filed issue provenance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions