diff --git a/.github/scripts/run-autofix-review-verification.sh b/.github/scripts/run-autofix-review-verification.sh
new file mode 100755
index 00000000000..551b6cbf73b
--- /dev/null
+++ b/.github/scripts/run-autofix-review-verification.sh
@@ -0,0 +1,162 @@
+#!/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
+# "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"
+git diff --name-only "origin/main...${BRANCH}" \
+ | bash "${RUNNER_TEMP}/check-autofix-contracts.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 4609f471b9a..b2fcf4a4083 100644
--- a/.github/workflows/qwen-autofix.yml
+++ b/.github/workflows/qwen-autofix.yml
@@ -2485,6 +2485,7 @@ jobs:
cp .github/scripts/check-settings-schema.sh "${RUNNER_TEMP}/check-settings-schema.sh"
cp .github/scripts/check-autofix-contracts.sh "${RUNNER_TEMP}/check-autofix-contracts.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
@@ -3030,161 +3031,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"
- git diff --name-only "origin/main...${BRANCH}" \
- | bash "${RUNNER_TEMP}/check-autofix-contracts.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/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"',
+ );
});
});
diff --git a/scripts/tests/qwen-autofix-workflow.test.js b/scripts/tests/qwen-autofix-workflow.test.js
index 310100cb7bf..cd020ea231e 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 autofixContractsScriptPath = '.github/scripts/check-autofix-contracts.sh';
const autofixContractsScript = readFileSync(autofixContractsScriptPath, 'utf8');
const autofixRunnerScriptPath = '.qwen/skills/autofix/scripts/run-agent.mjs';
@@ -105,6 +111,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,
@@ -4100,7 +4110,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');
@@ -4154,6 +4164,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
@@ -4168,9 +4183,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
@@ -4210,7 +4236,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();
@@ -4224,6 +4250,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',
+ );
expect(
reviewVerifyGate.indexOf(
'bash "${RUNNER_TEMP}/check-autofix-contracts.sh"',
@@ -4360,8 +4394,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.
@@ -4584,7 +4623,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();
@@ -4625,7 +4664,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/);
}
});
@@ -4637,8 +4676,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(
@@ -5419,7 +5461,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.
@@ -5431,7 +5473,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');
@@ -5793,7 +5835,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: )/,
@@ -5801,7 +5843,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-'));