Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2867,24 +2867,28 @@ jobs:
# line, driven against REAL captured vitest logs of the incident's own
# test file — one timeout, one assertion, byte-identical FAIL lines and
# opposite diagnoses, which is the confusion that produced a confidently
# wrong card (#10112) and cost a whole dispatch; and (2) the cross-PR
# wrong card (#10112) and cost a whole dispatch; (2) the cross-PR
# aggregation files exactly ONE anchor issue at >= 2 DISTINCT PRs,
# REFRESHES it on the next ejection instead of filing a second, and files
# none for two different keys or for one PR ejecting twice. The refresh
# leg is driven as a PAIR — run 2's world is built from run 1's output —
# because idempotency is a property of the pair and a hand-written marker
# would prove each half while leaving the two runs free to disagree.
# would prove each half while leaving the two runs free to disagree; and
# (3) the operative guidance prose (the timeout-vs-assertion rule, its
# named exception, and the boundary sentence that the re-queue allowance
# is governed elsewhere) is present in the posted body, one assertion per
# claim over the WHOLE body, not the excerpt.
#
# Its --self-test runs first and is the half that stops the battery
# rotting into decoration: it mutates the shipped script fourteen ways —
# rotting into decoration: it mutates the shipped script twenty ways —
# drop the reason lookahead, test the FAIL pattern before the reason
# pattern (which silently loses every assertion), count runs instead of
# distinct PRs, always create instead of refreshing, drop the sighting
# markers, downgrade each anti-no-op announcement to an info line, and
# more — and requires the battery to go RED for each, naming the scenario
# that catches it. A mutation whose anchor no longer exists is a failure
# too, so a rewrite of the workflow cannot leave the mutations silently
# matching nothing.
# markers, downgrade each anti-no-op announcement to an info line, drop
# each half of the guidance prose, and more — and requires the battery to
# go RED for each, naming the scenario that catches it. A mutation whose
# anchor no longer exists is a failure too, so a rewrite of the workflow
# cannot leave the mutations silently matching nothing.
#
# Invoked as `node` rather than through a `pnpm check:*` alias, same
# reason as the step above: see the GATE INVOCATION IDIOM note at the top
Expand Down
53 changes: 53 additions & 0 deletions scripts/check-merge-queue-triage-outcome.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,24 @@ const excerptLines = (r) => {
return out;
};

/**
* The whole posted body, for assertions about the operative GUIDANCE PROSE
* (the timeout-vs-assertion rule, its named exception, and the boundary
* sentence) rather than the extracted excerpt.
*
* This deliberately does NOT go through `excerptLines`: that helper is
* scoped to code-fence content precisely so the comment's own explanatory
* prose -- which names both `FAIL` and `AssertionError` -- cannot satisfy an
* assertion about the EXTRACTION (see its docblock). The guidance prose
* lives outside every fence, so `excerptLines` cannot see it at all; going
* through it here would silently pass on a body that shipped `excerptLines`
* still finds the FAIL/reason pair, but with the guidance paragraph deleted.
* The question this accessor answers is different and does not have that
* failure mode: is the guidance text the workflow composes into every
* comment actually present in what it posted?
*/
const guidanceProse = (r) => postedBody(r);

// ── Scenarios ───────────────────────────────────────────────────────────────

const REFUSED = httpError(503, 'No server is currently available to service your request.');
Expand Down Expand Up @@ -547,11 +565,19 @@ function scenarios(root) {
world: () => base(),
check(r, t) {
const lines = excerptLines(r);
const prose = guidanceProse(r);
return [
t(lines.some((l) => l.includes(`FAIL ${KEY_A}`)), 'the FAIL line is excerpted'),
t(lines.some((l) => l.startsWith('↳ 失败原因:') && l.includes('Error: Test timed out in 1ms.')),
`the reason line is excerpted and labelled, got: ${JSON.stringify(lines)}`),
t(!lines.some((l) => l.includes('AssertionError')), 'no assertion is reported for a timeout log'),
// ── operative guidance prose, read over the WHOLE posted body ──
t(prose.includes('负载/时序') && prose.includes('真实的行为改变'),
'the timeout-vs-assertion rule is present in the posted body: timeout -> load/timing, assertion -> real behaviour change'),
t(prose.includes('实验自身的有效性前提') && prose.includes('时长、一个时间戳、一个耗时计数'),
'the named exception is present in the posted body: an assertion about the experiment\'s own validity premise (a duration, a timestamp, an elapsed count) reads like a timeout'),
t(prose.includes('后者由别处管'),
'the boundary sentence is present in the posted body: this only changes how a red is read, not which tests may be re-queued'),
];
},
},
Expand Down Expand Up @@ -1213,6 +1239,33 @@ const MUTATIONS = [
to: " core.info('triage comment for this run already exists \u2014 skipping.');",
expect: ['A5'],
},
{
id: 'M17',
what: 'the timeout-vs-assertion rule silently loses its "load/timing" half',
from: '\u8d1f\u8f7d/\u65f6\u5e8f',
to: '',
expect: ['E1'],
// A1 files an anchor from the same triage comment but never reads the
// guidance prose -- the control that proves this mutation is caught by
// the new prose assertion, not by every scenario reacting to any change.
keepGreen: ['A1'],
},
{
id: 'M18',
what: 'the named exception silently loses the phrase identifying its own object -- the experiment\'s own validity premise',
from: '\u5b9e\u9a8c\u81ea\u8eab\u7684\u6709\u6548\u6027\u524d\u63d0',
to: '',
expect: ['E1'],
keepGreen: ['A1'],
},
{
id: 'M19',
what: 'the boundary sentence -- this only changes how a red is read, not which tests may be re-queued -- silently drops',
from: '\u540e\u8005\u7531\u522b\u5904\u7ba1',
to: '',
expect: ['E1'],
keepGreen: ['A1'],
},
];

// Returned by `selfTest()` only after its verdict is printed. The dispatch
Expand Down
Loading