test: add the non-blocking 'marathon' E2E tier

Full start-to-finish flows move out of the blocking lanes. E2E_TIERS and
E2ETier gain 'marathon'; describeE2ETier('marathon') is enabled only when
EVALS_TIER=marathon, so the gate/PR and periodic lanes (and the gate census)
never run those cases. The PR profile accepts marathon ids as scheduled
elsewhere and defers them with their own reason, even on full fallback.
This commit is contained in:
garrytan committed 2026-09-29 15:21:17 +00:00
1 parent 8c30ee9595
commit a41cdb7d7d
7 files changed
+43 -14

No files matched your search

+9 -7
View File
@@ -59,7 +59,7 @@ export const PR_PROFILE_FILES: Record<string, readonly string[]> = {
export interface PrProfileMaps { export interface PrProfileMaps {
e2eTouchfiles: Record<string, string[]>; e2eTouchfiles: Record<string, string[]>;
judgeTouchfiles: Record<string, string[]>; judgeTouchfiles: Record<string, string[]>;
tiers: Record<string, 'gate' | 'periodic'>; tiers: Record<string, 'gate' | 'periodic' | 'marathon'>;
globalTouchfiles: readonly string[]; globalTouchfiles: readonly string[];
} }
@@ -72,7 +72,7 @@ export interface PrProfileSelection {
mode: 'pr' | 'full-fallback'; mode: 'pr' | 'full-fallback';
e2e: string[]; e2e: string[];
judges: string[]; judges: string[];
deferred: Array<{ id: string; tier: 'gate' | 'periodic'; reason: string }>; deferred: Array<{ id: string; tier: 'gate' | 'periodic' | 'marathon'; reason: string }>;
unknownFiles: string[]; unknownFiles: string[];
deferredPromptFiles: string[]; deferredPromptFiles: string[];
missingCoverage: string[]; missingCoverage: string[];
@@ -92,8 +92,8 @@ export function validatePrProfileInventory(
} }
} }
for (const id of Object.keys(maps.e2eTouchfiles)) { for (const id of Object.keys(maps.e2eTouchfiles)) {
if (maps.tiers[id] !== 'gate' && maps.tiers[id] !== 'periodic') { if (maps.tiers[id] !== 'gate' && maps.tiers[id] !== 'periodic' && maps.tiers[id] !== 'marathon') {
throw new Error(`E2E case has no broad gate/periodic census: ${id}`); throw new Error(`E2E case has no broad gate/periodic/marathon census: ${id}`);
} }
} }
} }
@@ -182,9 +182,11 @@ export function selectPrProfile(options: {
const kept = new Set(e2e); const kept = new Set(e2e);
const deferred = candidates.filter(id => !kept.has(id)).map(id => ({ const deferred = candidates.filter(id => !kept.has(id)).map(id => ({
id, tier: maps.tiers[id], id, tier: maps.tiers[id],
reason: maps.tiers[id] === 'periodic' reason: maps.tiers[id] === 'marathon'
? 'Broad periodic/release coverage; not executed by the PR gate' ? 'Full end-to-end marathon coverage; non-blocking lane, not executed by the PR gate'
: 'Broad gate census/release coverage; outside the fast PR profile', : maps.tiers[id] === 'periodic'
? 'Broad periodic/release coverage; not executed by the PR gate'
: 'Broad gate census/release coverage; outside the fast PR profile',
})); }));
const noQuickCoverage = files.filter(file => isPromptFile(file) const noQuickCoverage = files.filter(file => isPromptFile(file)
&& !(depends(file, maps.globalTouchfiles) && (e2e.length > 0 || judges.length > 0)) && !(depends(file, maps.globalTouchfiles) && (e2e.length > 0 || judges.length > 0))
+3 -2
View File
@@ -24,11 +24,12 @@
import { describe } from 'bun:test'; import { describe } from 'bun:test';
export type E2ETier = 'gate' | 'periodic'; export type E2ETier = 'gate' | 'periodic' | 'marathon';
/** /**
* True when this process should run whole-file-gated paid tests of `tier`: * True when this process should run whole-file-gated paid tests of `tier`:
* EVALS=1 AND EVALS_TIER exactly equals the tier. * EVALS=1 AND EVALS_TIER exactly equals the tier. 'marathon' cases (full
* end-to-end flows) therefore never run in the gate/PR or periodic lanes.
* *
* Deliberate consequence: EVALS=1 with EVALS_TIER unset is false for BOTH * Deliberate consequence: EVALS=1 with EVALS_TIER unset is false for BOTH
* tiers. Tierless runs (`test:evals` / `eval:bg` / `eval:bg:all`) skip every * tiers. Tierless runs (`test:evals` / `eval:bg` / `eval:bg:all`) skip every
+13
View File
@@ -64,6 +64,19 @@ describe('e2e-gate: env matrix (read at call time)', () => {
expect(describeE2ETier('periodic')).toBe(describe.skip); expect(describeE2ETier('periodic')).toBe(describe.skip);
}); });
test('marathon runs only in its own lane; gate and periodic lanes skip it', () => {
process.env.EVALS = '1';
for (const lane of ['gate', 'periodic']) {
process.env.EVALS_TIER = lane;
expect(e2eTierEnabled('marathon')).toBe(false);
expect(describeE2ETier('marathon')).toBe(describe.skip);
}
process.env.EVALS_TIER = 'marathon';
expect(describeE2ETier('marathon')).toBe(describe);
expect(describeE2ETier('gate')).toBe(describe.skip);
expect(describeE2ETier('periodic')).toBe(describe.skip);
});
test('EVALS=1 + EVALS_TIER unset → skip both tiers (the tierless test:evals / eval:bg:all trap)', () => { test('EVALS=1 + EVALS_TIER unset → skip both tiers (the tierless test:evals / eval:bg:all trap)', () => {
process.env.EVALS = '1'; process.env.EVALS = '1';
expect(e2eTierEnabled('gate')).toBe(false); expect(e2eTierEnabled('gate')).toBe(false);
+2 -1
View File
@@ -116,9 +116,10 @@ export let selectedTests: string[] | null = resolveModuleSelection(
// EVALS_TIER: filter tests by tier after diff-based selection. // EVALS_TIER: filter tests by tier after diff-based selection.
// 'gate' = gate tests only (CI default — blocks merge) // 'gate' = gate tests only (CI default — blocks merge)
// 'periodic' = periodic tests only (weekly cron / manual) // 'periodic' = periodic tests only (weekly cron / manual)
// 'marathon' = full end-to-end flows only (non-blocking marathon lane)
// not set = run all selected tests (local dev default, backward compat) // not set = run all selected tests (local dev default, backward compat)
if (evalsEnabled && process.env.EVALS_TIER) { if (evalsEnabled && process.env.EVALS_TIER) {
const tier = process.env.EVALS_TIER as 'gate' | 'periodic'; const tier = process.env.EVALS_TIER as 'gate' | 'periodic' | 'marathon';
const tierTests = Object.entries(E2E_TIERS) const tierTests = Object.entries(E2E_TIERS)
.filter(([, t]) => t === tier) .filter(([, t]) => t === tier)
.map(([name]) => name); .map(([name]) => name);
+3 -2
View File
@@ -1118,10 +1118,11 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
}; };
/** /**
* E2E test tiers — 'gate' blocks PRs, 'periodic' runs weekly/on-demand. * E2E test tiers — 'gate' blocks PRs, 'periodic' runs weekly/on-demand,
* 'marathon' keeps full start-to-finish flows in a non-blocking lane only.
* Must have exactly the same keys as E2E_TOUCHFILES. * Must have exactly the same keys as E2E_TOUCHFILES.
*/ */
export const E2E_TIERS: Record<string, 'gate' | 'periodic'> = { export const E2E_TIERS: Record<string, 'gate' | 'periodic' | 'marathon'> = {
'ship-skipped-queued-finding': 'gate', 'ship-skipped-queued-finding': 'gate',
'investigate-owned-completion': 'gate', 'investigate-owned-completion': 'gate',
'investigate-owned-abort': 'gate', 'investigate-owned-abort': 'gate',
+1 -1
View File
@@ -46,7 +46,7 @@ if (evalsEnabled && !process.env.EVALS_ALL) {
// Apply EVALS_TIER filter (same logic as e2e-helpers.ts) // Apply EVALS_TIER filter (same logic as e2e-helpers.ts)
if (evalsEnabled && process.env.EVALS_TIER) { if (evalsEnabled && process.env.EVALS_TIER) {
const tier = process.env.EVALS_TIER as 'gate' | 'periodic'; const tier = process.env.EVALS_TIER as 'gate' | 'periodic' | 'marathon';
const tierTests = Object.entries(E2E_TIERS) const tierTests = Object.entries(E2E_TIERS)
.filter(([, t]) => t === tier) .filter(([, t]) => t === tier)
.map(([name]) => name); .map(([name]) => name);
+12 -1
View File
@@ -35,7 +35,7 @@ describe('fast PR coverage policy', () => {
expect(PR_PROFILE_CASE_IDS).toContain('plan-ceo-review-benefits'); expect(PR_PROFILE_CASE_IDS).toContain('plan-ceo-review-benefits');
expect(PR_PROFILE_CASE_IDS).toContain('plan-review-report'); expect(PR_PROFILE_CASE_IDS).toContain('plan-review-report');
expect(PR_PROFILE_CASE_IDS).toContain('auq-format-gate'); expect(PR_PROFILE_CASE_IDS).toContain('auq-format-gate');
for (const tier of Object.values(PR_PROFILE_MAPS.tiers)) expect(['gate', 'periodic']).toContain(tier); for (const tier of Object.values(PR_PROFILE_MAPS.tiers)) expect(['gate', 'periodic', 'marathon']).toContain(tier);
}); });
test('keeps relevant short probes and the failing CEO quality obligation; reports broad work', () => { test('keeps relevant short probes and the failing CEO quality obligation; reports broad work', () => {
@@ -151,6 +151,17 @@ describe('fast PR coverage policy', () => {
expect(result.needsFullValidation).toBe(false); expect(result.needsFullValidation).toBe(false);
}); });
test('marathon cases are deferred to their non-blocking lane, even on full fallback', () => {
const marathon: PrProfileMaps = { ...maps, tiers: { ...maps.tiers, 'ceo-full': 'marathon' } };
for (const changedFiles of [['plan-ceo-review/SKILL.md.tmpl'], ['lib/new-runtime.ts']]) {
const result = select({ maps: marathon, changedFiles });
expect(result.e2e).not.toContain('ceo-full');
expect(result.deferred.find(({ id }) => id === 'ceo-full')).toEqual({ id: 'ceo-full', tier: 'marathon',
reason: 'Full end-to-end marathon coverage; non-blocking lane, not executed by the PR gate' });
}
expect(() => select({ maps: marathon, profile: ['ceo-full'] })).toThrow('broad gate census');
});
test('rejects stale profile IDs, incorrect tiers, missing cadence, and unknown selections', () => { test('rejects stale profile IDs, incorrect tiers, missing cadence, and unknown selections', () => {
expect(() => select({ profile: ['missing'] })).toThrow('broad gate census'); expect(() => select({ profile: ['missing'] })).toThrow('broad gate census');
expect(() => select({ profile: ['ceo-periodic'] })).toThrow('broad gate census'); expect(() => select({ profile: ['ceo-periodic'] })).toThrow('broad gate census');