import { describe, expect, test } from 'bun:test'; import { balance, depsOf, GLOBAL, HEAVY, junitTimes, listTests, mergeTimings, relatedTests, rustNeeded, shardCount } from '../tools/ci-plan'; describe('tools/ci-plan: test shards', () => { test('shard count: cores - 1, between 1 and 8, env override', () => { expect(shardCount({}, 10)).toBe(8); expect(shardCount({}, 4)).toBe(3); expect(shardCount({}, 1)).toBe(1); expect(shardCount({ STOREBOUND_CI_SHARDS: '3' }, 10)).toBe(3); expect(shardCount({ STOREBOUND_CI_SHARDS: 'x' }, 10)).toBe(8); }); test('balance: every file once, longest first, heavy suites never share a shard', () => { const cost: Record = { h1: 10, h2: 9, a: 5, b: 4, c: 3, d: 2, e: 1 }; const shards = balance(Object.keys(cost), 3, f => cost[f]!, ['h1', 'h2']); expect(shards.flat().sort()).toEqual(Object.keys(cost).sort()); for (const s of shards) expect(s.filter(f => f.startsWith('h')).length).toBeLessThanOrEqual(1); const loads = shards.map(s => s.reduce((n, f) => n + cost[f]!, 0)); expect(Math.max(...loads) - Math.min(...loads)).toBeLessThanOrEqual(cost.a!); }); test('balance: more heavy suites than shards still places each once; empty shards dropped', () => { expect(balance(['h1', 'h2', 'h3'], 2, () => 1, ['h1', 'h2', 'h3']).flat().sort()).toEqual(['h1', 'h2', 'h3']); expect(balance(['a'], 8, () => 1)).toEqual([['a']]); }); test('JUnit report: per-file seconds from the top-level suites only', () => { const xml = ` `; expect(junitTimes(xml)).toEqual({ 'tests/a.test.ts': 1.5, 'tests/b.test.ts': 0.25 }); }); test('recorded timings: a new file takes its time; a known one moves halfway, never up past the new time', () => { const t = mergeTimings({ 'a.test.ts': 10, 'b.test.ts': 40, 'keep.test.ts': 3 }, { 'a.test.ts': 30, 'b.test.ts': 20, 'new.test.ts': 5 }); expect(t).toEqual({ 'a.test.ts': 20, 'b.test.ts': 20, 'keep.test.ts': 3, 'new.test.ts': 5 }); }); test('every heavy suite exists and every test file is found', () => { const all = listTests(); for (const h of HEAVY) expect(all).toContain(h); expect(all).toContain('tests/ci-plan.test.ts'); expect(all).toContain('apps/desktop/tests/webview.test.ts'); expect(all.some(f => f.includes('node_modules') || f.includes('/fixtures/'))).toBe(false); }); }); describe('tools/ci-plan: related tests (ci:quick)', () => { const all = listTests(); test('imports, transitive imports and spawned CLI paths', () => { expect(depsOf('tests/ci-plan.test.ts')).toEqual(['tools/ci-plan.ts']); expect(relatedTests(['tools/ci-plan.ts'], all)).toEqual(['tests/ci-plan.test.ts']); // a CLI library change reaches the tests that import it directly and through other modules const upsert = relatedTests(['cli/lib/upsert.ts'], all); expect(upsert).toContain('tests/idempotency.test.ts'); expect(upsert.length).toBeGreaterThan(3); }); test('a changed test runs itself; a global file runs everything; docs run nothing', () => { expect(relatedTests(['tests/pure.test.ts'], all)).toEqual(['tests/pure.test.ts']); expect(relatedTests(['package.json'], all)).toEqual(all); expect(GLOBAL.test('bunfig.toml')).toBe(true); expect(relatedTests([['docs/nobody', 'reads.md'].join('-')], all)).toEqual([]); }); test('a browser suite runs for its own module and its near imports, not for every shared core module', () => { expect(relatedTests(['cli/lib/upsert.ts'], all, undefined, HEAVY)).not.toContain('tests/console-fill.test.ts'); expect(relatedTests(['cli/lib/upsert.ts'], all)).toContain('tests/console-fill.test.ts'); expect(relatedTests(['cli/console/fill.ts'], all, undefined, HEAVY)).toContain('tests/console-fill.test.ts'); expect(relatedTests(['tests/capture-web.test.ts'], all, undefined, HEAVY)).toEqual(['tests/capture-web.test.ts']); }); test('Rust steps only for the Tauri crate or the sidecar build, or with CI=full / unknown diff', () => { expect(rustNeeded(['cli/index.ts'], {})).toBe(false); expect(rustNeeded(['apps/desktop/src-tauri/src/main.rs'], {})).toBe(true); expect(rustNeeded(['tools/build-sidecars.ts'], {})).toBe(true); expect(rustNeeded([], { CI: 'full' })).toBe(true); expect(rustNeeded(null, {})).toBe(true); }); });