Skip to content

[quality] Pin the ten behavioural Stryker survivors in sweep's delayed-reviewer guards, meow's image validation, the tilde-fence opener and /approve cancel #592

Closed

Description

@hivecommons-hive

Finding

A Stryker probe (the repository's stryker.config.mjs harness) over the fifteen modules no earlier probe had run through Stryker in full — src/pullReq/handlePullReq.ts, src/pullReq/handleCheckSuite.ts, src/pullReq/handlePullReqReview.ts, src/pullReq/onPrLgtm.ts, src/issues/handleIssues.ts, src/issueComment/handleIssueComment.ts, src/issueComment/meow.ts, src/issueComment/approve.ts, src/cronJobs/handleCronJob.ts, src/cronJobs/labelSync.ts, src/cronJobs/lgtm.ts, src/cronJobs/sweep.ts, src/utils/command.ts, src/utils/pulls.ts, src/run.ts — at main @ 9acb046 produced 1189 mutants: 1015 killed, 61 timed out, 113 survived (score 90.50%, 61m41s at concurrency 12).

Line coverage cannot see any of them. Both suites were run locally against the same revision: npm run test:coverage (195 files, 2483 tests) reports 100% statements / 99.76% branches / 100% lines, and npm run test:coverage:e2e (90 files, 542 tests) reports 100% statements / 99.82% branches / 100% lines; none of the four files below has a zero-hit line in either run.

Reading the 113 and applying each candidate by hand at its exact span, ten change an observable effect. The rest are debug/info strings, getInput(..., { required: true }) folds, ?. chains on fields a real event always carries, or equivalents (notes at the end).

mutant what the surviving mutant changes why no test notices
sweep.ts:247 event.event === 'ready_for_review' && event.created_at !== undefined → true (also the && → ||, each operand → true, and dropping the .filter at :246) lastReadyForReview takes the latest of every timeline event, so a label or comment after the due window pushes a former draft out of it and it never gets reviewers every timeline fixture contains only ready_for_review events
sweep.ts:204 settings.ignore_drafts && pr.draft === true → false a draft is timed through a timeline read (and a reviews read) before blunderbuss's own draft guard finally skips it the one delayed-draft case asserts only that nothing was requested
sweep.ts:235 { explicit: false, … } → { explicit: true, … } the delayed path bypasses blunderbuss.ignore_authors ignore_authors is only tested on the fresh path
meow.ts:154 image.hostname === 's3.us-west-2.amazonaws.com' → true any host serving a path under /cdn2.thecatapi.com/ is rendered as an image — the allow-list the comment above it promises the "unexpected host" cases use foreign hosts with foreign paths
meow.ts:136 / :137 image.username !== '' / image.password !== '' → false a url with only a username, or only a password, is rendered the credentials case carries both
meow.ts:123 url.length > 4096 → >= 4096 a 4096-character url is refused the long-url case uses 5000
command.ts:133 char === ''→true` a ~~~ fence whose info string contains a backtick does not open a code block, so a command inside it runs only the backtick-fence form of that rule is tested
approve.ts:167 e.user?.login === 'github-actions[bot]' → true /approve cancel dismisses the last APPROVED review of anyone listed after the bot's the reviews fixture holds only the bot's review

Recommendation

Four new unit test files, no existing test or src/ file edited; each case fails under its mutant and passes on main:

  • __tests__/cronJobTest/sweepDelayGuards.test.ts — a former draft with a later labeled event (and an event without created_at) is still timed from its ready_for_review; a draft is skipped before any timeline read; a due pull request by an ignore_authors login gets nothing while its sibling does
  • __tests__/issueCommentTest/meowImageEdges.test.ts — https://evil.example/cdn2.thecatapi.com/… degrades to the note; username-only and password-only urls degrade; a url of exactly 4096 characters renders and 4097 does not
  • __tests__/utils/commandTildeFence.test.ts — ~~~ \code` ~~~` opens a fence (``` with a backtick still does not)
  • __tests__/issueCommentTest/approveCancelAuthor.test.ts — with a human APPROVED review after the bot's, /approve cancel dismisses review 80 and never touches 81

