import { readdirSync, readFileSync } from 'fs'; import { join } from 'path'; import { applyHiveOps } from './hive-ops'; import { validateHiveState } from './hive-validate'; import type { HiveOp, HiveState } from './hive-types'; import { FIXTURE_DIR, deepFreeze, loadFixtureCatalog } from './hive-test-utils'; interface FixtureCase { name: string; inactivePartTypeIds?: string[]; state: HiveState; ops: HiveOp[]; expect: | { ok: true; state: HiveState } | { ok: false; opIndex: number; reasonCode: string }; } const cases = readdirSync(FIXTURE_DIR) .filter((file) => file.endsWith('.json') && file !== 'catalog.json') .sort() .map((file) => ({ file, data: JSON.parse( readFileSync(join(FIXTURE_DIR, file), 'utf8'), ) as FixtureCase, })); function findCase(name: string): FixtureCase { const found = cases.find((c) => c.data.name === name); if (!found) throw new Error(`fixture not found: ${name}`); return found.data; } describe('applyHiveOps fixtures', () => { it('has at least 20 cases', () => { expect(cases.length).toBeGreaterThanOrEqual(20); }); it.each(cases.map((c) => [c.file, c.data] as const))('%s', (_file, data) => { const catalog = loadFixtureCatalog(data.inactivePartTypeIds); const result = applyHiveOps( deepFreeze(structuredClone(data.state)), deepFreeze(structuredClone(data.ops)), catalog, ); if (data.expect.ok) { expect(result).toEqual({ ok: true, state: data.expect.state }); // The expected end state must itself be valid. expect(validateHiveState(data.expect.state, catalog)).toEqual([]); } else { expect(result.ok).toBe(false); if (!result.ok) { expect(result.opIndex).toBe(data.expect.opIndex); expect(result.reason.split(':')[0]).toBe(data.expect.reasonCode); } } }); }); describe('applyHiveOps purity', () => { it('does not mutate a deep-frozen input state or ops', () => { const catalog = loadFixtureCatalog(); const sample = findCase('multiple ops apply in order'); const state = deepFreeze(structuredClone(sample.state)); const ops = deepFreeze(structuredClone(sample.ops)); const before = JSON.stringify(state); const result = applyHiveOps(state, ops, catalog); expect(result.ok).toBe(true); expect(JSON.stringify(state)).toBe(before); }); it('does not share references between input and result', () => { const catalog = loadFixtureCatalog(); const state = structuredClone( findCase('setLabel sets and clears a label').state, ); const result = applyHiveOps(state, [], catalog); expect(result.ok).toBe(true); if (result.ok) { expect(result.state).not.toBe(state); expect(result.state.stack[0]).not.toBe(state.stack[0]); } }); }); describe('validateHiveState', () => { const catalog = loadFixtureCatalog(); it('rejects non-objects and wrong schemaVersion', () => { expect(validateHiveState(null, catalog).length).toBeGreaterThan(0); expect( validateHiveState({ schemaVersion: 2, stack: [] }, catalog).length, ).toBeGreaterThan(0); }); it('collects several violations', () => { const state = { schemaVersion: 1, stack: [ { id: 'a', partTypeId: 'hive-part-zl10-roof', state: {} }, { id: 'a', partTypeId: 'hive-part-zl10-floor', state: { x: 1 } }, ], }; const codes = validateHiveState(state, catalog).map((r) => r.split(':')[0]); expect(codes).toEqual( expect.arrayContaining([ 'DUPLICATE_ID', 'RULE_FLOOR_BOTTOM', 'RULE_ROOF_TOP', 'STATE_UNKNOWN_KEY', ]), ); }); });