/** * @license * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ import { execFileSync, spawn, spawnSync } from 'node:child_process'; import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { basename, join } from 'node:path'; import { describe, expect, it } from 'vitest'; import { parse } from 'yaml'; import { arePermissionBitsEnforced } from '../../packages/cli/src/test-utils/permission-bits.js'; const workflow = readFileSync('.github/workflows/qwen-triage.yml', 'utf8'); const cacheProducerWorkflow = readFileSync( '.github/workflows/pnpm-store.yml', 'utf8', ); const mavenRepoProducerWorkflow = readFileSync( '.github/workflows/verify-maven-repo.yml', 'utf8', ); const sdkJavaWorkflow = readFileSync('.github/workflows/sdk-java.yml', 'utf8'); const prSkill = readFileSync( '.qwen/skills/triage/references/pr-workflow.md', 'utf8', ); const triageSkillDoc = readFileSync('.qwen/skills/triage/SKILL.md', 'utf8'); const triagePublisher = readFileSync( '.github/scripts/triage-publish.mjs', 'utf8', ); import { ATTRIBUTIONS, VERDICTS, checkArtifacts, } from '../../scripts/verify-pr-check.mjs'; import { REVIEW_STATE_QUERY, reviewState, } from '../../scripts/verify-pr-prepare.mjs'; const verifySkill = readFileSync('.qwen/skills/verify-pr/SKILL.md', 'utf8'); const hasGnuRealpath = spawnSync('realpath', ['-m', '--', '/'], { stdio: 'ignore', }).status === 0; function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function step(name) { const escaped = escapeRegExp(name); const match = workflow.match( new RegExp( `\\n\\s+- name:\\s*(['"])${escaped}\\1[\\s\\S]*?(?=\\n\\s+- name:\\s*['"]|\\n\\s{2}[a-zA-Z0-9_-]+:|$)`, ), ); return match?.[0] ?? ''; } // Several step names exist in more than one job (both `tmux-testing` and // `verify` have "Install and build PR app"). `step()` returns the FIRST // match, so anything asserting on a verify-lane step must scope to the job // or it silently tests the tmux copy — which has bitten this suite before. function stepIn(jobName, stepName) { const scope = job(jobName); const escaped = escapeRegExp(stepName); const match = scope.match( new RegExp( `\\n\\s+- name:\\s*(['"])${escaped}\\1[\\s\\S]*?(?=\\n\\s+- name:\\s*['"]|$)`, ), ); return match?.[0] ?? ''; } // The paths publish-verify checks out at the trusted ref. function publisherSparseCheckout() { const checkout = parse(workflow).jobs['publish-verify'].steps.find( (s) => s.name === 'Checkout the OSS publisher scripts', ); return checkout.with['sparse-checkout'].split('\n'); } function job(name) { const start = workflow.indexOf(`\n ${name}:`); if (start === -1) { return ''; } const nextJob = workflow.slice(start + 1).search(/\n {2}\S/); return nextJob === -1 ? workflow.slice(start) : workflow.slice(start, start + 1 + nextJob); } // Executed-harness factory shared by the claim and finalize lifecycle tests // and the verify job's review snapshot: a stubbed `gh` on PATH with // failure-injection arms, the real step script run under GitHub's exact // shell flags, and capture of the body/call the stub saw, every call's argv, // and whatever the step appended to $GITHUB_OUTPUT. GitHub runs // `shell: bash` as `bash --noprofile --norc -eo pipefail {0}`, and a step's // own `set -uo pipefail` does not turn `-e` back off — match that, or an // unguarded failing command kills the real step in production while the // harness stays green. One shared copy keeps that fidelity contract in one // place: the two pasted copies already diverged once (only the finalize stub // had the failure arms), and a later fix made on only one copy would silently // degrade exactly one test. The failure arms stay inert unless a scenario // sets GH_STUB_FAIL_LIST / GH_STUB_FAIL_WRITE, or GH_STUB_EMPTY_LIST for a // paginated read that exits 0 with nothing on stdout; a paginated read // otherwise answers with `commentsFile`, and a POST (any write that is not a // PATCH) with GH_STUB_POST_ID (default 7777) like the comments API's // created-id payload. The temp `dir` is returned for steps that write under // $RUNNER_TEMP. function makeGhHarness(label) { const dir = mkdtempSync(join(tmpdir(), `triage-${label}-`)); const commentsFile = join(dir, 'comments.json'); const bodyOut = join(dir, 'body.md'); const callOut = join(dir, 'call.txt'); const outputFile = join(dir, 'github_output'); const argvFile = join(dir, 'argv'); writeFileSync( join(dir, 'gh'), [ '#!/usr/bin/env bash', // One record per call, NUL between arguments: a body spans lines. `printf '%s\\0' "$@" >> "$GH_STUB_ARGV"; printf '\\036' >> "$GH_STUB_ARGV"`, 'body=""', 'for a in "$@"; do case "$a" in body=*) body="${a#body=}";; esac; done', 'if [ -n "$body" ]; then', ' printf "%s" "$body" > "$GH_STUB_OUT"', ' printf "%s\\n" "$*" > "$GH_STUB_CALL"', 'fi', 'case "$*" in', " 'api user --jq .login') echo qwen-code-ci-bot ;;", ' *--paginate*)', ' [ -n "${GH_STUB_FAIL_LIST:-}" ] && exit 1', ' [ -n "${GH_STUB_EMPTY_LIST:-}" ] && exit 0', ' cat "$GH_STUB_COMMENTS" ;;', ' *PATCH*) [ -n "${GH_STUB_FAIL_WRITE:-}" ] && exit 1 ;;', ' *) [ -n "${GH_STUB_FAIL_WRITE:-}" ] && exit 1; echo "${GH_STUB_POST_ID:-7777}" ;;', 'esac', 'exit 0', ].join('\n'), { mode: 0o755 }, ); const RUN_URL = 'https://github.com/QwenLM/qwen-code/actions/runs/77'; const bashArgs = ['--noprofile', '--norc', '-eo', 'pipefail', '-c']; const run = (script, env) => { rmSync(bodyOut, { force: true }); rmSync(callOut, { force: true }); rmSync(outputFile, { force: true }); rmSync(argvFile, { force: true }); const proc = spawnSync('bash', [...bashArgs, script], { env: { ...process.env, PATH: `${dir}:${process.env.PATH}`, GH_TOKEN: 'x', GITHUB_REPOSITORY: 'QwenLM/qwen-code', NUMBER: '7999', RUN_URL, GITHUB_OUTPUT: outputFile, GH_STUB_OUT: bodyOut, GH_STUB_CALL: callOut, GH_STUB_COMMENTS: commentsFile, GH_STUB_ARGV: argvFile, ...env, }, encoding: 'utf8', }); expect(proc.status, proc.stderr).toBe(0); return { body: existsSync(bodyOut) ? readFileSync(bodyOut, 'utf8') : null, call: existsSync(callOut) ? readFileSync(callOut, 'utf8') : null, out: `${proc.stdout}${proc.stderr}`, outputs: existsSync(outputFile) ? readFileSync(outputFile, 'utf8') : '', calls: existsSync(argvFile) ? readFileSync(argvFile, 'utf8') .split('\x1e') .slice(0, -1) .map((call) => call.split('\0').slice(0, -1)) : [], }; }; return { commentsFile, dir, run, cleanup: () => rmSync(dir, { recursive: true, force: true }), RUN_URL, }; } // Spawns the real proxy against a streaming upstream (20 chunks, 200 ms // apart = 4 s total) and a stalling upstream (headers + one chunk, then // silence), with the proxy's 120 s watchdog shortened to 1.5 s. The healthy // stream spans longer than the idle window while each gap stays under it, so // it arrives in full only if the watchdog is idle (refreshed per chunk) and // not a total cap; and a mid-body stall must CLOSE the downstream response, // not strand the client on a silent socket until its own timeout. function runProxyWatchdogTest(proxy) { const dir = mkdtempSync(join(tmpdir(), 'proxy-watchdog-')); try { writeFileSync( join(dir, 'proxy.js'), proxy.replace(/^ {10}/gm, '').replaceAll('120_000', '1500'), ); writeFileSync( join(dir, 'stream.js'), [ "const http = require('node:http');", "const fs = require('node:fs');", 'const NL = String.fromCharCode(10);', 'const ticks = Number(process.argv[3]);', 'const tickMs = Number(process.argv[4]);', 'const s = http.createServer((q, r) => {', " r.writeHead(200, { 'content-type': 'text/event-stream' });", ' let i = 0;', ' const iv = setInterval(() => {', " r.write('data: ' + i++ + NL + NL);", ' if (i >= ticks) { clearInterval(iv); r.end(); }', ' }, tickMs);', '});', "s.listen(0, '127.0.0.1', () => fs.writeFileSync(process.argv[2], String(s.address().port)));", ].join('\n'), ); writeFileSync( join(dir, 'stall.js'), [ "const http = require('node:http');", "const fs = require('node:fs');", 'const NL = String.fromCharCode(10);', 'const s = http.createServer((q, r) => {', " r.writeHead(200, { 'content-type': 'text/event-stream' });", " r.write('data: 0' + NL + NL);", '});', "s.listen(0, '127.0.0.1', () => fs.writeFileSync(process.argv[2], String(s.address().port)));", ].join('\n'), ); const driver = [ 'set -u', 'node "$1/stream.js" "$1/stream.port" 20 200 & STREAM=$!', 'node "$1/stall.js" "$1/stall.port" & STALL=$!', 'for _ in 1 2 3 4 5 6 7 8 9 10; do [ -s "$1/stream.port" ] && [ -s "$1/stall.port" ] && break; sleep 0.3; done', 'REVIEW_OPENAI_BASE_URL="http://127.0.0.1:$(cat "$1/stream.port")/v1" REVIEW_OPENAI_API_KEY=k QWEN_PROXY_NONCE=n0nce PROXY_TOKEN=t0ken node "$1/proxy.js" "$1/px.port" & PX=$!', 'REVIEW_OPENAI_BASE_URL="http://127.0.0.1:$(cat "$1/stall.port")/v1" REVIEW_OPENAI_API_KEY=k QWEN_PROXY_NONCE=n0nce PROXY_TOKEN=t0ken node "$1/proxy.js" "$1/px2.port" & PX2=$!', 'for _ in 1 2 3 4 5 6 7 8 9 10; do [ -s "$1/px.port" ] && [ -s "$1/px2.port" ] && break; sleep 0.3; done', 'P="$(cat "$1/px.port")"; P2="$(cat "$1/px2.port")"', 'echo "chunks=$(curl -sS --max-time 15 -X POST -H "Authorization: Bearer t0ken" "http://127.0.0.1:$P/v1/chat/completions" | grep -c "^data:")"', 'curl -sS -o /dev/null --max-time 10 -X POST -H "Authorization: Bearer t0ken" "http://127.0.0.1:$P2/v1/chat/completions"', 'echo "stall_exit=$?"', 'kill $STREAM $STALL $PX $PX2 2>/dev/null', ].join('\n'); return spawnSync('bash', ['-c', driver, '_', dir], { encoding: 'utf8', timeout: 60000, }).stdout; } finally { rmSync(dir, { recursive: true, force: true }); } } describe('qwen-triage tmux workflow', () => { it('does not require fork PR authors to have write permission for automatic triage', () => { const precheckJob = job('precheck-pr'); const authorizeJob = job('authorize'); const authorizeStep = step('Check principal write permission'); expect(precheckJob).toContain("contents: 'read'"); expect(precheckJob).toContain("pull-requests: 'read'"); expect(precheckJob).toContain("issues: 'write'"); expect(authorizeJob).toContain( "needs.precheck-pr.outputs.decision == 'allow_triage'", ); expect(authorizeStep).toContain( 'if [ "$EVENT_NAME" = "pull_request_target" ]; then', ); expect(authorizeStep).toContain( 'echo "should_run=true" >> "$GITHUB_OUTPUT"', ); expect(authorizeStep).toContain( 'Automatic PR triage allowed for PR #${PR_NUMBER} after same-repo/precheck gate.', ); expect(authorizeStep).not.toContain( 'pull_request_target) principal="$PR_AUTHOR"', ); }); it('requires open issues or PRs for comment-triggered triage', () => { const authorizeJob = job('authorize'); const triageJob = job('triage'); const tmuxJob = job('tmux-testing'); expect(authorizeJob).toContain("github.event.issue.state == 'open'"); expect(triageJob).toContain("github.event.issue.state == 'open'"); expect(tmuxJob).toContain("github.event.issue.state == 'open'"); }); it('escapes embedded tmux artifacts without bash pattern replacement ampersands', () => { const postStep = step('Post tmux result comment'); expect(postStep).not.toContain('content="${content//&/&}"'); expect(postStep).not.toContain('content="${content///>}"'); expect(postStep).toContain("sed -e 's/&/\\&/g'"); expect(postStep).toContain("-e 's//\\>/g'"); expect(postStep).toContain('html_escape()'); expect(postStep).toContain("tr -d '\\000'"); expect(postStep).toContain('Log could not be rendered'); // The escape now writes to a file and the cap is applied afterwards, so // the guarantee is "a render failure is caught", not the old inline // capture shape. See the tmux-lane-parity suite for the cap itself. expect(postStep).toContain('html_escape > "$esc_file"'); expect(postStep).toContain('set -o pipefail'); expect(postStep).toContain('::warning::emit_block failed'); expect(postStep).toContain( 'summary_html="$(printf \'%s\' "$summary" | html_escape)"', ); expect(postStep).toContain( '\'
\\n%s\\n\\n
\\n\' "$summary_html"',
    );
  });

  it('passes the selected OpenAI model into the app under tmux test', () => {
    const runStep = step('Run tmux real-user testing');

    expect(runStep).toContain('if [ -n "${OPENAI_MODEL:-}" ]; then');
    expect(runStep).toContain('"OPENAI_MODEL=$OPENAI_MODEL"');
  });

  it('isolates agent state per run', () => {
    const cleanStep = step('Clean stale agent state');
    const runStep = step('Run Qwen Triage');

    expect(cleanStep).toContain('QWEN_HOME="${RUNNER_TEMP:?}/qwen-home"');
    expect(cleanStep).toContain('rm -rf "$QWEN_HOME"');
    expect(cleanStep).toContain('mkdir -p "$QWEN_HOME"');
    expect(cleanStep).toContain('rm -f /tmp/stage-*.md');
    expect(cleanStep).toContain('echo "stale agent state cleaned"');
    // The direct launch carries QWEN_HOME in its env -i list; it must stay
    // the same per-run path the clean step resets.
    expect(runStep).toContain('"QWEN_HOME=${RUNNER_TEMP:?}/qwen-home"');
  });

  it('pins the CLI install the direct launch runs', () => {
    expect(workflow).toContain("id: 'ensure_qwen'");
    expect(workflow).toContain(
      'echo "version=$(qwen --version)" >> "${GITHUB_OUTPUT}"',
    );
    // Install from RUNNER_TEMP with an explicit registry: the persistent
    // workspace may hold a previous run's tree whose .npmrc npm would read.
    const ensureStep = step('Ensure qwen CLI');
    expect(ensureStep).toContain('cd "${RUNNER_TEMP:?}"');
    expect(ensureStep).toContain('--registry=https://registry.npmjs.org');
    // The action's reinstall pin is gone with the action; the launch must
    // use the installed CLI, not reinstall one.
    expect(workflow).not.toContain('qwen_cli_version');
    expect(step('Run Qwen Triage')).toContain('command -v qwen');
  });

  it('passes triage output through env before bash reads it', () => {
    const checkStep = step('Check triage response');

    expect(checkStep).toContain(
      "RESPONSE: '${{ steps.triage.outputs.summary }}'",
    );
    expect(checkStep).not.toContain(
      'RESPONSE="${{ steps.triage.outputs.summary }}"',
    );
    expect(checkStep).toContain('if [[ -z "${RESPONSE}"');
  });

  it('tells an action crash apart from a silent agent, and replays both', () => {
    const checkStep = step('Check triage response');
    // The outcome must arrive through env like RESPONSE does — inlining the
    // expression into the script would be the injection shape this step
    // already avoids for RESPONSE.
    expect(checkStep).toContain(
      "TRIAGE_OUTCOME: '${{ steps.triage.outcome }}'",
    );
    expect(checkStep).not.toContain('TRIAGE_OUTCOME="${{');

    const body = checkStep.match(/run: \|-\n([\s\S]*)$/)?.[1];
    expect(body).toBeTruthy();
    const script = body.replace(/^ {10}/gm, '');
    const run = (env) => {
      const proc = spawnSync('bash', ['-c', script], {
        env: {
          ...process.env,
          RESPONSE: '',
          TRIAGE_OUTCOME: 'success',
          ...env,
        },
        encoding: 'utf8',
      });
      return { status: proc.status, out: `${proc.stdout}${proc.stderr}` };
    };

    // A crashed action: no model call happened, so nothing about the PR can
    // explain it and the guidance must say "re-run", not "read the log" —
    // the install runs under `npm --silent`, so there is no log to read.
    const crashed = run({ TRIAGE_OUTCOME: 'failure' });
    expect(crashed.status).not.toBe(0);
    expect(crashed.out).toContain('Triage did not start');
    expect(crashed.out).toContain('re-run the failed job');
    expect(crashed.out).not.toContain('Triage silent failure');

    // A completed action with no summary IS worth reading the step output for.
    const silent = run({ TRIAGE_OUTCOME: 'success' });
    expect(silent.status).not.toBe(0);
    expect(silent.out).toContain('Triage silent failure');
    expect(silent.out).toContain('model or prompt problem');
    expect(silent.out).not.toContain('Triage did not start');

    // A real response still passes, and 'null' still counts as no response.
    expect(run({ RESPONSE: 'triaged' }).status).toBe(0);
    expect(run({ RESPONSE: 'null' }).status).not.toBe(0);
  });

  it('fails an API-error response instead of passing it as a triage (#10314)', () => {
    const checkStep = step('Check triage response');
    const body = checkStep.match(/run: \|-\n([\s\S]*)$/)?.[1];
    expect(body).toBeTruthy();
    const script = body.replace(/^ {10}/gm, '');
    const run = (env) => {
      const proc = spawnSync('bash', ['-c', script], {
        env: {
          ...process.env,
          RESPONSE: '',
          TRIAGE_OUTCOME: 'success',
          ...env,
        },
        encoding: 'utf8',
      });
      return { status: proc.status, out: `${proc.stdout}${proc.stderr}` };
    };

    // The verbatim 268-char response of run 33070765162 (triage for #10285):
    // a bare model-layer connection error that the old check classified as a
    // successful triage, so nothing retried or alerted.
    const apiError =
      '[API Error: Connection error. (cause: connect ETIMEDOUT 47.94.20.201:443; connect ENETUNREACH 2408:400a:3e:effd:6ac1:ae6e:cde9:4efe:443 - Local (:::0); connect ETIMEDOUT 101.201.58.201:443; connect ENETUNREACH 2408:400a:3e:effb:c146:fb04:1e3d:5cc1:443 - Local (:::0))]';
    const bare = run({ RESPONSE: apiError });
    expect(bare.status).not.toBe(0);
    expect(bare.out).toContain('API error');

    // The stream-json adapter appends the formatted error LAST
    // (BaseJsonOutputAdapter appendText), optionally followed by rate-limit
    // guidance: an error after partial output and a quota error ending in
    // the guidance suffix must fail too. Same shapes as
    // qwen-code-pr-review.yml's classifier.
    expect(
      run({ RESPONSE: `Partial triage notes\n${apiError}` }).status,
    ).not.toBe(0);
    expect(
      run({
        RESPONSE:
          '[API Error: Quota exceeded.] Please wait and try again later. To increase your limits, request a quota increase through AI Studio, or switch to another /auth method',
      }).status,
    ).not.toBe(0);

    // A real summary still passes: one that merely QUOTES an API error
    // mid-prose keeps writing afterwards (parity with the pr-review
    // workflow's success_mentions_api_error case), and the normal
    // empty/'null' behavior is untouched.
    expect(
      run({
        RESPONSE:
          'This issue reports "[API Error: Connection error.]" which points at the model endpoint; needs the endpoint config.',
      }).status,
    ).toBe(0);
    expect(run({ RESPONSE: 'triaged' }).status).toBe(0);
    expect(run({ RESPONSE: '' }).status).not.toBe(0);
    expect(run({ RESPONSE: 'null' }).status).not.toBe(0);
  });

  it('notifies the author when a manual triage re-run posts no review', () => {
    const notifyStep = step('Notify silent triage re-run');

    expect(notifyStep).toContain("github.event_name == 'issue_comment'");
    expect(notifyStep).toContain('github.event.issue.pull_request');
    expect(notifyStep).toContain(
      "startsWith(github.event.comment.body, '@qwen-code /triage')",
    );
    expect(notifyStep).toContain('--method GET');
    expect(notifyStep).toContain('--paginate');
    expect(notifyStep).toContain('any(.[][];');
    expect(notifyStep).toContain('.user.login == $bot');
    expect(notifyStep).toContain('.submitted_at != null');
    expect(notifyStep).toContain('.submitted_at >= $since');
    // "Did this re-run post anything?" must stay state-agnostic: a re-posted
    // CHANGES_REQUESTED counts as a review just as much as an approval. The
    // separate head-commit probe below is the one that filters on state.
    const sinceQuery = notifyStep.match(/any\(\.\[\]\[\];[^\n]*\$since\)/)?.[0];
    expect(sinceQuery).toBeTruthy();
    expect(sinceQuery).not.toContain('.state');
    expect(notifyStep).toContain('HAS_REVIEW=false');
    expect(notifyStep).toContain(
      'gh api "repos/$GITHUB_REPOSITORY/issues/$NUMBER/comments"',
    );
    expect(notifyStep).toContain('');
    expect(notifyStep).toContain(
      'Triage re-run completed without a new review.',
    );
    expect(notifyStep).not.toContain('-X PATCH');
  });

  // The re-run summary used to have two outcomes, and the routine one was
  // reported as the broken one. The triage skill's confidence table sends 3/5
  // down a DEFER path that posts a COMMENTED review and deliberately does not
  // vote — a fork `refactor` hitting the approval guardrail, or a core change
  // escalated for maintainer awareness. Observed on #7948 and #8141: both
  // deferred exactly as designed, and both were told the bot had "no review of
  // its own" while its COMMENTED review was visible on the page.
  //
  // Execute the real classifier rather than matching its text: the states are
  // the contract, and a jq edit that silently reclassifies one is invisible to
  // a string assertion.
  it.skipIf(spawnSync('jq', ['--version']).status !== 0)(
    'classifies a deferring COMMENTED review apart from a missing one',
    () => {
      const notifyStep = step('Notify silent triage re-run');
      const program = notifyStep.match(
        /--arg sha "\$HEAD_SHA" \\\n\s*'([\s\S]*?)'\n/,
      )?.[1];
      expect(program).toBeTruthy();

      const dir = mkdtempSync(join(tmpdir(), 'rerun-standing-'));
      const progFile = join(dir, 'standing.jq');
      writeFileSync(progFile, program);
      const classify = (reviews) => {
        const r = spawnSync(
          'jq',
          [
            '-rs',
            '--arg',
            'bot',
            'bot',
            '--arg',
            'sha',
            'HEAD',
            '-f',
            progFile,
          ],
          { input: JSON.stringify(reviews), encoding: 'utf8' },
        );
        expect(r.status, r.stderr).toBe(0);
        return r.stdout.trim();
      };
      const rev = (state, login = 'bot', commit = 'HEAD') => ({
        user: { login },
        commit_id: commit,
        state,
      });

      try {
        // A vote of either kind stands.
        expect(classify([rev('APPROVED')])).toBe('own');
        expect(classify([rev('CHANGES_REQUESTED')])).toBe('own');

        // The defer path. The second arm is the real #7948/#8141 shape: the bot
        // deferred and a maintainer approved, which must NOT read as the bot's
        // own vote — `main` needs two, so the PR is still one short.
        expect(classify([rev('COMMENTED')])).toBe('deferred');
        expect(classify([rev('COMMENTED'), rev('APPROVED', 'wenshao')])).toBe(
          'deferred',
        );

        // DISMISSED is NOT a deferral, and this is the distinction the first
        // draft of this change got wrong. `dismiss_stale_reviews` voids the
        // bot's approval on every push, which is exactly when a fresh one is
        // required — reporting that as a routine defer would say the opposite
        // of what the maintainer needs to know. Same for an unsubmitted PENDING.
        expect(classify([rev('DISMISSED')])).toBe('none');
        expect(classify([rev('PENDING')])).toBe('none');
        // ...but a real COMMENTED alongside a DISMISSED one is still a defer.
        expect(classify([rev('DISMISSED'), rev('COMMENTED')])).toBe('deferred');

        // The original incident this whole probe exists for: a human approval
        // with no bot review must stay `none`, not be read as the bot's own.
        expect(classify([rev('APPROVED', 'wenshao')])).toBe('none');
        // A vote on an earlier commit does not carry over.
        expect(classify([rev('APPROVED', 'bot', 'OLD')])).toBe('none');
        expect(classify([])).toBe('none');
      } finally {
        rmSync(dir, { recursive: true, force: true });
      }
    },
  );

  // Both notes are posted in both languages, matching the status comment
  // composer in the same step. The re-run summary was the one body in this
  // workflow that shipped English only, so a Chinese reader got the bot's
  // bilingual stage-3 review followed by an English-only summary of it.
  it('writes the re-run summary bilingually, and warns on every no-vote state', () => {
    const notifyStep = step('Notify silent triage re-run');
    for (const key of ['NOTE_EN', 'NOTE_ZH']) {
      // own / deferred / none / unreadable
      expect(
        (notifyStep.match(new RegExp(`${key}=`, 'g')) ?? []).length,
        `${key} is missing a branch`,
      ).toBe(4);
    }
    expect(notifyStep).toContain('"$NOTE_EN"');
    expect(notifyStep).toContain('"$NOTE_ZH"');
    expect(notifyStep).toMatch(/[一-鿿]/);

    // The ::warning is the operator-facing signal. Both no-vote states
    // (deferred and none) fire it — see the note below for why silencing
    // the defer case would be wrong.
    const noneBranch = notifyStep.slice(
      notifyStep.indexOf('none)'),
      notifyStep.indexOf('*)'),
    );
    const deferredBranch = notifyStep.slice(
      notifyStep.indexOf('deferred)'),
      notifyStep.indexOf('none)'),
    );
    // BOTH warn. At the reviews-API level a COMMENTED-only review is
    // indistinguishable from a deliberate 3/5 defer — the same tuple is
    // produced by an approval a push DISMISSED — and either way the PR is one
    // approval short with the bot not supplying it. So the operator signal
    // fires for both and only the wording differs. Silencing the defer case
    // would have muted a warning that exists for a real incident.
    expect(noneBranch).toContain(
      '::warning title=Triage re-run left no bot review',
    );
    expect(deferredBranch).toContain(
      '::warning title=Triage re-run left no bot review',
    );
    // ...and each warning describes what was actually measured.
    expect(noneBranch).toContain('APPROVED, CHANGES_REQUESTED or COMMENTED');
    expect(deferredBranch).toContain('only a COMMENTED review');
  });

  it('posts an early live-progress status comment and finalizes the same one', () => {
    const statusStep = step('Post triage status comment');
    // Announced up front (before the long agent step) with the live run link.
    expect(statusStep).toContain("MARKER=''");
    expect(statusStep).toContain(
      "LEGACY_MARKER=''",
    );
    expect(statusStep).toContain('actions/runs/${{ github.run_id }}');
    expect(statusStep).toContain('watch live progress');
    // Upsert by marker so a re-run reuses the one comment instead of stacking.
    // startswith (not contains) prevents matching a comment that merely quotes
    // the marker; the bot-author filter prevents matching a human's comment.
    expect(statusStep).toContain("gh api user --jq '.login'");
    expect(statusStep).toContain(
      'Cannot resolve bot identity; skipping status comment upsert.',
    );
    expect(statusStep).toContain('select(.user.login == $bot)');
    expect(statusStep).toContain('startswith($m)');
    expect(statusStep).not.toContain('contains($m)');
    expect(statusStep).toContain('--method PATCH');
    // Best-effort: a failed status post warns and continues, never fails triage.
    expect(statusStep).toContain('set -uo pipefail');
    expect(statusStep).toContain('continuing.');
    // The claim exports the id of the comment it wrote; finalize PATCHes
    // exactly that id, so the two steps cannot disagree about which comment
    // this run owns — no body-URL matching, no second selection to race.
    expect(statusStep).toContain("id: 'status'");
    expect(statusStep).toContain(
      'echo "comment_id=$COMMENT_ID" >> "$GITHUB_OUTPUT"',
    );
    // The executed claim harness's stub answers a POST with the already-
    // extracted id, so nothing there exercises the POST arm's `--jq '.id'`
    // (removing the flag keeps the suite green). Pin the flag statically.
    expect(statusStep).toContain("--jq '.id'");
    expect(statusStep).toContain('[watch live progress]($RUN_URL)');
    expect(statusStep).toContain('[查看实时进度]($RUN_URL)');

    const finalizeStep = step('Finalize triage status comment');
    // Runs on EVERY terminal outcome and PATCHes exactly the comment the
    // claim step exported. always(), not success() || failure():
    // cancellation — cancel-in-progress superseding the run, job timeout, or
    // manual cancel — used to skip the step and leave the early comment
    // claiming the run was still in progress.
    expect(finalizeStep).toContain(
      'if: "always() && steps.resolve.outputs.number != \'\'"',
    );
    expect(finalizeStep).not.toContain('success() || failure()');
    expect(finalizeStep).toContain('steps.triage.outcome');
    expect(finalizeStep).toContain("JOB_STATUS: '${{ job.status }}'");
    expect(finalizeStep).toContain('Qwen Triage was cancelled');
    expect(finalizeStep).toContain('已取消');
    expect(finalizeStep).toContain(
      'elif [ "${JOB_STATUS:-}" = \'cancelled\' ]',
    );
    expect(finalizeStep).toContain(
      'if [ "${TRIAGE_OUTCOME:-}" = \'success\' ] && [ "${JOB_STATUS:-}" != \'failure\' ]; then',
    );
    // The id arrives through env like the other step inputs — and finalize
    // must NOT re-derive it by listing comments: re-running a selection at
    // finalize time is exactly the race this coupling removes.
    expect(finalizeStep).toContain(
      "STATUS_COMMENT_ID: '${{ steps.status.outputs.comment_id }}'",
    );
    expect(finalizeStep).not.toContain('--paginate');
    expect(finalizeStep).not.toContain('gh api user');
    expect(finalizeStep).toContain(
      'repos/$GITHUB_REPOSITORY/issues/comments/$STATUS_COMMENT_ID',
    );
    expect(finalizeStep).toContain('--method PATCH');
    // An empty id means this run never ended up owning a comment (a cancel
    // landing before the claim posted, or a transiently failed claim write):
    // no-op, never a fresh post or a lookup that could clobber a previous
    // run's terminal wording.
    expect(finalizeStep).toContain('if [ -z "${STATUS_COMMENT_ID:-}" ]; then');
    expect(finalizeStep).toContain(
      'This run claimed no status comment; nothing to finalize.',
    );
    expect(finalizeStep).toContain('Qwen Triage finished');
    expect(finalizeStep).toContain('ended early');
    expect(finalizeStep).toContain('[view run]($RUN_URL)');
  });

  // Unordered substring pinning cannot tell which terminal state PATCHes which
  // wording (swapping the success and cancelled bodies survives it), nor
  // whether the ZH half is really Chinese. Execute the real composer against
  // a stubbed `gh` and assert the body it sends for each terminal state.
  const finalizeWordings = {
    finished: ['Qwen Triage finished', 'Qwen Triage 已完成'],
    cancelled: ['Qwen Triage was cancelled', 'Qwen Triage 已取消'],
    early: ['Qwen Triage ended early', 'Qwen Triage 提前结束'],
  };
  const finalizeCombos = [
    [
      'green job',
      { TRIAGE_OUTCOME: 'success', JOB_STATUS: 'success' },
      'finished',
    ],
    // A timeout/manual cancel landing AFTER the triage step already
    // succeeded still reports the success.
    [
      'triage success, job cancelled after',
      { TRIAGE_OUTCOME: 'success', JOB_STATUS: 'cancelled' },
      'finished',
    ],
    // A red job despite a successful triage step — 'Check triage response'
    // exits 1 on an empty summary — must NOT say "finished" and point at
    // stage comments that were never posted.
    [
      'triage success, job failed',
      { TRIAGE_OUTCOME: 'success', JOB_STATUS: 'failure' },
      'early',
    ],
    [
      'triage cancelled',
      { TRIAGE_OUTCOME: 'cancelled', JOB_STATUS: 'cancelled' },
      'cancelled',
    ],
    [
      'triage failed, job cancelled',
      { TRIAGE_OUTCOME: 'failure', JOB_STATUS: 'cancelled' },
      'cancelled',
    ],
    [
      'triage failed, job failed',
      { TRIAGE_OUTCOME: 'failure', JOB_STATUS: 'failure' },
      'early',
    ],
    [
      'triage skipped, job failed',
      { TRIAGE_OUTCOME: 'skipped', JOB_STATUS: 'failure' },
      'early',
    ],
  ];

  it.each(finalizeCombos)(
    'finalizes the claimed comment: %s',
    (_label, env, expected) => {
      const finalizeStep = step('Finalize triage status comment');
      const script = finalizeStep
        .match(/run: \|-\n([\s\S]*)$/)?.[1]
        .replace(/^ {10}/gm, '');
      expect(script).toBeTruthy();

      const { run, cleanup } = makeGhHarness('finalize');
      try {
        const { body, call } = run(script, {
          ...env,
          STATUS_COMMENT_ID: '43',
        });
        expect(call).toContain('--method PATCH');
        expect(call).toContain('issues/comments/43');
        expect(body.startsWith(''), body).toBe(
          true,
        );
        for (const [kind, [en, zh]] of Object.entries(finalizeWordings)) {
          if (kind === expected) {
            expect(body, JSON.stringify(env)).toContain(en);
            expect(body, JSON.stringify(env)).toContain(zh);
          } else {
            expect(body, JSON.stringify(env)).not.toContain(en);
            expect(body, JSON.stringify(env)).not.toContain(zh);
          }
        }
      } finally {
        cleanup();
      }
    },
  );

  it('skips finalize when the run never claimed a comment, and survives a failed PATCH', () => {
    const finalizeStep = step('Finalize triage status comment');
    const script = finalizeStep
      .match(/run: \|-\n([\s\S]*)$/)?.[1]
      .replace(/^ {10}/gm, '');
    expect(script).toBeTruthy();

    const { run, cleanup } = makeGhHarness('finalize-noop');
    try {
      // A cancel landing before the claim posted leaves the id empty: no
      // write at all — a fresh post here would clobber a previous run's
      // terminal wording, and a lookup-based finalize would PATCH whatever
      // comment happens to be newest.
      const noop = run(script, {
        TRIAGE_OUTCOME: 'skipped',
        JOB_STATUS: 'cancelled',
      });
      expect(noop.body).toBe(null);
      expect(noop.call).toBe(null);
      expect(noop.out).toContain(
        'This run claimed no status comment; nothing to finalize.',
      );

      // A failing PATCH must not fail the step: under GitHub's `-eo
      // pipefail` an unguarded write turns a transient API error into a red
      // job even under if: always(). `run` asserts exit 0.
      const patchFailed = run(script, {
        TRIAGE_OUTCOME: 'failure',
        JOB_STATUS: 'cancelled',
        STATUS_COMMENT_ID: '43',
        GH_STUB_FAIL_WRITE: '1',
      });
      expect(patchFailed.out).toContain(
        'Failed to finalize triage status comment; continuing.',
      );
    } finally {
      cleanup();
    }
  });

  // The claim step is the only selection left in the lifecycle: newest-wins
  // marker reuse (the shared slot), with the chosen or created id exported
  // for finalize to PATCH. Execute the real claim script against a stubbed
  // `gh`.
  it.skipIf(spawnSync('jq', ['--version']).status !== 0)(
    'claims the newest marker and exports the comment id for finalize',
    () => {
      const statusStep = step('Post triage status comment');
      const script = statusStep
        .match(/run: \|-\n([\s\S]*)$/)?.[1]
        .replace(/^ {10}/gm, '');
      expect(script).toBeTruthy();

      const { commentsFile, run, cleanup } = makeGhHarness('claim');
      try {
        const marker = '';
        const comment = (id, body) => ({
          id,
          user: { login: 'qwen-code-ci-bot' },
          body,
        });
        const tombstone = (runId) =>
          `${marker}\n\n✅ earlier verdict [finalize run](https://github.com/QwenLM/qwen-code/actions/runs/${runId})`;

        // No marker yet: POST the running claim and export the created id.
        // The body must START with the lifecycle marker — the selector is
        // gated on startswith($m), so a composer that drops the marker posts
        // a claim nothing ever recognizes or flips.
        writeFileSync(commentsFile, '[]');
        const fresh = run(script, {});
        expect(fresh.call).toContain('issues/7999/comments');
        expect(fresh.call).not.toContain('--method PATCH');
        expect(fresh.body).toContain('Qwen Triage is running');
        expect(fresh.body.startsWith(marker), fresh.body).toBe(true);
        expect(fresh.outputs).toContain('comment_id=7777');

        // Existing markers: reuse the NEWEST one — the marker comment is a
        // shared slot, not a per-run log — and export the reused id.
        writeFileSync(
          commentsFile,
          JSON.stringify([
            comment(42, tombstone(55)),
            comment(44, tombstone(456)),
          ]),
        );
        const newest = run(script, {});
        expect(newest.call).toContain('--method PATCH');
        expect(newest.call).toContain('issues/comments/44');
        expect(newest.call).not.toContain('issues/comments/42');
        expect(newest.outputs).toContain('comment_id=44');

        // A legacy marker body is still a reusable slot: dropping
        // startswith($legacy) from the claim jq loses single-slot healing
        // for pre-migration threads and survives the suite.
        writeFileSync(
          commentsFile,
          JSON.stringify([
            comment(
              41,
              '\n\nlegacy status wording',
            ),
          ]),
        );
        const legacy = run(script, {});
        expect(legacy.call).toContain('--method PATCH');
        expect(legacy.call).toContain('issues/comments/41');
        expect(legacy.outputs).toContain('comment_id=41');

        // A failing write exports an EMPTY id — finalize must see "this run
        // never claimed anything", not a guessed id — and never fails the
        // step: under GitHub's `-eo pipefail` an unguarded POST/PATCH turns
        // a transient comments-API blip into a red job BEFORE 'Run Qwen
        // Triage' starts. `run` asserts exit 0 for each arm.
        writeFileSync(commentsFile, '[]');
        const postFailed = run(script, { GH_STUB_FAIL_WRITE: '1' });
        expect(postFailed.out).toContain(
          'Failed to post triage status comment; continuing.',
        );
        expect(postFailed.outputs).toContain('comment_id=');
        expect(postFailed.outputs).not.toContain('comment_id=7777');

        writeFileSync(
          commentsFile,
          JSON.stringify([comment(45, tombstone(55))]),
        );
        const patchFailed = run(script, { GH_STUB_FAIL_WRITE: '1' });
        expect(patchFailed.out).toContain(
          'Failed to update triage status comment; continuing.',
        );
        expect(patchFailed.outputs).toContain('comment_id=');
        expect(patchFailed.outputs).not.toContain('comment_id=45');

        // An unreadable comment list is best-effort too: `|| EXISTING_ID=''`
        // falls back to a fresh claim post; deleting the guard kills the
        // step on a transient blip.
        const listFailed = run(script, { GH_STUB_FAIL_LIST: '1' });
        expect(listFailed.call).toContain('issues/7999/comments');
        expect(listFailed.call).not.toContain('--method PATCH');
        expect(listFailed.outputs).toContain('comment_id=7777');
      } finally {
        cleanup();
      }
    },
  );

  it('reports timeout and infra-error without claiming the flow was exercised', () => {
    const postStep = step('Post tmux result comment');

    expect(postStep).toContain('case "${VERDICT:-}" in');
    expect(postStep).toContain("VERDICT_LABEL='infra-error (crash/OOM)'");
    expect(postStep).toContain("VERDICT_LABEL='timeout'");
    expect(postStep).toContain("VERDICT_LABEL='pass'");
    expect(postStep).toContain("VERDICT_LABEL='fail'");
    expect(postStep).toContain("VERDICT_LABEL='unknown'");
    expect(postStep).toContain('The tmux test did not complete');
    expect(postStep).toContain(
      'The tmux test did not complete before the time limit',
    );
    expect(postStep).toContain('not a pass/fail result');
    expect(postStep).toContain('crashes or memory leaks');
    expect(postStep).toContain(
      'Launched the changed app in a real tmux session and exercised the affected flow.',
    );
    expect(postStep).toContain('produced an unrecognized verdict');
    expect(postStep).toContain('UNKNOWN_VERDICT="$(');
    expect(postStep).toContain('::warning::Unrecognized tmux verdict');
    expect(postStep).toContain("tr '\\r\\n' '  '");
    expect(postStep).toContain('${UNKNOWN_VERDICT}');
    expect(postStep).toContain('"$VERDICT_LABEL" "$RUN_URL"');
    expect(postStep).toContain('printf \'%s\\n\\n\' "$DESCRIPTION"');
    expect(postStep).toContain('MISSING_ARTIFACTS_NOTE=');
    expect(postStep).toContain(
      'No report.md or tmux-readable-full.log was found in tmux-results',
    );
  });

  it('removes GitHub command files from PR-controlled lifecycle scripts', () => {
    const prepareStep = step('Install and build PR app');
    const strippedEnv =
      'env -u GITHUB_OUTPUT -u GITHUB_STATE -u GITHUB_ENV -u GITHUB_PATH -u GITHUB_STEP_SUMMARY';

    expect(prepareStep).toContain('-u GITHUB_OUTPUT');
    expect(prepareStep).toContain('-u GITHUB_STATE');
    expect(prepareStep).toContain('-u GITHUB_ENV');
    expect(prepareStep).toContain('-u GITHUB_PATH');
    expect(prepareStep).toContain('-u GITHUB_STEP_SUMMARY');
    expect(prepareStep).toMatch(
      new RegExp(`${escapeRegExp(strippedEnv)} \\\\\\s+corepack pnpm install`),
    );
    expect(prepareStep).toMatch(
      new RegExp(`${escapeRegExp(strippedEnv)} \\\\\\s+npm run build`),
    );
  });

  it('does not echo unrecognized prepare failure phases into comments', () => {
    const postStep = step('Post tmux result comment');

    expect(postStep).toContain("PREPARE_COMMAND='install/build'");
    expect(postStep).toContain('::warning::Unrecognized prepare failure phase');
    expect(postStep).toContain('PREPARE_LOG_NOTE=');
    expect(postStep).toContain(
      'No prepare.log was found in tmux-results, so the install/build log section is omitted.',
    );
    expect(postStep).not.toContain('PREPARE_COMMAND="$PREPARE_FAILURE_PHASE"');
  });

  it('installs the heavy tmux test harness only for runnable PRs', () => {
    const installStep = step('Install tmux runner tools');
    const resolverStep = step('Install PR resolver tools');

    expect(resolverStep).toContain('apt-get install');
    expect(installStep).toContain('if: "steps.pr.outputs.decision == \'run\'"');
    expect(installStep).toContain(
      'apt-get install -y --no-install-recommends tmux util-linux',
    );
    expect(installStep).toContain(
      "npm install -g --registry=https://registry.npmjs.org '@qwen-code/qwen-code@latest'",
    );
    expect(installStep).toContain('qwen --version');
    expect(installStep).toContain('tmux -V');
    expect(resolverStep).not.toContain('tmux');
    expect(resolverStep).not.toContain('npm install');
    expect(resolverStep).not.toContain('qwen --version');
    expect(
      workflow.indexOf("- name: 'Resolve PR and check state'"),
    ).toBeLessThan(workflow.indexOf("- name: 'Install tmux runner tools'"));
    expect(
      workflow.indexOf("- name: 'Install tmux runner tools'"),
    ).toBeLessThan(workflow.indexOf("- name: 'Checkout PR merge ref'"));
  });

  it('escapes injected model names and fails loudly when the signature literal is gone', () => {
    const injectStep = step('Inject model name into triage signature');
    const body = injectStep.match(/run: \|-\n([\s\S]*)$/)?.[1];
    expect(body).toBeTruthy();
    const script = body.replace(/^ {10}/gm, '');
    // The workflow step only ever executes on ubuntu runners (GNU sed), but
    // this suite also runs in the macOS merge-queue job, where BSD sed
    // requires an extension argument after -i. Shim ONLY on darwin: on GNU
    // sed a separated '' is parsed as the sed script (not the -i suffix), so
    // an unconditional rewrite would break the Linux runs that actually
    // mirror production.
    const portableScript =
      process.platform === 'darwin'
        ? script.replace(/sed -i /g, "sed -i '' ")
        : script;

    const run = (model, content) => {
      const dir = mkdtempSync(join(tmpdir(), 'triage-inject-'));
      try {
        const target = join(dir, '.qwen/skills/triage/references');
        mkdirSync(target, { recursive: true });
        writeFileSync(join(target, 'pr-workflow.md'), content);
        const proc = spawnSync('bash', ['-c', portableScript], {
          cwd: dir,
          env: { ...process.env, OPENAI_MODEL: model },
          encoding: 'utf8',
        });
        return {
          status: proc.status,
          out: readFileSync(join(target, 'pr-workflow.md'), 'utf8'),
          log: `${proc.stdout}${proc.stderr}`,
        };
      } finally {
        rmSync(dir, { recursive: true, force: true });
      }
    };

    // A model name carrying every sed replacement metacharacter (/ & \) must
    // land verbatim — the old unescaped sed corrupted the skill text on these.
    const meta = run('m1/pre&post\\x', 'sig: qwen3.7-max end');
    expect(meta.status).toBe(0);
    expect(meta.out).toBe('sig: m1/pre&post\\x end');

    // The signature literal disappearing from the skill must fail the step —
    // the old silent no-op shipped the wrong model name in every comment.
    const missing = run('m2', 'sig: some-other-model end');
    expect(missing.status).not.toBe(0);
    expect(missing.log).toContain('Signature literal');
    expect(missing.out).toBe('sig: some-other-model end');

    // No model configured → file untouched, step succeeds.
    const empty = run('', 'sig: qwen3.7-max end');
    expect(empty.status).toBe(0);
    expect(empty.out).toBe('sig: qwen3.7-max end');
  });

  it('pins the re-run comment-id recipe to startswith and the bot-author filter', () => {
    // The skill's stage_comment_id() exists because a re-run once PATCHed the
    // stage=3 comment with stage=1 content (#7693). Its two load-bearing
    // constraints must not silently regress: `startswith` (a `contains` match
    // would also hit a comment that merely quotes a marker) and the
    // bot-author filter (markers are public text; the PAT may be able to
    // edit other users' comments).
    const section = prSkill.slice(
      prSkill.indexOf('**Re-runs:**'),
      prSkill.indexOf('Never create duplicates'),
    );
    expect(section).toContain('stage_comment_id()');
    expect(section).toContain('select(.user.login == $bot)');
    expect(section).toContain('startswith($m)');
    expect(section).not.toContain('contains($m)');
    // Writes are drafts now: the publisher resolves the id at publish time,
    // so the agent never patches by a remembered id.
    expect(section).toContain('PUBLISH TIME');
  });

  it('keeps the sandboxed-lane recommendation out of the local-only section', () => {
    // Measured on 2026-07-28: of 16 open PRs whose AUTHOR had write access
    // and that already carried a triage comment, exactly 1 mentioned
    // `/verify`. The instruction existed the whole time — buried as a
    // conditional clause inside a section headed "local invocation ONLY"
    // that opens with "Never in unattended CI." An agent running in CI
    // reasonably skipped the whole section, so the instruction never fired.
    //
    // The fix is positional, so the test has to be positional too:
    // asserting merely that the file mentions `/verify` would have passed
    // throughout the entire period the recommendation was dead.
    const localOnly = prSkill.indexOf('local invocation ONLY');
    const recommendation = prSkill.indexOf(
      '#### 2b-bis. Name the sandboxed lane when CI cannot settle the claim',
    );
    expect(recommendation).toBeGreaterThan(-1);
    expect(localOnly).toBeGreaterThan(-1);
    expect(recommendation).toBeLessThan(localOnly);

    // Whitespace-normalised: these are prose assertions, and prettier
    // reflows this file. A test that goes red on a re-wrap teaches people to
    // ignore it.
    const section = prSkill
      .slice(recommendation, localOnly)
      .replace(/\s+/g, ' ');
    // It must be reachable on the CI path...
    expect(section).toContain('required element of the Stage 2 comment');
    expect(section).toContain('applies on an unattended run');
    expect(section).not.toContain('Never in unattended CI');
    // ...name the trigger and what it settles, not just the trigger...
    expect(section).toContain('@qwen-code /verify');
    expect(section).toContain('the specific claim it would settle');
    // ...and keep the author-permission case, which since the sponsored
    // lane shipped must offer the sponsored /verify (ephemeral runner +
    // risk screen) rather than declaring the lanes unavailable — while
    // still warning that an external author's report is adversarial input.
    expect(section).toContain('The author lacks write access');
    expect(section).toContain('sponsored run');
    // The controls, not a machine: external runs share the persistent pool
    // with everyone else, so the guidance must name what actually bounds
    // them. An "ephemeral runner" claim here would be describing a design
    // that no longer exists.
    expect(section).toContain('risk screen');
    expect(section).toContain('full workspace wipe');
    expect(section).not.toContain('ephemeral');
    expect(section).toContain('same skepticism as the fork');
    expect(section).not.toContain('sandboxed lanes are unavailable');

    // The assembly order is the other half: a section nothing assembles is
    // as dead as one nobody reads.
    const order = prSkill.slice(
      prSkill.indexOf('Draft a single Stage 2 comment'),
      prSkill.indexOf('### Stage 3'),
    );
    expect(order).toContain('(2b-bis)');
    expect(order).toContain('not an enrichment');
  });

  it('makes the not-verified sentence a mechanical 2b-bis trigger', () => {
    // Post-merge measurement of #7917 (2026-07-29): 9 eligible PRs, two
    // considered-and-declined mentions, zero positive recommendations — and
    // the one clear behavioural candidate (#7947, bounded reads) wrote
    // "author tested on macOS only" in its own Stage 2 comment and never
    // named a lane. The judgement-based rule ("when 2b cannot settle it")
    // failed exactly where the comment had already written the gap down. So
    // the trigger is now textual: the draft's own admission is the trigger,
    // and pending CI does not lift it.
    const section = prSkill
      .slice(
        prSkill.indexOf('#### 2b-bis.'),
        prSkill.indexOf('#### 2c. Real-Scenario'),
      )
      .replace(/\s+/g, ' ');
    expect(section).toContain('mechanical, not a judgement call');
    expect(section).toContain('grep your own draft');
    // The trigger phrases are the ones real comments actually emit — the
    // first two are verbatim from #7947's and #7951's Stage 2 comments.
    expect(section).toContain('not verified');
    expect(section).toContain('author tested on  only');
    expect(section).toContain('not independently re-run');
    // Pending CI must not lift the trigger: #7947's likely out was "the
    // ubuntu Test job is still in progress".
    expect(section).toContain('"CI is still running" does not lift');
    // The mechanical rule must sit BEFORE the skip cases, or the skip cases
    // read as outs from it rather than the other way around.
    expect(section.indexOf('mechanical, not a judgement call')).toBeLessThan(
      section.indexOf('Skip it — explicitly'),
    );
    // And the skip cases themselves must survive — the trigger tightens the
    // rule, it does not replace the two legitimate outs.
    expect(section).toContain('No behavioural claim to settle');
    expect(section).toContain('The author lacks write access');
  });

  it('names /verify on the high-risk paths, not just tmux', () => {
    // 1e is the strongest triage-time signal in the skill (10 of 31 reverted
    // PRs touched these paths vs 5 of 60 controls, p = 0.006) — and it used
    // to recommend tmux alone, pointing at the local-only 2c. So the PRs
    // most likely to be reverted were the ones never offered the lane that
    // proves a change is load-bearing.
    const highRisk = prSkill.slice(
      prSkill.indexOf('If any file matches (the strongest triage-time signal'),
      prSkill.indexOf('This signal is NOT a terminal gate'),
    );
    expect(highRisk).toContain('@qwen-code /verify');
    expect(highRisk).toContain('2b-bis');
    // The dead pointer into the local-only section must not come back.
    expect(highRisk).not.toContain('Stage 2c');
  });

  it('scopes the approve-skip check to the bot own approval on the reviewed commit', () => {
    // A maintainer approved a PR three minutes before re-triggering /triage.
    // The agent read that human approval as "existing approval from prior run
    // still valid", skipped its own approve, and reported 5/5 Approved — the
    // PR sat at 1 of the 2 required approvals with nothing marked wrong. The
    // skip is worth keeping (three re-runs must not stack three approvals),
    // but all three filters are load-bearing: login (another account's vote is
    // not the bot's), state (a DISMISSED review is not an approval), and
    // commit (dismiss_stale_reviews voids the bot's own on every push).
    const section = prSkill.slice(
      prSkill.indexOf('**Approve once per commit'),
      prSkill.indexOf("Only approve when you're genuinely confident"),
    );
    expect(section).toContain('.user.login == $bot');
    expect(section).toContain('.state == "APPROVED"');
    expect(section).toContain('.commit_id == $sha');
    expect(section).toContain('--paginate');
    expect(section).toContain('DISMISSED');

    // The terminal-gate dedup shares the same failure mode: an unpaginated
    // read sees only the first 30 reviews, and re-runs land on exactly the
    // heavily-reviewed PRs where the gating review sits on a later page.
    // Writes are drafts now, so the dedup lives in the publisher — the skill
    // must delegate to it, and the publisher must keep the paginated,
    // bot-scoped check.
    const terminalGate = prSkill.slice(
      prSkill.indexOf('Never create duplicates'),
      prSkill.indexOf('**Signature & footer:**'),
    );
    expect(terminalGate).toContain("publisher's");
    expect(terminalGate).toContain('CHANGES_REQUESTED');
    expect(triagePublisher).toContain('--paginate');
    expect(triagePublisher).toContain('CHANGES_REQUESTED');
    expect(triagePublisher).toMatch(/user.*login|login.*user/);
  });

  it.skipIf(spawnSync('jq', ['--version']).status !== 0)(
    'warns when a triage re-run leaves the bot with no review on the head commit',
    () => {
      const notifyStep = step('Notify silent triage re-run');
      const body = notifyStep.match(/run: \|-\n([\s\S]*)$/)?.[1];
      expect(body).toBeTruthy();
      const script = body.replace(/^ {10}/gm, '');

      const REVIEWS = {
        // The observed case: the bot's approval was dismissed by a push, its
        // only review on the head commit is a /review downgrade to COMMENTED,
        // and the sole APPROVED on that commit belongs to a maintainer.
        humanOnly: [
          { user: { login: 'bot' }, state: 'DISMISSED', commit_id: 'old' },
          { user: { login: 'bot' }, state: 'COMMENTED', commit_id: 'head' },
          {
            user: { login: 'human' },
            state: 'APPROVED',
            submitted_at: '2026-01-01T00:00:00Z',
            commit_id: 'head',
          },
        ],
        ownApproval: [
          { user: { login: 'bot' }, state: 'APPROVED', commit_id: 'head' },
          { user: { login: 'human' }, state: 'APPROVED', commit_id: 'head' },
        ],
        postedNow: [
          {
            user: { login: 'bot' },
            state: 'CHANGES_REQUESTED',
            submitted_at: '2030-01-01T00:00:00Z',
            commit_id: 'head',
          },
        ],
        botNone: [
          {
            user: { login: 'human' },
            state: 'APPROVED',
            submitted_at: '2026-01-01T00:00:00Z',
            commit_id: 'head',
          },
        ],
      };

      const run = (reviews, head) => {
        const dir = mkdtempSync(join(tmpdir(), 'triage-notify-'));
        try {
          const bin = join(dir, 'bin');
          mkdirSync(bin, { recursive: true });
          writeFileSync(join(dir, 'reviews.json'), JSON.stringify(reviews));
          // Stand-in for `gh`: serves the PR state, the review list and
          // the head SHA, and
          // captures the comment body the step would have posted.
          writeFileSync(
            join(bin, 'gh'),
            [
              '#!/usr/bin/env bash',
              'case "$*" in',
              `  "api user --jq .login") echo bot ;;`,
              `  "pr view 1 --repo QwenLM/qwen-code --json state,closedAt") echo '{"state":"OPEN","closedAt":null}' ;;`,
              `  *"/pulls/1/reviews"*) cat "${join(dir, 'reviews.json')}" ;;`,
              `  *"/pulls/1 --jq .head.sha") [ -n "$FAKE_HEAD" ] && echo "$FAKE_HEAD" || exit 1 ;;`,
              `  *"/issues/1/comments"*)`,
              `    for a in "$@"; do case "$a" in body=*) printf '%s' "\${a#body=}" > "${join(dir, 'comment.txt')}" ;; esac; done`,
              `    echo '{}' ;;`,
              '  *) echo "unexpected gh call: $*" >&2; exit 1 ;;',
              'esac',
            ].join('\n'),
            { mode: 0o755 },
          );
          const proc = spawnSync('bash', ['-c', script], {
            env: {
              ...process.env,
              PATH: `${bin}:${process.env.PATH}`,
              GITHUB_REPOSITORY: 'QwenLM/qwen-code',
              NUMBER: '1',
              TRIGGERED_AT: '2026-01-02T00:00:00Z',
              RUN_URL: 'https://example.invalid/run',
              FAKE_HEAD: head,
            },
            encoding: 'utf8',
          });
          let comment = '';
          try {
            comment = readFileSync(join(dir, 'comment.txt'), 'utf8');
          } catch {
            comment = '';
          }
          return {
            status: proc.status,
            log: `${proc.stdout}${proc.stderr}`,
            comment,
          };
        } finally {
          rmSync(dir, { recursive: true, force: true });
        }
      };

      // The silent skip must surface as a warning AND in the posted comment.
      const humanOnly = run(REVIEWS.humanOnly, 'head');
      expect(humanOnly.status).toBe(0);
      expect(humanOnly.log).toContain('Triage re-run left no bot review');
      // Wording fixed: the bot DOES have a review here — a COMMENTED one,
      // visible on the page — it just carries no vote. "No review of its own"
      // told a reader the opposite of what they could see. The warning stays:
      // from the reviews API this state is indistinguishable from a deliberate
      // 3/5 defer, and both mean the PR is one approval short.
      expect(humanOnly.comment).toContain('carries no vote');
      expect(humanOnly.comment).not.toContain('no review of its own');
      expect(humanOnly.comment).toContain('does not count as the bot');

      // The bot's own approval standing on the head commit is the benign case.
      const ownApproval = run(REVIEWS.ownApproval, 'head');
      expect(ownApproval.status).toBe(0);
      expect(ownApproval.log).not.toContain('Triage re-run left no bot review');
      expect(ownApproval.comment).toContain('review of its own');
      expect(ownApproval.comment).toContain('still stands');

      // A review posted by this very re-run short-circuits before commenting.
      const postedNow = run(REVIEWS.postedNow, 'head');
      expect(postedNow.status).toBe(0);
      expect(postedNow.log).toContain('no summary comment needed');
      expect(postedNow.comment).toBe('');

      // A true none: the bot has no review at all on the head commit.
      // This exercises the none) arm's bash, which the humanOnly fixture
      // no longer reaches after its reclassification to deferred.
      const botNone = run(REVIEWS.botNone, 'head');
      expect(botNone.status).toBe(0);
      expect(botNone.log).toContain('Triage re-run left no bot review');
      expect(botNone.comment).toContain('neither a verdict nor a deferral');

      // An unreadable head SHA must not masquerade as either verdict.
      const noHead = run(REVIEWS.humanOnly, '');
      expect(noHead.status).toBe(0);
      expect(noHead.log).not.toContain('Triage re-run left no bot review');
      expect(noHead.comment).toContain('could not be read');
    },
  );

  // A Stage 1-pre duplicate-close exit leaves no review at all, and the
  // review-list check above cannot tell it apart from a re-run that did
  // nothing: it announced "completed without a new review ... it did not"
  // on the PR the same run just closed, and the ::warning dispatched a
  // human to a correctly-handled run. The close IS the terminal action:
  // the step must read the PR's own state and exit quietly. A close BEFORE
  // the trigger keeps the old behaviour — that run still owes its summary.
  it.skipIf(spawnSync('jq', ['--version']).status !== 0)(
    'exits quietly when the PR was closed at/after the trigger comment',
    () => {
      const notifyStep = step('Notify silent triage re-run');
      // The exemption reads the PR itself, not only the review list.
      expect(notifyStep).toContain(
        'gh pr view "$NUMBER" --repo "$GITHUB_REPOSITORY" --json state,closedAt',
      );
      const body = notifyStep.match(/run: \|-\n([\s\S]*)$/)?.[1];
      expect(body).toBeTruthy();
      const script = body.replace(/^ {10}/gm, '');

      const run = (prState, reviews = []) => {
        const dir = mkdtempSync(join(tmpdir(), 'triage-close-exit-'));
        try {
          const bin = join(dir, 'bin');
          mkdirSync(bin, { recursive: true });
          writeFileSync(join(dir, 'pr-state.json'), JSON.stringify(prState));
          writeFileSync(join(dir, 'reviews.json'), JSON.stringify(reviews));
          // Stand-in for `gh`: serves the PR state, an (empty) review list
          // and the head SHA, and captures the comment body the step would
          // have posted.
          writeFileSync(
            join(bin, 'gh'),
            [
              '#!/usr/bin/env bash',
              'case "$*" in',
              `  "api user --jq .login") echo bot ;;`,
              `  "pr view 1 --repo QwenLM/qwen-code --json state,closedAt") cat "${join(dir, 'pr-state.json')}" ;;`,
              `  *"/pulls/1/reviews"*) cat "${join(dir, 'reviews.json')}" ;;`,
              `  *"/pulls/1 --jq .head.sha") echo head ;;`,
              `  *"/issues/1/comments"*)`,
              `    for a in "$@"; do case "$a" in body=*) printf '%s' "\${a#body=}" > "${join(dir, 'comment.txt')}" ;; esac; done`,
              `    echo '{}' ;;`,
              '  *) echo "unexpected gh call: $*" >&2; exit 1 ;;',
              'esac',
            ].join('\n'),
            { mode: 0o755 },
          );
          const proc = spawnSync('bash', ['-c', script], {
            env: {
              ...process.env,
              PATH: `${bin}:${process.env.PATH}`,
              GITHUB_REPOSITORY: 'QwenLM/qwen-code',
              NUMBER: '1',
              TRIGGERED_AT: '2026-01-02T00:00:00Z',
              RUN_URL: 'https://example.invalid/run',
            },
            encoding: 'utf8',
          });
          let comment = '';
          try {
            comment = readFileSync(join(dir, 'comment.txt'), 'utf8');
          } catch {
            comment = '';
          }
          return {
            status: proc.status,
            log: `${proc.stdout}${proc.stderr}`,
            comment,
          };
        } finally {
          rmSync(dir, { recursive: true, force: true });
        }
      };

      // The close-exit shape: closed after the trigger, no review left.
      // Neither the summary comment nor the warning may fire.
      const closeExit = run({
        state: 'CLOSED',
        closedAt: '2026-01-03T00:00:00Z',
      });
      expect(closeExit.status).toBe(0);
      expect(closeExit.comment).toBe('');
      expect(closeExit.log).not.toContain('Triage re-run left no bot review');
      expect(closeExit.log).toContain('no re-run summary needed');

      // The boundary: a close BEFORE the trigger comment is not this run's
      // terminal action, so the old notify behaviour stays intact.
      const closedBefore = run({
        state: 'CLOSED',
        closedAt: '2026-01-01T00:00:00Z',
      });
      expect(closedBefore.status).toBe(0);
      expect(closedBefore.log).toContain('Triage re-run left no bot review');
      expect(closedBefore.comment).toContain(
        '',
      );
    },
  );

  it('includes high-risk path detection in the triage skill', () => {
    expect(prSkill).toContain('1e. High-risk path');
    expect(prSkill).toContain('openaiContentGenerator');
    expect(prSkill).toContain('streamingToolCallParser');
    expect(prSkill).toContain('geminiChat');
    expect(prSkill).toContain('acpConnection');
    expect(prSkill).toContain('(^|/)shell\\.ts$');
    expect(prSkill).toContain('shellExecutionService');
    expect(prSkill).toContain('mcp-client');
    expect(prSkill).toContain('mcp-pool');
    expect(prSkill).toContain('LspServer');
    expect(prSkill).toContain('acp-integration');
    expect(prSkill).toContain('(^|/)relaunch\\.ts$');
    expect(prSkill).toContain('(^|/)sandbox\\.ts$');
    expect(prSkill).toContain('electron-run-as-node');
    expect(prSkill).toContain('p = 0.006');
    expect(prSkill).toContain('do not skip any Stage 2 enrichment');
    expect(prSkill).toContain('gh api --paginate');
    expect(prSkill).toContain('|| true');
    expect(prSkill).toContain('WARNING: could not fetch PR files');
  });

  it('includes Risk field in the Stage 1 comment template', () => {
    expect(prSkill).toContain('Risk:  {
  // Replay the authorize principal gate with a stubbed gh: /verify must
  // require write from BOTH the PR author (whose code executes) and the
  // commenter (who spends the runner slot + model budget). A refactor that
  // drops the /verify patterns from the case statement falls back to
  // commenter-only gating, which this catches via the author-without-write
  // arm; dropping the commenter check is caught by the drive-by arm.
  it('classifies /verify trust by author and gates on the commenter', () => {
    // The commenter GATES (they spend the runner slot and model budget);
    // the author's permission sets a TRUST LEVEL. Both run on the
    // persistent pool (maintainer decision), so the level selects the
    // extra controls an external run gets: pinned head OID, risk screen,
    // workspace wipe. Every lookup failure denies: a request that cannot
    // be classified must not pick a trust level.
    const permStep = step('Check principal write permission');
    const body = permStep.match(/run: \|-\n([\s\S]*)$/)?.[1];
    expect(body).toBeTruthy();
    const script = body.replace(/^ {10}/gm, '');

    const dir = mkdtempSync(join(tmpdir(), 'verify-auth-'));
    writeFileSync(
      join(dir, 'gh'),
      [
        '#!/usr/bin/env bash',
        'if [ "$1" = pr ]; then',
        '  # gh pr view  ... --json headRefOid: the sponsorship snapshot.',
        '  [ "$3" = "99" ] && exit 1',
        '  echo deadbeefcafe',
        '  exit 0',
        'fi',
        'u="${2##*collaborators/}"; u="${u%%/*}"',
        'case "$u" in',
        '  alice) echo write ;;',
        '  bob) echo admin ;;',
        '  mallory) echo none ;;',
        '  *) echo "HTTP 404" >&2; exit 1 ;;',
        'esac',
      ].join('\n'),
      { mode: 0o755 },
    );

    let n = 0;
    const gate = (
      commentBody,
      author,
      commenter,
      issueNumber = '1',
      isPr = 'true',
    ) => {
      const out = join(dir, `out-${n++}`);
      writeFileSync(out, '');
      spawnSync('bash', ['-c', script], {
        env: {
          ...process.env,
          PATH: `${dir}:${process.env.PATH}`,
          GH_TOKEN: 'x',
          GITHUB_REPOSITORY: 'QwenLM/qwen-code',
          GITHUB_STEP_SUMMARY: '/dev/null',
          GITHUB_OUTPUT: out,
          EVENT_NAME: 'issue_comment',
          COMMENT_BODY: commentBody,
          ISSUE_AUTHOR: author,
          COMMENT_USER: commenter,
          PR_NUMBER: '1',
          ISSUE_NUMBER: issueNumber,
          IS_PR: isPr,
          TMUX_PR: '',
        },
        encoding: 'utf8',
      });
      const lines = readFileSync(out, 'utf8').trim().split('\n');
      const val = (k) =>
        lines
          .filter((l) => l.startsWith(`${k}=`))
          .pop()
          ?.slice(k.length + 1);
      return {
        run: val('should_run'),
        trust: val('verify_trust'),
        oid: val('verify_head_oid'),
        lane: val('verify_lane'),
      };
    };

    try {
      // Drive-by commenter without write cannot spend the sandbox budget,
      // whoever the author is.
      expect(gate('@qwen-code /verify', 'alice', 'mallory').run).toBe('false');
      // Write-access author -> trusted, no snapshot needed.
      const trusted = gate('@qwen-code /verify', 'alice', 'bob');
      expect(trusted.run).toBe('true');
      expect(trusted.trust).toBe('trusted');
      expect(trusted.oid).toBeUndefined();
      // EXTERNAL author + trusted commenter -> allowed as a sponsored run
      // with the head OID snapped now. This is the case that used to deny.
      const sponsored = gate('@qwen-code /verify', 'mallory', 'bob');
      expect(sponsored.run).toBe('true');
      expect(sponsored.trust).toBe('external');
      expect(sponsored.oid).toBe('deadbeefcafe');
      // Author commenting on their own PR is still trusted.
      expect(gate('@qwen-code /verify', 'alice', 'alice').trust).toBe(
        'trusted',
      );
      // Author permission unreadable -> deny; routing must not guess.
      expect(gate('@qwen-code /verify', 'charlie', 'bob').run).toBe('false');
      // Deleted author (empty login) -> deny, same reason.
      expect(gate('@qwen-code /verify', '', 'bob').run).toBe('false');
      // Head OID snapshot failure -> deny: a sponsored run without a
      // pinned head would execute whatever gets pushed next.
      expect(gate('@qwen-code /verify', 'mallory', 'bob', '99').run).toBe(
        'false',
      );
      // /tmux keeps its author-only gate; /triage keeps the commenter gate.
      expect(gate('@qwen-code /tmux', 'alice', 'mallory').run).toBe('true');
      expect(gate('@qwen-code /tmux', 'mallory', 'bob').run).toBe('false');
      expect(gate('@qwen-code /triage', 'mallory', 'bob').run).toBe('true');

      // /triage on a PR ALSO starts the verify lane, in parallel — and the
      // point of routing it through the same classifier is that an external
      // author still gets the external control set. If this ever emits
      // `trusted` (or no trust at all) for `mallory`, the head-OID pin, the
      // risk screen and both workspace wipes silently stop firing while the
      // lane keeps executing that author's code on the persistent pool.
      const triagePr = gate('@qwen-code /triage', 'mallory', 'bob');
      expect(triagePr.run).toBe('true');
      expect(triagePr.lane).toBe('true');
      expect(triagePr.trust).toBe('external');
      expect(triagePr.oid).toBe('deadbeefcafe');
      expect(gate('@qwen-code /triage', 'alice', 'bob').trust).toBe('trusted');

      // The lane fails closed DIFFERENTLY from an explicit /verify. An
      // unreadable author permission denies `/verify` outright, because the
      // commenter asked for exactly that; on `/triage` it must only close
      // the lane, or a flaky permission API silently costs the reviewer
      // their triage too. Same for a failed head-OID snapshot (issue 99).
      const laneFail = gate('@qwen-code /triage', 'charlie', 'bob');
      expect(laneFail.run).toBe('true');
      expect(laneFail.lane).toBe('false');
      expect(laneFail.trust).toBeUndefined();
      const oidFail = gate('@qwen-code /triage', 'mallory', 'bob', '99');
      expect(oidFail.run).toBe('true');
      expect(oidFail.lane).toBe('false');
      expect(gate('@qwen-code /verify', 'charlie', 'bob').run).toBe('false');

      // On a plain ISSUE there is nothing to build, so the lane stays off
      // and no author lookup is spent. This assertion used to be written as
      // "/triage emits no trust outputs" with a fixture that never set
      // IS_PR — true for the wrong reason, and it would have stayed green
      // through the change above.
      const triageIssue = gate('@qwen-code /triage', 'mallory', 'bob', '1', '');
      expect(triageIssue.run).toBe('true');
      expect(triageIssue.lane).toBeUndefined();
      expect(triageIssue.trust).toBeUndefined();
      expect(triageIssue.oid).toBeUndefined();

      const tmux = gate('@qwen-code /tmux', 'alice', 'bob');
      expect(tmux.trust).toBeUndefined();
      expect(tmux.oid).toBeUndefined();
      expect(tmux.lane).toBeUndefined();
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });
  it('sweeps planted verify artifacts after the last PR-controlled process', () => {
    const runStep = step('Run verification agent');
    const sweep =
      "find tmp -maxdepth 2 -type d -name '*-verify-*' -exec rm -rf {} +";
    expect(step('Pin agent inputs from base')).toContain(sweep);
    expect(runStep).toContain(sweep);
    // Order inside the agent step: sweep first, model proxy and qwen after.
    expect(runStep.indexOf(sweep)).toBeGreaterThan(-1);
    expect(runStep.indexOf(sweep)).toBeLessThan(
      runStep.indexOf('start_openai_proxy'),
    );
    // Uploaded artifacts must not carry node-planted symlinks (or FIFOs/
    // sockets/devices, which can hang or redirect the collection):
    // actions/upload-artifact dereferences symlinks.
    expect(runStep).toContain(
      'find "$RUNNER_TEMP/verify-results" \\( -type l -o -type p -o -type s -o -type b -o -type c \\) -delete',
    );
  });

  // RUNNER_TEMP hygiene between jobs is runner-managed; this pool is
  // persistent, so both result dirs are flushed before reuse — a stale
  // report or previous-report.md from ANOTHER PR's run must never leak
  // into this run's artifacts or agent context.
  it('resets RUNNER_TEMP verify dirs before reuse on the persistent pool', () => {
    const resolveStep = step('Resolve PR and snapshot metadata');
    expect(resolveStep).toContain('rm -rf "$RUNNER_TEMP/verify-context"');
    // 'Install and build PR app' also exists in the tmux job, so scope the
    // prepare assertions to the verify job's text.
    const verifyJob = job('verify');
    const rm = verifyJob.indexOf('rm -rf "$RUNNER_TEMP/verify-results"');
    const mk = verifyJob.indexOf('mkdir -p "$RUNNER_TEMP/verify-results"');
    expect(rm).toBeGreaterThan(-1);
    expect(mk).toBeGreaterThan(rm);
  });

  // The token-free agent learns the PR's review state only from this file,
  // and the local preflight writes the same file with its own transform. One
  // set of pages through both must give one answer, or a local round and a CI
  // round read different review states for the same PR.
  // Executes the step's real jq program, so it needs jq, like its siblings.
  it.skipIf(spawnSync('jq', ['--version']).status !== 0)(
    'snapshots review threads in the shape the local preflight writes',
    () => {
      const resolveStep = stepIn('verify', 'Resolve PR and snapshot metadata');
      const snippet = resolveStep
        .match(/\n( {10}if ! gh api graphql[\s\S]*?\n {10}fi)\n/)?.[1]
        ?.replace(/^ {10}/gm, '');
      expect(snippet).toBeTruthy();
      // After the context directory is reset, or the file never survives to
      // the agent; before the decision line, so only a run pays for it.
      const at = resolveStep.indexOf(snippet.split('\n')[0]);
      const reset = resolveStep.indexOf('rm -rf "$RUNNER_TEMP/verify-context"');
      const made = resolveStep.indexOf(
        'mkdir -p "$RUNNER_TEMP/verify-context"',
      );
      expect(reset).toBeGreaterThan(-1);
      expect(made).toBeGreaterThan(reset);
      expect(at).toBeGreaterThan(made);
      expect(at).toBeLessThan(
        resolveStep.lastIndexOf('echo "decision=run" >> "$GITHUB_OUTPUT"'),
      );
      const { commentsFile, dir, ...harness } = makeGhHarness(
        'verify-review-snapshot',
      );
      try {
        const context = join(dir, 'verify-context');
        const file = join(context, 'review-threads.json');
        const run = (pages, env = {}) => {
          rmSync(context, { recursive: true, force: true });
          mkdirSync(context);
          writeFileSync(commentsFile, JSON.stringify(pages));
          const { out, calls } = harness.run(`set -euo pipefail\n${snippet}`, {
            RUNNER_TEMP: dir,
            PR_NUMBER: '7999',
            ...env,
          });
          expect(calls).toHaveLength(1);
          return {
            out,
            args: calls[0],
            state: existsSync(file)
              ? JSON.parse(readFileSync(file, 'utf8'))
              : null,
          };
        };
        const comment = (body, login = 'reviewer', review = null) => ({
          createdAt: '2026-11-01T00:00:00Z',
          url: `https://github.com/QwenLM/qwen-code/pull/7999#discussion_${body.length}`,
          body,
          author: login === null ? null : { login },
          authorAssociation: 'MEMBER',
          originalCommit: login === null ? null : { oid: 'c'.repeat(40) },
          pullRequestReview: review === null ? null : { url: review },
        });
        const opinionated = {
          nodes: [
            {
              state: 'CHANGES_REQUESTED',
              submittedAt: '2026-11-01T01:00:00Z',
              url: 'https://github.com/QwenLM/qwen-code/pull/7999#pullrequestreview-1',
              body: 'Blocking: the retry drops the cursor.',
              author: { login: 'maintainer' },
              authorAssociation: 'COLLABORATOR',
              commit: { oid: 'a'.repeat(40) },
            },
            {
              state: 'APPROVED',
              submittedAt: '2026-11-01T02:00:00Z',
              url: 'https://github.com/QwenLM/qwen-code/pull/7999#pullrequestreview-2',
              body: 'LGTM',
              author: { login: 'colleague' },
              authorAssociation: 'MEMBER',
              commit: { oid: 'b'.repeat(40) },
            },
            {
              state: 'CHANGES_REQUESTED',
              submittedAt: '2026-11-01T03:00:00Z',
              url: 'https://github.com/QwenLM/qwen-code/pull/7999#pullrequestreview-3',
              body: '',
              author: null,
              authorAssociation: 'NONE',
              commit: null,
            },
          ],
        };
        // Two pages, as --paginate --slurp returns them: the threads continue,
        // and the change requests repeat with every page.
        const pages = [
          {
            data: {
              repository: {
                pullRequest: {
                  latestOpinionatedReviews: opinionated,
                  reviewThreads: {
                    pageInfo: { hasNextPage: true, endCursor: 'c1' },
                    nodes: [
                      {
                        isResolved: false,
                        isOutdated: false,
                        path: 'packages/core/src/a.ts',
                        line: 12,
                        originalLine: 10,
                        comments: {
                          totalCount: 2,
                          nodes: [
                            comment(
                              '**[Critical]** R1-1: the cursor is lost',
                              'maintainer',
                              'https://github.com/QwenLM/qwen-code/pull/7999#pullrequestreview-1',
                            ),
                            comment('Fixed in abc1234.', 'author'),
                          ],
                        },
                      },
                      {
                        isResolved: true,
                        isOutdated: false,
                        path: 'packages/core/src/b.ts',
                        line: null,
                        originalLine: 7,
                        comments: {
                          totalCount: 1,
                          nodes: [comment('**[Blocker]** R1-2: unbounded')],
                        },
                      },
                    ],
                  },
                },
              },
            },
          },
          {
            data: {
              repository: {
                pullRequest: {
                  latestOpinionatedReviews: opinionated,
                  reviewThreads: {
                    pageInfo: { hasNextPage: false, endCursor: 'c2' },
                    nodes: [
                      {
                        isResolved: false,
                        isOutdated: true,
                        path: 'docs/c.md',
                        line: null,
                        originalLine: null,
                        // Longer than the 50 the query asks for.
                        comments: {
                          totalCount: 51,
                          nodes: [comment('Typo?', null)],
                        },
                      },
                    ],
                  },
                },
              },
            },
          },
        ];

        const ok = run(pages);
        expect(ok.state).toEqual(reviewState(pages));
        // `line` and `originalLine` stay apart: an outdated thread's line in
        // the current diff and its line in the comment's commit differ.
        expect(
          ok.state.threads.map((t) => [
            t.resolved,
            t.outdated,
            t.path,
            t.line,
            t.originalLine,
            t.commentCount,
          ]),
        ).toEqual([
          [false, false, 'packages/core/src/a.ts', 12, 10, 2],
          [true, false, 'packages/core/src/b.ts', null, 7, 1],
          [false, true, 'docs/c.md', null, null, 51],
        ]);
        // An inline comment names the review it belongs to, so a change
        // request with an empty body still has its defects.
        expect(
          ok.state.threads[0].comments.map((c) => [
            c.author,
            c.review,
            c.commit,
          ]),
        ).toEqual([
          [
            'maintainer',
            'https://github.com/QwenLM/qwen-code/pull/7999#pullrequestreview-1',
            'c'.repeat(40),
          ],
          ['author', null, 'c'.repeat(40)],
        ]);
        expect(ok.state.threads[2].comments[0].author).toBeNull();
        expect(
          ok.state.changeRequests.map((r) => [r.author, r.commit]),
        ).toEqual([
          ['maintainer', 'a'.repeat(40)],
          [null, null],
        ]);
        // The query the local preflight sends, against this PR of this repo.
        expect(ok.args).toEqual(
          expect.arrayContaining([
            'graphql',
            '--paginate',
            '--slurp',
            'owner=QwenLM',
            'name=qwen-code',
            'number=7999',
            `query=${REVIEW_STATE_QUERY}`,
          ]),
        );
        expect(ok.args[ok.args.indexOf('number=7999') - 1]).toBe('-F');
        expect(ok.args[ok.args.indexOf('owner=QwenLM') - 1]).toBe('-f');

        // A failed fetch, or pages that are not this query's answer, leave no
        // file: the skill reads its absence as "unknown", and a partial file
        // would read as "nothing open".
        for (const [input, env] of [
          [pages, { GH_STUB_FAIL_LIST: '1' }],
          // gh exits 0 with nothing on stdout, and jq then exits 0 too.
          [pages, { GH_STUB_EMPTY_LIST: '1' }],
          [[{ data: null }], {}],
          [[], {}],
        ]) {
          const failed = run(input, env);
          expect(failed.state).toBeNull();
          expect(failed.out).toContain(
            "Could not snapshot the PR's review threads",
          );
        }
        expect(reviewState([{ data: null }])).toBeNull();
        expect(reviewState([])).toBeNull();
      } finally {
        harness.cleanup();
      }
    },
  );

  // The snapshot only helps if the round treats it as the skill says: an
  // absent file is unknown, a resolved thread is a claim, and an open
  // blocking finding nobody measured keeps the round from merge-ready.
  it('tells the agent what the review snapshot means and what it rules out', () => {
    const flat = verifySkill.replace(/\s+/g, ' ');
    expect(flat).toContain(
      'If that directory also holds `review-threads.json`',
    );
    // Absent is unknown: disclosed, and not a verdict of its own.
    expect(flat).toContain(
      'A missing file means the snapshot failed, not that nothing is open: it does not change the verdict by itself, but say so under _Not covered_ and in the verdict line',
    );
    // PR code the round runs can rewrite the directory.
    expect(flat).toContain('Read it before you run any PR code');
    // Every term the verdict turns on is computed from snapshot fields, so
    // two rounds reading one snapshot reach one verdict.
    expect(flat).toContain(
      "A thread is **blocking** when its first comment is tagged Critical, Blocker, P0 or P1 and that comment's `association` is `OWNER`, `MEMBER` or `COLLABORATOR`",
    );
    // An author resolving a change request's thread does not close it:
    // GitHub's review decision still counts the change request.
    expect(flat).toContain(
      "A blocking thread is **open** when it is unresolved, or when its first comment's `review` is a standing change request's `url`, resolved or not: resolving a thread is a claim, not a measurement",
    );
    // A body is prose: listed and counted, never parsed into the verdict.
    expect(flat).toContain(
      'It does not hold the verdict by itself, because a body carries no severity the round can read the same way twice; a blocker its body names with no thread of its own is counted with the blocking threads that are not open',
    );
    expect(flat).toContain(
      'a finding the previous-finding table already carries keeps that row instead of a second one here',
    );
    expect(flat).toContain(
      "`line` is the thread's line in the current diff, null when GitHub could not carry it forward, and `originalLine` its line in the first comment's `commit`",
    );
    expect(flat).toContain(
      'Measure every open blocking thread at the verified head.',
    );
    // `findings` at fail 0 publishes ✅ "non-blocking findings", which an
    // open blocker nobody measured is not.
    expect(flat).toContain(
      'One left `not measured` rules out `merge-ready`; with nothing failed, the verdict is `inconclusive`',
    );
    expect(flat).toContain(
      'no new blocking finding, and no known open blocking review thread left unmeasured',
    );
    expect(flat).toContain(
      'central claim, or an open blocking review thread, from being tested',
    );
    // A blocker that is not open, left unmeasured, is disclosed, not
    // silently covered.
    expect(flat).toContain(
      'the verdict line says how many there are, so a `merge-ready` cannot read as having checked them',
    );
    expect(flat).toContain(
      'the verdict line also gives the open blocking threads measured out of their total and how many of the others were left unmeasured',
    );
    // The plan the agent follows is the budget list, not the report contract.
    expect(verifySkill).toMatch(/^5\. \*\*Open blocking review threads\*\*/m);
    expect(flat).toContain(
      'measure each at the verified head (report item 4 has the rule). One left unmeasured rules out `merge-ready`, so this is budget, not breadth. The blocking threads that are not open get what is left after this list, and are not out of scope',
    );
    // A VM boundary gets the file under the name the agent looks for.
    expect(flat).toContain(
      "with prepare's `pr-review-threads.json`, when it wrote one, beside it as `review-threads.json`",
    );
    // Report items are cited by number elsewhere in the skill.
    expect(flat).toContain('It feeds report item 4 below');
    expect(verifySkill).toMatch(/^4\. \*\*Existing review findings\*\*/m);
    expect(flat).toContain('report item 7 (_Not covered_)');
    expect(verifySkill).toMatch(/^7\. \*\*Not covered\*\*/m);
    expect(flat).toContain('Title, body, comments, review threads,');
  });

  // The lane trigger was unpinned: deleting the gate from the verify job's
  // `if` left every assertion green while the job became reachable from any
  // comment on any open PR. Measured as a mutation before this test existed.
  //
  // `verify_lane` is also the single source of truth for WHICH comments
  // start the lane. Re-enumerating the command strings in the job predicate
  // is what let the trigger and the trust classification drift apart — the
  // classifier keyed on `/verify` while the predicate would have keyed on
  // `/verify` or `/triage`, so a `/triage`-started run would have executed
  // an external author's code with `verify_trust` empty and all four
  // external-only controls (head-OID pin, risk screen, both wipes) skipped.
  it('gates the verify lane on authorize alone, not on re-matched commands', () => {
    // Slice the two predicates APART. Asserting over the whole job text
    // cannot tell them apart: the first version of this test did exactly
    // that, and deleting the gate from `if:` still matched the copy inside
    // `concurrency:` — the mutation survived its own regression test.
    const raw = job('verify');
    const ifBlock = raw.slice(
      raw.indexOf('if: >-'),
      raw.indexOf('concurrency:'),
    );
    const concBlock = raw.slice(
      raw.indexOf('concurrency:'),
      raw.indexOf('timeout-minutes:'),
    );
    expect(ifBlock).toBeTruthy();
    expect(concBlock).toBeTruthy();
    for (const predicate of [ifBlock, concBlock]) {
      expect(predicate).toContain(
        "needs.authorize.outputs.verify_lane == 'true'",
      );
    }
    // Concurrency is evaluated after `needs`, so it can and must read the
    // same output; an unguarded group would let non-runnable triggers share
    // the per-PR group and cancel a real run.
    expect(ifBlock).not.toContain("comment.body == '@qwen-code /verify'");
    expect(concBlock).not.toContain("comment.body == '@qwen-code /verify'");

    // And the authorize job has to actually publish it.
    expect(job('authorize')).toContain(
      "verify_lane: '${{ steps.perm.outputs.verify_lane }}'",
    );
  });

  // Positive control for the mutations above: a guard this suite is known
  // to pin, asserted per predicate. Written first as one `toContain` over
  // the whole job, it survived its own mutation — the copy in
  // `concurrency:` matched after the `if:` copy was deleted, which is the
  // same coarse-assertion defect the test above documents. A control that
  // cannot fail proves nothing about the assertions it vouches for.
  it('pins the ECS kill switch on both verify predicates (control)', () => {
    const raw = job('verify');
    const ifBlock = raw.slice(
      raw.indexOf('if: >-'),
      raw.indexOf('concurrency:'),
    );
    const concBlock = raw.slice(
      raw.indexOf('concurrency:'),
      raw.indexOf('timeout-minutes:'),
    );
    expect(ifBlock).toContain("vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true'");
    expect(concBlock).toContain(
      "vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true'",
    );
  });
});

