chore(ci): migrate autofix prose to the design record and ratchet growth (#9677)

* chore(ci): migrate autofix prose to the design record and ratchet growth

`qwen-autofix.yml` was at 462,720 bytes — 90% of GitHub's 500 KB
start-runs limit and 7,280 under this repo's gate. The prose migration in
#9517 had regained 78 KB; a single feature commit gave 25 KB of it back
two days later, 53% of it comment lines that belonged in the design
record. Nothing objected, because the gate is a ceiling and only speaks
when a file is already at the wall.

Two changes, addressing the level and the slope.

Level: 76 comment blocks of 8+ lines move into qwen-autofix.md as
af-073..af-148, each leaving its opening two lines plus the usual
pointer. The file drops to 419,995 — 50 KB under the gate.

Slope: every workflow's recorded size now lives in
.github/workflows/.size-baseline, and check-workflow-size.sh fails a file
that exceeds its entry by more than 4 KB. Growing a file is still
allowed; the ratchet only insists the growth be visible in review instead
of discovered at the wall. A file well under its baseline warns so the
slack is reclaimed rather than banked.

Two things the migration had to learn, both caught by contract tests
rather than by inspection:

Identical prose shares one af id. A step inlined into several jobs must
stay byte-identical across copies, and minting a separate id per copy
broke that for the git-config sanitize step — same length, different
pointer digits.

A cross-file editing contract is not prose. "This copy and the one in
qwen-triage must be edited together" only does its job where the editor
will see it; moving it to the design record is how a lockstep silently
breaks. Blocks naming a sibling workflow or script, or spelling the
contract out, stay put.

Behaviour is unchanged and checked, not assumed: both YAML documents were
parsed and compared with comment lines dropped from every string, and
they are equal.

* fix(ci): harden the workflow-size ratchet per review

- key the vitest baseline lookups by separator-agnostic file name so the
  merge-queue Windows lane resolves entries (split('/') missed win32 joins)
- fail closed on malformed .size-baseline lines: non-numeric values,
  leading zeros (bash octal), and extra fields no longer disable or
  mis-key the ratchet; keep an unterminated final line
- execute the gate script end-to-end in tests (growth, missing entry,
  missing baseline, slack warning, malformed line) so its decision
  branches are witnessed
- correct af-079/af-123/af-084 attributions in the design record

* fix(ci): pin ratchet boundaries and skip pre-bash-4 test hosts

* fix(ci): end migrated autofix teasers at sentence boundaries per review

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Shaojin Wen 2026-08-22 16:31:56 +00:00 committed by GitHub
parent 6c742ec792
commit a5d77eb817
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 2064 additions and 611 deletions

View file

@ -2,10 +2,12 @@
# Push the round's commit to the PR head and post the round report.
#
# The body below is the 'Push and report' step of review-address in
# .github/workflows/qwen-autofix.yml, byte-identical to the inline block it
# came from: 626 lines, ~41 KB. The file it left is within a few KB of the
# repo's 470,000-byte gate, and GitHub stops starting runs past 512,000 without
# saying so (.github/scripts/check-workflow-size.sh). No absolute size is
# .github/workflows/qwen-autofix.yml — the inline block it came from (626
# lines, ~41 KB at the move), its long comments since migrated to
# qwen-autofix.md pointers like the rest of the workflow. The file it left
# is within a few KB of the repo's 470,000-byte gate, and GitHub stops starting
# runs past 512,000 without saying so (.github/scripts/check-workflow-size.sh).
# No absolute size is
# quoted here on purpose — main moves it every day, and a number that decays is
# how this comment earned three review rounds. It is also
# the step docs/design/autofix-gate-runner-isolation.md moves into its own
@ -78,15 +80,8 @@ MODEL_DISPLAY="${MODEL:-default}"
# Growth-audit trail (+ re-arm on sound): audit rounds record the
# verdict under the key the baseline was READ under — same rule as
# the growth markers, same dead-key hazard (a supersede-exempt
# round can report under a stale WINDOW after a re-arm). The
# verdict comes from AUDIT_VERDICT — the verdict the verification
# GATE validated and surfaced as a step output — NOT a re-read of
# growth-audit.json: the branch's own build/tests run as the runner
# user and WORKDIR is a predictable path they can write, so the
# file could change after the gate looked. Re-arming is allowed
# for completed rounds only ($1 = allow): a sound verdict whose
# round then FAILED must not re-anchor the window — the failure
# path re-measures under the same window instead.
# round can report under a stale WINDOW after a re-arm).
# Full rationale → qwen-autofix.md#af-131
emit_growth_audit_marker() {
local allow_rearm="${1:-false}"
[[ "${KISS_AUDIT}" == 'true' ]] || return 0
@ -233,12 +228,7 @@ resolve_and_reply_threads() {
fi
# The mirror of the resolve above: a finding the agent did NOT
# resolve keeps its thread open, and this answers it IN that thread.
# Without it the reason sits only in the round summary, so the
# reviewer who opens the still-open thread sees silence and cannot
# tell their finding was read. Same neutralisation as the summary
# body — a reply is model output posted verbatim under the bot
# identity, so it could otherwise smuggle a forged control marker.
# Best-effort: a reply failure must never fail a good push.
# Full rationale → qwen-autofix.md#af-132
if [[ -s "${WORKDIR}/comment-replies.json" ]] &&
jq -e 'type == "array"' "${WORKDIR}/comment-replies.json" > /dev/null 2>&1; then
REPLIED_N=0
@ -271,12 +261,8 @@ resolve_and_reply_threads() {
# later round whose agent rewrites an unchanged declination
# must not post the same bot reply twice on one thread
# (observed 2026-08-16: an identical reply posted three
# times, #9296). Skip when the thread already carries a
# comment by the bot whose body EQUALS the neutralised body
# about to be posted; a changed body — a new reason in a
# later round — still posts. Best-effort like the rest: with
# a stale or empty threads view this degrades to the old
# post-always behavior.
# times, #9296).
# Full rationale → qwen-autofix.md#af-133
if jq -e --argjson id "${root_id}" --arg bot "${AUTOFIX_BOT}" \
--arg body "${REPLY_BODY}" '
map(select(any(.comments.nodes[]; .databaseId == $id)))
@ -382,13 +368,8 @@ bash "${RUNNER_TEMP}/resanitize-git-config.sh"
if [[ "${OUTCOME}" == "fixed" ]]; then
NEXT_ROUND="$(( ROUND + 1 ))"
# The tree the gate verified is what gets pushed: assert HEAD is
# the gate's verified_head before touching credentials. A repo
# redirect (a planted .git/commondir/GIT_DIR — the first defused
# by resanitize, the second by the env strip) would otherwise let
# `git rev-parse HEAD` and the push read an attacker repo whose
# HEAD differs; this compares against the value the gate recorded
# in GITHUB_OUTPUT (unreachable from a disk write). Empty
# verified_head only on a noop, which does not reach this push.
# the gate's verified_head before touching credentials.
# Full rationale → qwen-autofix.md#af-134
HEAD_NOW="$(git rev-parse HEAD)"
if [[ -z "${VERIFIED_HEAD}" || "${HEAD_NOW}" != "${VERIFIED_HEAD}" ]]; then
echo "::error::HEAD ${HEAD_NOW} is not the gate's verified head ${VERIFIED_HEAD:-<empty>} — refusing to push"
@ -560,13 +541,8 @@ fi
# Bounded retry on the report post: this one comment carries the
# round's ENTIRE persisted state (autofix-eval watermark/round,
# redcheck head, growth baseline). The push has already landed, so
# a transient API failure here loses the marker while keeping the
# growth — the retry scan would re-anchor the baseline at the
# post-push size and re-evaluate feedback it already addressed.
# Three attempts bound that to genuine outages; the final failure
# keeps today's semantics (step fails, no marker, next scan
# retries the round).
# redcheck head, growth baseline).
# Full rationale → qwen-autofix.md#af-135
REPORT_POSTED='false'
for attempt in 1 2 3; do
if gh pr comment "${PR}" --repo "${REPO}" --body-file "${WORKDIR}/report.md"; then
@ -590,13 +566,8 @@ if [[ "${OUTCOME}" == "fixed" && "${MAX_ROUNDS}" == "${TAKEOVER_MAX_ROUNDS}" ]]
# Crossing trigger, not an equality test: failure rounds also
# advance the round counter, so `push@9, crash@10, push@11`
# would skip an exact %10 check forever — and a failure-heavy
# PR is the very PR the digest exists for. Post on the first
# PUSHED round once 10+ rounds have accumulated since the last
# digest in THIS window (or since the window opened). The
# window opens at the round SEED, not at zero: a '/takeover
# from 60' counter starts at 60, so the no-digest-yet baseline
# is the seed — otherwise the seed-inflated counter digests on
# the window's first push with a 1-2 round census.
# PR is the very PR the digest exists for.
# Full rationale → qwen-autofix.md#af-136
MS_LAST="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg win "${WINDOW:-none}" --argjson start "${ROUND_START:-0}" '
[ .[] | select((.user.login // "") == $ab) | (.body // "")
| [ scan("<!-- autofix-milestone round=([0-9]+) win=([^ ]+) -->") ] | .[]

View file

@ -16,7 +16,44 @@ GITHUB_LIMIT_BYTES=512000
GATE_BYTES="${WORKFLOW_SIZE_GATE_BYTES:-470000}"
WARN_BYTES=$((GATE_BYTES - 25000))
# The gate above is the ceiling; the baseline below is the RATCHET. The gate
# alone only objects once a file is nearly at the wall, so growth accumulates
# invisibly until one unlucky PR has to pay for everyone: qwen-autofix.yml
# regained 78 KB when its prose moved out (#9517) and gave 25 KB of it back in
# a single feature commit two days later, unremarked. Each file's recorded size
# lives in .size-baseline; exceeding it by more than the allowance fails until
# the number is updated in the same PR, which turns the drift into one line a
# reviewer sees.
BASELINE_FILE='.github/workflows/.size-baseline'
GROWTH_ALLOWANCE="${WORKFLOW_SIZE_GROWTH_ALLOWANCE:-4096}"
# Loose enough that ordinary edits do not churn the manifest, tight enough that
# a file which shed real weight gets its baseline reclaimed rather than banking
# the slack for the next unreviewed 25 KB.
SLACK_BYTES=20000
status=0
declare -A baseline=()
if [[ -r "${BASELINE_FILE}" ]]; then
# The || clause keeps an unterminated final line, which read reports as a
# failure and the loop would otherwise silently drop.
while read -r recorded name extra || [[ -n "${recorded}" ]]; do
[[ -z "${recorded}" || "${recorded}" == \#* ]] && continue
# Fail closed on malformed lines: bash evaluates a leading-zero value as
# OCTAL at the arithmetic sites below, a non-numeric one errors both
# comparisons to false (the ratchet would fail OPEN), and extra fields
# key differently in the vitest mirror.
if [[ -z "${name}" || -n "${extra}" || ! "${recorded}" =~ ^(0|[1-9][0-9]*)$ ]]; then
echo "::error file=${BASELINE_FILE}::${BASELINE_FILE} entry '${recorded}${name:+ ${name}}${extra:+ ${extra}}' is malformed — expected exactly '<bytes> <file>' with a decimal byte count (no leading zeros)"
status=1
continue
fi
baseline["${name}"]="${recorded}"
done <"${BASELINE_FILE}"
else
echo "::error::${BASELINE_FILE} is missing or unreadable — the growth ratchet cannot run"
exit 1
fi
shopt -s nullglob
for file in .github/workflows/*.yml .github/workflows/*.yaml; do
if ! size="$(wc -c <"${file}")"; then
@ -24,6 +61,7 @@ for file in .github/workflows/*.yml .github/workflows/*.yaml; do
status=1
continue
fi
size="${size// /}"
pct=$((size * 100 / GITHUB_LIMIT_BYTES))
if ((size > GATE_BYTES)); then
echo "::error file=${file}::${file} is ${size} bytes — ${pct}% of GitHub's ${GITHUB_LIMIT_BYTES}-byte start-runs limit, past this repo's ${GATE_BYTES}-byte gate. Move prose into a sibling .md and long steps into .github/scripts/; do not raise the gate."
@ -31,9 +69,20 @@ for file in .github/workflows/*.yml .github/workflows/*.yaml; do
elif ((size > WARN_BYTES)); then
echo "::warning file=${file}::${file} is ${size} bytes (${pct}% of GitHub's limit) — approaching the ${GATE_BYTES}-byte gate."
fi
base="${baseline[${file##*/}]:-}"
if [[ -z "${base}" ]]; then
echo "::error file=${file}::${file} has no entry in ${BASELINE_FILE}. Add '${size} ${file##*/}' so its growth is tracked."
status=1
elif ((size > base + GROWTH_ALLOWANCE)); then
echo "::error file=${file}::${file} grew to ${size} bytes, $((size - base)) over its recorded ${base} (allowance ${GROWTH_ALLOWANCE}). Move prose into a sibling .md and long steps into .github/scripts/ — or, if the growth is real, update ${BASELINE_FILE} in this PR and say why."
status=1
elif ((size + SLACK_BYTES < base)); then
echo "::warning file=${file}::${file} is ${size} bytes, $((base - size)) under its recorded ${base} — lower the entry in ${BASELINE_FILE} so the slack is not banked."
fi
done
if ((status == 0)); then
echo "✅ every workflow file is under the ${GATE_BYTES}-byte gate"
echo "✅ every workflow file is under the ${GATE_BYTES}-byte gate and within ${GROWTH_ALLOWANCE} bytes of its recorded baseline"
fi
exit "${status}"

66
.github/workflows/.size-baseline vendored Normal file
View file

@ -0,0 +1,66 @@
# Recorded byte size of each workflow file, enforced by
# .github/scripts/check-workflow-size.sh.
#
# The absolute gate in that script is the ceiling; this file is the ratchet.
# A workflow may not grow past its recorded size by more than the allowance
# without updating the number here IN THE SAME PR — which is the point: it
# turns invisible drift into one reviewed line. qwen-autofix.yml regained
# 78 KB when its prose moved to qwen-autofix.md (#9517), then gave 25 KB of
# it back in a single feature commit two days later, with nothing objecting
# until the next PR ran out of ceiling.
#
# Growing a file is allowed — deliberately. Prefer moving prose into the
# sibling .md and long steps into .github/scripts/ first; if the growth is
# real, bump the number and say why in the PR.
2226 assign-issue-owner.yml
3480 audio-capture-prebuilds.yml
9023 auto-minimize-spam.yml
4638 build-and-publish-image.yml
29715 cd-cua-driver.yml
2076 cd-mobile-mcp.yml
69782 ci.yml
1482 codeql.yml
9389 comment-attachment-guard.yml
31677 desktop-release.yml
2038 docs-page-action.yml
10005 dsw-swe-verified-release.yml
12340 e2e.yml
11394 finalize-release.yml
15871 live-host-release.yml
6384 live-host.yml
7642 main-ci-failure-issue.yml
1686 npm-cache.yml
7299 pr-force-push-reminder.yml
6495 pr-self-report-label.yml
9646 qwen-autofix-fork-bridge.yml
5942 qwen-autofix-fork-signal.yml
392111 qwen-autofix.yml
7061 qwen-ci-flaky-rerun.yml
151937 qwen-code-pr-review.yml
79041 qwen-fleet-shepherd.yml
20525 qwen-issue-followup-bot.yml
5760 qwen-pr-safety-precheck.yml
27648 qwen-triage-finalize.yml
344548 qwen-triage.yml
9657 release-sdk-java.yml
22037 release-sdk-python.yml
19094 release-sdk.yml
14546 release-vscode-companion.yml
39291 release.yml
43717 repo-hygiene.yml
1079 scorecard-monthly.yml
10691 sdk-java.yml
3886 sdk-python.yml
3197 security-checks.yml
6777 serve-ab-publish.yml
17013 serve-ab.yml
2641 stale.yml
8653 sync-cua-driver-to-oss.yml
10920 sync-desktop-to-oss.yml
10018 sync-live-host-to-oss.yml
10138 sync-release-to-oss.yml
3303 update-ecs-runner-qwen.yml
2307 web-shell-visuals-cleanup.yml
15380 web-shell-visuals-publish.yml
16384 web-shell-visuals.yml
4712 windows-runner-smoke.yml

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,17 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
import {
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join, win32 } from 'node:path';
import { describe, expect, it } from 'vitest';
// GitHub does not start runs for a workflow file over 500 KB (512,000 bytes)
@ -23,9 +32,10 @@ const gateBytes = Number(
gateScript.match(/GATE_BYTES="\$\{WORKFLOW_SIZE_GATE_BYTES:-(\d+)\}"/)?.[1],
);
const workflowFiles = readdirSync(WORKFLOW_DIR)
.filter((name) => name.endsWith('.yml') || name.endsWith('.yaml'))
.map((name) => join(WORKFLOW_DIR, name));
const workflowNames = readdirSync(WORKFLOW_DIR).filter(
(name) => name.endsWith('.yml') || name.endsWith('.yaml'),
);
const workflowFiles = workflowNames.map((name) => join(WORKFLOW_DIR, name));
describe('workflow file size', () => {
it('keeps the gate below GitHub 500 KB start-runs limit', () => {
@ -52,6 +62,245 @@ describe('workflow file size', () => {
});
});
describe('workflow size growth ratchet', () => {
// The absolute gate is a ceiling: it only objects once a file is nearly at
// the wall, so growth accrues unremarked until one PR has to pay for
// everyone. qwen-autofix.yml regained 78 KB when its prose moved out and
// gave 25 KB back in one feature commit two days later. The ratchet turns
// that drift into a reviewed line.
const baselinePath = join(WORKFLOW_DIR, '.size-baseline');
const baselineLines = readFileSync(baselinePath, 'utf8')
.split('\n')
.filter((l) => l.trim() && !l.trimStart().startsWith('#'));
const baseline = new Map(
baselineLines
.map((l) => l.trim().split(/\s+/))
.map(([bytes, name]) => [name, Number(bytes)]),
);
// node:path join emits backslashes on the merge-queue Windows lane, where
// splitting on '/' alone finds no separator and hands back the whole path
// as the key — every baseline lookup must accept both separators.
const workflowName = (file) => file.split(/[\\/]/).pop();
const allowance = Number(
gateScript.match(
/GROWTH_ALLOWANCE="\$\{WORKFLOW_SIZE_GROWTH_ALLOWANCE:-(\d+)\}"/,
)?.[1],
);
it('reads a positive allowance from the gate script', () => {
expect(allowance).toBeGreaterThan(0);
});
it('keys win32-style paths by the file name too (merge-queue Windows lane)', () => {
for (const name of workflowNames) {
expect(workflowName(win32.join(WORKFLOW_DIR, name))).toBe(name);
}
});
it.each(workflowFiles)('%s has a baseline entry', (file) => {
expect(baseline.has(workflowName(file))).toBe(true);
});
it.each(workflowFiles)('%s is within its baseline allowance', (file) => {
const bytes = Buffer.byteLength(readFileSync(file));
const recorded = baseline.get(workflowName(file));
expect(bytes).toBeLessThanOrEqual(recorded + allowance);
});
it('records no file that no longer exists', () => {
const present = new Set(workflowFiles.map((f) => workflowName(f)));
expect([...baseline.keys()].filter((n) => !present.has(n))).toEqual([]);
});
it('keeps every baseline at or under the gate', () => {
// A baseline above the gate would let the ratchet pass a file the ceiling
// rejects, so the two gates can never disagree about what is allowed.
expect([...baseline].filter(([, b]) => b > gateBytes)).toEqual([]);
});
it('keeps every baseline entry in the format the gate parses', () => {
// The gate fails closed on lines that are not exactly '<bytes> <file>'
// with a decimal byte count; this mirror must red on the same lines here
// instead of keying on field 2 while CI keys on the rest of the line.
for (const line of baselineLines) {
const fields = line.trim().split(/\s+/);
expect(fields, line).toHaveLength(2);
expect(fields[0], line).toMatch(/^(0|[1-9][0-9]*)$/);
}
});
});
// The gate script's `declare -A baseline=()` needs bash 4+. The merge-queue
// macOS lane ships bash 3.2, where the assoc-array errors leave the ratchet
// failing open, so probe the capability rather than the platform: that lane
// must skip instead of reporting red on a script it cannot execute.
const bashSupportsAssocArrays =
spawnSync('bash', ['-c', 'declare -A t=()'], { stdio: 'ignore' }).status ===
0;
describe.skipIf(process.platform === 'win32' || !bashSupportsAssocArrays)(
'check-workflow-size.sh execution',
() => {
// The block above re-implements the gate's arithmetic in JS; only running
// the real script pins its decision branches (growth, missing entry,
// missing baseline, slack warning, malformed line).
const gatePath = join(
process.cwd(),
'.github',
'scripts',
'check-workflow-size.sh',
);
const runGate = ({ files, baseline }) => {
const dir = mkdtempSync(join(tmpdir(), 'workflow-size-gate-'));
try {
const fixtureDir = join(dir, WORKFLOW_DIR);
mkdirSync(fixtureDir, { recursive: true });
for (const [name, bytes] of Object.entries(files)) {
writeFileSync(join(fixtureDir, name), 'a'.repeat(bytes));
}
if (baseline !== undefined) {
writeFileSync(join(fixtureDir, '.size-baseline'), baseline);
}
return spawnSync('bash', [gatePath], { cwd: dir, encoding: 'utf8' });
} finally {
rmSync(dir, { recursive: true, force: true });
}
};
it('passes a workflow at its recorded size', () => {
const result = runGate({
files: { 'small.yml': 100 },
baseline: '100 small.yml\n',
});
expect(result.status).toBe(0);
expect(result.stdout).toContain('✅');
});
it('passes a workflow grown within its allowance', () => {
const result = runGate({
files: { 'small.yml': 4000 },
baseline: '100 small.yml\n',
});
expect(result.status).toBe(0);
expect(result.stdout).toContain('✅');
});
it('passes a workflow at exactly baseline plus allowance', () => {
const result = runGate({
files: { 'small.yml': 4196 },
baseline: '100 small.yml\n',
});
expect(result.status).toBe(0);
expect(result.stdout).toContain('✅');
});
it('fails a workflow one byte past baseline plus allowance', () => {
const result = runGate({
files: { 'small.yml': 4197 },
baseline: '100 small.yml\n',
});
expect(result.status).toBe(1);
expect(result.stdout).toContain('grew to 4197 bytes');
});
it('fails a workflow grown past its baseline plus allowance', () => {
const result = runGate({
files: { 'small.yml': 5000 },
baseline: '100 small.yml\n',
});
expect(result.status).toBe(1);
expect(result.stdout).toContain('grew to 5000 bytes');
});
it('fails a workflow with no baseline entry', () => {
const result = runGate({
files: { 'small.yml': 100 },
baseline: '# header only\n',
});
expect(result.status).toBe(1);
expect(result.stdout).toContain('has no entry');
expect(result.stdout).toContain("Add '100 small.yml'");
});
it('fails closed when the baseline file is missing', () => {
const result = runGate({ files: { 'small.yml': 100 } });
expect(result.status).toBe(1);
expect(result.stdout).toContain('missing or unreadable');
});
it('fails closed on a value that is not a decimal byte count', () => {
// Bash evaluates leading zeros as octal and errors on non-numeric
// values at the arithmetic sites; either failure mode used to leave
// the ratchet green.
for (const bad of ['4l9995', '1e3', '09023', '0070142']) {
const result = runGate({
files: { 'small.yml': 100 },
baseline: `${bad} small.yml\n`,
});
expect(result.status, bad).toBe(1);
expect(result.stdout, bad).toContain('is malformed');
}
});
it('fails closed on a line with extra fields', () => {
const result = runGate({
files: { 'small.yml': 100 },
baseline: '70142 small.yml # bumped for the build-cache job\n',
});
expect(result.status).toBe(1);
expect(result.stdout).toContain('is malformed');
});
it('keeps an unterminated final baseline line', () => {
const result = runGate({
files: { 'small.yml': 100 },
baseline: '100 small.yml',
});
expect(result.status).toBe(0);
});
it('warns when a file shrinks far below its baseline', () => {
const result = runGate({
files: { 'small.yml': 100 },
baseline: '30000 small.yml\n',
});
expect(result.status).toBe(0);
expect(result.stdout).toContain('::warning');
expect(result.stdout).toContain('under its recorded 30000');
});
// SLACK_BYTES is 20000 in the gate script; these two fixtures pin the
// boundary itself, not just the warning branch.
it('warns when a file sits more than the slack under its baseline', () => {
const result = runGate({
files: { 'small.yml': 100 },
baseline: '20101 small.yml\n',
});
expect(result.status).toBe(0);
expect(result.stdout).toContain('::warning');
expect(result.stdout).toContain('under its recorded 20101');
});
it('does not warn at exactly the slack under its baseline', () => {
const result = runGate({
files: { 'small.yml': 100 },
baseline: '20100 small.yml\n',
});
expect(result.status).toBe(0);
expect(result.stdout).not.toContain('::warning');
});
it('fails a file past the absolute gate', () => {
const result = runGate({
files: { 'big.yml': 470_001 },
baseline: '470001 big.yml\n',
});
expect(result.status).toBe(1);
expect(result.stdout).toContain("past this repo's");
});
},
);
describe('qwen-autofix.yml design-record pointers', () => {
const workflow = readFileSync(join(WORKFLOW_DIR, 'qwen-autofix.yml'), 'utf8');
const doc = readFileSync(join(WORKFLOW_DIR, 'qwen-autofix.md'), 'utf8');
@ -80,6 +329,16 @@ describe('qwen-autofix.yml design-record pointers', () => {
expect(anchors.filter((id) => !pointers.includes(id))).toEqual([]);
});
it('allocates each section id exactly once', () => {
// A double allocation (two blocks minted with the same id, e.g. a branch
// that numbered a new block before a same-numbered block landed on main)
// passes every other check here: pointers resolve, anchors stay pointed
// at, and the contents table mirrors the duplication. Browsers resolve
// the anchor to the FIRST occurrence, so one feature's rationale pointer
// silently shows the other's block.
expect(new Set(anchors).size).toBe(anchors.length);
});
it('lists every section in the contents table', () => {
const listed = [...doc.matchAll(/^- \[\d+\..*?\]\(#(af-\d+)\)$/gm)].map(
(m) => m[1],