Files
ApiCore/ApiCore-Backend/src/hive/hive-ops.spec.ts
T
Nick SzaunigandClaude Opus 5.5 b48fb157c8
Backend Image / build-and-push (push) Successful in 5m5s
Frontend Image / build-and-push (push) Successful in 11m2s
feat: hive setup data model and API (Beute L1)
Each colony can now hold its hive setup (Ist-Zustand) as validated
JSON with an append-only change log:

- catalog: hive systems and part types with dimensions, placement
  rules and state fields; Liebig-Zander 10 seeded with fixed ids
  (owner's equipment incl. Bausperre, Futterzarge, Nassenheider)
- pure domain module (operations, validator, derived facts) with
  shared JSON fixtures the frontend runs too
- API: catalog (hive-systems:manage), setup create/change/history/
  state at a day (colonies:read/update), 409 HIVE_CONFLICT with the
  current state, idempotent writes, HiveChanged event
- inspections can carry hive ops saved in the same transaction
- backup export/import of the new tables (backup wins per colony)
- docs: ARCHITECTURE section 13, Annahmen 95-100

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-15 21:23:23 +02:00

115 lines
3.6 KiB
TypeScript

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',
]),
);
});
});