describe('qwen-triage verify hardening', () => {
  const verifyJob = job('verify');

  // GitHub Actions expression comparisons are case-insensitive, so
  // `@QWEN-CODE /VERIFY` satisfies the job predicates and reaches the shell.
  // A case-sensitive `case` would fall through to commenter-only gating and
  // run the PR author's code without ever checking the author.
  it('matches verify/tmux commands case-insensitively in the shell gate', () => {
    const permStep = step('Check principal write permission');
    expect(permStep).toContain("tr '[:upper:]' '[:lower:]'");
    expect(permStep).toMatch(/case "\$body_lc" in/);
  });

  // /verify on a plain issue would be acknowledged with 👀 while the verify
  // job's PR guard skips it and publish-verify skips with it — accepted
  // looking, permanently silent. Every step that answers a /verify request
  // carries the same guard.
  // With external code on the PERSISTENT pool, the wipe replaces machine
  // destruction as the deny-by-default control — so it must run, must run
  // BEFORE the checkout that brings PR code in, and must fail the job
  // rather than proceed on a workspace it could not clear.
  it('wipes the workspace before external code, ahead of checkout', () => {
    const verifyJob = job('verify');
    // Both trust levels share one machine class now; nothing routes.
    expect(verifyJob).toContain(
      "runs-on: ['self-hosted', 'linux', 'x64', 'ecs-qwen']",
    );
    expect(verifyJob).not.toContain('ubuntu-latest');

    const wipe = stepIn('verify', 'Wipe workspace before external code');
    expect(wipe).not.toBe('');
    // Gated on the trust level, not on everything: a trusted run keeps the
    // warm workspace it has always had.
    expect(wipe).toContain(
      "needs.authorize.outputs.verify_trust == 'external'",
    );
    // Ordering is the whole point — after checkout it would delete the
    // code under test instead of the previous run's leftovers.
    expect(
      verifyJob.indexOf('Wipe workspace before external code'),
    ).toBeLessThan(verifyJob.indexOf('Checkout PR merge ref'));
    // Fails the job when the workspace is not actually empty afterwards.
    expect(wipe).toContain('refusing to execute external code on it');
    expect(wipe).toContain('exit 1');
    // The ported guard layers (#9265) — see the post-run wipe test for
    // what each pin catches.
    expect(wipe).toContain('realpath -m');
    expect(wipe).toContain('realpath -m -- "$RWS"');
    expect(wipe).toContain('RUNNER_WORKSPACE:?');
    expect(wipe).toContain('"$RWS"/*');
    // RWS-side layers: the '..' arm and the degenerate-root refusal that
    // keeps a stripped-empty runner workspace from degenerating the
    // allowlist pattern to `/*`.
    expect(wipe).toContain("refusing runner workspace path containing '..'");
    expect(wipe).toContain('runner workspace resolved to /');
  });

  it('carries the symlink heal in both wipes, ordered and bounded (#9480)', () => {
    // The guard's own wedge: a workspace replaced by a symlink resolves to
    // its target, the allowlist refuses, and the refusal removes nothing —
    // so the runner dies here on every later job. The heal must therefore
    // run BEFORE canonicalization, AFTER the allowlist root that bounds it,
    // and after the raw trailing-slash strip, since both of its predicates
    // resolve through a link when the path ends in '/'.
    for (const stepName of [
      'Wipe workspace before external code',
      'Wipe workspace after external code',
    ]) {
      const run = stepIn('verify', stepName);
      const healAt = run.indexOf('[ -L "$WS" ] || [ ! -d "$WS" ]');
      expect(healAt, `${stepName} has no heal`).toBeGreaterThan(-1);
      expect(healAt).toBeLessThan(run.indexOf('realpath -m -- "$WS"'));
      expect(run.indexOf('RWS="${RUNNER_WORKSPACE:?}"')).toBeLessThan(healAt);
      expect(run.indexOf('while [ "${WS%/}" != "$WS" ]')).toBeLessThan(healAt);
      // Containment on the canonical PARENT — resolving $WS would follow
      // the very link being removed, and a raw match cannot see
      // intermediate symlink components.
      expect(run).toContain(
        'HEAL_PARENT="$(realpath -m -- "$(dirname -- "$WS")" 2>/dev/null)"',
      );
      expect(run).toContain('"$RWS"|"$RWS"/*)');
      expect(run).toContain('refusing to heal workspace outside');
      // Both legs fail closed, and the incident leaves a trace.
      expect(run).toContain('rm -f -- "$WS" || {');
      expect(run).toContain('mkdir -- "$WS" || {');
      expect(run).toContain('::warning::healing workspace');
    }
  });

  // The wipe is the deny-by-default control, so run the real step text
  // against a workspace carrying the vectors the allowlist sweep is built
  // to enumerate — plus one it is not — and require every one to be gone.
  it.skipIf(!hasGnuRealpath)(
    'removes planted persistence vectors, known and unknown',
    () => {
      const wipe = stepIn('verify', 'Wipe workspace before external code')
        .match(/run: \|-\n([\s\S]*)$/)?.[1]
        .replace(/^ {10}/gm, '');
      expect(wipe).toBeTruthy();

      const dir = mkdtempSync(join(tmpdir(), 'verify-wipe-'));
      try {
        const ws = join(dir, 'workspace');
        mkdirSync(join(ws, '.git', 'hooks'), { recursive: true });
        // Vectors the sweep enumerates...
        writeFileSync(
          join(ws, '.git', 'hooks', 'pre-commit'),
          '#!/bin/sh\nid\n',
        );
        writeFileSync(
          join(ws, '.git', 'config.worktree'),
          '[core]\n\thooksPath = /\n',
        );
        // ...and ones it does not: a dotfile the next npm run would read,
        // and an ordinary file. Deny-by-default has to take all of them.
        writeFileSync(join(ws, '.npmrc'), 'script-shell=/tmp/evil\n');
        writeFileSync(join(ws, 'package.json'), '{}');
        mkdirSync(join(ws, 'node_modules'), { recursive: true });

        // GitHub Actions runs `shell: bash` steps with `-eo pipefail`,
        // so the battery must too: bare `bash -c` masks a failing
        // sweep command into a green test (probed with a failing `find`
        // stub on PATH: exit 0 here, exit 1 under the real step flags).
        const res = spawnSync('bash', ['-e', '-o', 'pipefail', '-c', wipe], {
          encoding: 'utf8',
          env: {
            ...process.env,
            GITHUB_WORKSPACE: ws,
            // The ported guard allowlists against the runner workspace
            // (#9265): the test workspace must sit inside it.
            RUNNER_WORKSPACE: dir,
            GITHUB_STEP_SUMMARY: join(dir, 'summary'),
          },
        });
        expect(res.status).toBe(0);
        expect(readdirSync(ws)).toEqual([]);
      } finally {
        rmSync(dir, { recursive: true, force: true });
      }
    },
  );

  // The guard has to be exercised with the REAL dangerous paths — that is
  // the whole point of it — but a test that passes `/` to a live `rm -rf`
  // is only safe while the guard exists. Written that way it detonates on
  // the developer's machine the moment anyone removes the guard, which is
  // exactly when the test is supposed to protect them. (It did: a mutation
  // run that deleted the guard spent six minutes attempting to delete `/`
  // before it was killed. macOS permissions absorbed it; nothing was lost,
  // and nothing about that outcome was by design.)
  //
  // So `rm` is stubbed to a recorder on PATH. The destructive primitive
  // cannot fire here under ANY edit, and the assertion is on the decision
  // rather than on filesystem effects: with the guard gone the recorder
  // shows an attempted delete and the test fails, having deleted nothing.
  it('refuses a suspicious workspace path without invoking rm', () => {
    const wipe = stepIn('verify', 'Wipe workspace before external code')
      .match(/run: \|-\n([\s\S]*)$/)?.[1]
      .replace(/^ {10}/gm, '');
    expect(wipe).toBeTruthy();

    const dir = mkdtempSync(join(tmpdir(), 'verify-wipe-guard-'));
    try {
      const calls = join(dir, 'rm-calls');
      writeFileSync(calls, '');
      writeFileSync(
        join(dir, 'rm'),
        [
          '#!/usr/bin/env bash',
          'printf "%s\\n" "$*" >> "$RM_CALLS"',
          'exit 0',
        ].join('\n'),
        { mode: 0o755 },
      );

      // The second half are the spellings the ported guard layers exist
      // for (#9265): the kernel resolves each of them to a guarded root,
      // yet the bare denylist they faced before let every one through
      // (measured on main in the issue). `/tmp` and `/opt` are refused by
      // the allowlist alone — the denylist has no arm for them.
      for (const bad of [
        '/',
        '/usr',
        '/etc',
        '/var',
        '/root',
        '/home',
        '',
        '/home/',
        '/root/',
        '/var/',
        '//',
        '/home//',
        '/home/.',
        '/home/..',
        '//usr',
        '//home',
        '/tmp',
        '/opt',
      ]) {
        writeFileSync(calls, '');
        const guard = spawnSync('bash', ['-c', wipe], {
          encoding: 'utf8',
          env: {
            ...process.env,
            PATH: `${dir}:${process.env.PATH}`,
            RM_CALLS: calls,
            GITHUB_WORKSPACE: bad,
            // The recorder dir doubles as the allowlist root: every bad
            // path sits outside it, so the refusal is the guard's, not a
            // side effect of the fixture layout.
            RUNNER_WORKSPACE: dir,
            GITHUB_STEP_SUMMARY: join(dir, 'summary'),
          },
        });
        // Non-zero, not exactly 1: several mechanisms refuse here — the
        // `case` exits 1 for a named root, an empty path never reaches it
        // because `${GITHUB_WORKSPACE:?}` aborts the shell first (127),
        // and the allowlist exits 1 for everything outside the runner
        // workspace. Which one fires depends on the host's realpath.
        expect(
          guard.status,
          `path ${bad || ''} was not refused`,
        ).not.toBe(0);
        // The load-bearing assertion: no delete was even attempted.
        expect(
          readFileSync(calls, 'utf8'),
          `rm was invoked for ${bad || ''}`,
        ).toBe('');
      }
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  // The canonicalization layer needs its own pin: every bad path above
  // sits OUTSIDE the recorder dir, so the allowlist refuses them
  // identically whether `realpath -m` ran or not — deleting that line
  // ships green against the battery. A raw '..' spelling does not pin it
  // either: the '..' case arm refuses that vector first, mutant or not.
  // What does pin it is a path whose INTERMEDIATE component leaves the
  // runner workspace: it matches "$RWS"/* as a string and names a
  // directory outside it, so with the line deleted find resolves the link
  // through the kernel and hands that directory's entries to the rm
  // recorder. It is deliberately a directory at the far end rather than
  // the link itself — a workspace that IS a link is now healed rather
  // than refused (#9480), and this test exists for the refusal.
  const extractRun = (stepName) => {
    const run = stepIn('verify', stepName)
      .match(/run: \|-\n([\s\S]*)$/)?.[1]
      .replace(/^ {10}/gm, '');
    expect(run, `run block for ${stepName}`).toBeTruthy();
    return run;
  };

  it.skipIf(!hasGnuRealpath)(
    'refuses an allowlist-escaping path reached through an intermediate symlink',
    () => {
      const dir = mkdtempSync(join(tmpdir(), 'verify-wipe-escape-'));
      const outside = mkdtempSync(join(tmpdir(), 'verify-wipe-outside-'));
      mkdirSync(join(outside, 'sub'));
      writeFileSync(join(outside, 'sub', 'canary'), 'x');
      symlinkSync(outside, join(dir, 'link'));
      try {
        const calls = join(dir, 'rm-calls');
        writeFileSync(
          join(dir, 'rm'),
          `#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\nexit 0\n`,
          { mode: 0o755 },
        );

        for (const stepName of [
          'Wipe workspace before external code',
          'Wipe workspace after external code',
        ]) {
          writeFileSync(calls, '');
          const res = spawnSync(
            'bash',
            ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
            {
              encoding: 'utf8',
              env: {
                ...process.env,
                PATH: `${dir}:${process.env.PATH}`,
                GITHUB_WORKSPACE: join(dir, 'link', 'sub'),
                RUNNER_WORKSPACE: dir,
                GITHUB_STEP_SUMMARY: join(dir, 'summary'),
              },
            },
          );
          expect(res.status, `${stepName} did not refuse`).not.toBe(0);
          expect(res.stdout + res.stderr).toContain(
            'outside the runner workspace',
          );
          expect(
            readFileSync(calls, 'utf8'),
            `${stepName} invoked rm on the escaping path`,
          ).toBe('');
          expect(readdirSync(join(outside, 'sub'))).toEqual(['canary']);
        }
      } finally {
        rmSync(dir, { recursive: true, force: true });
        rmSync(outside, { recursive: true, force: true });
      }
    },
  );

  // The wedge the guard itself created, and the hole the first attempt at
  // healing it shipped. Both wipes carry the same layer, so both are driven.
  const WIPE_STEPS = [
    'Wipe workspace before external code',
    'Wipe workspace after external code',
  ];

  it.skipIf(!hasGnuRealpath)(
    'heals a workspace a previous job replaced with a symlink',
    () => {
      // Without the heal this is permanent: canonicalization resolves the
      // link to its target, the allowlist refuses, the step exits 1 having
      // removed nothing, and every later job on the runner dies at the same
      // line. The unlink must take the LINK and leave the target alone —
      // which is also what pins the heal judging the canonical PARENT
      // rather than $WS itself, since resolving $WS follows the very link
      // being removed and would refuse a repair that must succeed.
      for (const stepName of WIPE_STEPS) {
        const parent = mkdtempSync(join(tmpdir(), 'verify-heal-'));
        const outside = mkdtempSync(join(tmpdir(), 'verify-heal-outside-'));
        const ws = join(parent, 'workspace');
        writeFileSync(join(outside, 'canary'), 'x');
        symlinkSync(outside, ws);
        try {
          const res = spawnSync(
            'bash',
            ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
            {
              encoding: 'utf8',
              env: {
                ...process.env,
                GITHUB_WORKSPACE: ws,
                RUNNER_WORKSPACE: parent,
                GITHUB_STEP_SUMMARY: join(parent, 'summary'),
              },
            },
          );
          expect(res.status, `${stepName}: ${res.stdout}${res.stderr}`).toBe(0);
          expect(res.stdout + res.stderr).toContain('healing workspace');
          expect(res.stdout + res.stderr).toContain(outside);
          expect(lstatSync(ws).isSymbolicLink()).toBe(false);
          expect(readdirSync(ws)).toEqual([]);
          expect(readdirSync(outside)).toEqual(['canary']);
        } finally {
          rmSync(parent, { recursive: true, force: true });
          rmSync(outside, { recursive: true, force: true });
        }
      }
    },
  );

  it.skipIf(!hasGnuRealpath)(
    'refuses to heal through an intermediate symlink, before touching anything',
    () => {
      // The defect the first attempt kept through three rounds: it matched
      // the RAW path, and `$RWS/link/sub` matches "$RWS"/* as a string while
      // naming a file outside the runner workspace — so the unlink and the
      // mkdir landed OUTSIDE, and only then did the allowlist refuse.
      for (const stepName of WIPE_STEPS) {
        const parent = mkdtempSync(join(tmpdir(), 'verify-heal-inter-'));
        const outside = mkdtempSync(join(tmpdir(), 'verify-heal-outside-'));
        writeFileSync(join(outside, 'sub'), 'canary');
        symlinkSync(outside, join(parent, 'link'));
        try {
          const calls = join(parent, 'rm-calls');
          writeFileSync(calls, '');
          writeFileSync(
            join(parent, 'rm'),
            `#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\nexit 0\n`,
            { mode: 0o755 },
          );
          const res = spawnSync(
            'bash',
            ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
            {
              encoding: 'utf8',
              env: {
                ...process.env,
                PATH: `${parent}:${process.env.PATH}`,
                GITHUB_WORKSPACE: join(parent, 'link', 'sub'),
                RUNNER_WORKSPACE: parent,
                GITHUB_STEP_SUMMARY: join(parent, 'summary'),
              },
            },
          );
          expect(res.status, stepName).not.toBe(0);
          expect(res.stdout + res.stderr).toContain(
            'refusing to heal workspace outside the runner workspace',
          );
          expect(readFileSync(calls, 'utf8')).toBe('');
          // The mutation the old shape performed before refusing: the file
          // at the resolved target is still a file, with its contents.
          expect(lstatSync(join(outside, 'sub')).isFile()).toBe(true);
          expect(readFileSync(join(outside, 'sub'), 'utf8')).toBe('canary');
        } finally {
          rmSync(parent, { recursive: true, force: true });
          rmSync(outside, { recursive: true, force: true });
        }
      }
    },
  );

  it.skipIf(!hasGnuRealpath)(
    'heals a non-directory workspace, and sees it through a trailing slash',
    () => {
      // The `|| [ ! -d "$WS" ]` half, and the raw strip that has to precede
      // both predicates: `[ -L "$WS/" ]` is false and `[ ! -d "$WS/" ]`
      // resolves through the link, so one trailing slash hides the
      // corruption entirely.
      for (const stepName of WIPE_STEPS) {
        const parent = mkdtempSync(join(tmpdir(), 'verify-heal-file-'));
        const ws = join(parent, 'workspace');
        writeFileSync(ws, 'not a directory');
        try {
          const res = spawnSync(
            'bash',
            ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
            {
              encoding: 'utf8',
              env: {
                ...process.env,
                GITHUB_WORKSPACE: ws,
                RUNNER_WORKSPACE: parent,
                GITHUB_STEP_SUMMARY: join(parent, 'summary'),
              },
            },
          );
          expect(res.status, `${stepName}: ${res.stdout}${res.stderr}`).toBe(0);
          expect(res.stdout + res.stderr).toContain('it was not a directory');
          expect(lstatSync(ws).isDirectory()).toBe(true);
        } finally {
          rmSync(parent, { recursive: true, force: true });
        }

        const linkParent = mkdtempSync(join(tmpdir(), 'verify-heal-slash-'));
        const outside = mkdtempSync(join(tmpdir(), 'verify-heal-outside-'));
        const linkWs = join(linkParent, 'workspace');
        writeFileSync(join(outside, 'canary'), 'x');
        symlinkSync(outside, linkWs);
        try {
          const res = spawnSync(
            'bash',
            ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
            {
              encoding: 'utf8',
              env: {
                ...process.env,
                GITHUB_WORKSPACE: `${linkWs}/`,
                RUNNER_WORKSPACE: linkParent,
                GITHUB_STEP_SUMMARY: join(linkParent, 'summary'),
              },
            },
          );
          expect(res.status, `${stepName}: ${res.stdout}${res.stderr}`).toBe(0);
          expect(lstatSync(linkWs).isSymbolicLink()).toBe(false);
          expect(readdirSync(outside)).toEqual(['canary']);
        } finally {
          rmSync(linkParent, { recursive: true, force: true });
          rmSync(outside, { recursive: true, force: true });
        }
      }
    },
  );

  it.skipIf(!hasGnuRealpath)(
    'keeps a forged workflow command in the symlink target out of the log',
    () => {
      // On this lane the previous job may have run a contributor's code, and
      // the runner parses `::` at the start of ANY stdout line as a workflow
      // command — so a target of $'…\n::error::forged' would forge an
      // annotation from the very step reporting the corruption.
      for (const stepName of WIPE_STEPS) {
        const parent = mkdtempSync(join(tmpdir(), 'verify-heal-inject-'));
        const outside = mkdtempSync(join(tmpdir(), 'verify-heal-outside-'));
        const ws = join(parent, 'workspace');
        symlinkSync(`${outside}\n::error::forged-annotation`, ws);
        try {
          const res = spawnSync(
            'bash',
            ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
            {
              encoding: 'utf8',
              env: {
                ...process.env,
                GITHUB_WORKSPACE: ws,
                RUNNER_WORKSPACE: parent,
                GITHUB_STEP_SUMMARY: join(parent, 'summary'),
              },
            },
          );
          expect(res.status, `${stepName}: ${res.stdout}${res.stderr}`).toBe(0);
          const out = res.stdout + res.stderr;
          expect(out).toContain('healing workspace');
          expect(out).toContain('pointed at');
          for (const line of out.split('\n')) {
            expect(line.startsWith('::error::')).toBe(false);
          }
        } finally {
          rmSync(parent, { recursive: true, force: true });
          rmSync(outside, { recursive: true, force: true });
        }
      }
    },
  );

  it.skipIf(!hasGnuRealpath)(
    'fails closed when the healed workspace cannot be recreated',
    () => {
      // The mkdir leg's own refusal, reachable without a permission trick:
      // `rm -f` returns 0 for a path whose parent is not a directory, and
      // the mkdir that follows cannot succeed. Swallowed, the wipe would run
      // against a path that does not exist.
      for (const stepName of WIPE_STEPS) {
        const parent = mkdtempSync(join(tmpdir(), 'verify-heal-mkdir-'));
        writeFileSync(join(parent, 'file'), 'not a directory');
        try {
          const res = spawnSync(
            'bash',
            ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
            {
              encoding: 'utf8',
              env: {
                ...process.env,
                GITHUB_WORKSPACE: join(parent, 'file', 'sub'),
                RUNNER_WORKSPACE: parent,
                GITHUB_STEP_SUMMARY: join(parent, 'summary'),
              },
            },
          );
          expect(res.status, stepName).not.toBe(0);
          expect(res.stdout + res.stderr).toContain('could not recreate');
        } finally {
          rmSync(parent, { recursive: true, force: true });
        }
      }
    },
  );

  it.skipIf(!hasGnuRealpath || !arePermissionBitsEnforced())(
    'fails closed when the corrupt workspace cannot be unlinked',
    () => {
      // A swallowed `rm -f` failure would let the mkdir and the wipe run on
      // a path that is still a symlink. Root with its DAC capabilities
      // bypasses the mode bits, so the fixture cannot produce the refusal
      // there.
      for (const stepName of WIPE_STEPS) {
        const parent = mkdtempSync(join(tmpdir(), 'verify-heal-perm-'));
        const outside = mkdtempSync(join(tmpdir(), 'verify-heal-outside-'));
        const ws = join(parent, 'workspace');
        writeFileSync(join(outside, 'canary'), 'x');
        symlinkSync(outside, ws);
        chmodSync(parent, 0o555);
        try {
          const res = spawnSync(
            'bash',
            ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
            {
              encoding: 'utf8',
              env: {
                ...process.env,
                GITHUB_WORKSPACE: ws,
                RUNNER_WORKSPACE: parent,
                GITHUB_STEP_SUMMARY: join(parent, 'summary'),
              },
            },
          );
          expect(res.status, stepName).not.toBe(0);
          expect(res.stdout + res.stderr).toContain('could not remove');
          expect(lstatSync(ws).isSymbolicLink()).toBe(true);
          expect(readdirSync(outside)).toEqual(['canary']);
        } finally {
          chmodSync(parent, 0o755);
          rmSync(parent, { recursive: true, force: true });
          rmSync(outside, { recursive: true, force: true });
        }
      }
    },
  );

  // Fronts PATH with a failing realpath so the script must fail closed instead
  // of matching and wiping a raw, potentially misleading spelling.
  const stubRealpath = () => {
    const bin = mkdtempSync(join(tmpdir(), 'verify-wipe-bin-'));
    writeFileSync(join(bin, 'realpath'), '#!/bin/sh\nexit 1\n');
    chmodSync(join(bin, 'realpath'), 0o755);
    return bin;
  };

  it('refuses to wipe when realpath is absent', () => {
    const parent = mkdtempSync(join(tmpdir(), 'verify-wipe-rws-'));
    const ws = join(parent, 'repo');
    mkdirSync(ws);
    writeFileSync(join(ws, 'leftover'), 'x');
    const bin = stubRealpath();
    try {
      // Both copies carry the fail-closed realpath leg; exercise each so
      // a fail-open regression of either copy is caught.
      for (const stepName of [
        'Wipe workspace before external code',
        'Wipe workspace after external code',
      ]) {
        const res = spawnSync(
          'bash',
          ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
          {
            encoding: 'utf8',
            env: {
              ...process.env,
              PATH: `${bin}:${process.env.PATH}`,
              GITHUB_WORKSPACE: ws,
              RUNNER_WORKSPACE: `${parent}/`,
              GITHUB_STEP_SUMMARY: join(parent, 'summary'),
            },
          },
        );
        expect(res.status, `${stepName} did not fail closed`).not.toBe(0);
        expect(readdirSync(ws), `${stepName} wiped without realpath`).toEqual([
          'leftover',
        ]);
      }
    } finally {
      rmSync(parent, { recursive: true, force: true });
      rmSync(bin, { recursive: true, force: true });
    }
  });

  it('refuses a trailing-slash GITHUB_WORKSPACE when realpath is absent', () => {
    const dir = mkdtempSync(join(tmpdir(), 'verify-wipe-ws-'));
    const bin = stubRealpath();
    try {
      const calls = join(dir, 'rm-calls');
      writeFileSync(calls, '');
      writeFileSync(
        join(dir, 'rm'),
        `#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\nexit 0\n`,
        { mode: 0o755 },
      );

      for (const stepName of [
        'Wipe workspace before external code',
        'Wipe workspace after external code',
      ]) {
        writeFileSync(calls, '');
        const res = spawnSync(
          'bash',
          ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
          {
            encoding: 'utf8',
            env: {
              ...process.env,
              PATH: `${dir}:${bin}:${process.env.PATH}`,
              GITHUB_WORKSPACE: '/home/',
              RUNNER_WORKSPACE: '/home',
              GITHUB_STEP_SUMMARY: join(dir, 'summary'),
            },
          },
        );
        expect(res.status, `${stepName} did not refuse`).not.toBe(0);
        expect(readFileSync(calls, 'utf8'), `${stepName} invoked rm`).toBe('');
      }
    } finally {
      rmSync(dir, { recursive: true, force: true });
      rmSync(bin, { recursive: true, force: true });
    }
  });

  it('refuses an allowlist-escaping .. path when realpath is absent', () => {
    const dir = mkdtempSync(join(tmpdir(), 'verify-wipe-fallback-'));
    const outside = mkdtempSync(join(tmpdir(), 'verify-wipe-outside-'));
    const bin = stubRealpath();
    mkdirSync(join(dir, 'sub'));
    try {
      const calls = join(dir, 'rm-calls');
      writeFileSync(calls, '');
      writeFileSync(
        join(dir, 'rm'),
        `#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\nexit 0\n`,
        { mode: 0o755 },
      );

      for (const stepName of [
        'Wipe workspace before external code',
        'Wipe workspace after external code',
      ]) {
        writeFileSync(calls, '');
        const res = spawnSync(
          'bash',
          ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
          {
            encoding: 'utf8',
            env: {
              ...process.env,
              PATH: `${dir}:${bin}:${process.env.PATH}`,
              GITHUB_WORKSPACE: `${dir}/sub/../../${basename(outside)}`,
              RUNNER_WORKSPACE: dir,
              GITHUB_STEP_SUMMARY: join(dir, 'summary'),
            },
          },
        );
        expect(res.status, `${stepName} did not refuse`).not.toBe(0);
        expect(
          readFileSync(calls, 'utf8'),
          `${stepName} invoked rm on the escaping path`,
        ).toBe('');
      }
    } finally {
      rmSync(dir, { recursive: true, force: true });
      rmSync(outside, { recursive: true, force: true });
      rmSync(bin, { recursive: true, force: true });
    }
  });

  // The degenerate-root arm keeps a stripped-empty RUNNER_WORKSPACE from
  // turning the allowlist pattern into `/*` (which admits every absolute
  // path). The reference suite covers the review workflow; each backported
  // copy needs its own case — deleting the arm ships green otherwise.
  it('refuses a runner workspace that resolves to / without invoking rm', () => {
    const parent = mkdtempSync(join(tmpdir(), 'verify-wipe-root-'));
    const ws = join(parent, 'repo');
    mkdirSync(ws);
    writeFileSync(join(ws, 'leftover'), 'x');
    try {
      const calls = join(parent, 'rm-calls');
      writeFileSync(calls, '');
      writeFileSync(
        join(parent, 'rm'),
        `#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\nexit 0\n`,
        { mode: 0o755 },
      );

      for (const stepName of [
        'Wipe workspace before external code',
        'Wipe workspace after external code',
      ]) {
        writeFileSync(calls, '');
        const res = spawnSync(
          'bash',
          ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
          {
            encoding: 'utf8',
            env: {
              ...process.env,
              PATH: `${parent}:${process.env.PATH}`,
              GITHUB_WORKSPACE: ws,
              RUNNER_WORKSPACE: '/',
              GITHUB_STEP_SUMMARY: join(parent, 'summary'),
            },
          },
        );
        expect(res.status, `${stepName} did not refuse`).not.toBe(0);
        expect(readFileSync(calls, 'utf8'), `${stepName} invoked rm`).toBe('');
        expect(readdirSync(ws), `${stepName} wiped`).toEqual(['leftover']);
      }
    } finally {
      rmSync(parent, { recursive: true, force: true });
    }
  });

  // The RWS realpath line has no refusal of its own to observe, so pin it
  // from the happy side: a RUNNER_WORKSPACE spelled with '..' that
  // canonicalizes back to the real parent must still be allowed to wipe.
  // Deleting the RWS realpath line leaves the raw spelling to the '..'
  // arm, which refuses — and this test fails on that mutant.
  it.skipIf(!hasGnuRealpath)(
    'canonicalizes a ..-spelled runner workspace instead of refusing it',
    () => {
      const parent = mkdtempSync(join(tmpdir(), 'verify-wipe-rwsdot-'));
      const ws = join(parent, 'repo');
      mkdirSync(ws);
      try {
        for (const stepName of [
          'Wipe workspace before external code',
          'Wipe workspace after external code',
        ]) {
          writeFileSync(join(ws, 'leftover'), 'x');
          const res = spawnSync(
            'bash',
            ['-e', '-o', 'pipefail', '-c', extractRun(stepName)],
            {
              encoding: 'utf8',
              env: {
                ...process.env,
                GITHUB_WORKSPACE: ws,
                RUNNER_WORKSPACE: join(ws, '..'),
                GITHUB_STEP_SUMMARY: join(parent, 'summary'),
              },
            },
          );
          expect(res.status, `${stepName} refused a canonical RWS`).toBe(0);
          expect(readdirSync(ws), `${stepName} did not wipe`).toEqual([]);
        }
      } finally {
        rmSync(parent, { recursive: true, force: true });
      }
    },
  );

  // The pre-run wipe answers what an external run inherits; the
  // post-run wipe answers what it leaves behind for the next pool job.
  it('wipes the workspace after external code, on every outcome', () => {
    const verifyJob = job('verify');
    const postWipe = stepIn('verify', 'Wipe workspace after external code');
    expect(postWipe).not.toBe('');
    // Gated on the trust level: a trusted run keeps its warm workspace.
    expect(postWipe).toContain(
      "needs.authorize.outputs.verify_trust == 'external'",
    );
    // Must run on every outcome, including cancellation and timeout.
    expect(postWipe).toContain('always()');
    // Ordering: after the agent, not before it.
    expect(verifyJob.indexOf('Run verification agent')).toBeLessThan(
      verifyJob.indexOf('Wipe workspace after external code'),
    );
    // Same path guard as the pre-run wipe.
    expect(postWipe).toContain('refusing to wipe suspicious workspace path');
    // The ported guard layers (#9265), pinned on THIS copy: canonicalize
    // before matching, and allowlist the target inside the runner
    // workspace. The exec tests below prove behavior; these text pins
    // catch a mutation that deletes a layer from this copy alone.
    expect(postWipe).toContain('realpath -m');
    expect(postWipe).toContain('realpath -m -- "$RWS"');
    expect(postWipe).toContain('RUNNER_WORKSPACE:?');
    expect(postWipe).toContain('"$RWS"/*');
    expect(postWipe).toContain(
      "refusing runner workspace path containing '..'",
    );
    expect(postWipe).toContain('runner workspace resolved to /');
  });

  // The pre-run guard battery runs the post-run wipe too: it is a
  // separate copy of the script, and the pre-run battery passing says
  // nothing about mutations to this one.
  it('refuses a suspicious workspace path in the post-run wipe without invoking rm', () => {
    const wipe = extractRun('Wipe workspace after external code');

    const dir = mkdtempSync(join(tmpdir(), 'verify-postwipe-guard-'));
    try {
      const calls = join(dir, 'rm-calls');
      writeFileSync(
        join(dir, 'rm'),
        `#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\nexit 0\n`,
        { mode: 0o755 },
      );

      for (const bad of [
        '/',
        '/usr',
        '/etc',
        '/var',
        '/root',
        '/home',
        '',
        '/home/',
        '/root/',
        '/var/',
        '//',
        '/home//',
        '/home/.',
        '/home/..',
        '//usr',
        '//home',
        '/tmp',
        '/opt',
      ]) {
        writeFileSync(calls, '');
        const guard = spawnSync('bash', ['-e', '-o', 'pipefail', '-c', wipe], {
          encoding: 'utf8',
          env: {
            ...process.env,
            PATH: `${dir}:${process.env.PATH}`,
            GITHUB_WORKSPACE: bad,
            RUNNER_WORKSPACE: dir,
            GITHUB_STEP_SUMMARY: join(dir, 'summary'),
          },
        });
        expect(
          guard.status,
          `path ${bad || ''} was not refused`,
        ).not.toBe(0);
        expect(
          readFileSync(calls, 'utf8'),
          `rm was invoked for ${bad || ''}`,
        ).toBe('');
      }
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  // The post-run wipe still has to wipe: its `if: always()` means it is
  // the ONLY cleanup a cancelled or timed-out external run gets, so a
  // guard regression that refuses the legitimate workspace would leak
  // every aborted run's tree into the next pool job.
  it.skipIf(!hasGnuRealpath)(
    'wipes a legitimate workspace in the post-run wipe and writes the summary',
    () => {
      const wipe = extractRun('Wipe workspace after external code');

      const parent = mkdtempSync(join(tmpdir(), 'verify-postwipe-ok-'));
      const ws = join(parent, 'workspace');
      mkdirSync(join(ws, '.git', 'hooks'), { recursive: true });
      writeFileSync(
        join(ws, '.git', 'hooks', 'post-checkout'),
        '#!/bin/sh\nid\n',
      );
      writeFileSync(join(ws, 'leftover.o'), 'x');
      const summary = join(parent, 'summary');
      try {
        const res = spawnSync('bash', ['-e', '-o', 'pipefail', '-c', wipe], {
          encoding: 'utf8',
          env: {
            ...process.env,
            GITHUB_WORKSPACE: ws,
            RUNNER_WORKSPACE: parent,
            GITHUB_STEP_SUMMARY: summary,
          },
        });
        expect(res.status).toBe(0);
        expect(readdirSync(ws)).toEqual([]);
        expect(readFileSync(summary, 'utf8')).toContain(
          'Workspace wiped after external code',
        );
      } finally {
        rmSync(parent, { recursive: true, force: true });
      }
    },
  );

  // The sponsored lane's pre-execution risk screen, driven for real: the
  // actual resolve-step text runs against a stubbed gh and a live local
  // HTTP server standing in for the model endpoint, so the heredoc's
  // fetch/parse/fail-closed logic is what executes — not a re-statement
  // of it. Every arm asserts on the step's real GITHUB_OUTPUT.
  it('screens sponsored runs mechanically and via the model, fail-closed', async () => {
    const resolveStep = stepIn('verify', 'Resolve PR and snapshot metadata');
    const script = resolveStep
      .match(/run: \|-\n([\s\S]*)$/)?.[1]
      .replace(/^ {10}/gm, '');
    expect(script).toBeTruthy();
    // The screen prompt must describe the ACTUAL environment. The pool is
    // persistent and network-isolated, not ephemeral — a stale prompt
    // calibrates the model for leniency it should not have.
    expect(script).not.toContain('ephemeral');
    expect(script).not.toContain('credential-free');
    expect(script).toContain('network-isolated');

    // The model endpoint must be a SEPARATE process: the scenarios run the
    // step via spawnSync, which blocks this process's event loop — an
    // in-process server could never answer, and the screen's fetch would
    // sit on its 120 s abort timer instead of testing anything.
    const dir = mkdtempSync(join(tmpdir(), 'verify-screen-'));
    const replyFile = join(dir, 'model-reply');
    const hitsFile = join(dir, 'model-hits');
    const portFile = join(dir, 'model-port');
    writeFileSync(portFile, '');
    writeFileSync(replyFile, '{"risk":"clear"}');
    writeFileSync(hitsFile, '');
    const serverProc = spawn(
      process.execPath,
      [
        '-e',
        [
          "const http=require('http'),fs=require('fs');",
          'const s=http.createServer((q,r)=>{',
          "fs.appendFileSync(process.env.HITS,'x');",
          "r.setHeader('content-type','application/json');",
          "r.end(JSON.stringify({choices:[{message:{content:fs.readFileSync(process.env.REPLY,'utf8')}}]}));",
          '});',
          "s.listen(0,'127.0.0.1',()=>fs.writeFileSync(process.env.PORTF,String(s.address().port)));",
        ].join(''),
      ],
      {
        env: {
          ...process.env,
          HITS: hitsFile,
          REPLY: replyFile,
          PORTF: portFile,
        },
        stdio: 'ignore',
      },
    );
    for (let i = 0; i < 100 && !readFileSync(portFile, 'utf8').trim(); i++) {
      await new Promise((r) => setTimeout(r, 50));
    }
    const port = readFileSync(portFile, 'utf8').trim();
    expect(port).not.toBe('');
    const modelHits = () => readFileSync(hitsFile, 'utf8').length;
    const setReply = (v) => writeFileSync(replyFile, v);
    writeFileSync(
      join(dir, 'gh'),
      [
        '#!/usr/bin/env bash',
        'case "$*" in',
        '  *"--json state,isDraft,mergeable"*)',

        '    echo \'{"state":"OPEN","isDraft":false,"mergeable":"MERGEABLE"}\' ;;',
        '  *pulls/*/files*) echo packages/core/src/x.ts ;;',
        '  *"pr diff"*) cat "$GH_STUB_DIFF" ;;',
        '  *"--json number,title"*)',
        '    printf \'{"headRefOid":"%s","author":{"login":"ext"}}\' "$GH_STUB_OID" ;;',
        '  *collaborators/*) echo none ;;',
        '  *"api user"*) echo bot ;;',
        '  *comments*) echo "[]" ;;',
        '  *) exit 0 ;;',
        'esac',
      ].join('\n'),
      { mode: 0o755 },
    );

    let n = 0;
    const run = ({ trust, diff, prOid, sponsoredOid, baseUrl, apiKey }) => {
      const temp = join(dir, `t${n}`);
      mkdirSync(temp, { recursive: true });
      const out = join(dir, `out-${n}`);
      const diffFile = join(dir, `diff-${n}`);
      n += 1;
      writeFileSync(out, '');
      writeFileSync(diffFile, diff);
      const res = spawnSync('bash', ['-c', script], {
        cwd: temp,
        encoding: 'utf8',
        env: {
          ...process.env,
          PATH: `${dir}:${process.env.PATH}`,
          GH_TOKEN: 'x',
          GH_STUB_DIFF: diffFile,
          GH_STUB_OID: prOid ?? 'oid-1',
          GITHUB_REPOSITORY: 'QwenLM/qwen-code',
          GITHUB_RUN_ID: '1',
          GITHUB_STEP_SUMMARY: '/dev/null',
          GITHUB_OUTPUT: out,
          RUNNER_TEMP: temp,
          PR_NUMBER: '7',
          RUN_URL: 'u',
          VERIFY_TRUST: trust,
          SPONSORED_OID: sponsoredOid ?? 'oid-1',
          REVIEW_OPENAI_API_KEY: apiKey === undefined ? 'k' : apiKey,
          REVIEW_OPENAI_BASE_URL: baseUrl ?? `http://127.0.0.1:${port}/v1`,
          OPENAI_MODEL: 'screen-model',
        },
      });
      expect(res.status).toBe(0);
      const lines = readFileSync(out, 'utf8').trim().split('\n');
      const val = (k) =>
        lines
          .filter((l) => l.startsWith(`${k}=`))
          .pop()
          ?.slice(k.length + 1);
      return { decision: val('decision'), reason: val('skip_reason') ?? '' };
    };

    const CLEAN = '+++ b/packages/core/src/x.ts\n+const x = 1;\n';
    try {
      const before = modelHits();

      // Fail closed on an unfetchable diff: a transient `gh pr diff`
      // failure or an empty result must refuse rather than hand the model
      // an empty string it would almost certainly call "clear". No model
      // call is spent: the fetch guard runs before the screen.
      const noDiff = run({ trust: 'external', diff: '' });
      expect(noDiff.decision).toBe('skip');
      expect(noDiff.reason).toContain('could not be fetched');
      expect(modelHits()).toBe(before);

      // Mechanical: an added npm lifecycle script refuses WITHOUT spending
      // a model call — a mechanical flag cannot be un-refused by a second
      // opinion, so paying for one would be pure waste.
      const lifecycle = run({
        trust: 'external',
        diff: '+++ b/package.json\n+    "postinstall": "node evil.js",\n',
      });
      expect(lifecycle.decision).toBe('skip');
      expect(lifecycle.reason).toContain('risk screen');
      expect(lifecycle.reason).toContain('npm lifecycle');
      expect(modelHits()).toBe(before);

      // npm runs pre/post hooks for ANY `npm run