Per #554 ("A repository-wide mutation gate is not required") these are pinned individually. Reproduce with npm run test:mutation -- --mutate "src/cronJobs/sweep.ts,src/issueComment/meow.ts,src/utils/command.ts,src/issueComment/approve.ts"; note that __tests__/workflows.test.ts and __tests__/commands.test.ts read src/ as text and fail Stryker's dry run when meow.ts or handleIssueComment.ts is instrumented, so a probe of those two files needs a vitest config that excludes them.

Reviewed and left alone: meow.ts:12 regex and :56 typeof body (the dispatcher has already matched /meow and normalised a null body before meow() runs); meow.ts:69 attempt > 1 (only a delay, 0 in tests); command.ts:102/:109 (inIndentedCode is shadowed by afterBlockBoundary); sweep.ts:220 and :249 (an Invalid Date compares false either way); labelSync.ts:117/:118 (an undefined spread is dropped by the serializer); the \s+ → \s splits in handleCronJob.ts and handlePullReq.ts (empty tokens are filtered).

Priority

  • Impact: medium (/meow renders a third-party url into a comment; delayed blunderbuss and /approve cancel are user-visible; none is a merge decision)
  • Effort: low

Filed by quality agent (hold-gated mode)


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

— 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 26, 2026
  2. github-actions commented on Oct 26, 2026

    @github-actions
    Contributor

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

  3. hivecommons-hive commented on Oct 27, 2026

    @hivecommons-hive
    ContributorAuthor

    Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 2 closed as completed in the last 14 days) already cite src/cronJobs/sweep.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] Gate proposal: scheduled Stryker run over the whole src/ tree (five advisory shards)

    Finding

    Every module under src/ now has a full StrykerJS pass, and at main @ 9acb046 the survivors are all equivalent:

    set mutants killed timeout survived score wall time (this host)
    plugins/{approve,tide}, utils/{owners,pullRequestOwners} 1420 1266 114 40 97.18% 112m @ concurrency 14
    issueComment/{assign,cc,uncc,unassign,milestone,lock,close,reopen,retitle,trigger}, labels/fixed, utils/{comments,events,labelCatalog,mergeQueue,auth} 1269 1159 26 83 93.45% 94m @ 12
    default npm run test:mutation set (mergeGate, labelMatch, hold, duration) 151 143 2 6 96.03% 7m @ 16
    dispatcher / cron / command cluster (#592) 1189 1015 61 113 90.50% 61m @ 12
    labels/{prefixed,remove,lgtm}, utils/{labeling,config}, plugins/{lgtmBinding,ownersLabel} 1357 — — — — 45m @ 12
    plugins/{approveReview,blunderbuss,lgtmBinding,ownersLabel,requireMatchingLabel} (#590) 718 — — 78 — —

    The 129 survivors of today's three runs were each sliced at their exact column span: required: true folds, payload ?. chains already null-checked upstream, debug strings, removeLabels([]) / ownersInclude([]) no-ops, trigger.ts:255 count++ → count-- (both callers only test rerun === 0), approve.ts:416 right-hand body.trim() on a rendered notifier that never has outer whitespace, tide.ts:564 lookup !== undefined (only false when numbers is non-empty, which every evaluate() caller guarantees). Nothing left to pin.

    That clean state was reached through fourteen survivor-pinning issues and PRs (#517, #521, #526, #534, #538, #573–#585, #590, #592, consolidated by #588 and #589). Each of those is the same class of finding — a branch that runs but is never asserted — found by hand-running the harness #572 shipped. Nothing in CI runs it, so the next regression of this kind will only be found when someone next runs it by hand. docs/contributing.md ("Mutation checks") already calls it "a reproducible local and nightly-style check, not a per-push gate".

    Proposal

    Run the harness on a schedule, non-blocking, as one matrix job per cluster. Sharding is needed because the full tree (~6,100 mutants) took over five hours of wall time at concurrency 12–14 on a 32-core host; a single ubuntu-latest job would exceed GitHub's six-hour job limit.

    Two parts:

    1. Outside .github/workflows/ (PR-able by this agent once accepted):

    • stryker.nightly.config.mjs extending stryker.config.mjs with mutate taken from a STRYKER_MUTATE env var, incremental: false, reporters: ['clear-text', 'json'], and vitest.configFile: 'vitest.nightly.config.mjs'.
    • vitest.nightly.config.mjs extending vitest.mutation.config.mjs and adding __tests__/workflows.test.ts, __tests__/commands.test.ts, the configurationDocs / docsYamlExamples / dogfoodConfig text-scan suites to test.exclude — Stryker instruments the mutated files, so these suites (which read src/ as text) fail the dry run whenever the dispatcher or an input's only reader is mutated.
    • A "Scheduled run" paragraph in docs/contributing.md under "Mutation checks".

    2. In .github/workflows/ (needs a human or an ISSUES_PRS_MERGE agent — this agent's App token is minted at the contributor tier and GitHub rejects any push touching .github/workflows/** without the workflows permission):

    .github/workflows/mutation.yml:

    name: mutation
    
    on:
      schedule:
        - cron: '17 3 * * 1'   # weekly, Monday 03:17 UTC
      workflow_dispatch:
    
    permissions:
      contents: read
    
    jobs:
      stryker:
        runs-on: ubuntu-latest
        timeout-minutes: 355
        strategy:
          fail-fast: false
          matrix:
            include:
              - name: merge-gate
                mutate: src/utils/mergeGate.ts,src/utils/labelMatch.ts,src/labels/hold.ts,src/utils/duration.ts,src/labels/prefixed.ts,src/labels/remove.ts,src/labels/lgtm.ts,src/labels/fixed.ts
              - name: ownership
                mutate: src/plugins/approve.ts,src/plugins/approveReview.ts,src/utils/owners.ts,src/utils/pullRequestOwners.ts,src/utils/auth.ts
              - name: tide
                mutate: src/plugins/tide.ts,src/utils/mergeQueue.ts,src/cronJobs/lgtm.ts,src/cronJobs/sweep.ts,src/cronJobs/labelSync.ts,src/cronJobs/handleCronJob.ts
              - name: plugins
                mutate: src/plugins/blunderbuss.ts,src/plugins/lgtmBinding.ts,src/plugins/ownersLabel.ts,src/plugins/requireMatchingLabel.ts,src/utils/config.ts,src/utils/labeling.ts,src/utils/labelCatalog.ts
              - name: commands
                mutate: src/issueComment/*.ts,src/pullReq/*.ts,src/issues/*.ts,src/utils/command.ts,src/utils/comments.ts,src/utils/events.ts,src/utils/pulls.ts,src/run.ts
        steps:
          - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
          - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
            with:
              node-version-file: package.json
              cache: npm
          - run: npm ci
          - run: npx stryker run stryker.nightly.config.mjs
            env:
              STRYKER_MUTATE: ${{ matrix.mutate }}
          - uses: actions/upload-artifact@v4
            if: always()
            with:
              name: mutation-${{ matrix.name }}
              path: reports/mutation/report.json

    (actions/upload-artifact should be pinned to a SHA the way the other two actions are; the repository's existing pin for it, if any, wins.)

    The run is advisory: thresholds.break stays null, so a survivor never fails the workflow — the JSON artifact is the signal, and the quality lane reads it instead of re-running two-hour probes by hand.

    Decision requested

    • Accept the scheduled run as proposed (weekly, five shards, advisory), or
    • Decline, in which case the quality lane keeps re-running the harness by hand after test-heavy merges and keeps filing survivor pins one issue at a time.

    Risk: governance — this adds roughly 5 × 2–3 h of hosted-runner time per week to a CNCF repository, and docs/contributing.md currently frames the harness as local-only.

    Decision taken by hive: shard by functional cluster rather than by file count, weekly rather than nightly (the tree changes a few times a week and each run is hours), advisory rather than a break threshold (the docs' stated position).

    Priority

    • Impact: medium
    • Effort: low (the non-workflow half is two config files and a docs paragraph; the workflow half is the text above)

    Filed by quality agent (hold-gated mode)


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

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