mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
080c592567
## Summary & Motivation Job conclusions hide tests that pass on retry, and the evidence only lived in overwritten PR comments and 7-day artifacts, so recurring flakes couldn't be ranked against how often they ran. - retry sidecars are now named after the Vitest report they came from, so lanes, VMs, and worlds no longer overwrite each other when artifacts merge — and the aggregate comment can attribute a flake to an exact lane - `generate-e2e-flake-history.js` publishes a bounded 30-run history to gh-pages: one series per (lane, app, world, vm, platform) × test, carrying both an executed count and a passed-on-retry count so a rate has a denominator - the reporter always writes the sidecar, which is what lets a clean run be distinguished from a lane that reported no retry telemetry at all ## Test Plan Unit tests added for dimension parsing, denominators, schema validation, and the 30-run window; verified every current report filename maps to explicit dimensions, and a one-run history built from a full artifact set came out around 100 KiB.
195 lines
6.8 KiB
JavaScript
195 lines
6.8 KiB
JavaScript
const assert = require('node:assert');
|
|
const { execFileSync } = require('node:child_process');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { test } = require('node:test');
|
|
|
|
const SCRIPT = path.join(__dirname, 'aggregate-e2e-results.js');
|
|
|
|
// vitest JSON-reporter shape for one result file: `passed` passing assertions
|
|
// plus one failed assertion per title in `failedTitles`.
|
|
function resultJson(fileLabel, passed, failedTitles = []) {
|
|
const assertionResults = [];
|
|
for (let i = 0; i < passed; i++) {
|
|
assertionResults.push({
|
|
status: 'passed',
|
|
title: `pass ${i}`,
|
|
fullName: `e2e pass ${i}`,
|
|
});
|
|
}
|
|
for (const title of failedTitles) {
|
|
assertionResults.push({
|
|
status: 'failed',
|
|
title,
|
|
fullName: `e2e ${title}`,
|
|
failureMessages: ['AssertionError: boom'],
|
|
});
|
|
}
|
|
return JSON.stringify({
|
|
testResults: [
|
|
{ name: `/x/${fileLabel}`, duration: 1000, assertionResults },
|
|
],
|
|
});
|
|
}
|
|
|
|
// Writes fixture files (map of `e2e-<category>-<app>.json` -> JSON) into a fresh
|
|
// temp dir, runs the aggregate script against it, and returns stdout. The
|
|
// script exits non-zero when tests failed, so read stdout off the thrown error.
|
|
function renderAggregate(files) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-agg-'));
|
|
for (const [name, contents] of Object.entries(files)) {
|
|
fs.writeFileSync(path.join(dir, name), contents);
|
|
}
|
|
try {
|
|
return execFileSync(
|
|
process.execPath,
|
|
[SCRIPT, dir, '--mode', 'aggregate', '--run-url', 'https://gh/run/1'],
|
|
{ encoding: 'utf8' }
|
|
);
|
|
} catch (error) {
|
|
return error.stdout;
|
|
}
|
|
}
|
|
|
|
// Same, but the per-job summary one CI job posts for itself (no `--mode`).
|
|
function renderJob(files) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-agg-'));
|
|
for (const [name, contents] of Object.entries(files)) {
|
|
fs.writeFileSync(path.join(dir, name), contents);
|
|
}
|
|
try {
|
|
return execFileSync(
|
|
process.execPath,
|
|
[SCRIPT, dir, '--job-name', 'E2E Python Conformance'],
|
|
{ encoding: 'utf8' }
|
|
);
|
|
} catch (error) {
|
|
return error.stdout;
|
|
}
|
|
}
|
|
|
|
test('few failures list inline above a collapsed summary section', () => {
|
|
const body = renderAggregate({
|
|
'e2e-vercel-prod-nextjs-turbopack.json': resultJson('a', 40, [
|
|
'sleeps forever',
|
|
'hook race',
|
|
]),
|
|
'e2e-local-dev-hono.json': resultJson('b', 30),
|
|
});
|
|
|
|
// Failed section is first, renamed, and appears before the summary section.
|
|
assert.match(body, /### ❌ Failed E2E Tests/);
|
|
assert.doesNotMatch(body, /### ❌ Failed Tests\n/);
|
|
assert.ok(
|
|
body.indexOf('### ❌ Failed E2E Tests') <
|
|
body.indexOf('### E2E Test Summary'),
|
|
'Failed E2E Tests should come before E2E Test Summary'
|
|
);
|
|
|
|
// Under the inline threshold → heading, not a <details>, and every test listed.
|
|
assert.match(body, /#### ▲ Vercel Production \(2 failed\)/);
|
|
assert.match(body, /- `sleeps forever`/);
|
|
assert.match(body, /- `hook race`/);
|
|
|
|
// Summary and per-category breakdown are both collapsible under the section.
|
|
assert.match(body, /### E2E Test Summary/);
|
|
assert.match(body, /<details>\n<summary>Summary<\/summary>/);
|
|
assert.match(body, /<details>\n<summary>Details by Category<\/summary>/);
|
|
|
|
// One workflow-run link, and no leftover redundant blurbs.
|
|
assert.match(body, /📋 \[View full workflow run\]\(https:\/\/gh\/run\/1\)/);
|
|
assert.doesNotMatch(body, /Some E2E test jobs failed/);
|
|
assert.doesNotMatch(body, /Check the \[workflow run\]/);
|
|
});
|
|
|
|
test('a category with >= 10 failures collapses into a <details>', () => {
|
|
const failedTitles = Array.from({ length: 12 }, (_, i) => `fail-${i}`);
|
|
const body = renderAggregate({
|
|
'e2e-vercel-prod-nextjs-turbopack.json': resultJson('a', 5, failedTitles),
|
|
});
|
|
|
|
assert.match(body, /### ❌ Failed E2E Tests/);
|
|
// The category heading is now the <summary>, not a #### heading.
|
|
assert.match(
|
|
body,
|
|
/<details>\n<summary>▲ Vercel Production \(12 failed\)<\/summary>/
|
|
);
|
|
assert.doesNotMatch(body, /#### ▲ Vercel Production/);
|
|
assert.match(body, /- `fail-11`/);
|
|
});
|
|
|
|
test('all-passing runs omit the Failed E2E Tests section entirely', () => {
|
|
const body = renderAggregate({
|
|
'e2e-vercel-prod-vite.json': resultJson('a', 20),
|
|
});
|
|
|
|
assert.match(body, /✅ \*\*All tests passed\*\*/);
|
|
assert.doesNotMatch(body, /Failed E2E Tests/);
|
|
// The summary section is still present.
|
|
assert.match(body, /### E2E Test Summary/);
|
|
assert.match(body, /<details>\n<summary>Summary<\/summary>/);
|
|
});
|
|
|
|
test('paired flaky sidecars retain exact lane and VM attribution', () => {
|
|
const body = renderAggregate({
|
|
'e2e-local-postgres-nextjs-turbopack-stable-quickjs.json': resultJson(
|
|
'a',
|
|
1
|
|
),
|
|
'e2e-local-postgres-nextjs-turbopack-stable-quickjs.flaky.json':
|
|
JSON.stringify([
|
|
{
|
|
testName: 'passes on retry',
|
|
fullName: 'e2e passes on retry',
|
|
retryCount: 1,
|
|
},
|
|
]),
|
|
});
|
|
|
|
assert.match(body, /`passes on retry`/);
|
|
assert.match(
|
|
body,
|
|
/nextjs-turbopack · local-postgres \/ postgres \/ quickjs \/ stable/
|
|
);
|
|
assert.doesNotMatch(body, /Results by File[\s\S]*\.flaky\.json/);
|
|
});
|
|
|
|
test('Details by Category has no nested collapsibles', () => {
|
|
const body = renderAggregate({
|
|
'e2e-vercel-prod-nextjs-turbopack.json': resultJson('a', 40, ['x']),
|
|
'e2e-local-dev-hono.json': resultJson('b', 30),
|
|
});
|
|
|
|
// Isolate the Details-by-Category block and assert it opens exactly one
|
|
// <details> (its own) — categories inside are plain bold headings.
|
|
const start = body.indexOf('<summary>Details by Category</summary>');
|
|
const block = body.slice(start);
|
|
const nestedDetails = (block.match(/<details>/g) || []).length;
|
|
assert.strictEqual(nestedDetails, 0, 'no nested <details> inside the block');
|
|
assert.match(block, /\*\*❌ ▲ Vercel Production\*\*/);
|
|
assert.match(block, /\*\*✅ 💻 Local Development\*\*/);
|
|
});
|
|
|
|
test('conformance declarations are not mistaken for result files', () => {
|
|
// Per-job mode, because that is where a stray file shows up: it lists one row
|
|
// per report file it found.
|
|
const body = renderJob({
|
|
'e2e-conformance-python.json': resultJson('a', 2),
|
|
// Both of these live in the repo, match `e2e-*.json`, and are conformance
|
|
// declarations rather than vitest reports.
|
|
'e2e-conformance.json': JSON.stringify({
|
|
language: 'python',
|
|
fixtures: ['nullByteWorkflow'],
|
|
}),
|
|
'e2e-conformance.example.json': JSON.stringify({ fixtures: [] }),
|
|
});
|
|
|
|
// The one real report is the only one found, so there is no by-file table at
|
|
// all. With the strays counted there was, listing them as 0/0/0 rows.
|
|
assert.doesNotMatch(body, /Results by File/);
|
|
assert.match(body, /\| \*\*Total\*\* \| \*\*2\*\* \|/);
|
|
assert.doesNotMatch(body, /e2e-conformance\.json/);
|
|
assert.doesNotMatch(body, /e2e-conformance\.example\.json/);
|
|
});
|