diff --git a/scripts/tests/qwen-autofix-workflow.test.js b/scripts/tests/qwen-autofix-workflow.test.js
index afe2fad4dca..46db4f5ed89 100644
--- a/scripts/tests/qwen-autofix-workflow.test.js
+++ b/scripts/tests/qwen-autofix-workflow.test.js
@@ -8,6 +8,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
import {
chmodSync,
existsSync,
+ mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
@@ -2107,6 +2108,70 @@ describe('qwen-autofix workflow', () => {
);
});
+ it('resolves the staged SKILL end-to-end by running the real runner (stage↔resolve contract)', () => {
+ // The string test above pins the mirrored LAYOUT, but it re-implements
+ // run-agent.mjs's `
/../SKILL.md` convention. If that coupling ever
+ // moves in the RUNNER, the string test stays green while prod breaks —
+ // the same class of blind spot that let #7165 ship. This test runs the
+ // ACTUAL runner against the staged layout and asserts it reads the
+ // staged SKILL, exercising the stage↔resolve contract for real.
+ const runner = readFileSync(autofixRunnerScriptPath, 'utf8');
+ const printPrompt = (scriptPath, dir) =>
+ spawnSync(
+ process.execPath,
+ [
+ scriptPath,
+ '--mode',
+ 'address-review',
+ '--pr',
+ '1',
+ '--issue',
+ '1',
+ '--workdir',
+ dir,
+ '--print-prompt',
+ ],
+ // spawnSync blocks the event loop, so vitest's async timeout can't
+ // fire — bound each subprocess directly against a hung runner.
+ { encoding: 'utf8', timeout: 10_000 },
+ );
+ withRunnerDir((dir) => {
+ // Mirror the workflow's staging: autofix-skill/{SKILL.md,scripts/run-agent.mjs}.
+ mkdirSync(join(dir, 'autofix-skill', 'scripts'), { recursive: true });
+ writeFileSync(
+ join(dir, 'autofix-skill', 'SKILL.md'),
+ '---\nname: autofix\n---\nSTAGED_SKILL_SENTINEL\n',
+ );
+ const stagedRunner = join(
+ dir,
+ 'autofix-skill',
+ 'scripts',
+ 'run-agent.mjs',
+ );
+ writeFileSync(stagedRunner, runner);
+ const ok = printPrompt(stagedRunner, dir);
+ expect(ok.status).toBe(0);
+ // The real runner resolved ../SKILL.md to the STAGED copy and inlined it.
+ expect(ok.stdout).toContain('STAGED_SKILL_SENTINEL');
+ // Skill directory ends in the mirrored dir name (basename, not the full
+ // temp path — macOS canonicalizes /var → /private/var).
+ expect(ok.stdout).toMatch(/Skill directory: \S*[/\\]autofix-skill\n/);
+
+ // And the FLAT layout #7165 shipped (runner alone, no ../SKILL.md) must
+ // crash with ENOENT — proving this test catches that regression. Nest it
+ // under dir/flat/ so its ../SKILL.md resolves to dir/SKILL.md (which this
+ // test never creates) rather than a shared tmpdir()/SKILL.md a concurrent
+ // job could leave behind and make the runner exit 0 spuriously.
+ mkdirSync(join(dir, 'flat'), { recursive: true });
+ const flatRunner = join(dir, 'flat', 'run-agent.mjs');
+ writeFileSync(flatRunner, runner);
+ const flat = printPrompt(flatRunner, dir);
+ expect(flat.status).not.toBe(0);
+ expect(flat.stderr).toContain('ENOENT');
+ expect(flat.stderr).toContain("SKILL.md'");
+ });
+ });
+
it('surfaces the running model in every autofix report for diagnosis and attribution', () => {
// The model is a repo variable (already the agent's OPENAI_MODEL), not a
// secret, so it is safe to echo into a public comment. Each reporting
@@ -2599,9 +2664,17 @@ describe('qwen-autofix workflow', () => {
expect(workflow).toMatch(
/git config core\.hooksPath \/dev\/null\n[\s\S]{0,2200}git checkout -B "\$\{BRANCH\}" "origin\/\$\{BRANCH\}"/,
);
- expect(workflow).toMatch(
- /git config core\.hooksPath \.husky\n[\s\S]{0,200}node "\$\{RUNNER_TEMP\}\/autofix-skill\/scripts\/run-agent\.mjs"/,
+ // The agent step re-points hooks to .husky BEFORE invoking the runner.
+ // Assert the ordering directly (not a fixed-width window) so adding a
+ // comment between the two lines can't fail the test spuriously.
+ const huskyAt = triageAndAddressStep.indexOf(
+ 'git config core.hooksPath .husky',
+ );
+ const stagedNodeAt = triageAndAddressStep.indexOf(
+ 'node "${RUNNER_TEMP}/autofix-skill/scripts/run-agent.mjs"',
);
+ expect(huskyAt).toBeGreaterThanOrEqual(0);
+ expect(stagedNodeAt).toBeGreaterThan(huskyAt);
});
it('keeps sandbox image fallback covered by a reusable script', () => {