/** * Unit tests for the Imhotep geometry constraint solver. * * Covers: * - Relation evaluation (leftOf, rightOf, above, below, overlaps, inside) * - Size evaluation (atLeast, atMost, between, aspectRatio) * - Quantifier behavior (all, any, none, pairwise) * - Proof generation */ import { describe, it } from 'node:test'; import assert from 'node:assert'; import { clearRegistry, type GeometryWorld, type ClauseDescriptor, } from './registry.js'; import { evaluateLeftOf, evaluateRightOf, evaluateAbove, evaluateBelow, evaluateOverlaps, evaluateInside, } from './relations.js'; import { evaluateAtLeast, evaluateAtMost, evaluateBetween, evaluateAspectRatio, } from './size.js'; import { evaluateQuantifier, type QuantifierClause } from './quantifiers.js'; import { generateProof, resetProofCounter } from './proofs.js'; import { evaluate, registerDefaultClauses, collectRequiredFacts, } from './engine.js'; // --- Test helpers ------------------------------------------------------------ function makeWorld(overrides?: Partial): GeometryWorld { return { sceneId: 'scene_1', snapshotId: 'snap_1', env: { viewportWidth: 1280, viewportHeight: 800, deviceScaleFactor: 1, colorScheme: 'light', pointer: 'fine', hover: false, reducedMotion: false, locale: 'en', writingMode: 'horizontal-tb', }, strings: { values: [] }, subjects: { ids: [1, 2], domNodeId: [10, 20], subjectKind: [1, 1], primaryBoxId: [100, 200], firstFragmentId: [0, 0], fragmentCount: [0, 0], }, dom: { nodeId: [10, 20], parentNodeId: [0, 0], childCount: [0, 0], tagNameStringId: [0, 0], }, boxes: { boxId: [100, 200], subjectId: [1, 2], frameId: [1, 1], borderLeft: [0, 110], borderTop: [0, 50], borderRight: [100, 210], borderBottom: [40, 90], paddingLeft: [0, 0], paddingTop: [0, 0], paddingRight: [0, 0], paddingBottom: [0, 0], contentLeft: [0, 0], contentTop: [0, 0], contentRight: [0, 0], contentBottom: [0, 0], }, visualBoxes: { boxId: [], subjectId: [], frameId: [], borderLeft: [], borderTop: [], borderRight: [], borderBottom: [], paddingLeft: [], paddingTop: [], paddingRight: [], paddingBottom: [], contentLeft: [], contentTop: [], contentRight: [], contentBottom: [], }, transforms: { transformId: [], subjectId: [], matrixStart: [], matrixLength: [], originX: [], originY: [], }, matrices: { values: [] }, rects: { rectId: [], left: [], top: [], right: [], bottom: [], }, topology: { containingBlockOf: [0, 0], nearestPositionedAncestorOf: [0, 0], scrollContainerOf: [0, 0], stackingContextOf: [0, 0], formattingContextOf: [0, 0], clippingRootOf: [0, 0], paintOrderBucket: [0, 0], paintOrderIndex: [0, 0], }, scroll: { containerId: [], scrollLeft: [], scrollTop: [], scrollWidth: [], scrollHeight: [], clientWidth: [], clientHeight: [], }, clipping: { clipNodeId: [], subjectId: [], clipKind: [], clipLeft: [], clipTop: [], clipRight: [], clipBottom: [], parentClipNodeId: [], }, visibility: { subjectId: [], isRendered: [], isVisible: [], visibleArea: [], clippedArea: [], }, ...overrides, }; } function makeClause(kind: string, overrides?: Partial): ClauseDescriptor { return { clauseId: 'clause_1', clauseKind: kind, version: 1, subjectRef: 1, referenceRef: 2, ...overrides, }; } // --- Relation tests ---------------------------------------------------------- describe('relation evaluators', () => { it('leftOf passes when gap is inside bounds', () => { const world = makeWorld(); const clause = makeClause('relation.leftOf', { bounds: { minGap: 5, maxGap: 15 } }); const result = evaluateLeftOf(world, clause); assert.strictEqual(result.status, 'pass'); assert.strictEqual(result.truth, 'determinate'); assert.strictEqual(result.metrics?.observedGap, 10); }); it('leftOf fails when gap is too small', () => { const world = makeWorld(); const clause = makeClause('relation.leftOf', { bounds: { minGap: 15, maxGap: 20 } }); const result = evaluateLeftOf(world, clause); assert.strictEqual(result.status, 'fail'); assert.strictEqual(result.metrics?.observedGap, 10); }); it('rightOf passes when gap is inside bounds', () => { // Subject 1 to the right of reference 2 with a 5px gap. const world = makeWorld({ boxes: { ...makeWorld().boxes, borderLeft: [105, 0], borderTop: [0, 0], borderRight: [205, 100], borderBottom: [40, 40], }, }); const clause = makeClause('relation.rightOf', { bounds: { minGap: 5, maxGap: 15 } }); const result = evaluateRightOf(world, clause); assert.strictEqual(result.status, 'pass'); assert.strictEqual(result.metrics?.observedGap, 5); }); it('above passes when vertical gap is inside bounds', () => { const world = makeWorld(); const clause = makeClause('relation.above', { bounds: { minGap: 5, maxGap: 15 } }); const result = evaluateAbove(world, clause); assert.strictEqual(result.status, 'pass'); assert.strictEqual(result.metrics?.observedGap, 10); }); it('below passes when vertical gap is inside bounds', () => { // Subject 1 below reference 2 with a 5px gap. const world = makeWorld({ boxes: { ...makeWorld().boxes, borderLeft: [0, 0], borderTop: [45, 0], borderRight: [100, 100], borderBottom: [85, 40], }, }); const clause = makeClause('relation.below', { bounds: { minGap: 5, maxGap: 15 } }); const result = evaluateBelow(world, clause); assert.strictEqual(result.status, 'pass'); assert.strictEqual(result.metrics?.observedGap, 5); }); it('leftOf without gap bounds fails when subject is to the right', () => { // Subject 1 at [150,250], reference 2 at [0,100] — subject is to the right const world = makeWorld({ boxes: { ...makeWorld().boxes, borderLeft: [150, 0], borderRight: [250, 100], }, }); const clause = makeClause('relation.leftOf'); // no bounds const result = evaluateLeftOf(world, clause); assert.strictEqual(result.status, 'fail'); }); it('rightOf without gap bounds fails when subject is to the left', () => { const world = makeWorld(); // subject [0,100], reference [110,210] const clause = makeClause('relation.rightOf'); // no bounds const result = evaluateRightOf(world, clause); assert.strictEqual(result.status, 'fail'); }); it('above without gap bounds fails when subject is below', () => { // Subject 1 at [50,90], reference 2 at [0,40] — subject is below const world = makeWorld({ boxes: { ...makeWorld().boxes, borderTop: [50, 0], borderBottom: [90, 40], }, }); const clause = makeClause('relation.above'); // no bounds const result = evaluateAbove(world, clause); assert.strictEqual(result.status, 'fail'); }); it('below without gap bounds fails when subject is above', () => { const world = makeWorld(); // subject [0,40], reference [50,90] const clause = makeClause('relation.below'); // no bounds const result = evaluateBelow(world, clause); assert.strictEqual(result.status, 'fail'); }); it('overlaps detects intersection', () => { const world = makeWorld(); // Overlap: subject 1 is [0,0,100,40], reference 2 is [110,50,210,90] — no overlap let result = evaluateOverlaps(world, makeClause('relation.overlaps')); assert.strictEqual(result.status, 'fail'); // Move reference to overlap const overlappingWorld = makeWorld({ boxes: { ...makeWorld().boxes, borderLeft: [0, 50], borderTop: [0, 20], borderRight: [100, 150], borderBottom: [40, 60], }, }); result = evaluateOverlaps(overlappingWorld, makeClause('relation.overlaps')); assert.strictEqual(result.status, 'pass'); }); it('inside detects containment', () => { const world = makeWorld(); // Subject [0,0,100,40] is inside reference [110,50,210,90]? No. let result = evaluateInside(world, makeClause('relation.inside')); assert.strictEqual(result.status, 'fail'); const containedWorld = makeWorld({ boxes: { ...makeWorld().boxes, borderLeft: [120, 100], borderTop: [60, 50], borderRight: [180, 210], borderBottom: [80, 90], }, }); result = evaluateInside(containedWorld, makeClause('relation.inside')); assert.strictEqual(result.status, 'pass'); }); it('relation evaluators return error for missing subject', () => { const world = makeWorld(); const clause = makeClause('relation.leftOf', { subjectRef: 999 }); const result = evaluateLeftOf(world, clause); assert.strictEqual(result.status, 'error'); assert.strictEqual(result.truth, 'indeterminate'); }); }); // --- Size tests -------------------------------------------------------------- describe('size evaluators', () => { it('atLeast passes when dimension is large enough', () => { const world = makeWorld(); const clause = makeClause('size.atLeast', { bounds: { min: 80 }, options: { dimension: 'width' }, }); const result = evaluateAtLeast(world, clause); assert.strictEqual(result.status, 'pass'); assert.strictEqual(result.metrics?.observed, 100); }); it('atLeast fails when dimension is too small', () => { const world = makeWorld(); const clause = makeClause('size.atLeast', { bounds: { min: 120 }, options: { dimension: 'width' }, }); const result = evaluateAtLeast(world, clause); assert.strictEqual(result.status, 'fail'); }); it('atMost passes when dimension is small enough', () => { const world = makeWorld(); const clause = makeClause('size.atMost', { bounds: { max: 120 }, options: { dimension: 'width' }, }); const result = evaluateAtMost(world, clause); assert.strictEqual(result.status, 'pass'); }); it('between passes when dimension is inside range', () => { const world = makeWorld(); const clause = makeClause('size.between', { bounds: { min: 80, max: 120 }, options: { dimension: 'width' }, }); const result = evaluateBetween(world, clause); assert.strictEqual(result.status, 'pass'); assert.strictEqual(result.metrics?.observed, 100); }); it('aspectRatio passes when ratio is inside range', () => { const world = makeWorld(); // Subject 1: width 100, height 40 => ratio 2.5 const clause = makeClause('size.aspectRatio', { bounds: { minRatio: 2, maxRatio: 3 }, }); const result = evaluateAspectRatio(world, clause); assert.strictEqual(result.status, 'pass'); assert.strictEqual(result.metrics?.observed, 2.5); }); it('aspectRatio returns error when height is zero', () => { const world = makeWorld({ boxes: { ...makeWorld().boxes, borderBottom: [40, 90], borderTop: [40, 90], }, }); const clause = makeClause('size.aspectRatio'); const result = evaluateAspectRatio(world, clause); assert.strictEqual(result.status, 'error'); assert.strictEqual(result.truth, 'indeterminate'); }); }); // --- Quantifier tests -------------------------------------------------------- describe('quantifier evaluators', () => { it('all passes when every sub-result passes', () => { const subResults = [ { clauseId: 'c1', status: 'pass' as const, truth: 'determinate' as const }, { clauseId: 'c2', status: 'pass' as const, truth: 'determinate' as const }, ]; const clause: QuantifierClause = { clauseId: 'q1', clauseKind: 'quantifier.all', version: 1, quantifier: 'all', subClauseIds: ['c1', 'c2'], }; const result = evaluateQuantifier(subResults, clause); assert.strictEqual(result.status, 'pass'); assert.strictEqual(result.metrics?.total, 2); }); it('all fails when any sub-result fails', () => { const subResults = [ { clauseId: 'c1', status: 'pass' as const, truth: 'determinate' as const }, { clauseId: 'c2', status: 'fail' as const, truth: 'determinate' as const }, ]; const clause: QuantifierClause = { clauseId: 'q1', clauseKind: 'quantifier.all', version: 1, quantifier: 'all', subClauseIds: ['c1', 'c2'], }; const result = evaluateQuantifier(subResults, clause); assert.strictEqual(result.status, 'fail'); assert.strictEqual(result.metrics?.failed, 1); }); it('any passes when at least one sub-result passes', () => { const subResults = [ { clauseId: 'c1', status: 'fail' as const, truth: 'determinate' as const }, { clauseId: 'c2', status: 'pass' as const, truth: 'determinate' as const }, ]; const clause: QuantifierClause = { clauseId: 'q1', clauseKind: 'quantifier.any', version: 1, quantifier: 'any', subClauseIds: ['c1', 'c2'], }; const result = evaluateQuantifier(subResults, clause); assert.strictEqual(result.status, 'pass'); }); it('none passes when no sub-result passes', () => { const subResults = [ { clauseId: 'c1', status: 'fail' as const, truth: 'determinate' as const }, { clauseId: 'c2', status: 'fail' as const, truth: 'determinate' as const }, ]; const clause: QuantifierClause = { clauseId: 'q1', clauseKind: 'quantifier.none', version: 1, quantifier: 'none', subClauseIds: ['c1', 'c2'], }; const result = evaluateQuantifier(subResults, clause); assert.strictEqual(result.status, 'pass'); assert.strictEqual(result.metrics?.passed, 0); }); it('pairwise requires at least two sub-clauses', () => { const clause: QuantifierClause = { clauseId: 'q1', clauseKind: 'quantifier.pairwise', version: 1, quantifier: 'pairwise', subClauseIds: ['c1'], }; const result = evaluateQuantifier( [{ clauseId: 'c1', status: 'pass' as const, truth: 'determinate' as const }], clause, ); assert.strictEqual(result.status, 'error'); assert.strictEqual(result.truth, 'indeterminate'); }); it('quantifier returns error for empty sub-results', () => { const clause: QuantifierClause = { clauseId: 'q1', clauseKind: 'quantifier.all', version: 1, quantifier: 'all', subClauseIds: [], }; const result = evaluateQuantifier([], clause); assert.strictEqual(result.status, 'error'); assert.strictEqual(result.truth, 'indeterminate'); }); }); // --- Proof generation tests -------------------------------------------------- describe('proof generation', () => { it('generateProof creates a proof with outcome and witness', () => { resetProofCounter(); const world = makeWorld(); const clause = makeClause('relation.leftOf'); const result = evaluateLeftOf(world, clause); const proof = generateProof(result, clause, world, [101, 102]); assert.strictEqual(proof.proofId, 'proof_1'); assert.strictEqual(proof.clauseId, 'clause_1'); assert.strictEqual(proof.outcome, result.status); assert.strictEqual(proof.truth, result.truth); assert.deepStrictEqual(proof.usedFacts, [101, 102]); assert.strictEqual(proof.witness?.subjectId, 1); assert.strictEqual(proof.witness?.referenceId, 2); assert.strictEqual(proof.witness?.snapshotId, 'snap_1'); }); it('generateProof synthesizes failedPredicate on failure', () => { resetProofCounter(); const world = makeWorld(); const clause = makeClause('relation.leftOf', { bounds: { minGap: 50 } }); const result = evaluateLeftOf(world, clause); assert.strictEqual(result.status, 'fail'); const proof = generateProof(result, clause, world); assert.ok(proof.failedPredicate); assert.strictEqual(typeof proof.failedPredicate?.left, 'number'); assert.strictEqual(typeof proof.failedPredicate?.right, 'number'); }); it('generateProof omits failedPredicate on pass', () => { resetProofCounter(); const world = makeWorld(); const clause = makeClause('relation.leftOf'); const result = evaluateLeftOf(world, clause); assert.strictEqual(result.status, 'pass'); const proof = generateProof(result, clause, world); assert.strictEqual(proof.failedPredicate, undefined); }); }); // --- Engine integration tests ------------------------------------------------ describe('solver engine', () => { it('evaluate runs registered evaluators and produces proofs', () => { clearRegistry(); registerDefaultClauses(); const world = makeWorld(); const clauses: ClauseDescriptor[] = [ makeClause('relation.leftOf', { clauseId: 'c1', bounds: { minGap: 5 } }), makeClause('size.atLeast', { clauseId: 'c2', subjectRef: 1, referenceRef: undefined, bounds: { min: 50 }, options: { dimension: 'width' }, }), ]; const output = evaluate(world, clauses); assert.strictEqual(output.clauseResults.length, 2); assert.strictEqual(output.proofs.length, 2); assert.strictEqual(output.clauseResults[0].status, 'pass'); assert.strictEqual(output.clauseResults[1].status, 'pass'); }); it('collectRequiredFacts gathers facts across clauses', () => { clearRegistry(); registerDefaultClauses(); const clauses: ClauseDescriptor[] = [ makeClause('relation.leftOf'), makeClause('size.atLeast', { subjectRef: 1, referenceRef: undefined, }), ]; const facts = collectRequiredFacts(clauses); assert.ok(facts.includes('subject.primaryBox')); assert.ok(facts.includes('reference.primaryBox')); }); it('evaluate emits error for unregistered clause kinds', () => { clearRegistry(); const world = makeWorld(); const clauses: ClauseDescriptor[] = [ makeClause('relation.unknown', { clauseId: 'c1' }), ]; const output = evaluate(world, clauses); assert.strictEqual(output.clauseResults[0].status, 'error'); assert.ok(output.diagnostics.some((d) => d.code === 'IMH_EVALUATOR_MISSING')); }); });