mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-09-11 19:46:21 +00:00
fix(autofix): close the escape hatches the review found in the weakening gate
- count-test-surface: see through type-only wrappers (`as`, `<T>x`, `satisfies`, `!`) in constant folding, options objects and call chains; fold comparison/equality/logical operators of two constants; treat template-literal returns as non-thenable - count-test-surface: an `if (true)` wrapper or a catch whose try holds an assertion no longer shelters a body skip; a registration callback handed by name resolves to its module-scope binding; a collector factory binding (it.skipIf / it.each / test.extend) registers nothing - af-155: charge a regression only when the run's window IS the live re-arm key, and never stamp pre=green when the push carries a merge past the head prepare classified - tests: exercise all five own-lane names in the charge classifier, and gate the verbatim staging witness on the host having sha256sum Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
d089bd867b
commit
a211bc0851
5 changed files with 597 additions and 154 deletions
5
.github/scripts/autofix-push-and-report.sh
vendored
5
.github/scripts/autofix-push-and-report.sh
vendored
|
|
@ -619,6 +619,11 @@ if [[ "${OUTCOME}" == "fixed" ]]; then
|
|||
PUSH_PRE="${CHECK_STATE:-none}"
|
||||
[[ "${PUSH_RACE_MERGED}" == 'true' ]] && PUSH_PRE='none'
|
||||
[[ "${CONFLICT:-false}" == 'true' ]] && PUSH_PRE='none'
|
||||
# A merge commit between the head prepare classified and the head this
|
||||
# round pushes -- a clean in-round merge of main trips neither arm above
|
||||
# -- means the pushed head carries content prepare never classified: the
|
||||
# premise is unknown, and an unknown premise is never green.
|
||||
[[ -n "$(git rev-list --merges "${REPORT_HEAD}..${PUSHED_HEAD}" 2>/dev/null)" ]] && PUSH_PRE='none'
|
||||
{
|
||||
echo "🤖 Addressed the latest review feedback (round ${NEXT_ROUND}/${MAX_ROUNDS}). What changed, and what I pushed back on: · 已处理最新评审反馈(第 ${NEXT_ROUND}/${MAX_ROUNDS} 轮)。改动内容与我反驳保留之处如下:"
|
||||
echo
|
||||
|
|
|
|||
281
.github/scripts/count-test-surface.mjs
vendored
281
.github/scripts/count-test-surface.mjs
vendored
|
|
@ -47,23 +47,33 @@
|
|||
// `fails` is a truthy constant (vitest truthy-checks them, so a reason
|
||||
// string disables), a body-level `skip()`/`ctx.skip()` whose first
|
||||
// argument is absent or any constant other than `false` (the runner's
|
||||
// own rule) and that is not itself under a condition, and every
|
||||
// registration nested inside a disabled `describe`. A body skip at file
|
||||
// own rule) and that is not itself under a REAL condition — an
|
||||
// `if (true)` wrapper never withholds its branch, and a `catch`
|
||||
// whose `try` holds an assertion fires exactly when that assertion
|
||||
// fails, so neither shelters a skip — and every registration
|
||||
// nested inside a disabled `describe`. A body skip at file
|
||||
// scope — a statement of the module, or inside a `beforeEach`/
|
||||
// `beforeAll`/`afterEach`/`afterAll` callback the file registers —
|
||||
// disables the whole file, which is what the runner does with it. A
|
||||
// constant is a literal of any kind (object, array, regex and bigint
|
||||
// included), `undefined`/`void 0`/`NaN`/`Infinity`, a unary
|
||||
// `!`/`-`/`+`/`~` of a constant, or `+` of two constants.
|
||||
// registration callback handed by NAME resolves to the single
|
||||
// module-scope function declaration or function-valued variable
|
||||
// initializer of that name — the runner receives that very
|
||||
// function — while an absent, redeclared or nested binding stays
|
||||
// opaque. A constant is a literal of any kind (object, array,
|
||||
// regex and bigint included), `undefined`/`void 0`/`NaN`/`Infinity`,
|
||||
// a unary `!`/`-`/`+`/`~` of a constant, `+` of two string/number
|
||||
// constants, a comparison or equality of two primitive constants,
|
||||
// a logical `&&`/`||`/`??` of two constants, and any of those
|
||||
// behind a type-only wrapper (`as`, `<T>x`, `satisfies`, `!`).
|
||||
// Deliberately NOT measured, because they are runtime facts the runner is the
|
||||
// authority for, not declarations: whether an assertion is REACHABLE (dead
|
||||
// code, a condition that is false in CI, a helper never called), a
|
||||
// condition-valued guard (`it.skipIf(process.platform === 'win32')`,
|
||||
// `skip(cond, reason)`, `if (cond) ctx.skip()`, a skip in a `catch` —
|
||||
// this repository's environment-guard idiom; the assertions an honest
|
||||
// guard shelters are measured, its condition is not), and options or
|
||||
// collector names carried by a binding (`test('x', opts, fn)`,
|
||||
// `it[S]('x')`).
|
||||
// `skip(cond, reason)`, `if (cond) ctx.skip()`, a skip in a `catch`
|
||||
// whose `try` asserts nothing — this repository's environment-guard
|
||||
// idiom; the assertions an honest guard shelters are measured, its
|
||||
// condition is not), and options or collector names carried by a
|
||||
// binding (`test('x', opts, fn)`, `it[S]('x')`).
|
||||
//
|
||||
// `measure` takes {"path", "tip", "pre", "events":
|
||||
// [{"before", "after", "landed", "mainHolds"}]} — blob files (null =
|
||||
|
|
@ -145,15 +155,36 @@ function isStringLike(n) {
|
|||
return ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n);
|
||||
}
|
||||
|
||||
// Type-only wrappers carry no value of their own — `as T`, `<T>x`,
|
||||
// `x satisfies T`, `x!` and parentheses all evaluate to their operand —
|
||||
// so every fold in constant() and the options-object test see through
|
||||
// them. `await` and `void` are NOT transparent (one unwraps a thenable,
|
||||
// the other discards the value) and keep their own handling instead.
|
||||
function unwrap(node) {
|
||||
let n = node;
|
||||
while (
|
||||
ts.isParenthesizedExpression(n) ||
|
||||
ts.isNonNullExpression(n) ||
|
||||
ts.isAsExpression(n) ||
|
||||
ts.isTypeAssertionExpression(n) ||
|
||||
(ts.isSatisfiesExpression && ts.isSatisfiesExpression(n))
|
||||
) {
|
||||
n = n.expression;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// The constant value of an expression the parser can decide without a
|
||||
// binding: literals of every kind (including object, array, regex and
|
||||
// bigint), `undefined`/`void 0`/`NaN`/`Infinity`, a unary `!`/`-`/`+`/`~`
|
||||
// of a constant, `+` of two constants, parentheses. `{ known: false }`
|
||||
// for anything else — so one operator away from a shape this folds is
|
||||
// never one operator away from escaping a signal.
|
||||
// of a constant, `+` of two string/number constants, a comparison or
|
||||
// equality of two primitive constants, a logical `&&`/`||`/`??` of two
|
||||
// constants, and any of those behind a type-only wrapper. `{ known:
|
||||
// false }` for anything else — so one operator away from a shape this
|
||||
// folds is never one operator away from escaping a signal.
|
||||
function constant(node) {
|
||||
if (!node) return { known: false };
|
||||
if (ts.isParenthesizedExpression(node)) return constant(node.expression);
|
||||
node = unwrap(node);
|
||||
if (node.kind === ts.SyntaxKind.TrueKeyword)
|
||||
return { known: true, value: true };
|
||||
if (node.kind === ts.SyntaxKind.FalseKeyword)
|
||||
|
|
@ -198,20 +229,76 @@ function constant(node) {
|
|||
return { known: false };
|
||||
}
|
||||
}
|
||||
if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind === ts.SyntaxKind.PlusToken
|
||||
) {
|
||||
if (ts.isBinaryExpression(node)) {
|
||||
const l = constant(node.left);
|
||||
const r = constant(node.right);
|
||||
if (
|
||||
l.known &&
|
||||
r.known &&
|
||||
(typeof l.value === 'string' || typeof l.value === 'number') &&
|
||||
(typeof r.value === 'string' || typeof r.value === 'number')
|
||||
) {
|
||||
return { known: true, value: l.value + r.value };
|
||||
if (!l.known || !r.known) return { known: false };
|
||||
const op = node.operatorToken.kind;
|
||||
// The logical operators select an operand by truthiness alone, which
|
||||
// the object/array/regex placeholder (`true`) answers faithfully.
|
||||
if (op === ts.SyntaxKind.AmpersandAmpersandToken) {
|
||||
return { known: true, value: l.value ? r.value : l.value };
|
||||
}
|
||||
if (op === ts.SyntaxKind.BarBarToken) {
|
||||
return { known: true, value: l.value ? l.value : r.value };
|
||||
}
|
||||
if (op === ts.SyntaxKind.QuestionQuestionToken) {
|
||||
return {
|
||||
known: true,
|
||||
value: l.value === null || l.value === undefined ? r.value : l.value,
|
||||
};
|
||||
}
|
||||
if (op === ts.SyntaxKind.PlusToken) {
|
||||
if (
|
||||
(typeof l.value === 'string' || typeof l.value === 'number') &&
|
||||
(typeof r.value === 'string' || typeof r.value === 'number')
|
||||
) {
|
||||
return { known: true, value: l.value + r.value };
|
||||
}
|
||||
return { known: false };
|
||||
}
|
||||
// Comparison and equality evaluate natively — JS semantics ARE the
|
||||
// runner's — but only on primitives: the object/array/regex fold is a
|
||||
// truthiness placeholder, not a value (`{} === {}` must never fold
|
||||
// true), and a bigint literal folds to a Number for arithmetic, which
|
||||
// `1n === 1` would mis-fold.
|
||||
const opaque = (n) => {
|
||||
const u = unwrap(n);
|
||||
return (
|
||||
ts.isObjectLiteralExpression(u) ||
|
||||
ts.isArrayLiteralExpression(u) ||
|
||||
ts.isRegularExpressionLiteral(u) ||
|
||||
ts.isBigIntLiteral(u)
|
||||
);
|
||||
};
|
||||
if (opaque(node.left) || opaque(node.right)) return { known: false };
|
||||
const a = l.value;
|
||||
const b = r.value;
|
||||
if (op === ts.SyntaxKind.EqualsEqualsEqualsToken) {
|
||||
return { known: true, value: a === b };
|
||||
}
|
||||
if (op === ts.SyntaxKind.ExclamationEqualsEqualsToken) {
|
||||
return { known: true, value: a !== b };
|
||||
}
|
||||
if (op === ts.SyntaxKind.EqualsEqualsToken) {
|
||||
return { known: true, value: a == b };
|
||||
}
|
||||
if (op === ts.SyntaxKind.ExclamationEqualsToken) {
|
||||
return { known: true, value: a != b };
|
||||
}
|
||||
if (op === ts.SyntaxKind.LessThanToken) {
|
||||
return { known: true, value: a < b };
|
||||
}
|
||||
if (op === ts.SyntaxKind.LessThanEqualsToken) {
|
||||
return { known: true, value: a <= b };
|
||||
}
|
||||
if (op === ts.SyntaxKind.GreaterThanToken) {
|
||||
return { known: true, value: a > b };
|
||||
}
|
||||
if (op === ts.SyntaxKind.GreaterThanEqualsToken) {
|
||||
return { known: true, value: a >= b };
|
||||
}
|
||||
return { known: false };
|
||||
}
|
||||
return { known: false };
|
||||
}
|
||||
|
|
@ -255,7 +342,15 @@ function chainOf(call) {
|
|||
) {
|
||||
segments.push({ member: memberName(n) });
|
||||
n = n.expression;
|
||||
} else if (ts.isNonNullExpression(n) || ts.isParenthesizedExpression(n)) {
|
||||
} else if (
|
||||
ts.isNonNullExpression(n) ||
|
||||
ts.isParenthesizedExpression(n) ||
|
||||
ts.isAsExpression(n) ||
|
||||
ts.isTypeAssertionExpression(n) ||
|
||||
(ts.isSatisfiesExpression && ts.isSatisfiesExpression(n))
|
||||
) {
|
||||
// Type-only wrappers are chain-transparent: `(it as any).skip(...)`
|
||||
// IS a skip of `it`.
|
||||
n = n.expression;
|
||||
} else {
|
||||
break;
|
||||
|
|
@ -340,16 +435,18 @@ function propertyName(name) {
|
|||
}
|
||||
|
||||
function optionsDisable(call) {
|
||||
return (call.arguments ?? []).some(
|
||||
(o) =>
|
||||
return (call.arguments ?? []).some((arg) => {
|
||||
const o = unwrap(arg);
|
||||
return (
|
||||
ts.isObjectLiteralExpression(o) &&
|
||||
o.properties.some(
|
||||
(p) =>
|
||||
ts.isPropertyAssignment(p) &&
|
||||
DISABLING_OPTIONS.has(propertyName(p.name)) &&
|
||||
truthyConstant(p.initializer),
|
||||
),
|
||||
);
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function titleOf(call, sf) {
|
||||
|
|
@ -387,15 +484,34 @@ function isBodySkip({ root, members, calls }) {
|
|||
return c.known && c.value !== false;
|
||||
}
|
||||
|
||||
// True when `node` sits under a condition inside the nearest enclosing
|
||||
// function: an if/switch/loop, a ternary, or a short-circuit operand.
|
||||
function underCondition(node) {
|
||||
// True when `node` sits under a REAL condition inside the nearest
|
||||
// enclosing function: an if/switch/loop, a ternary, or a short-circuit
|
||||
// operand. Two wrappers only look conditional and are walked through: an
|
||||
// `if` whose test constant-folds TRUE never withholds its branch, and a
|
||||
// `catch` whose try block holds a counted assertion fires exactly when
|
||||
// that assertion fails — a skip in either is the runner's unconditional
|
||||
// outcome, not an environment guard. `assertionPositions` must be complete
|
||||
// when this runs: callers collect their skips during the visit and resolve
|
||||
// them after it.
|
||||
function underCondition(node, assertionPositions) {
|
||||
for (let p = node.parent; p && !ts.isFunctionLike(p); p = p.parent) {
|
||||
if (ts.isIfStatement(p)) {
|
||||
if (truthyConstant(p.expression)) continue;
|
||||
return true;
|
||||
}
|
||||
if (ts.isCatchClause(p)) {
|
||||
const tryBlock = ts.isTryStatement(p.parent) ? p.parent.tryBlock : null;
|
||||
if (
|
||||
tryBlock &&
|
||||
assertionPositions.some((a) => a > tryBlock.pos && a < tryBlock.end)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
ts.isIfStatement(p) ||
|
||||
ts.isConditionalExpression(p) ||
|
||||
ts.isSwitchStatement(p) ||
|
||||
ts.isCatchClause(p) ||
|
||||
ts.isIterationStatement(p, false) ||
|
||||
(ts.isBinaryExpression(p) &&
|
||||
(p.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
||||
|
|
@ -416,6 +532,10 @@ function underCondition(node) {
|
|||
// ordinary control flow the runner awaits.
|
||||
function returnsNothing(ret) {
|
||||
if (!ret.expression) return true;
|
||||
// A template's value is a string however its substitutions evaluate —
|
||||
// never a thenable — so the runner ignores it exactly like the literal
|
||||
// spellings constant() folds.
|
||||
if (ts.isTemplateExpression(unwrap(ret.expression))) return true;
|
||||
return constant(ret.expression).known;
|
||||
}
|
||||
|
||||
|
|
@ -474,6 +594,37 @@ export function count(text, path) {
|
|||
const registrations = [];
|
||||
const bodySkips = [];
|
||||
const hookBodies = [];
|
||||
// A callback handed by NAME resolves to the single module-scope function
|
||||
// declaration or function-valued variable initializer of that name — the
|
||||
// runner receives that very function, so the registration measures
|
||||
// through its body. An absent, redeclared or nested binding stays
|
||||
// opaque.
|
||||
const topFns = new Map();
|
||||
const ambiguousFns = new Set();
|
||||
const bindTopFn = (name, fnNode) => {
|
||||
if (ambiguousFns.has(name) || topFns.has(name)) {
|
||||
ambiguousFns.add(name);
|
||||
topFns.delete(name);
|
||||
} else {
|
||||
topFns.set(name, fnNode);
|
||||
}
|
||||
};
|
||||
for (const st of sf.statements) {
|
||||
if (ts.isFunctionDeclaration(st) && st.name && st.body) {
|
||||
bindTopFn(st.name.text, st);
|
||||
} else if (ts.isVariableStatement(st)) {
|
||||
for (const d of st.declarationList.declarations) {
|
||||
if (
|
||||
ts.isIdentifier(d.name) &&
|
||||
d.initializer &&
|
||||
(ts.isArrowFunction(d.initializer) ||
|
||||
ts.isFunctionExpression(d.initializer))
|
||||
) {
|
||||
bindTopFn(d.name.text, d.initializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const visit = (node) => {
|
||||
if (ts.isCallExpression(node) && !extendsChain(node)) {
|
||||
const chain = chainOf(node);
|
||||
|
|
@ -483,16 +634,41 @@ export function count(text, path) {
|
|||
chain.calls.length > 0
|
||||
) {
|
||||
const last = chain.calls[chain.calls.length - 1].call;
|
||||
const fns = (last.arguments ?? []).filter(
|
||||
(a) => ts.isArrowFunction(a) || ts.isFunctionExpression(a),
|
||||
);
|
||||
registrations.push({
|
||||
kind: ROOTS.get(chain.root) ?? XROOTS.get(chain.root),
|
||||
title: titleOf(last, sf),
|
||||
disabled: registrationDisabled(chain),
|
||||
pos: node.getStart(sf),
|
||||
fn: fns.length ? fns[fns.length - 1] : null,
|
||||
});
|
||||
const lastArgs = last.arguments ?? [];
|
||||
// A chain that never reaches a registration call registers
|
||||
// nothing: `it.skipIf(cond)`, `it.each(cases)` and
|
||||
// `test.extend({})` are collector FACTORIES, and binding one to a
|
||||
// variable is not a test. The terminal call carries the title or
|
||||
// the callback.
|
||||
const terminates =
|
||||
lastArgs.length > 0 &&
|
||||
(isStringLike(lastArgs[0]) ||
|
||||
ts.isTemplateExpression(lastArgs[0]) ||
|
||||
lastArgs.some(
|
||||
(a) => ts.isArrowFunction(a) || ts.isFunctionExpression(a),
|
||||
) ||
|
||||
(lastArgs.length > 1 &&
|
||||
ts.isIdentifier(lastArgs[lastArgs.length - 1]) &&
|
||||
topFns.has(lastArgs[lastArgs.length - 1].text)));
|
||||
if (terminates) {
|
||||
const fns = lastArgs.filter(
|
||||
(a) => ts.isArrowFunction(a) || ts.isFunctionExpression(a),
|
||||
);
|
||||
let fn = fns.length ? fns[fns.length - 1] : null;
|
||||
if (!fn) {
|
||||
const lastArg = lastArgs[lastArgs.length - 1];
|
||||
if (ts.isIdentifier(lastArg)) {
|
||||
fn = topFns.get(lastArg.text) ?? null;
|
||||
}
|
||||
}
|
||||
registrations.push({
|
||||
kind: ROOTS.get(chain.root) ?? XROOTS.get(chain.root),
|
||||
title: titleOf(last, sf),
|
||||
disabled: registrationDisabled(chain),
|
||||
pos: node.getStart(sf),
|
||||
fn,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
chain.root !== null &&
|
||||
HOOKS.has(chain.root) &&
|
||||
|
|
@ -504,7 +680,10 @@ export function count(text, path) {
|
|||
if (fns.length) hookBodies.push(fns[fns.length - 1]);
|
||||
} else if (isStatementLevel(node) && isAssertion(chain)) {
|
||||
assertionPositions.push(node.getStart(sf));
|
||||
} else if (isBodySkip(chain) && !underCondition(node)) {
|
||||
} else if (isBodySkip(chain)) {
|
||||
// Conditional-or-not is decided after the visit: a catch clause is
|
||||
// a condition only when its try holds no assertion, which needs
|
||||
// the complete assertionPositions.
|
||||
bodySkips.push(node);
|
||||
}
|
||||
}
|
||||
|
|
@ -513,6 +692,7 @@ export function count(text, path) {
|
|||
visit(sf);
|
||||
let fileDisabled = false;
|
||||
for (const skip of bodySkips) {
|
||||
if (underCondition(skip, assertionPositions)) continue;
|
||||
const target = skipTarget(skip, registrations);
|
||||
if (!target.applies) continue;
|
||||
if (target.scope) target.scope.disabled = true;
|
||||
|
|
@ -575,7 +755,14 @@ export function count(text, path) {
|
|||
r.disabled = true;
|
||||
}
|
||||
}
|
||||
const silenced = registrations.filter((r) => r.disabled && r.fn);
|
||||
// A body shared between a disabled and an enabled registration stays
|
||||
// live: the enabled one executes it, so its assertions are surface.
|
||||
const liveFns = new Set(
|
||||
registrations.filter((r) => !r.disabled && r.fn).map((r) => r.fn),
|
||||
);
|
||||
const silenced = registrations.filter(
|
||||
(r) => r.disabled && r.fn && !liveFns.has(r.fn),
|
||||
);
|
||||
const executes = (p) => !silenced.some((r) => p > r.fn.pos && p < r.fn.end);
|
||||
const key = (r) => `${r.kind}:${r.title}`;
|
||||
const declaredAssertions = fileDisabled
|
||||
|
|
|
|||
7
.github/workflows/qwen-autofix.md
vendored
7
.github/workflows/qwen-autofix.md
vendored
|
|
@ -4270,7 +4270,12 @@ the marker's head to the checked-out head, and the check
|
|||
rollup to the commit it describes (`headRefOid` is read in
|
||||
the same call as the rollup; a rollup for any other commit
|
||||
classifies `none`, unknown, never chargeable). A re-arm
|
||||
changes the window key and drops the whole set with it. A
|
||||
changes the window key and drops the whole set with it —
|
||||
and the charge itself fires only when the run's matrix
|
||||
window IS the live re-arm key: a supersede-exempt conflict
|
||||
round can still run under a stale window, and a charge
|
||||
keyed to that dead window is one the brake's live-window
|
||||
headline walk would never read, so it is not charged. A
|
||||
head classified from a base-conflict merge or a salvage merge
|
||||
stamps `pre=none`: the pushed head did not start from the
|
||||
head prepare classified. A cancelled check is neither red
|
||||
|
|
|
|||
3
.github/workflows/qwen-autofix.yml
vendored
3
.github/workflows/qwen-autofix.yml
vendored
|
|
@ -4785,8 +4785,11 @@ jobs:
|
|||
| "\(.r // "") \(.h // "") \(.p // "") \(.k // "")"' \
|
||||
"${WORKDIR}/ic.json" 2> /dev/null || echo '')"
|
||||
read -r LP_ROUND LP_HEAD LP_PRE LP_KEY <<< "${LAST_PUSH}" || true
|
||||
# Charged only in the live window: a supersede-exempt round's
|
||||
# stale matrix WINDOW must not key a charge the brake never walks.
|
||||
if [[ -n "${LP_ROUND:-}" && "${LP_HEAD:-}" == "${CHECKED_OUT_HEAD}" \
|
||||
&& "${LP_PRE:-}" == 'green' && "${LP_KEY:-}" == "${WINDOW:-none}" \
|
||||
&& "${WINDOW:-none}" == "${LIVE_REARM_KEY}" \
|
||||
&& "${CHECK_STATE}" == 'red' ]]; then
|
||||
REGRESSED_ROUND="${LP_ROUND}"
|
||||
echo "🩸 #${PR}: round ${LP_ROUND} pushed ${CHECKED_OUT_HEAD:0:9} onto a green head and it is red now — charging that round as a regression"
|
||||
|
|
|
|||
|
|
@ -254,6 +254,16 @@ const hasBashMapfile =
|
|||
spawnSync('bash', ['-c', 'mapfile -d "" -t x <<< y'], { stdio: 'ignore' })
|
||||
.status === 0;
|
||||
|
||||
// sha256sum is GNU coreutils: a macOS host without the GNU toolchain
|
||||
// (Homebrew ships it as gsha256sum) cannot run the staging step's digest
|
||||
// lines — the step is written for the runner, where coreutils is always
|
||||
// present. Probe the host like hasBashMapfile does; the pin gates only the
|
||||
// verbatim phase of the staging witness, never its text pins.
|
||||
const hasSha256sum =
|
||||
spawnSync('bash', ['-c', 'command -v sha256sum > /dev/null 2>&1'], {
|
||||
stdio: 'ignore',
|
||||
}).status === 0;
|
||||
|
||||
// GitHub Actions expressions return operand VALUES from &&/||, not
|
||||
// booleans: && yields the first falsy operand (else the last operand), ||
|
||||
// the first truthy (else the last), '' is falsy, and && binds tighter
|
||||
|
|
@ -26923,6 +26933,23 @@ describe('review verification gate: baseline A/B on deterministic rejection', ()
|
|||
],
|
||||
}),
|
||||
},
|
||||
// The same shape with the callback handed by NAME: the instrument
|
||||
// resolves the module-scope binding, so the skipped body's assertions
|
||||
// still measure as removed.
|
||||
'skip-with-standin-named': {
|
||||
files: { 'pkg/a.test.ts': WT_BASE },
|
||||
round: roundWrites({
|
||||
'pkg/a.test.ts': [
|
||||
WT_IMPORT,
|
||||
'function body() {',
|
||||
' expect(one()).toBe(1);',
|
||||
' expect(two()).toBe(2);',
|
||||
'}',
|
||||
"it.skip('a', body);",
|
||||
"it('a', () => {});",
|
||||
],
|
||||
}),
|
||||
},
|
||||
'delete-behind-guard': {
|
||||
files: {
|
||||
'pkg/a.test.ts': [
|
||||
|
|
@ -27621,6 +27648,9 @@ describe('review verification gate: baseline A/B on deterministic rejection', ()
|
|||
// Skip the body and plant an empty same-titled stand-in: the title
|
||||
// count balances, the executed assertions do not.
|
||||
rejectsWeakening('skip-with-standin', 'net 2 assertion(s) removed');
|
||||
// ...and the same silencing with the callback handed by name: the
|
||||
// binding resolves, so the skipped body still measures as removed.
|
||||
rejectsWeakening('skip-with-standin-named', 'net 2 assertion(s) removed');
|
||||
});
|
||||
|
||||
it('charges a weakening by the tip, whichever commit sequence produced it', () => {
|
||||
|
|
@ -28347,61 +28377,72 @@ describe('review verification gate: baseline A/B on deterministic rejection', ()
|
|||
// ...and the branch that runs in PRODUCTION once this lands: the
|
||||
// counter present, the parser where npm ci puts it. Text pins cannot
|
||||
// see ordering or a newly-failing command inside that `if`, and this
|
||||
// step is unconditional, so a break there kills every round. Run it.
|
||||
writeFileSync(
|
||||
join(surfaceProbeDir, '.github', 'scripts', 'count-test-surface.mjs'),
|
||||
readFileSync('.github/scripts/count-test-surface.mjs'),
|
||||
);
|
||||
mkdirSync(join(surfaceProbeDir, 'node_modules', 'typescript', 'lib'), {
|
||||
recursive: true,
|
||||
});
|
||||
const plantedParser = '// planted typescript build\nmodule.exports={};\n';
|
||||
writeFileSync(
|
||||
join(
|
||||
surfaceProbeDir,
|
||||
'node_modules',
|
||||
'typescript',
|
||||
'lib',
|
||||
'typescript.js',
|
||||
),
|
||||
plantedParser,
|
||||
);
|
||||
rmSync(join(surfaceRunnerTemp, 'count-test-surface.mjs'), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
rmSync(join(surfaceRunnerTemp, 'weaken-parser'), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
writeFileSync(surfaceOutput, '');
|
||||
const staged = runStage();
|
||||
expect(staged.stderr).toBe('');
|
||||
expect(staged.status).toBe(0);
|
||||
expect(
|
||||
readFileSync(join(surfaceRunnerTemp, 'count-test-surface.mjs'), 'utf8'),
|
||||
).toBe(readFileSync('.github/scripts/count-test-surface.mjs', 'utf8'));
|
||||
expect(
|
||||
readFileSync(
|
||||
join(surfaceRunnerTemp, 'weaken-parser', 'typescript.cjs'),
|
||||
'utf8',
|
||||
),
|
||||
).toBe(plantedParser);
|
||||
// The digests the gate verifies against are of the bytes that were
|
||||
// actually staged, and BOTH reach expression context.
|
||||
const digestOf = (file) =>
|
||||
createHash('sha256').update(readFileSync(file)).digest('hex');
|
||||
const emitted = readFileSync(surfaceOutput, 'utf8');
|
||||
expect(emitted).toContain(
|
||||
`weaken_counter_sha256=${digestOf(
|
||||
join(surfaceRunnerTemp, 'count-test-surface.mjs'),
|
||||
)}\n`,
|
||||
);
|
||||
expect(emitted).toContain(
|
||||
`weaken_parser_sha256=${digestOf(
|
||||
join(surfaceRunnerTemp, 'weaken-parser', 'typescript.cjs'),
|
||||
)}\n`,
|
||||
);
|
||||
// step is unconditional, so a break there kills every round. Run it —
|
||||
// on hosts with the GNU coreutils the step's digest lines assume:
|
||||
// they call sha256sum bare, and a macOS host without it (Homebrew
|
||||
// ships gsha256sum) would fail the spawn for a reason the runner can
|
||||
// never hit. The witness is the string pins PLUS the redacted-arm
|
||||
// probes above on such a host.
|
||||
if (hasSha256sum) {
|
||||
writeFileSync(
|
||||
join(surfaceProbeDir, '.github', 'scripts', 'count-test-surface.mjs'),
|
||||
readFileSync('.github/scripts/count-test-surface.mjs'),
|
||||
);
|
||||
mkdirSync(join(surfaceProbeDir, 'node_modules', 'typescript', 'lib'), {
|
||||
recursive: true,
|
||||
});
|
||||
const plantedParser =
|
||||
'// planted typescript build\nmodule.exports={};\n';
|
||||
writeFileSync(
|
||||
join(
|
||||
surfaceProbeDir,
|
||||
'node_modules',
|
||||
'typescript',
|
||||
'lib',
|
||||
'typescript.js',
|
||||
),
|
||||
plantedParser,
|
||||
);
|
||||
rmSync(join(surfaceRunnerTemp, 'count-test-surface.mjs'), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
rmSync(join(surfaceRunnerTemp, 'weaken-parser'), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
writeFileSync(surfaceOutput, '');
|
||||
const staged = runStage();
|
||||
expect(staged.stderr).toBe('');
|
||||
expect(staged.status).toBe(0);
|
||||
expect(
|
||||
readFileSync(
|
||||
join(surfaceRunnerTemp, 'count-test-surface.mjs'),
|
||||
'utf8',
|
||||
),
|
||||
).toBe(readFileSync('.github/scripts/count-test-surface.mjs', 'utf8'));
|
||||
expect(
|
||||
readFileSync(
|
||||
join(surfaceRunnerTemp, 'weaken-parser', 'typescript.cjs'),
|
||||
'utf8',
|
||||
),
|
||||
).toBe(plantedParser);
|
||||
// The digests the gate verifies against are of the bytes that were
|
||||
// actually staged, and BOTH reach expression context.
|
||||
const digestOf = (file) =>
|
||||
createHash('sha256').update(readFileSync(file)).digest('hex');
|
||||
const emitted = readFileSync(surfaceOutput, 'utf8');
|
||||
expect(emitted).toContain(
|
||||
`weaken_counter_sha256=${digestOf(
|
||||
join(surfaceRunnerTemp, 'count-test-surface.mjs'),
|
||||
)}\n`,
|
||||
);
|
||||
expect(emitted).toContain(
|
||||
`weaken_parser_sha256=${digestOf(
|
||||
join(surfaceRunnerTemp, 'weaken-parser', 'typescript.cjs'),
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
rmSync(surfaceProbeDir, { recursive: true, force: true });
|
||||
rmSync(surfaceRunnerTemp, { recursive: true, force: true });
|
||||
|
|
@ -28686,6 +28727,62 @@ describe('count-test-surface: the declared test surface of a test file', () => {
|
|||
],
|
||||
{ a: 0, e: 2, d: ['test:a', 'test:b', 'test:c', 'test:d'] },
|
||||
],
|
||||
[
|
||||
'reads a constant through a type-only wrapper',
|
||||
[
|
||||
"it.skipIf(true as boolean)('a', fn);",
|
||||
"it.runIf(false as boolean)('b', fn);",
|
||||
"it('c', { skip: true } as const, fn);",
|
||||
"it('d', { skip: (true as boolean) }, fn);",
|
||||
"it.skipIf(<boolean>true)('e', fn);",
|
||||
"it.skipIf(true!)('f', fn);",
|
||||
"it.skipIf(true satisfies boolean)('g', fn);",
|
||||
// A wrapper around a runtime value stays a condition.
|
||||
"it.skipIf(process.env.CI as boolean)('h', fn);",
|
||||
],
|
||||
{
|
||||
a: 0,
|
||||
e: 1,
|
||||
d: [
|
||||
'test:a',
|
||||
'test:b',
|
||||
'test:c',
|
||||
'test:d',
|
||||
'test:e',
|
||||
'test:f',
|
||||
'test:g',
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'folds a comparison, equality or logical operator of two constants',
|
||||
[
|
||||
"it.skipIf(1 === 1)('a', fn);",
|
||||
"describe.skipIf(2 > 1)('b', fn);",
|
||||
"it.skipIf('x' !== '')('c', fn);",
|
||||
"it.skipIf(1 && true)('d', fn);",
|
||||
"it.runIf(1 > 2)('e', fn);",
|
||||
"it('f', { skip: 1 === 1 }, fn);",
|
||||
// Placeholder folds and bigint arithmetic stay opaque to
|
||||
// comparison: `{} === {}` is false at runtime, `1n === 1` too.
|
||||
"it.skipIf({} === {})('g', fn);",
|
||||
"it.skipIf(1n === 1)('h', fn);",
|
||||
"it['' || 'skip']('i', fn);",
|
||||
],
|
||||
{
|
||||
a: 0,
|
||||
e: 2,
|
||||
d: [
|
||||
'test:a',
|
||||
'describe:b',
|
||||
'test:c',
|
||||
'test:d',
|
||||
'test:e',
|
||||
'test:f',
|
||||
'test:i',
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
"applies the runner's own rule to body skips: only `false` keeps the test",
|
||||
[
|
||||
|
|
@ -28790,6 +28887,18 @@ describe('count-test-surface: the declared test surface of a test file', () => {
|
|||
],
|
||||
{ a: 1, e: 6, d: ['test:g', 'test:h', 'test:i'] },
|
||||
],
|
||||
[
|
||||
'reads a constant return through wrappers and templates alike',
|
||||
[
|
||||
"it('a', () => { if (!r) return undefined as void; expect(x).toBe(1); });",
|
||||
"it('b', () => { if (!r) return `full suite only: ${process.env.QWEN_FULL}`; expect(x).toBe(1); });",
|
||||
"it('c', () => { if (!r) return `reason`; expect(x).toBe(1); });",
|
||||
// A possibly-thenable return stays ordinary control flow.
|
||||
"it('d', () => { if (!r) return go(); expect(x).toBe(1); });",
|
||||
"it('e', () => { return expect(p).resolves.toBe(1); });",
|
||||
],
|
||||
{ a: 2, e: 5, d: [] },
|
||||
],
|
||||
[
|
||||
'silences the assertions a describe body or a hook shelters',
|
||||
[
|
||||
|
|
@ -28846,6 +28955,78 @@ describe('count-test-surface: the declared test surface of a test file', () => {
|
|||
],
|
||||
{ a: 2, e: 2, d: [] },
|
||||
],
|
||||
[
|
||||
'reads a skip that cannot be escaped as a disable, however wrapped',
|
||||
[
|
||||
// The catch fires exactly when the assertion fails: the test can
|
||||
// never report a failure, so the registration is disabled — the
|
||||
// setup-failure guard this is one token away from.
|
||||
"it('a', (ctx) => { try { expect(x).toBe(1); } catch { ctx.skip(); } });",
|
||||
"it('b', (ctx) => { if (true) ctx.skip(); expect(x).toBe(1); });",
|
||||
"it('c', (ctx) => { if (1 === 1) { ctx.skip(); } expect(x).toBe(1); });",
|
||||
],
|
||||
{ a: 0, e: 0, d: ['test:a', 'test:b', 'test:c'] },
|
||||
],
|
||||
[
|
||||
'measures a registration through a callback handed by name',
|
||||
[
|
||||
'function body(ctx) {',
|
||||
' ctx.skip();',
|
||||
' expect(1).toBe(1);',
|
||||
'}',
|
||||
"it('a', body);",
|
||||
"it('b', fn);",
|
||||
],
|
||||
{ a: 0, e: 1, d: ['test:a'] },
|
||||
],
|
||||
[
|
||||
'measures the named-body shapes exactly like their inline spellings',
|
||||
[
|
||||
'function body() {',
|
||||
' expect(1).toBe(1);',
|
||||
'}',
|
||||
"it.skip('a', body);",
|
||||
"it('a', () => {});",
|
||||
'function suite() {',
|
||||
" it('b', () => { expect(2).toBe(2); });",
|
||||
'}',
|
||||
"describe.skip('c', suite);",
|
||||
],
|
||||
{ a: 0, e: 1, d: ['test:a', 'test:b', 'describe:c'] },
|
||||
],
|
||||
[
|
||||
'keeps a body shared with an enabled registration live',
|
||||
[
|
||||
'function body() {',
|
||||
' expect(1).toBe(1);',
|
||||
'}',
|
||||
"it('a', body);",
|
||||
"it.skip('b', body);",
|
||||
],
|
||||
{ a: 1, e: 1, d: ['test:b'] },
|
||||
],
|
||||
[
|
||||
'reads a collector factory binding as no registration at all',
|
||||
[
|
||||
"const posixOnly = it.skipIf(process.platform !== 'linux');",
|
||||
'const rows = it.each(getCases());',
|
||||
'const myTest = test.extend({});',
|
||||
"it('a', fn);",
|
||||
],
|
||||
{ a: 0, e: 1, d: [] },
|
||||
],
|
||||
[
|
||||
'sees a chain through a type-only wrapper',
|
||||
[
|
||||
"(it as any).skip('a', () => { expect(1).toBe(1); });",
|
||||
"(it.skip as any)('b', () => { expect(1).toBe(1); });",
|
||||
"it('c', (ctx) => { (ctx as any).skip(); expect(1).toBe(1); });",
|
||||
"it('d', () => { (expect(1) as any).toBe(1); });",
|
||||
"(describe as any).skip('e', () => { it('f', () => { expect(1).toBe(1); }); });",
|
||||
],
|
||||
// test:d stays enabled — its body only ASSERTS through a wrapper.
|
||||
{ a: 1, e: 1, d: ['test:a', 'test:b', 'test:c', 'describe:e', 'test:f'] },
|
||||
],
|
||||
[
|
||||
'counts assertions only where the runner would execute them',
|
||||
[
|
||||
|
|
@ -28873,6 +29054,59 @@ describe('count-test-surface: the declared test surface of a test file', () => {
|
|||
expect(surface(source, path)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('resolves a registration callback bound by name, exactly like the inline spelling', () => {
|
||||
// The runner receives the SAME function whether the registration
|
||||
// hands it over inline or by name, so the two spellings must measure
|
||||
// alike: the body's skip reaches the registration, its assertions
|
||||
// leave the surface, and a disabled describe's body propagates.
|
||||
expect(
|
||||
countTestSurface(
|
||||
[
|
||||
'function body(ctx) {',
|
||||
' ctx.skip();',
|
||||
' expect(1).toBe(1);',
|
||||
'}',
|
||||
"it('x', body);",
|
||||
].join('\n'),
|
||||
'a.test.ts',
|
||||
),
|
||||
).toMatchObject({
|
||||
assertions: 0,
|
||||
enabled: 0,
|
||||
disabled: ['test:x'],
|
||||
enabledTitles: [],
|
||||
});
|
||||
expect(
|
||||
countTestSurface(
|
||||
[
|
||||
'function suite() {',
|
||||
" it('a', () => { expect(1).toBe(1); });",
|
||||
'}',
|
||||
"describe.skip('d', suite);",
|
||||
].join('\n'),
|
||||
'a.test.ts',
|
||||
),
|
||||
).toMatchObject({ enabledTitles: [], enabled: 0, assertions: 0 });
|
||||
// The inline spellings these must agree with.
|
||||
expect(
|
||||
countTestSurface(
|
||||
"it('x', (ctx) => { ctx.skip(); expect(1).toBe(1); });",
|
||||
'a.test.ts',
|
||||
),
|
||||
).toMatchObject({
|
||||
assertions: 0,
|
||||
enabled: 0,
|
||||
disabled: ['test:x'],
|
||||
enabledTitles: [],
|
||||
});
|
||||
expect(
|
||||
countTestSurface(
|
||||
"describe.skip('d', () => { it('a', () => { expect(1).toBe(1); }); });",
|
||||
'a.test.ts',
|
||||
),
|
||||
).toMatchObject({ enabledTitles: [], enabled: 0, assertions: 0 });
|
||||
});
|
||||
|
||||
it('measures every dialect the gate selects, not only .ts', () => {
|
||||
// The pathspec selects `*.test.*` and `*.spec.*` whatever the
|
||||
// extension, and the repository tracks hundreds of `*.test.js` files.
|
||||
|
|
@ -29206,6 +29440,10 @@ describe('review-address: regression accounting (af-155)', () => {
|
|||
// The commit the rollup describes (headRefOid read with the rollup).
|
||||
checksHead = HEAD,
|
||||
window = 'w1',
|
||||
// The live re-arm key prepare computed: defaults to the run's window
|
||||
// (the ordinary case); a supersede-exempt conflict round can still run
|
||||
// under a stale matrix window.
|
||||
liveKey = window,
|
||||
}) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'af148-'));
|
||||
try {
|
||||
|
|
@ -29217,7 +29455,7 @@ describe('review-address: regression accounting (af-155)', () => {
|
|||
'bash',
|
||||
[
|
||||
'-c',
|
||||
`set -euo pipefail\nWORKDIR=${JSON.stringify(dir)}\nAUTOFIX_BOT=qwen-code-dev-bot\nDISPATCH_STATUS_CONTEXT='qwen-autofix/dispatch-pending'\nCHECKED_OUT_HEAD='${checkedOutHead}'\nROLLUP_HEAD='${checksHead}'\nWINDOW='${window}'\nPR=1\nGITHUB_OUTPUT=${JSON.stringify(outFile)}\n${script}`,
|
||||
`set -euo pipefail\nWORKDIR=${JSON.stringify(dir)}\nAUTOFIX_BOT=qwen-code-dev-bot\nDISPATCH_STATUS_CONTEXT='qwen-autofix/dispatch-pending'\nCHECKED_OUT_HEAD='${checkedOutHead}'\nROLLUP_HEAD='${checksHead}'\nWINDOW='${window}'\nLIVE_REARM_KEY='${liveKey}'\nPR=1\nGITHUB_OUTPUT=${JSON.stringify(outFile)}\n${script}`,
|
||||
],
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
|
|
@ -29380,56 +29618,43 @@ describe('review-address: regression accounting (af-155)', () => {
|
|||
// pushed code introduced, and an in-flight own check is observer
|
||||
// noise on the triggers whose suite attaches to the PR head. The
|
||||
// carve-out survives at the FEEDBACK sites (N_FAILED_CHECKS /
|
||||
// N_RED_NOW).
|
||||
expect(
|
||||
run({
|
||||
checks: [
|
||||
{
|
||||
name: 'review-scan',
|
||||
conclusion: 'FAILURE',
|
||||
workflowName: 'Qwen Autofix',
|
||||
},
|
||||
...GREEN,
|
||||
],
|
||||
}).state,
|
||||
).toBe('green');
|
||||
expect(
|
||||
run({
|
||||
checks: [
|
||||
{
|
||||
name: 'review-address (1)',
|
||||
conclusion: 'FAILURE',
|
||||
workflowName: 'Qwen Autofix',
|
||||
},
|
||||
...GREEN,
|
||||
],
|
||||
}).state,
|
||||
).toBe('green');
|
||||
expect(
|
||||
run({
|
||||
checks: [
|
||||
{
|
||||
name: 'review-address (1)',
|
||||
conclusion: 'FAILURE',
|
||||
workflowName: 'Qwen Autofix',
|
||||
},
|
||||
],
|
||||
}).state,
|
||||
).toBe('none');
|
||||
// A red AUXILIARY lane of the fleet is the loop's own business too.
|
||||
expect(
|
||||
run({
|
||||
checks: [
|
||||
{
|
||||
name: 'review-pr',
|
||||
status: 'COMPLETED',
|
||||
conclusion: 'FAILURE',
|
||||
workflowName: '🧐 Qwen Pull Request Review',
|
||||
},
|
||||
...GREEN,
|
||||
],
|
||||
}).state,
|
||||
).toBe('green');
|
||||
// N_RED_NOW). All FIVE own-lane workflow names are exercised — a name
|
||||
// dropped from the jq filter would read as a PR-side regression here.
|
||||
for (const loopWorkflow of [
|
||||
'Qwen Autofix',
|
||||
'🧐 Qwen Pull Request Review',
|
||||
'Qwen CI Failure Patrol',
|
||||
'Qwen Autofix Fork Bridge',
|
||||
'Qwen Autofix Fork Signal',
|
||||
]) {
|
||||
expect(
|
||||
run({
|
||||
checks: [
|
||||
{
|
||||
name: 'build',
|
||||
status: 'COMPLETED',
|
||||
conclusion: 'FAILURE',
|
||||
workflowName: loopWorkflow,
|
||||
},
|
||||
...GREEN,
|
||||
],
|
||||
}).state,
|
||||
).toBe('green');
|
||||
// An own lane ALONE is no signal at all: excluded by the filter, it
|
||||
// leaves an empty set — unknown, never the red a charge needs.
|
||||
expect(
|
||||
run({
|
||||
checks: [
|
||||
{
|
||||
name: 'build',
|
||||
status: 'COMPLETED',
|
||||
conclusion: 'FAILURE',
|
||||
workflowName: loopWorkflow,
|
||||
},
|
||||
],
|
||||
}).state,
|
||||
).toBe('none');
|
||||
}
|
||||
// The round's own in-flight check must not hold the verdict pending.
|
||||
expect(
|
||||
run({
|
||||
|
|
@ -29474,6 +29699,18 @@ describe('review-address: regression accounting (af-155)', () => {
|
|||
expect(
|
||||
run({ checks: RED, comments: pushMarker({ key: 'w0' }) }).regressed,
|
||||
).toBe('');
|
||||
// ...and when this run's own matrix window is the stale one (a
|
||||
// supersede-exempt conflict round after a re-arm), the charge stays
|
||||
// with the live window: the brake walks headlines under the live key,
|
||||
// so a charge keyed to the dead window would never land there.
|
||||
expect(
|
||||
run({
|
||||
checks: RED,
|
||||
comments: pushMarker({ key: 'w0' }),
|
||||
window: 'w0',
|
||||
liveKey: 'w1',
|
||||
}).regressed,
|
||||
).toBe('');
|
||||
// Head is green now — nothing to charge.
|
||||
expect(run({ checks: GREEN, comments: pushMarker({}) }).regressed).toBe('');
|
||||
// A failed own address run beside a green suite is feedback, not a
|
||||
|
|
@ -29556,6 +29793,12 @@ describe('review-address: regression accounting (af-155)', () => {
|
|||
expect(pushAndReportScript).toContain(
|
||||
"[[ \"${CONFLICT:-false}\" == 'true' ]] && PUSH_PRE='none'",
|
||||
);
|
||||
// ...and so does any merge commit the push carries past the head
|
||||
// prepare classified (a clean in-round merge of main trips neither arm
|
||||
// above): the pushed head then holds content prepare never classified.
|
||||
expect(pushAndReportScript).toContain(
|
||||
'[[ -n "$(git rev-list --merges "${REPORT_HEAD}..${PUSHED_HEAD}" 2>/dev/null)" ]] && PUSH_PRE=\'none\'',
|
||||
);
|
||||
// When the push landed but the report post failed, a marker-only
|
||||
// comment still carries the push record — the only record a later
|
||||
// round can charge a regression against.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue