mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
add gstack 2 parity and lifecycle gates
This commit is contained in:
@@ -3,10 +3,17 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import {
|
||||
DEFAULT_MAX_FILES_PER_SHARD,
|
||||
FREE_TEST_ROOTS,
|
||||
isFreeTestFile,
|
||||
collectFreeTestFiles,
|
||||
containsScheduledProcessExitZero,
|
||||
containsTopLevelProcessEnvMutation,
|
||||
detectWindowsFragility,
|
||||
curateWindowsSafe,
|
||||
hasScheduledProcessExitZero,
|
||||
hasTopLevelProcessEnvMutation,
|
||||
planBoundedFreeTestShards,
|
||||
stableHash,
|
||||
assignFilesToShards,
|
||||
normalizeRelativePath,
|
||||
@@ -15,6 +22,16 @@ import {
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
|
||||
describe('test-free-shards: enumeration', () => {
|
||||
test('uses the canonical five free-test roots', () => {
|
||||
expect(FREE_TEST_ROOTS).toEqual([
|
||||
'browse/test',
|
||||
'test',
|
||||
'make-pdf/test',
|
||||
'design/test',
|
||||
'ios-qa/daemon/test',
|
||||
]);
|
||||
});
|
||||
|
||||
test('isFreeTestFile rejects non-test files', () => {
|
||||
expect(isFreeTestFile('test/foo.ts')).toBe(false);
|
||||
expect(isFreeTestFile('test/foo.test.ts')).toBe(true);
|
||||
@@ -23,8 +40,10 @@ describe('test-free-shards: enumeration', () => {
|
||||
});
|
||||
|
||||
test('isFreeTestFile rejects paid eval tests', () => {
|
||||
expect(isFreeTestFile('browse/test/security-review-fullstack.test.ts')).toBe(false);
|
||||
expect(isFreeTestFile('test/skill-e2e-foo.test.ts')).toBe(false);
|
||||
expect(isFreeTestFile('test/skill-llm-eval.test.ts')).toBe(false);
|
||||
expect(isFreeTestFile('test/skill-routing-e2e.test.ts')).toBe(false);
|
||||
expect(isFreeTestFile('test/codex-e2e.test.ts')).toBe(false);
|
||||
expect(isFreeTestFile('test/gemini-e2e.test.ts')).toBe(false);
|
||||
});
|
||||
@@ -125,4 +144,65 @@ describe('test-free-shards: sharding', () => {
|
||||
const b = assignFilesToShards(files, 5);
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
|
||||
test('detects scheduled exit-zero cleanup without treating direct fixture exits as scheduled', () => {
|
||||
const scheduledArrow = ['setTimeout(() => process', '.exit(0), 500);'].join('');
|
||||
const scheduledFunction = ['setImmediate(function cleanup() { process', '.exit(0); });'].join('');
|
||||
const directExit = ['process', '.exit(0);'].join('');
|
||||
const fixtureExit = ["const fixture = 'process", ".exit(0)';"].join('');
|
||||
expect(containsScheduledProcessExitZero(scheduledArrow)).toBe(true);
|
||||
expect(containsScheduledProcessExitZero(scheduledFunction)).toBe(true);
|
||||
expect(containsScheduledProcessExitZero(directExit)).toBe(false);
|
||||
expect(containsScheduledProcessExitZero(fixtureExit)).toBe(false);
|
||||
});
|
||||
|
||||
test('detects only definite column-zero process.env mutations', () => {
|
||||
expect(containsTopLevelProcessEnvMutation("process.env.GSTACK_HOME = '/tmp/test';")).toBe(true);
|
||||
expect(containsTopLevelProcessEnvMutation('delete process.env.GSTACK_HOME; // reset module setup')).toBe(true);
|
||||
expect(containsTopLevelProcessEnvMutation(" process.env.GSTACK_HOME = '/tmp/test';")).toBe(false);
|
||||
expect(containsTopLevelProcessEnvMutation('\tdelete process.env.GSTACK_HOME;')).toBe(false);
|
||||
expect(containsTopLevelProcessEnvMutation("process.env.GSTACK_HOME === '/tmp/test';")).toBe(false);
|
||||
expect(containsTopLevelProcessEnvMutation("// process.env.GSTACK_HOME = '/tmp/test';")).toBe(false);
|
||||
});
|
||||
|
||||
test('bounded planner isolates scheduled exits and module-scope env mutations', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bounded-shards-'));
|
||||
const files = [
|
||||
'a.test.ts',
|
||||
'b.test.ts',
|
||||
'c.test.ts',
|
||||
'd.test.ts',
|
||||
'module-env.test.ts',
|
||||
'scheduled.test.ts',
|
||||
];
|
||||
try {
|
||||
for (const file of files) fs.writeFileSync(path.join(dir, file), 'test("ok", () => {});');
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'scheduled.test.ts'),
|
||||
['afterAll(() => setTimeout(() => process', '.exit(0), 500));'].join(''),
|
||||
);
|
||||
fs.writeFileSync(path.join(dir, 'module-env.test.ts'), "process.env.GSTACK_HOME = '/tmp/test';");
|
||||
|
||||
const shards = planBoundedFreeTestShards(files, { rootDir: dir, maxFilesPerShard: 2 });
|
||||
expect(shards.flat().sort()).toEqual([...files].sort());
|
||||
expect(shards.find((shard) => shard.includes('scheduled.test.ts'))).toEqual(['scheduled.test.ts']);
|
||||
expect(shards.find((shard) => shard.includes('module-env.test.ts'))).toEqual(['module-env.test.ts']);
|
||||
expect(shards.every((shard) => shard.length <= 2)).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('repository plan is bounded and isolates every process-global test', () => {
|
||||
const files = collectFreeTestFiles(ROOT);
|
||||
const shards = planBoundedFreeTestShards(files, { rootDir: ROOT });
|
||||
expect(shards.flat().sort()).toEqual([...files].sort());
|
||||
expect(shards.every((shard) => shard.length <= DEFAULT_MAX_FILES_PER_SHARD)).toBe(true);
|
||||
for (const file of files.filter((candidate) => hasScheduledProcessExitZero(path.join(ROOT, candidate)))) {
|
||||
expect(shards.find((shard) => shard.includes(file))).toEqual([file]);
|
||||
}
|
||||
for (const file of files.filter((candidate) => hasTopLevelProcessEnvMutation(path.join(ROOT, candidate)))) {
|
||||
expect(shards.find((shard) => shard.includes(file))).toEqual([file]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user