Skip to content

PR review never converges: each pass reports a few findings and every push starts a new pass #731

Description

@SaulMoro

The automated PR review reports a few findings per pass, and every push starts a new pass. A PR with many latent findings needs one push per handful, so it never converges.

PR #698 measured (passes that completed; cancel-in-progress: true drops the ones a new push interrupted):

Review passes 12
Findings per pass 2, 5, 3, 4, 3, 3, 2, 2, 2, 4, 3, 4
Total findings 37
Most in any one pass 5
flowchart LR
    A[Author pushes] --> B[Review fires on synchronize]
    B --> C[Reports a few of what it found]
    C --> D[Author fixes those]
    D --> A
Loading

The placement collision bug was visible in the round 1 diff. The review raised it in round 6.

Cause 1: nothing tells the review to report everything, and it cannot see earlier passes

.github/workflows/codex-review-on-assign.yml

- Flag bugs, risks, and rule violations. Be concise and specific.
+ Flag bugs, risks, and rule violations. Keep each finding short and never
+ cap how many you report: a finding present in this diff that you leave
+ for a later pass costs the author another round trip. The severity label
+ ranks them; the list does not need to.

Codex runs read-only on the base checkout with no GitHub token, so it cannot read the PR's earlier review comments. A rule in AGENTS.md asking it to would have no effect. The workflow has to feed them in:

   - name: Fetch PR head objects (data only, never executed)
+  - name: Collect earlier review comments
+    id: prior
+    uses: actions/github-script@v7
+    # issues.listComments, keep the github-actions[bot] ones, join as text
   - name: Run Codex review
       prompt: |
+        Earlier passes of this review already raised the findings below.
+        A finding already fixed in the current diff is resolved: say so,
+        do not raise it again.
+        ----
+        ${{ steps.prior.outputs.comments }}

Cause 2: the review reads the PR body from the moment the push landed

${{ github.event.pull_request.body }} is captured when the workflow fires. A body edited after the push is invisible to that pass.

sequenceDiagram
    participant Author
    participant Workflow
    Author->>Workflow: push
    Workflow->>Workflow: snapshot body and head
    Author->>Author: update PR body with the new test record
    Workflow-->>Author: "the test plan names an older commit"
Loading

This produced a false blocking finding in one pass, and two already fixed findings in another. Two changes:

.github/workflows/codex-review-on-assign.yml: re-read the body right before the Codex step instead of using the event snapshot. It is still a snapshot, but taken after the author's usual post-push edit.

+  - name: Read current PR body
+    id: pr
+    uses: actions/github-script@v7
+    # pulls.get -> outputs.body
   - name: Run Codex review
       prompt: |
-        ${{ github.event.pull_request.body }}
+        ${{ steps.pr.outputs.body }}

AGENTS.md

  - The PR description must document sufficient testing...
+   A test record naming an older commit than the head is a note, not a
+   blocking finding. The body may have been edited after this pass started.

Cause 3: test files hand-list the exports of config.js

One mechanical error cost four separate debugging detours in PR #698.

52  files write  vi.mock('../config.js', () => ({ ... }))
 6  files spread importOriginal() and override a few keys
16  exports in src/config.ts

Production code that calls an export a given test did not list fails that test with No "X" export is defined on the mock. No linter catches a missing key in a vi.mock factory. A shared helper does:

// src/__tests__/helpers/mock-config.ts
export const mockConfig = (overrides: Partial<typeof import('../../config.js')> = {}) =>
  vi.mock('../config.js', async (importOriginal) => ({ ...(await importOriginal()), ...overrides }));

Migrating the 52 literal factories to it removes the class of failure.

Cause 4: fork PRs get no e2e in CI, and the docs do not name the e2e command

AGENTS.md requires a real CLI verification record. CI does run the full e2e surface (E2E (GitHub provider, full surface) runs vitest run --config vitest.e2e.config.ts), but that job is gated on vars.TEAMAI_TEST_REPO_URL != '', so a PR from a fork skips it and the rule holds only because a reviewer reads prose about it.

CLAUDE.md and AGENTS.md list npx vitest run in their command line and not npm run test:e2e, so an agent has to find the split on its own.

- Commands: `npm run build`, `npx tsc --noEmit`, `npx vitest run`.
+ Commands: `npm run build`, `npx tsc --noEmit`, `npx vitest run`, `npm run test:e2e`.

What is not the review's fault

Some passes found real defects, and some of those defects came from the previous pass's fix. Adding a mechanism to fix a mechanism makes the chain longer. A self review of the whole branch diff before pushing caught half of one pass's findings before the review saw them, which suggests the same habit written down would shorten the chain.

  ## PR 前测试
+ ## Self review before push
+ Review the whole branch diff, not only the last change. For every piece of
+ shared state, list every reader and every writer.

Suggested split

Causes 1 and 2 are one workflow PR. Cause 3 is a test-only PR. Cause 4's doc line is trivial; the fork gap is a CI decision on its own.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions