qwen-code/scripts/tests/qwen-triage-workflow.test.js
易良 2a1807f08d
fix(ci): detect silent triage failures with empty-response check (#6566)
* fix(ci): detect silent triage failures with empty-response check

The qwen-triage workflow relied on qwen-code-action to surface failures,
but when the CLI exited 0 with empty output the step reported success
with no triage comments posted. Add a post-step that checks the action's
summary output and fails the job if it is empty, making silent failures
visible in the Actions log.

Defense-in-depth for QwenLM/qwen-code#6553 (primary fix in
qwen-code-action).

* ci: bump qwen-code-action pin to 6d08e91

Picks up the stderr visibility fix (QwenLM/qwen-code-action#12) that
always emits stderr to the step log and warns on empty responses.
Bumps all 5 workflows pinned to the action.

* fix(ci): pass triage summary through env
2026-07-09 11:10:44 +00:00

212 lines
8.4 KiB
JavaScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const workflow = readFileSync('.github/workflows/qwen-triage.yml', 'utf8');
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] ?? '';
}
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);
}
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//</&lt;}"');
expect(postStep).not.toContain('content="${content//>/&gt;}"');
expect(postStep).toContain("sed -e 's/&/\\&amp;/g'");
expect(postStep).toContain("-e 's/</\\&lt;/g'");
expect(postStep).toContain("-e 's/>/\\&gt;/g'");
expect(postStep).toContain('html_escape()');
expect(postStep).toContain("tr -d '\\000'");
expect(postStep).toContain('Log could not be rendered');
expect(postStep).toContain('if ! content="$(');
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(
'\'<details>\\n<summary>%s</summary>\\n\\n<pre><code>\\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"');
expect(runStep).toContain("QWEN_HOME: '${{ runner.temp }}/qwen-home'");
});
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('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('<code>${UNKNOWN_VERDICT}</code>');
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+npm ci`),
);
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'"));
});
});