mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
Twelve detector families move into one owner test each: 73 incident files become describe blocks in ceo-section-loading-fixture (stale-fill race), model-overlays, coverage-audit-evidence, autoplan-phase-observer, native-auto-decide, outside-voice-evidence, eng-first-review, plan-count-completion, plan-count-file-permission, ceo-mode-option, plan-scope-selection and plan-count-prerequisite. Each block keeps its original code and fixture, so every case still runs; only tests asserting the incident file's own touchfile registration are dropped (41). Touchfile lists that named an incident now name its owner.
240 lines
13 KiB
TypeScript
240 lines
13 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { OVERLAY_CASE_FILES, OVERLAY_MIN_FILE_WALL_MS } from './helpers/overlay-case-policy';
|
|
import {
|
|
applyHollowShardGuard, buildPaidShardArgs, buildRunManifest,
|
|
DEFAULT_SHARD_TIMEOUT_MS, isOverlayTestFile, OVERLAY_MAX_ACTIVE_SHARDS,
|
|
parseCliOptions, parseRunManifest, planPaidShards, resolvePaidShardBudget, resolvePaidShardTimeoutMs,
|
|
retriesForFiles, runPaidShards, summarize, summaryExitCode,
|
|
} from '../scripts/test-paid-shards';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const overlayFiles = Object.values(OVERLAY_CASE_FILES).map(id => `test/skill-e2e-overlay-harness-${id}.test.ts`);
|
|
const normalFile = 'test/skill-e2e-normal-fixture.test.ts';
|
|
const fakeEnv = {
|
|
PATH: path.dirname(process.execPath),
|
|
GSTACK_CLAUDE_CLI_VERSION: 'free-fixture',
|
|
GSTACK_SDK_MAX_CONCURRENCY: '7',
|
|
};
|
|
|
|
describe('overlay file policy', () => {
|
|
test('grouped planning isolates every overlay and preserves ordinary retries', () => {
|
|
const workflow = 'test/skill-e2e-workflow.test.ts';
|
|
const files = [...overlayFiles, normalFile, workflow];
|
|
for (const maxFilesPerShard of [2, 3, 10]) {
|
|
const shards = planPaidShards(files, { maxFilesPerShard });
|
|
expect(shards.flat().sort()).toEqual([...files].sort());
|
|
for (const file of overlayFiles) expect(shards).toContainEqual([file]);
|
|
const workflowShard = shards.find(shard => shard.includes(workflow))!;
|
|
expect(workflowShard.some(isOverlayTestFile)).toBe(false);
|
|
expect(retriesForFiles(workflowShard)).toBe(2);
|
|
const args = buildPaidShardArgs(workflowShard, resolvePaidShardTimeoutMs(workflowShard), 2, retriesForFiles(workflowShard));
|
|
expect(args[args.indexOf('--retry') + 1]).toBe('2');
|
|
expect(planPaidShards(files.map(file => file.replaceAll('/', '\\')), { maxFilesPerShard })).toEqual(shards);
|
|
}
|
|
});
|
|
|
|
test('mixed or grouped overlay jobs reject before any child starts', async () => {
|
|
const invalidGroups = [[overlayFiles[0], normalFile], [overlayFiles[0], overlayFiles[1]]];
|
|
for (const files of invalidGroups) {
|
|
expect(() => resolvePaidShardBudget(files)).toThrow('own shard');
|
|
expect(() => resolvePaidShardBudget(files, 1_900_000)).toThrow('own shard');
|
|
let launched = 0;
|
|
await expect(runPaidShards([[normalFile], files], {
|
|
commandFor: () => { launched++; throw new Error('must never launch'); },
|
|
})).rejects.toThrow('own shard');
|
|
expect(launched).toBe(0);
|
|
}
|
|
});
|
|
|
|
test('only the exact wrapper family gets one attempt and the extra process grace', () => {
|
|
expect(overlayFiles).toHaveLength(4);
|
|
expect(OVERLAY_MAX_ACTIVE_SHARDS).toBe(1);
|
|
expect(OVERLAY_MIN_FILE_WALL_MS).toBe(1_830_000);
|
|
for (const file of overlayFiles) {
|
|
expect(isOverlayTestFile(file)).toBe(true);
|
|
expect(isOverlayTestFile(file.replaceAll('/', '\\'))).toBe(true);
|
|
expect(resolvePaidShardTimeoutMs([file])).toBe(1_830_000);
|
|
expect(retriesForFiles([file])).toBe(0);
|
|
expect(buildPaidShardArgs([file], resolvePaidShardTimeoutMs([file]), 2, retriesForFiles([file])))
|
|
.toContain('--timeout=1830000');
|
|
}
|
|
for (const file of [normalFile, 'test/skill-e2e-overlay-harness.test.ts', 'test/model-overlays.test.ts']) {
|
|
expect(isOverlayTestFile(file)).toBe(false);
|
|
expect(resolvePaidShardTimeoutMs([file])).toBe(DEFAULT_SHARD_TIMEOUT_MS);
|
|
expect(retriesForFiles([file])).toBe(1);
|
|
}
|
|
expect(resolvePaidShardTimeoutMs([normalFile], 1234)).toBe(1234);
|
|
expect(resolvePaidShardTimeoutMs([overlayFiles[0]], 1_900_000)).toBe(1_900_000);
|
|
expect(() => resolvePaidShardTimeoutMs([overlayFiles[0]], 1_800_000)).toThrow('explicit wall');
|
|
});
|
|
|
|
test('default, env and CLI walls retain their distinct meanings and CLI precedence', () => {
|
|
expect(parseCliOptions([], {}).timeoutExplicit).toBe(false);
|
|
const env = { EVALS_SHARD_TIMEOUT_MS: '1800000' };
|
|
const configured = parseCliOptions([], env);
|
|
expect(configured.timeoutExplicit).toBe(true);
|
|
expect(() => resolvePaidShardTimeoutMs([overlayFiles[0]], configured.timeoutMs)).toThrow('explicit wall');
|
|
const overridden = parseCliOptions(['--timeout', '1900'], env);
|
|
expect(overridden.timeoutExplicit).toBe(true);
|
|
expect(resolvePaidShardTimeoutMs([overlayFiles[0]], overridden.timeoutMs)).toBe(1_900_000);
|
|
expect(env.EVALS_SHARD_TIMEOUT_MS).toBe('1800000');
|
|
});
|
|
|
|
test('an invalid explicit wall rejects the whole batch before starting any normal or overlay child', async () => {
|
|
let launched = 0;
|
|
await expect(runPaidShards([[normalFile], [overlayFiles[0]]], {
|
|
timeoutMs: 1_800_000,
|
|
commandFor: () => { launched++; throw new Error('must never launch'); },
|
|
})).rejects.toThrow('explicit wall');
|
|
expect(launched).toBe(0);
|
|
});
|
|
|
|
test('CLI rejects insufficient overlay walls before API preflight', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'overlay-wall-preflight-'));
|
|
try {
|
|
const manifest = {
|
|
version: 1, tier: 'periodic', evalsAll: true, sliceCount: 1, selectionReason: 'free fixture',
|
|
entries: [{ file: overlayFiles[0], slice: 1, status: 'planned' }],
|
|
};
|
|
const planPath = path.join(dir, 'manifest.json');
|
|
fs.writeFileSync(planPath, JSON.stringify(manifest));
|
|
const result = spawnSync(process.execPath, [path.join(ROOT, 'scripts/test-paid-shards.ts'),
|
|
'--tier', 'periodic', '--plan', planPath, '--slice', '1', '--timeout', '1800'], {
|
|
cwd: ROOT, encoding: 'utf8', timeout: 10_000,
|
|
// No credentials or preflight bypass. A nonempty missing PATH avoids
|
|
// runtime fallback to a default shell search path during negative probes.
|
|
env: { PATH: path.join(dir, 'no-executables'), HOME: dir, GSTACK_EVAL_DIR: dir, GSTACK_CLAUDE_CLI_VERSION: 'free-fixture',
|
|
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}) },
|
|
});
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('Overlay shard requires at least 1830000ms; explicit wall 1800000ms');
|
|
expect(result.stderr).not.toContain('preflight');
|
|
expect(fs.existsSync(path.join(dir, 'slice-1.json'))).toBe(false);
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('overlay manifest affinity and CI capacity', () => {
|
|
test('actual lifecycle wrapper guards include all four in periodic and exclude all four from gate', () => {
|
|
for (const tier of ['periodic', 'gate'] as const) {
|
|
const manifest = buildRunManifest({ tier, sliceCount: 6, evalsAll: true, env: { EVALS_ALL: '1' } });
|
|
const entries = manifest.entries.filter(entry => isOverlayTestFile(entry.file));
|
|
expect(entries).toHaveLength(4);
|
|
expect(entries.every(entry => entry.status === (tier === 'periodic' ? 'planned' : 'excluded'))).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('93 files retain every case, reserve slice six, and fit 330 minutes with actual family walls', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'overlay-affinity-'));
|
|
const normalFiles = Array.from({ length: 89 }, (_, i) => `test/skill-e2e-normal-${i.toString().padStart(2, '0')}.test.ts`);
|
|
const discovered = [...normalFiles, ...overlayFiles];
|
|
try {
|
|
fs.mkdirSync(path.join(dir, 'test'));
|
|
for (const file of discovered) {
|
|
fs.writeFileSync(path.join(dir, file), isOverlayTestFile(file)
|
|
? "const shouldRun = process.env.EVALS_TIER === 'periodic';\n" : '// Free census fixture only.\n');
|
|
}
|
|
const opts = { tier: 'periodic' as const, sliceCount: 6, evalsAll: true, discovered, rootDir: dir, env: { EVALS_ALL: '1' } };
|
|
const manifest = buildRunManifest(opts);
|
|
expect(manifest.entries).toHaveLength(93);
|
|
expect(new Set(manifest.entries.map(e => e.file)).size).toBe(93);
|
|
expect(manifest.entries.every(e => e.status === 'planned')).toBe(true);
|
|
const counts = [1, 2, 3, 4, 5, 6].map(slice => manifest.entries.filter(e => e.slice === slice).length);
|
|
expect(counts).toEqual([18, 18, 18, 18, 17, 4]);
|
|
expect(manifest.entries.filter(e => e.slice === 6).map(e => e.file).sort()).toEqual([...overlayFiles].sort());
|
|
expect(buildRunManifest({ ...opts, discovered: [...discovered].reverse() })).toEqual(manifest);
|
|
expect(parseRunManifest(JSON.stringify(manifest))).toEqual(manifest);
|
|
const stale = { ...manifest, entries: manifest.entries.map(e => isOverlayTestFile(e.file) ? { ...e, slice: 1 } : e) };
|
|
expect(() => parseRunManifest(JSON.stringify(stale))).toThrow('final ordinary slice');
|
|
|
|
const workflow = Bun.YAML.parse(fs.readFileSync(path.join(ROOT, '.github/workflows/evals-periodic.yml'), 'utf8')) as {
|
|
jobs: Record<string, {
|
|
steps: Array<{ run?: string; env?: NodeJS.ProcessEnv }>;
|
|
strategy: { matrix: { slice: number[] } };
|
|
'timeout-minutes': number;
|
|
}>;
|
|
};
|
|
const job = workflow.jobs['eval-slices'];
|
|
const step = job.steps.find(step => step.run?.includes('--plan '))!;
|
|
const jobs = parseCliOptions([], step.env).jobs;
|
|
expect(jobs).toBe(2);
|
|
expect(parseCliOptions([], step.env).withinShardConcurrency).toBe(2);
|
|
expect(job.strategy.matrix.slice).toEqual([1, 2, 3, 4, 5, 6, 7]);
|
|
const normalMinutes = Math.ceil(18 / jobs) * resolvePaidShardTimeoutMs([normalFiles[0]]) / 60_000;
|
|
const overlayMinutes = Math.ceil(overlayFiles.length / OVERLAY_MAX_ACTIVE_SHARDS)
|
|
* Math.max(...overlayFiles.map(file => resolvePaidShardTimeoutMs([file]))) / 60_000;
|
|
expect(normalMinutes).toBe(270);
|
|
expect(overlayMinutes).toBe(122);
|
|
expect(job['timeout-minutes']).toBeGreaterThanOrEqual(Math.max(normalMinutes, overlayMinutes) + 20);
|
|
|
|
// Gate selection keeps its original periodic exclusion and all six
|
|
// ordinary slices; reservation does not spend an empty slot in gate.
|
|
const gate = buildRunManifest({ ...opts, tier: 'gate' });
|
|
expect(gate.entries.filter(e => e.status === 'excluded').map(e => e.file).sort()).toEqual([...overlayFiles].sort());
|
|
expect(gate.entries.filter(e => e.status === 'planned' && e.slice === 6)).toHaveLength(14);
|
|
const single = buildRunManifest({ ...opts, sliceCount: 1 });
|
|
expect(single.entries.every(e => e.slice === 1)).toBe(true);
|
|
expect(parseRunManifest(JSON.stringify(single))).toEqual(single);
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('overlay admission uses real child processes', () => {
|
|
test('one overlay holds its slot while ordinary work proceeds; failure releases admission', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'overlay-admission-'));
|
|
const receipts = path.join(dir, 'events.jsonl');
|
|
const lines: string[] = [];
|
|
try {
|
|
const summary = await runPaidShards([[overlayFiles[0]], [overlayFiles[1]], [normalFile]], {
|
|
jobs: 2, env: fakeEnv, logDir: dir, log: line => lines.push(line),
|
|
commandFor: files => {
|
|
const role = files[0] === overlayFiles[0] ? 'first' : files[0] === overlayFiles[1] ? 'second' : 'normal';
|
|
return {
|
|
command: process.execPath,
|
|
args: [path.join(ROOT, 'test/fixtures/overlay-admission-child.ts'), dir, role, receipts],
|
|
};
|
|
},
|
|
});
|
|
expect(summary.outcomes.map(o => o.status)).toEqual(['failed', 'passed', 'passed']);
|
|
expect(summary.outcomes[0].exitCode).toBe(3);
|
|
const events = fs.readFileSync(receipts, 'utf8').trim().split('\n').map(line => JSON.parse(line));
|
|
expect(events.map(e => `${e.role}:${e.event}`)).toEqual([
|
|
'first:start', 'normal:ran-during-overlay', 'first:end', 'second:start', 'second:end',
|
|
]);
|
|
expect(events.every(e => e.sdk === '7')).toBe(true);
|
|
expect(lines.filter(line => line.includes('START') && line.includes('timeout 1830s'))).toHaveLength(2);
|
|
expect(lines.filter(line => line.includes('START') && line.includes('timeout 1800s'))).toHaveLength(1);
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}, 30_000);
|
|
|
|
test('a thrown overlay launch releases admission and hollow output remains a failing verdict', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'overlay-admission-throw-'));
|
|
try {
|
|
const summary = await runPaidShards([[overlayFiles[0]], [overlayFiles[1]], [normalFile]], {
|
|
jobs: 8, env: fakeEnv, logDir: dir, log: () => {},
|
|
commandFor: files => {
|
|
if (files[0] === overlayFiles[0]) throw new Error('fake overlay launch failure');
|
|
const count = files[0] === overlayFiles[1] ? 0 : 1;
|
|
return { command: process.execPath, args: ['-e', `console.log('Ran ${count} tests across 1 file. [1ms]')`] };
|
|
},
|
|
});
|
|
const guarded = applyHollowShardGuard(summary.outcomes, { evalsAll: true });
|
|
expect(guarded.map(o => o.status)).toEqual(['failed', 'passed-empty', 'passed']);
|
|
expect(summaryExitCode(summarize(guarded))).toBe(1);
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|