mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
437465f37b
The loop that #1874 is investigated with could not tell the truth about itself. It classified every non-`passed` iteration as a stall, which after #2035 gave the looped test an XCTSkipIf meant an environment flip would report a 100% stall rate; it captured cadence only for failures, though an absorbed episode now passes; and it read its logs with shell pipelines whose exit status means "did this match", so an iteration that legitimately matched nothing killed the job before it could be summarized. scripts/diagnose-1874-iteration.ts reads one iteration: xcodebuild's own verdict, the `type-all` duration, and the cadence worth keeping. A nonzero exit outranks a green measured test — in `pair` mode the neighbour or the runner can fail while the measured test passes — and a run that produced no verdict is named as ours rather than counted as a stall. The workflow gains the #1781 lane declaration it never had. Its kill criterion names #2080, which the loop can now serve rather than merely claim to: the looped test is a dispatch input, so the fill route that #2080 traces loops the same way. One test pins the contract the script cannot check about itself — that the workflow hands it the status xcodebuild returned rather than a literal. Closes #1874. Both filed symptoms are resolved. `smoke:form-input` was root-caused and fixed in #2035: the fixture's placeholder was identical to the value every suite filled, so `fill` could never be verified on the penalized route — deterministic, not a flake, and only visible under load because that route is gated on a penalized XCTest channel. The targeted XCTest is mitigated by the progress-aware commit budget, with 200 consecutive green loop iterations across two dispatches. The issue's remaining question — why the input pipeline throttles — is answered by the second dispatch, and the premise was wrong: it does not. Posting 17 characters took 484 ms and the commit was observed on the first poll, inside an iteration whose `type-all` measured 14334 ms. The ~12.6 s went to accessibility round-trips before any character was posted, which is #1105's path, not the input pipeline's.
81 lines
2.6 KiB
TypeScript
81 lines
2.6 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
const SLOW_PASS_MS = 2000;
|
|
const CADENCE_LIMIT = 40;
|
|
const SUMMARY = 'stall-summary.txt';
|
|
const EVIDENCE_DIR = '.tmp';
|
|
|
|
type MeasuredVerdict = 'passed' | 'skipped' | 'failed';
|
|
|
|
export type IterationReport = {
|
|
readonly verdict: MeasuredVerdict | 'no-result' | 'run-failed';
|
|
readonly keepEvidence: boolean;
|
|
readonly lines: readonly string[];
|
|
};
|
|
|
|
export function readIteration(
|
|
log: string,
|
|
testName: string,
|
|
iteration: number,
|
|
rc: number,
|
|
): IterationReport {
|
|
const lines = log.split('\n');
|
|
const marker = `${testName}]' `;
|
|
const word = lines
|
|
.filter((line) => line.includes(marker))
|
|
.map((line) => line.slice(line.indexOf(marker) + marker.length).split(' ')[0])
|
|
.at(-1);
|
|
const measured = isMeasuredVerdict(word) ? word : 'no-result';
|
|
const exitContradictsMeasured = rc !== 0 && measured !== 'failed' && measured !== 'no-result';
|
|
const verdict = exitContradictsMeasured ? 'run-failed' : measured;
|
|
const durationMs = Number(
|
|
lines
|
|
.flatMap((line) => /phase=type-all durationMs=(\d+(?:\.\d+)?)/.exec(line)?.[1] ?? [])
|
|
.at(-1) ?? 0,
|
|
);
|
|
const cadence = lines.filter((line) => line.includes('[DEBUG-1874]'));
|
|
const keepEvidence = verdict !== 'passed' || durationMs > SLOW_PASS_MS;
|
|
|
|
const polls = cadence.filter((line) => line.includes('] poll')).length;
|
|
const summary = `iter=${iteration} verdict=${verdict} rc=${rc} durationMs=${durationMs} polls=${polls}`;
|
|
const dropped = cadence.length - CADENCE_LIMIT;
|
|
return {
|
|
verdict,
|
|
keepEvidence,
|
|
lines: !keepEvidence
|
|
? [summary]
|
|
: [
|
|
summary,
|
|
...cadence.slice(0, CADENCE_LIMIT),
|
|
...(dropped > 0 ? [`… ${dropped} more DEBUG-1874 lines (full log in the artifact)`] : []),
|
|
],
|
|
};
|
|
}
|
|
|
|
function isMeasuredVerdict(word: string | undefined): word is MeasuredVerdict {
|
|
return word === 'passed' || word === 'skipped' || word === 'failed';
|
|
}
|
|
|
|
function main(): number {
|
|
const [logPath, testName, iteration, rc] = process.argv.slice(2);
|
|
if (!logPath || !testName || !iteration || !rc) {
|
|
throw new Error('usage: diagnose-1874-iteration.ts <log> <testName> <iteration> <rc>');
|
|
}
|
|
const report = readIteration(
|
|
fs.readFileSync(logPath, 'utf8'),
|
|
testName,
|
|
Number(iteration),
|
|
Number(rc),
|
|
);
|
|
fs.appendFileSync(SUMMARY, `${report.lines.join('\n')}\n`);
|
|
if (report.keepEvidence) {
|
|
fs.copyFileSync(logPath, path.join(EVIDENCE_DIR, `stall-evidence-${iteration}.log`));
|
|
}
|
|
process.stdout.write(report.verdict);
|
|
return 0;
|
|
}
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) process.exit(main());
|