From 0b6298c60eabbe05f2d1e392daa95b6ec8d4f197 Mon Sep 17 00:00:00 2001 From: qqqys Date: Fri, 24 Jul 2026 11:30:18 +0800 Subject: [PATCH 1/3] refactor(autofix): extract review verification runner --- .../run-autofix-review-verification.sh | 156 ++++++++++++++++++ .github/workflows/qwen-autofix.yml | 155 +---------------- scripts/tests/qwen-autofix-workflow.test.js | 70 ++++++-- 3 files changed, 214 insertions(+), 167 deletions(-) create mode 100755 .github/scripts/run-autofix-review-verification.sh diff --git a/.github/scripts/run-autofix-review-verification.sh b/.github/scripts/run-autofix-review-verification.sh new file mode 100755 index 00000000000..c11c4f96e7d --- /dev/null +++ b/.github/scripts/run-autofix-review-verification.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +set -eo pipefail + +# Record whether the agent left a commit FIRST β€” this is a ref-only +# diff, so it runs before the failure.md early-exits and covers an +# agent that commits and then aborts. The failure handoff keys its +# "was NOT pushed / commit discarded" wording on this, NOT on +# outcome=failed: abort / pre-commit-gate paths that never committed +# keep the neutral framing. `git diff --quiet` exits 1 for a real diff +# (committed) but 128 on a bad ref β€” only 1 counts as a commit, so a +# git error is not misreported as a discarded commit. +committed_rc=0 +git diff --quiet "origin/${BRANCH}...${BRANCH}" || committed_rc=$? +if [[ "${committed_rc}" -eq 1 ]]; then + echo "committed=true" >> "${GITHUB_OUTPUT}" +fi + +if [[ -f "${WORKDIR}/failure.md" && -n "$(git status --porcelain)" ]]; then + echo "❌ Agent wrote failure.md after leaving a dirty workspace:" + git status --short + cat "${WORKDIR}/failure.md" + echo "outcome=failed" >> "${GITHUB_OUTPUT}" + exit 1 +fi + +if [[ -f "${WORKDIR}/failure.md" ]]; then + echo "πŸ›‘ Agent aborted intentionally:" + cat "${WORKDIR}/failure.md" + echo "outcome=failed" >> "${GITHUB_OUTPUT}" + exit 1 +fi + +# Convention: hooks are severed at EVERY host checkout of the PR +# branch (no secret sits in this step's env, but a post-checkout +# hook still runs branch code on the host). +git config core.hooksPath /dev/null +git checkout "${BRANCH}" + +# Settings-schema freshness is a STRUCTURAL guard, checked BEFORE the +# no-op/unchanged return: on a stale-schema PR the agent can wrongly +# write no-action.md, and without this the no-op path would report the +# feedback as evaluated (acted=false) while CI stays red β€” the exact bug +# this PR fixes. So it runs on EVERY path. The gate is shared with the +# issue-fix verify step (rationale + the generator crash guard live in +# the script); the write is on a tracked file compared by `git status`, +# not the commit-level no-op git-diff below, and it is restored on +# failure. On failure it writes outcome=failed and exits 1. +# Run the copy staged from the trusted base checkout: a PR branch +# that predates the script does not contain it (bash would exit 127 +# and kill the gate with no outcome), and the gate logic must come +# from the trusted base, not the branch under verification. +bash "${RUNNER_TEMP}/check-settings-schema.sh" + +if git diff --quiet "origin/${BRANCH}...${BRANCH}"; then + # No new commit. That is only legitimate as a deliberate no-action. + if [[ -s "${WORKDIR}/no-action.md" ]]; then + echo "🟰 No action needed:" + cat "${WORKDIR}/no-action.md" + echo "outcome=noop" >> "${GITHUB_OUTPUT}" + exit 0 + fi + echo "❌ Branch unchanged and no no-action.md β€” agent produced nothing" + echo "outcome=failed" >> "${GITHUB_OUTPUT}" + exit 1 +fi + +if [[ ! -s "${WORKDIR}/address-summary.md" ]]; then + echo "❌ Branch changed but address-summary.md is missing" + echo "outcome=failed" >> "${GITHUB_OUTPUT}" + exit 1 +fi + +# Every check below can legitimately REJECT the agent's attempt, so +# each declares that verdict explicitly. That is what lets the handoff +# tell a rejection apart from the gate's OWN death: an empty outcome +# on a failed job means the gate never reached a verdict (its own bug, +# an infra blip), and the agent's work must then be retried rather +# than buried by a watermark advance. +# Capture each check's output. A rejection has to tell the agent WHY +# its change was refused: without that, the next round re-reads only +# the original review feedback and re-makes the same mistake - #7208 +# was handed to a human over a two-character TS4111 fix its own +# compiler output already spelled out. +GATE_LOG="${WORKDIR}/gate-output.log" +: > "${GATE_LOG}" +reject_fix() { + echo "❌ ${1}" + # Declare the verdict FIRST. The handoff routes on outcome=, and an + # empty outcome on a failed job means "the gate never reached a + # verdict" β€” i.e. a crash, which is RETRIED. So a rejection that + # dies while writing its detail file would be re-attempted forever + # instead of reported once. A detail we cannot write is a degraded + # message; it must never cost the verdict, hence this order and the + # non-fatal write below. + echo "outcome=failed" >> "${GITHUB_OUTPUT}" + { + echo "**${1}**" + echo + # A four-backtick fence cannot be closed by a ``` line, so + # captured output containing its own fences stays inside the + # block when this is posted verbatim as a PR comment. + echo '````' + tail -c 3000 "${GATE_LOG}" 2> /dev/null + echo '````' + } > "${WORKDIR}/gate-rejection.md" || + echo "::warning::could not write the gate rejection detail; the verdict stands." + exit 1 +} +run_check() { + # pipefail makes the pipeline carry the command's status, not tee's. + local label="${1}" + shift + if ! "$@" 2>&1 | tee -a "${GATE_LOG}"; then + reject_fix "${label}" + fi +} + +echo 'πŸ”¬ Re-running deterministic checks (independent of the agent)...' +run_check 'build failed on the agent-committed fix' npm run build +run_check 'typecheck failed on the agent-committed fix' npm run typecheck +run_check 'lint failed on the agent-committed fix' npm run lint + +# Test changed/related files for the packages this PR touches. +# --changed follows the import graph so transitive breakage is caught. +# Full regression is covered by regular CI on the PR after the push. +# Map each changed file to its OWNING npm workspace via the trusted +# staged resolver, shared with the other verify gate so both resolve +# packages identically. It expands the on-disk root package.json +# workspaces globs (so a workspace the branch ADDS is included) and +# takes each file's longest-prefix workspace β€” never a flat +# 'packages/' (ENOENT-crashes on nested packages) nor a fixture +# package.json inside a workspace's src tree (would skip the owning +# workspace's tests). No '|| true': a resolver error (missing node, an +# unreadable manifest) must fail the gate loudly rather than silently +# skip package tests; legitimate no-match input already exits 0 empty. +CHANGED_PKGS="$(git diff --name-only "origin/main...${BRANCH}" \ + | bash "${RUNNER_TEMP}/resolve-owning-packages.sh")" +if [[ -z "${CHANGED_PKGS}" ]]; then + echo 'No package changes detected; skipping package tests.' +else + for p in ${CHANGED_PKGS}; do + if [[ ! -f "${p}/package.json" ]]; then + echo "Skipping ${p}: no package.json." + continue + fi + test_script="$(node -e 'const fs = require("node:fs"); const pkg = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(pkg.scripts?.test || "");' "${p}/package.json")" + if [[ "${test_script}" != *vitest* ]]; then + echo "Skipping ${p}: test script is not Vitest." + continue + fi + echo "πŸ§ͺ Testing ${p} (changed files only)..." + run_check "tests failed in ${p}" \ + npm run test --workspace "${p}" --if-present -- --changed origin/main --passWithNoTests + done +fi +echo "outcome=fixed" >> "${GITHUB_OUTPUT}" diff --git a/.github/workflows/qwen-autofix.yml b/.github/workflows/qwen-autofix.yml index dc209d65ce8..7d3d4a2e293 100644 --- a/.github/workflows/qwen-autofix.yml +++ b/.github/workflows/qwen-autofix.yml @@ -2481,6 +2481,7 @@ jobs: run: |- cp .github/scripts/check-settings-schema.sh "${RUNNER_TEMP}/check-settings-schema.sh" cp .github/scripts/resolve-owning-packages.sh "${RUNNER_TEMP}/resolve-owning-packages.sh" + cp .github/scripts/run-autofix-review-verification.sh "${RUNNER_TEMP}/run-autofix-review-verification.sh" # The agent step runs AFTER prepare checks out the PR branch, so # invoking the runner from the working tree would execute # branch-controlled code on the host with the model key in env @@ -3026,159 +3027,7 @@ jobs: if: |- ${{ always() && steps.prepare.outputs.stale != 'true' }} run: |- - # Record whether the agent left a commit FIRST β€” this is a ref-only - # diff, so it runs before the failure.md early-exits and covers an - # agent that commits and then aborts. The failure handoff keys its - # "was NOT pushed / commit discarded" wording on this, NOT on - # outcome=failed: abort / pre-commit-gate paths that never committed - # keep the neutral framing. `git diff --quiet` exits 1 for a real diff - # (committed) but 128 on a bad ref β€” only 1 counts as a commit, so a - # git error is not misreported as a discarded commit. - committed_rc=0 - git diff --quiet "origin/${BRANCH}...${BRANCH}" || committed_rc=$? - if [[ "${committed_rc}" -eq 1 ]]; then - echo "committed=true" >> "${GITHUB_OUTPUT}" - fi - - if [[ -f "${WORKDIR}/failure.md" && -n "$(git status --porcelain)" ]]; then - echo "❌ Agent wrote failure.md after leaving a dirty workspace:" - git status --short - cat "${WORKDIR}/failure.md" - echo "outcome=failed" >> "${GITHUB_OUTPUT}" - exit 1 - fi - - if [[ -f "${WORKDIR}/failure.md" ]]; then - echo "πŸ›‘ Agent aborted intentionally:" - cat "${WORKDIR}/failure.md" - echo "outcome=failed" >> "${GITHUB_OUTPUT}" - exit 1 - fi - - # Convention: hooks are severed at EVERY host checkout of the PR - # branch (no secret sits in this step's env, but a post-checkout - # hook still runs branch code on the host). - git config core.hooksPath /dev/null - git checkout "${BRANCH}" - - # Settings-schema freshness is a STRUCTURAL guard, checked BEFORE the - # no-op/unchanged return: on a stale-schema PR the agent can wrongly - # write no-action.md, and without this the no-op path would report the - # feedback as evaluated (acted=false) while CI stays red β€” the exact bug - # this PR fixes. So it runs on EVERY path. The gate is shared with the - # issue-fix verify step (rationale + the generator crash guard live in - # the script); the write is on a tracked file compared by `git status`, - # not the commit-level no-op git-diff below, and it is restored on - # failure. On failure it writes outcome=failed and exits 1. - # Run the copy staged from the trusted base checkout: a PR branch - # that predates the script does not contain it (bash would exit 127 - # and kill the gate with no outcome), and the gate logic must come - # from the trusted base, not the branch under verification. - bash "${RUNNER_TEMP}/check-settings-schema.sh" - - if git diff --quiet "origin/${BRANCH}...${BRANCH}"; then - # No new commit. That is only legitimate as a deliberate no-action. - if [[ -s "${WORKDIR}/no-action.md" ]]; then - echo "🟰 No action needed:" - cat "${WORKDIR}/no-action.md" - echo "outcome=noop" >> "${GITHUB_OUTPUT}" - exit 0 - fi - echo "❌ Branch unchanged and no no-action.md β€” agent produced nothing" - echo "outcome=failed" >> "${GITHUB_OUTPUT}" - exit 1 - fi - - if [[ ! -s "${WORKDIR}/address-summary.md" ]]; then - echo "❌ Branch changed but address-summary.md is missing" - echo "outcome=failed" >> "${GITHUB_OUTPUT}" - exit 1 - fi - - # Every check below can legitimately REJECT the agent's attempt, so - # each declares that verdict explicitly. That is what lets the handoff - # tell a rejection apart from the gate's OWN death: an empty outcome - # on a failed job means the gate never reached a verdict (its own bug, - # an infra blip), and the agent's work must then be retried rather - # than buried by a watermark advance. - # Capture each check's output. A rejection has to tell the agent WHY - # its change was refused: without that, the next round re-reads only - # the original review feedback and re-makes the same mistake - #7208 - # was handed to a human over a two-character TS4111 fix its own - # compiler output already spelled out. - GATE_LOG="${WORKDIR}/gate-output.log" - : > "${GATE_LOG}" - reject_fix() { - echo "❌ ${1}" - # Declare the verdict FIRST. The handoff routes on outcome=, and an - # empty outcome on a failed job means "the gate never reached a - # verdict" β€” i.e. a crash, which is RETRIED. So a rejection that - # dies while writing its detail file would be re-attempted forever - # instead of reported once. A detail we cannot write is a degraded - # message; it must never cost the verdict, hence this order and the - # non-fatal write below. - echo "outcome=failed" >> "${GITHUB_OUTPUT}" - { - echo "**${1}**" - echo - # A four-backtick fence cannot be closed by a ``` line, so - # captured output containing its own fences stays inside the - # block when this is posted verbatim as a PR comment. - echo '````' - tail -c 3000 "${GATE_LOG}" 2> /dev/null - echo '````' - } > "${WORKDIR}/gate-rejection.md" || - echo "::warning::could not write the gate rejection detail; the verdict stands." - exit 1 - } - run_check() { - # pipefail makes the pipeline carry the command's status, not tee's. - local label="${1}" - shift - if ! "$@" 2>&1 | tee -a "${GATE_LOG}"; then - reject_fix "${label}" - fi - } - - echo 'πŸ”¬ Re-running deterministic checks (independent of the agent)...' - run_check 'build failed on the agent-committed fix' npm run build - run_check 'typecheck failed on the agent-committed fix' npm run typecheck - run_check 'lint failed on the agent-committed fix' npm run lint - - # Test changed/related files for the packages this PR touches. - # --changed follows the import graph so transitive breakage is caught. - # Full regression is covered by regular CI on the PR after the push. - # Map each changed file to its OWNING npm workspace via the trusted - # staged resolver, shared with the other verify gate so both resolve - # packages identically. It expands the on-disk root package.json - # workspaces globs (so a workspace the branch ADDS is included) and - # takes each file's longest-prefix workspace β€” never a flat - # 'packages/' (ENOENT-crashes on nested packages) nor a fixture - # package.json inside a workspace's src tree (would skip the owning - # workspace's tests). No '|| true': a resolver error (missing node, an - # unreadable manifest) must fail the gate loudly rather than silently - # skip package tests; legitimate no-match input already exits 0 empty. - CHANGED_PKGS="$(git diff --name-only "origin/main...${BRANCH}" \ - | bash "${RUNNER_TEMP}/resolve-owning-packages.sh")" - if [[ -z "${CHANGED_PKGS}" ]]; then - echo 'No package changes detected; skipping package tests.' - else - for p in ${CHANGED_PKGS}; do - if [[ ! -f "${p}/package.json" ]]; then - echo "Skipping ${p}: no package.json." - continue - fi - test_script="$(node -e 'const fs = require("node:fs"); const pkg = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(pkg.scripts?.test || "");' "${p}/package.json")" - if [[ "${test_script}" != *vitest* ]]; then - echo "Skipping ${p}: test script is not Vitest." - continue - fi - echo "πŸ§ͺ Testing ${p} (changed files only)..." - run_check "tests failed in ${p}" \ - npm run test --workspace "${p}" --if-present -- --changed origin/main --passWithNoTests - done - fi - echo "outcome=fixed" >> "${GITHUB_OUTPUT}" + bash "${RUNNER_TEMP}/run-autofix-review-verification.sh" - name: 'Show run artifacts' if: |- diff --git a/scripts/tests/qwen-autofix-workflow.test.js b/scripts/tests/qwen-autofix-workflow.test.js index 1948bf8cb58..310dc4d81f0 100644 --- a/scripts/tests/qwen-autofix-workflow.test.js +++ b/scripts/tests/qwen-autofix-workflow.test.js @@ -25,6 +25,12 @@ const sandboxImageResolverScript = readFileSync( '.github/scripts/resolve-sandbox-image.mjs', 'utf8', ); +const reviewVerificationRunnerPath = + '.github/scripts/run-autofix-review-verification.sh'; +const reviewVerificationRunner = readFileSync( + reviewVerificationRunnerPath, + 'utf8', +); const autofixRunnerScriptPath = '.qwen/skills/autofix/scripts/run-agent.mjs'; const checkBotCredentialsStep = workflow.match( @@ -103,6 +109,10 @@ const resetAutofixWorkspaceSteps = const verificationGateSteps = workflow.match(/- name: 'Verification gate'[\s\S]*?(?=\n[ ]{6}- name: ')/g) ?? []; +const verificationGateBodies = [ + verificationGateSteps[0] ?? '', + reviewVerificationRunner, +]; const resolveSandboxImageSteps = workflow.match( /- name: 'Resolve sandbox image'[\s\S]*?(?=\n[ ]{6}- name: ')/g, @@ -4088,7 +4098,7 @@ describe('qwen-autofix workflow', () => { it('allows non-package fixes after deterministic verification', () => { expect(verificationGateSteps).toHaveLength(2); - for (const step of verificationGateSteps) { + for (const step of verificationGateBodies) { expect(step).toContain('npm run build'); expect(step).toContain('npm run typecheck'); expect(step).toContain('npm run lint'); @@ -4131,6 +4141,11 @@ describe('qwen-autofix workflow', () => { /cp \.github\/scripts\/resolve-owning-packages\.sh "\$\{RUNNER_TEMP\}\/resolve-owning-packages\.sh"/g, ) ?? [], ).toHaveLength(2); + expect( + workflow.match( + /cp \.github\/scripts\/run-autofix-review-verification\.sh "\$\{RUNNER_TEMP\}\/run-autofix-review-verification\.sh"/g, + ) ?? [], + ).toHaveLength(1); // In the issue-autofix job the staging must happen BEFORE the verify gate's // `git checkout "${BRANCH}"` (first occurrence in the file is the issue // job's): the agent's commits can touch .github/scripts, so a post-checkout @@ -4145,9 +4160,20 @@ describe('qwen-autofix workflow', () => { // In the review-address job the staging must happen BEFORE the branch switch // ("Prepare branch and feedback" exists only in that job; the job's staging // step is the last occurrence of the staging step name in the file). - expect( - workflow.lastIndexOf("- name: 'Stage trusted schema gate'"), - ).toBeLessThan(workflow.indexOf("- name: 'Prepare branch and feedback'")); + const reviewStagingAt = workflow.indexOf( + "- name: 'Stage trusted schema gate and agent runner'", + ); + expect(reviewStagingAt).toBeGreaterThanOrEqual(0); + expect(reviewStagingAt).toBeLessThan( + workflow.indexOf("- name: 'Prepare branch and feedback'"), + ); + const reviewRunnerCopyAt = workflow.indexOf( + 'cp .github/scripts/run-autofix-review-verification.sh', + ); + expect(reviewRunnerCopyAt).toBeGreaterThan(reviewStagingAt); + expect(reviewRunnerCopyAt).toBeLessThan( + workflow.indexOf("- name: 'Prepare branch and feedback'"), + ); // The shared script mirrors CI's freshness gate: regenerate + `git status // --porcelain` (version-agnostic β€” the generator's --check was reverted from // main by #7031 and must NOT be relied on), with a generator-crash guard, and @@ -4187,7 +4213,7 @@ describe('qwen-autofix workflow', () => { // must run BEFORE the no-op/unchanged return, so a stale-schema PR the agent // wrongly no-ops fails (outcome=failed) instead of being reported as evaluated // while CI stays red (the motivating bug). - const reviewVerifyGate = verificationGateSteps.find((s) => + const reviewVerifyGate = verificationGateBodies.find((s) => s.includes('outcome=noop'), ); expect(reviewVerifyGate).toBeTruthy(); @@ -4201,6 +4227,14 @@ describe('qwen-autofix workflow', () => { 'bash "${RUNNER_TEMP}/check-settings-schema.sh"', ), ).toBeLessThan(reviewVerifyGate.indexOf('outcome=noop')); + const reviewVerificationGateStep = verificationGateSteps[1]; + expect(reviewVerificationGateStep).toContain( + 'bash "${RUNNER_TEMP}/run-autofix-review-verification.sh"', + ); + expect(reviewVerificationGateStep).not.toContain('npm run build'); + expect(reviewVerificationGateStep).not.toContain( + 'bash .github/scripts/run-autofix-review-verification.sh', + ); }); it('passes model credentials directly to qwen subprocesses', () => { @@ -4255,8 +4289,13 @@ describe('qwen-autofix workflow', () => { // AND both no-secret verification checkouts (convention: every host // checkout of an agent-writable branch severs hooks). expect( - workflow.split('git config core.hooksPath /dev/null').length - 1, + `${workflow}\n${reviewVerificationRunner}`.split( + 'git config core.hooksPath /dev/null', + ).length - 1, ).toBe(5); + expect(reviewVerificationRunner).toMatch( + /git config core\.hooksPath \/dev\/null\ngit checkout "\$\{BRANCH\}"/, + ); // …both pushes AND the prepare checkout (post-checkout hooks fire with // the PAT in env there); the agent step β€” no PAT, sandboxed tools β€” // re-points .husky itself so its commits still get checked. @@ -4479,7 +4518,7 @@ describe('qwen-autofix workflow', () => { // 1 for a real diff but 128 on a bad ref β€” only 1 is a commit, so a git // error must not be misreported as a discarded commit. Drive the extracted // snippet with a stubbed git whose exit is scripted. - const snippet = verificationGateSteps[1].match( + const snippet = reviewVerificationRunner.match( /committed_rc=0[\s\S]*?committed=true[^\n]*\n\s*fi/, )?.[0]; expect(snippet).toBeTruthy(); @@ -4520,7 +4559,7 @@ describe('qwen-autofix workflow', () => { // Neither gate carries an EXIT trap: the wording keys on committed, so an // outcome=failed-forcing trap (which would also fire on pre-commit // failures) must not creep back into either verify step. - for (const gate of verificationGateSteps) { + for (const gate of verificationGateBodies) { expect(gate).not.toMatch(/\btrap\b/); } }); @@ -4532,8 +4571,11 @@ describe('qwen-autofix workflow', () => { expect(reviewVerificationGateStep).toContain( "if: |-\n ${{ always() && steps.prepare.outputs.stale != 'true' }}", ); - expect(reviewVerificationGateStep).toContain('failure.md'); - expect(reviewVerificationGateStep).toContain('outcome=failed'); + expect(reviewVerificationGateStep).toContain( + 'bash "${RUNNER_TEMP}/run-autofix-review-verification.sh"', + ); + expect(reviewVerificationRunner).toContain('failure.md'); + expect(reviewVerificationRunner).toContain('outcome=failed'); expect(reviewAddressReportStep.length).toBeGreaterThan(0); expect(reviewAddressReportStep).toContain('GITHUB_STEP_SUMMARY'); expect(reviewAddressReportStep).toContain( @@ -5314,7 +5356,7 @@ describe('qwen-autofix workflow', () => { // The retry/advance split above is only sound while each real rejection // writes outcome=failed; an unwired check would read as a gate crash and be // retried instead of reported. Drive the extracted helper for real. - const gate = verificationGateSteps[1]; + const gate = reviewVerificationRunner; // Each check runs through run_check, which tees its output to GATE_LOG and // calls reject_fix on failure - so the verdict is declared AND the reason // is captured for the retry. @@ -5326,7 +5368,7 @@ describe('qwen-autofix workflow', () => { ]) { expect(gate).toContain(check); } - const helper = gate.match(/reject_fix\(\) \{\n[\s\S]*?\n {10}\}/)?.[0]; + const helper = gate.match(/reject_fix\(\) \{\n[\s\S]*?\n\}/)?.[0]; expect(helper).toBeTruthy(); const dir = mkdtempSync(join(tmpdir(), 'reject-')); const out = join(dir, 'gh_output'); @@ -5688,7 +5730,7 @@ describe('qwen-autofix workflow', () => { // compiler output already spelled out: the gate rejected the commit, the // handoff showed only the agent's optimistic summary, and the next round // re-read the original review points with no idea why it had been refused. - const gate = verificationGateSteps[1]; + const gate = reviewVerificationRunner; const prep = workflow.match( /- name: 'Prepare branch and feedback'[\s\S]*?(?=\n {6}- name: )/, @@ -5696,7 +5738,7 @@ describe('qwen-autofix workflow', () => { // 1. A failing check records WHY, not just THAT, it failed. const capture = gate.match( - /GATE_LOG="\$\{WORKDIR\}\/gate-output\.log"[\s\S]*?\n {10}\}\n {10}run_check\(\) \{[\s\S]*?\n {10}\}/, + /GATE_LOG="\$\{WORKDIR\}\/gate-output\.log"[\s\S]*?\n\}\nrun_check\(\) \{[\s\S]*?\n\}/, )?.[0]; expect(capture).toBeTruthy(); const dir = mkdtempSync(join(tmpdir(), 'gate-')); From abddb3f7abbe520fd95cff9d1d0a33522f262e7e Mon Sep 17 00:00:00 2001 From: qqqys Date: Fri, 24 Jul 2026 12:11:19 +0800 Subject: [PATCH 2/3] test(ci): follow extracted autofix verifier --- scripts/tests/package-scripts.test.js | 29 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/scripts/tests/package-scripts.test.js b/scripts/tests/package-scripts.test.js index 6061f0abf8f..a9f4e91c3a0 100644 --- a/scripts/tests/package-scripts.test.js +++ b/scripts/tests/package-scripts.test.js @@ -498,22 +498,33 @@ describe('package scripts', () => { it('runs changed autofix tests instead of full touched-package suites', () => { const workflow = readWorkflow('.github/workflows/qwen-autofix.yml'); + const reviewVerificationRunner = readWorkflow( + '.github/scripts/run-autofix-review-verification.sh', + ); + const reviewJob = getWorkflowJob(workflow, 'review-address'); - for (const jobName of ['issue-autofix', 'review-address']) { - const job = getWorkflowJob(workflow, jobName); - const verifyStep = getWorkflowStep(job, 'Verification gate'); - - expect(verifyStep).toContain( + for (const verificationBody of [ + getWorkflowStep( + getWorkflowJob(workflow, 'issue-autofix'), + 'Verification gate', + ), + reviewVerificationRunner, + ]) { + expect(verificationBody).toContain( 'npm run test --workspace "${p}" --if-present -- --changed origin/main --passWithNoTests', ); - expect(verifyStep).toContain( + expect(verificationBody).toContain( 'bash "${RUNNER_TEMP}/resolve-owning-packages.sh"', ); - expect(verifyStep).toContain('pkg.scripts?.test'); - expect(verifyStep).toContain('!= *vitest*'); - expect(verifyStep).not.toContain( + expect(verificationBody).toContain('pkg.scripts?.test'); + expect(verificationBody).toContain('!= *vitest*'); + expect(verificationBody).not.toContain( 'npm run test --workspace "${p}" --if-present\n', ); } + + expect(getWorkflowStep(reviewJob, 'Verification gate')).toContain( + 'bash "${RUNNER_TEMP}/run-autofix-review-verification.sh"', + ); }); }); From 4ec8d2ea08e77b0cd3fd5e2b81c03a54ace33b07 Mon Sep 17 00:00:00 2001 From: qqqys Date: Fri, 24 Jul 2026 07:59:45 +0000 Subject: [PATCH 3/3] docs(autofix): document the review verification runner env contract (#7644) --- .github/scripts/run-autofix-review-verification.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/scripts/run-autofix-review-verification.sh b/.github/scripts/run-autofix-review-verification.sh index 27d20705ff9..551b6cbf73b 100755 --- a/.github/scripts/run-autofix-review-verification.sh +++ b/.github/scripts/run-autofix-review-verification.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -eo pipefail +# Invoked as a child `bash` from the review-address verify step; inherits its +# environment from the caller. WORKDIR and BRANCH are job-level env; +# GITHUB_OUTPUT and RUNNER_TEMP are runner-provided. None is defined here. + # Record whether the agent left a commit FIRST β€” this is a ref-only # diff, so it runs before the failure.md early-exits and covers an # agent that commits and then aborts. The failure handoff keys its