import { filterEntryServices, filterMatrixForJob, mergeRequiredTiers, normalizeJobEntry, parseMatrixInput, parseRawConfig, resolveJobs, } from './parse'; import { JobDefaults, Matrix } from './types'; const FULL_SERVICES = { mysql: { image: 'mysql:8' }, opensearch: { image: 'opensearchproject/opensearch:2' }, rabbitmq: { image: 'rabbitmq:3' }, valkey: { image: 'valkey:8' }, nginx: { image: 'nginx:1.27' }, 'php-fpm': { image: 'php:8.3-fpm' }, }; const MATRIX: Matrix = { include: [{ php: '8.3', services: { ...FULL_SERVICES } }], }; const noDefaults: JobDefaults = { services: [] }; const smokeDefaults: JobDefaults = { services: ['search', 'queue', 'cache', 'web'] }; describe('normalizeJobEntry', () => { it('defaults enabled=true and uses the default tiers when entry is undefined', () => { expect(normalizeJobEntry('smoke-test', undefined, smokeDefaults)).toEqual({ enabled: true, tiers: smokeDefaults.services, }); }); it('treats true shorthand as enabled with defaults', () => { expect(normalizeJobEntry('smoke-test', true, smokeDefaults)).toEqual({ enabled: true, tiers: smokeDefaults.services, }); }); it('treats false shorthand as disabled with defaults', () => { expect(normalizeJobEntry('smoke-test', false, smokeDefaults)).toEqual({ enabled: false, tiers: smokeDefaults.services, }); }); it('empty object is enabled with defaults', () => { expect(normalizeJobEntry('smoke-test', {}, smokeDefaults)).toEqual({ enabled: true, tiers: smokeDefaults.services, }); }); it('preserves enabled when explicitly set', () => { expect(normalizeJobEntry('smoke-test', { enabled: false }, smokeDefaults)).toEqual({ enabled: false, tiers: smokeDefaults.services, }); }); it('overrides the default tiers when services is set', () => { expect(normalizeJobEntry('smoke-test', { services: ['cache', 'web'] }, smokeDefaults)).toEqual({ enabled: true, tiers: ['cache', 'web'], }); }); it('accepts an empty services array as "no services"', () => { expect(normalizeJobEntry('smoke-test', { services: [] }, smokeDefaults)).toEqual({ enabled: true, tiers: [], }); }); it('throws when entry is a non-array primitive other than boolean', () => { expect(() => normalizeJobEntry('unit-test', 'true' as never, noDefaults)).toThrowError( /must be a boolean or an object/ ); }); it('throws when entry is an array', () => { expect(() => normalizeJobEntry('unit-test', [] as never, noDefaults)).toThrowError(/got array/); }); it('throws when services is not an array', () => { expect(() => normalizeJobEntry('smoke-test', { services: 'search' } as never, smokeDefaults)).toThrowError( /services must be an array of tier names/ ); }); it('throws when services contains an unknown tier', () => { expect(() => normalizeJobEntry('smoke-test', { services: ['llm'] } as never, smokeDefaults)).toThrowError( /services contains unknown tier "llm"/ ); }); }); describe('mergeRequiredTiers', () => { it('returns the input list when required is undefined', () => { expect(mergeRequiredTiers(['cache'], undefined)).toEqual(['cache']); }); it('returns the input list when required is empty', () => { expect(mergeRequiredTiers(['cache'], [])).toEqual(['cache']); }); it('prepends required tiers ahead of the input tiers', () => { expect(mergeRequiredTiers(['cache', 'web'], ['db'])).toEqual(['db', 'cache', 'web']); }); it('deduplicates when a required tier already appears in the input', () => { expect(mergeRequiredTiers(['db', 'cache'], ['db'])).toEqual(['db', 'cache']); }); it('deduplicates within required itself', () => { expect(mergeRequiredTiers(['cache'], ['db', 'db'])).toEqual(['db', 'cache']); }); }); describe('filterEntryServices', () => { it('returns services={} for an empty tier list', () => { const out = filterEntryServices({ php: '8.3', services: FULL_SERVICES }, []); expect(out.services).toEqual({}); expect(out.php).toBe('8.3'); }); it('keeps only services in the requested tiers', () => { const out = filterEntryServices({ php: '8.3', services: FULL_SERVICES }, ['cache', 'web']); expect(Object.keys(out.services!).sort()).toEqual(['nginx', 'php-fpm', 'valkey']); }); it('drops services that the matrix doesn\'t carry (elasticsearch absent)', () => { const out = filterEntryServices({ services: { opensearch: FULL_SERVICES.opensearch } }, ['search']); expect(Object.keys(out.services!)).toEqual(['opensearch']); }); it('emits services={} when the entry has no services map', () => { const out = filterEntryServices({ php: '8.3' }, ['cache']); expect(out.services).toEqual({}); }); }); describe('filterMatrixForJob', () => { it('preserves matrix shape, mapping every entry through filterEntryServices', () => { const out = filterMatrixForJob(MATRIX, ['queue']); expect(out.include).toHaveLength(1); expect(Object.keys(out.include[0].services!)).toEqual(['rabbitmq']); }); it('passes through unrelated top-level matrix keys', () => { const out = filterMatrixForJob({ ...MATRIX, magento: ['2.4.7'] } as Matrix, []); expect((out as Matrix).magento).toEqual(['2.4.7']); }); }); describe('resolveJobs', () => { const jobs: Record = { 'unit-test': noDefaults, 'smoke-test': smokeDefaults, }; it('emits every known job, defaulted-enabled, when raw is empty', () => { const out = resolveJobs({}, 'store', jobs, MATRIX); expect(Object.keys(out).sort()).toEqual(['smoke-test', 'unit-test']); expect(out['unit-test'].enabled).toBe(true); expect(out['smoke-test'].enabled).toBe(true); }); it('emits services={} on entries for a no-default job', () => { const out = resolveJobs({}, 'store', jobs, MATRIX); expect(out['unit-test'].matrix.include[0].services).toEqual({}); }); it('expands the smoke-test default tiers across the matrix entry', () => { const out = resolveJobs({}, 'store', jobs, MATRIX); expect(Object.keys(out['smoke-test'].matrix.include[0].services!).sort()).toEqual([ 'nginx', 'opensearch', 'php-fpm', 'rabbitmq', 'valkey', ]); }); it('applies a caller-supplied services override', () => { const out = resolveJobs( { jobs: { 'smoke-test': { services: ['cache'] } } }, 'store', jobs, MATRIX, ); expect(Object.keys(out['smoke-test'].matrix.include[0].services!)).toEqual(['valkey']); }); it('always merges requiredServices into the matrix even when caller overrides services', () => { const withRequired: Record = { 'smoke-test': { ...smokeDefaults, requiredServices: ['db'] }, }; const out = resolveJobs( { jobs: { 'smoke-test': { services: ['cache'] } } }, 'store', withRequired, MATRIX, ); expect(Object.keys(out['smoke-test'].matrix.include[0].services!).sort()).toEqual(['mysql', 'valkey']); }); it('keeps requiredServices even when caller overrides services to []', () => { const withRequired: Record = { 'smoke-test': { ...smokeDefaults, requiredServices: ['db'] }, }; const out = resolveJobs( { jobs: { 'smoke-test': { services: [] } } }, 'store', withRequired, MATRIX, ); expect(Object.keys(out['smoke-test'].matrix.include[0].services!)).toEqual(['mysql']); }); it('honors caller enabled=false but still emits a filtered matrix', () => { const out = resolveJobs( { jobs: { 'smoke-test': false } }, 'store', jobs, MATRIX, ); expect(out['smoke-test'].enabled).toBe(false); expect(out['smoke-test'].matrix.include[0].services).toBeDefined(); }); it('throws on unknown job names with the kind in the message', () => { expect(() => resolveJobs({ jobs: { taco: false } }, 'store', jobs, MATRIX)).toThrowError( /unknown job "taco" for kind "store"/ ); }); it('throws when `jobs` is not an object', () => { expect(() => resolveJobs({ jobs: 'oops' } as never, 'store', jobs, MATRIX)).toThrowError( /`jobs` must be an object/ ); }); }); describe('parseRawConfig', () => { it('returns an empty object for empty input', () => { expect(parseRawConfig('')).toEqual({}); }); it('returns an empty object for whitespace input', () => { expect(parseRawConfig(' \n ')).toEqual({}); }); it('parses a valid object', () => { expect(parseRawConfig('{"jobs": {"unit-test": true}}')).toEqual({ jobs: { 'unit-test': true }, }); }); it('throws on syntactically invalid JSON', () => { expect(() => parseRawConfig('{not json}')).toThrowError(/failed to parse JSON/); }); it('throws when top level is an array', () => { expect(() => parseRawConfig('[]')).toThrowError(/top-level value must be an object/); }); it('throws when top level is a primitive', () => { expect(() => parseRawConfig('"hello"')).toThrowError(/top-level value must be an object/); }); it('throws when top level is null', () => { expect(() => parseRawConfig('null')).toThrowError(/top-level value must be an object/); }); }); describe('parseMatrixInput', () => { it('parses a valid matrix', () => { const out = parseMatrixInput('{"include": [{"php": "8.3"}]}'); expect(out.include).toEqual([{ php: '8.3' }]); }); it('throws on empty input', () => { expect(() => parseMatrixInput('')).toThrowError(/`matrix` input is required/); }); it('throws on invalid JSON', () => { expect(() => parseMatrixInput('{nope}')).toThrowError(/failed to parse `matrix` input/); }); it('throws when top level is an array', () => { expect(() => parseMatrixInput('[]')).toThrowError(/`matrix` must be a JSON object/); }); it('throws when include is missing', () => { expect(() => parseMatrixInput('{}')).toThrowError(/`matrix.include` must be an array/); }); it('throws when include is not an array', () => { expect(() => parseMatrixInput('{"include": "nope"}')).toThrowError(/`matrix.include` must be an array/); }); });