Gumroad watchdog · finding · antiwork/gumroad PR #7654

Adding a co-located test file silently switches off the full-suite escalation this PR promises

PR #7654 Escalate the two DownloadPage components whose consumers leave the download flow author gumclawmerged 2026-09-14 17:55 UTCcommit ee397989e category logicarea infra

Independently verified by a second review before publication.

We trained an AI agent for Gumroad. It reviews each PR merged to antiwork/gumroad with knowledge of how the codebase has evolved and which changes have caused bugs before, and every finding is independently verified before it is published, phrased as a question to verify. This is its first published finding.

The PR excludes Layout.tsx and WithContent.tsx from the DownloadPage mapping so that changes to them escalate to the full rspec suite. But an earlier rule exempts any TypeScript file that has a co-located .test.tsx. The moment someone adds Layout.test.tsx or WithContent.test.tsx, source-only changes to those components map to nothing, the exemption swallows them, the selector exits 0 with an empty list, and CI runs a placeholder spec instead of the full suite. bin/branch-specs:175 · merged diff touched 2 files, 37 insertions

What the change did

bin/branch-specs maps changed files to the rspec files a PR must run, escalating to the full suite when a change maps to nothing. The PR narrows the DownloadPage mapping with a negative lookahead so its two shared components no longer resolve to DOWNLOAD_PAGE_SPECS, and adds tests asserting that changing either file escalates. Neither component has a co-located Vitest file today, which is why the new tests pass.

The failing path

  1. A future PR adds app/javascript/components/DownloadPage/Layout.test.tsx, a normal thing to do for a shared component.
  2. A later PR changes only Layout.tsx. The mapping at line 175 now excludes it, so spec_candidates and the fanout contribute nothing.
  3. The exemption at lines 507 and 508 sees that a co-located .test.tsx exists and hits next at 516, so the file is never added to the unattributed set that triggers escalation.
  4. The selector exits 0 with an empty spec list.
  5. In .github/workflows/tests.yml an empty list is treated as "nothing rspec-affecting" and CI substitutes spec/models/user_spec.rb as a floor. The full-suite run the PR was written to guarantee never happens.
bin/branch-specs · the new mapping with its lookahead
173  %r{\Aapp/javascript/components/DiscordButton} => DOWNLOAD_PAGE_SPECS + CHECKOUT_FLOW_SPECS,
174  # Importers: DownloadPage/WithContent.tsx:19, Checkout/Receipt.tsx:13.
175  %r{\Aapp/javascript/components/DownloadPage/(?!Layout\.tsx\z|WithContent\.tsx\z)} => DOWNLOAD_PAGE_SPECS,
176  # Layout.tsx and WithContent.tsx escalate: the former renders the
177  # UrlRedirects pages, the latter reaches ProductEdit/ContentTab through
178  # components/Download/{RichContent,FileList}.
bin/branch-specs · the co-located-test exemption that runs first
505  elsif path.match?(%r{\Aapp/javascript/.*\.test\.(ts|tsx)\z}) ||
506        path.match?(%r{\Aapp/javascript/.*\.d\.ts\z}) ||
507        (path.match?(%r{\Aapp/javascript/.*\.(ts|tsx)\z}) &&
508         File.exist?(path.sub(/\.(ts|tsx)\z/, '.test.\1'))) ||
509        path == "app/javascript/components/EvaporateUploader.tsx" ||
510        path == "vendor/assets/javascripts/evaporate.cjs"
511    # Covered by vitest, which lint_js runs on every PR (npm test). A
512    # co-located .test file is that module's suite; type-only .d.ts
513    # files ride with the consuming module. Evaporate's context hook and
514    # vendored client are exercised by useConfigureEvaporate vitest.
515    # None of these need rspec.
516    next
.github/workflows/tests.yml · what an empty selection turns into
986          COUNT=$(echo "$SPECS" | grep -c . || true)
987          # An empty list here means the diff touched nothing rspec-affecting
988          # (selector escalates on app-code diffs that map to nothing).
989          if [ "$COUNT" -eq 0 ]; then
990            echo "No rspec-affecting changes; running the schema smoke spec as a floor."
991            SPECS="spec/models/user_spec.rb"
992            COUNT=1
993          fi

How to verify

Add a case to spec/bin/branch_specs_test.rb whose base contains Layout.tsx and Layout.test.tsx and whose head changes only Layout.tsx, with expect_escalate: true, then run ruby spec/bin/branch_specs_test.rb. It reports expected exit 3 but got 0 with empty stdout. Repeat for WithContent.tsx.

The selector already has the right mechanism: ESCALATE_PATTERNS, checked before the per-path loop, lists files that must always run the full suite. One pattern for these two paths there replaces the lookahead, and a test case with a co-located test file present locks it in.

Codebase history the agent used

CommitWhat it showed
e5cd221c1 2026-08-06
Close branch-specs mapping gaps that escalated four PRs this week (#7102)
Introduced the co-located Vitest exemption that this PR's mapping does not account for. The two rules were written five weeks apart by the same agent with opposite goals for the same files.
ee397989e 2026-09-14
This PR
Adds the lookahead and tests for the no-test-file case only.
Both changed files are recent, so their own history is short; the agent's record of earlier cross-flow DownloadPage regressions is what made the shared components worth escalating in the first place.

Draft comment

In bin/branch-specs line 175: the lookahead excludes Layout.tsx and WithContent.tsx from the DownloadPage mapping so they escalate, but the co-located-test exemption at lines 507 to 516 runs first. If either component gains a .test.tsx, a source-only change appears to map to nothing, get exempted, and exit 0 with an empty selection, which CI turns into the user_spec.rb floor rather than the full suite. Verifiable by a branch_specs_test.rb case with Layout.test.tsx present in base and only Layout.tsx changed, expecting escalation; it currently exits 0. Adding the two paths to ESCALATE_PATTERNS, which already runs before the loop, would make the promise hold regardless of test files.

These reviews improve over time

The agent learns from every merged PR and from every reply. When a finding is confirmed or fixed, that pattern is weighted up; when one is refuted, the correction is fed back and the agent stops raising it. Each review is more precise than the one before it.

If this was useful

These reviews run on our own time and compute. If Gumroad finds the findings worth something, pay whatever you believe is fair: https://buy.stripe.com/4gMeVdfpVgl18Td4hg5AQ00. No obligation, and the findings stand either way.

Produced by an automated review with independent verification, read-only, no tests executed. These reviews improve over time: confirmed findings sharpen the agent, refuted ones are fed back so they are not raised again. All claims are code-reading claims phrased for verification.