import { describe, it } from 'node:test'; import assert from 'node:assert'; import fc from 'fast-check'; import { type GeometryWorld, type ClauseDescriptor, } from './registry.js'; import { evaluateLeftOf, evaluateRightOf, evaluateAbove, evaluateBelow, evaluateOverlaps, evaluateInside, } from './relations.js'; import { evaluateQuantifier, type QuantifierClause, } from './quantifiers.js'; import { generateProof, resetProofCounter, } from './proofs.js'; // ---- Test Helpers ---- function makeWorldFromRects( sLeft: number, sTop: number, sRight: number, sBottom: number, rLeft: number, rTop: number, rRight: number, rBottom: number, ): 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: [sLeft, rLeft], borderTop: [sTop, rTop], borderRight: [sRight, rRight], borderBottom: [sBottom, rBottom], 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: [], }, }; } function makeClause(kind: string, overrides?: Partial): ClauseDescriptor { return { clauseId: 'clause_1', clauseKind: kind, version: 1, subjectRef: 1, referenceRef: 2, ...overrides, }; } // ---- Arbitraries ---- const coordArb = fc.integer({ min: 0, max: 500 }); const rectArb = fc .tuple(coordArb, coordArb, coordArb, coordArb) .filter(([l, t, r, b]) => l < r && t < b) .map(([l, t, r, b]) => ({ left: l, top: t, right: r, bottom: b })); const gapArb = fc.integer({ min: 0, max: 200 }); // ---- Property Tests ---- describe('solver property tests', () => { it('property: leftOf and rightOf are inverse relations with same gap', () => { fc.assert( fc.property( rectArb, rectArb, gapArb, (subjectRect, referenceRect, gap) => { // Place subject left of reference with exact gap const sLeft = 0; const sTop = 0; const sRight = sLeft + (subjectRect.right - subjectRect.left); const sBottom = sTop + (subjectRect.bottom - subjectRect.top); const rLeft = sRight + gap; const rTop = 0; const rRight = rLeft + (referenceRect.right - referenceRect.left); const rBottom = rTop + (referenceRect.bottom - referenceRect.top); // World where subject is left of reference const worldLR = makeWorldFromRects(sLeft, sTop, sRight, sBottom, rLeft, rTop, rRight, rBottom); // World where subject is right of reference (swapped) const worldRL = makeWorldFromRects(rLeft, rTop, rRight, rBottom, sLeft, sTop, sRight, sBottom); const leftOfClause = makeClause('relation.leftOf', { bounds: { minGap: gap, maxGap: gap }, }); const rightOfClause = makeClause('relation.rightOf', { bounds: { minGap: gap, maxGap: gap }, }); const leftResult = evaluateLeftOf(worldLR, leftOfClause); const rightResult = evaluateRightOf(worldRL, rightOfClause); assert.strictEqual(leftResult.status, 'pass'); assert.strictEqual(rightResult.status, 'pass'); assert.strictEqual(leftResult.metrics?.observedGap, gap); assert.strictEqual(rightResult.metrics?.observedGap, gap); } ), { numRuns: 200, seed: 42 } ); }); it('property: above and below are inverse relations', () => { fc.assert( fc.property( rectArb, rectArb, gapArb, (subjectRect, referenceRect, gap) => { const sLeft = 0; const sTop = 0; const sRight = sLeft + (subjectRect.right - subjectRect.left); const sBottom = sTop + (subjectRect.bottom - subjectRect.top); const rLeft = 0; const rTop = sBottom + gap; const rRight = rLeft + (referenceRect.right - referenceRect.left); const rBottom = rTop + (referenceRect.bottom - referenceRect.top); // World where subject is above reference const worldAB = makeWorldFromRects(sLeft, sTop, sRight, sBottom, rLeft, rTop, rRight, rBottom); // World where subject is below reference (swapped) const worldBA = makeWorldFromRects(rLeft, rTop, rRight, rBottom, sLeft, sTop, sRight, sBottom); const aboveClause = makeClause('relation.above', { bounds: { minGap: gap, maxGap: gap }, }); const belowClause = makeClause('relation.below', { bounds: { minGap: gap, maxGap: gap }, }); const aboveResult = evaluateAbove(worldAB, aboveClause); const belowResult = evaluateBelow(worldBA, belowClause); assert.strictEqual(aboveResult.status, 'pass'); assert.strictEqual(belowResult.status, 'pass'); assert.strictEqual(aboveResult.metrics?.observedGap, gap); assert.strictEqual(belowResult.metrics?.observedGap, gap); } ), { numRuns: 200, seed: 123 } ); }); it('property: if A is inside B, then B contains A', () => { fc.assert( fc.property( rectArb, fc.integer({ min: 0, max: 50 }), (innerRect, padding) => { const outerRect = { left: innerRect.left - padding, top: innerRect.top - padding, right: innerRect.right + padding, bottom: innerRect.bottom + padding, }; if (outerRect.left >= outerRect.right || outerRect.top >= outerRect.bottom) { return; // Skip invalid outer rect } // A inside B const world1 = makeWorldFromRects( innerRect.left, innerRect.top, innerRect.right, innerRect.bottom, outerRect.left, outerRect.top, outerRect.right, outerRect.bottom ); const insideClause = makeClause('relation.inside'); const insideResult = evaluateInside(world1, insideClause); assert.strictEqual(insideResult.status, 'pass', 'A should be inside B'); // B contains A (swap subject/reference) const world2 = makeWorldFromRects( outerRect.left, outerRect.top, outerRect.right, outerRect.bottom, innerRect.left, innerRect.top, innerRect.right, innerRect.bottom ); // Use leftOf with a huge gap as a proxy for contains, or just verify the geometry relation // Since there is no direct "contains" evaluator in relations.ts, we verify via inside logic: // If A is inside B, then B's rect fully contains A's rect. assert.ok( innerRect.left >= outerRect.left && innerRect.top >= outerRect.top && innerRect.right <= outerRect.right && innerRect.bottom <= outerRect.bottom, 'Containment geometry should hold' ); } ), { numRuns: 200, seed: 77 } ); }); it('property: if A overlaps B, then B overlaps A', () => { fc.assert( fc.property( rectArb, rectArb, (rectA, rectB) => { const world = makeWorldFromRects( rectA.left, rectA.top, rectA.right, rectA.bottom, rectB.left, rectB.top, rectB.right, rectB.bottom ); const overlapsClause = makeClause('relation.overlaps'); const resultAB = evaluateOverlaps(world, overlapsClause); // Swap subject and reference by creating a new world with swapped boxes const worldBA = makeWorldFromRects( rectB.left, rectB.top, rectB.right, rectB.bottom, rectA.left, rectA.top, rectA.right, rectA.bottom ); const resultBA = evaluateOverlaps(worldBA, overlapsClause); assert.strictEqual(resultAB.status, resultBA.status, 'Overlap should be symmetric'); } ), { numRuns: 300, seed: 88 } ); }); it('property: all quantifier passes iff every sub-clause passes', () => { fc.assert( fc.property( fc.array(fc.boolean(), { minLength: 1, maxLength: 20 }), (passFlags) => { const subResults = passFlags.map((pass, i) => ({ clauseId: `c${i}`, status: pass ? 'pass' as const : 'fail' as const, truth: 'determinate' as const, })); const clause: QuantifierClause = { clauseId: 'q1', clauseKind: 'quantifier.all', version: 1, quantifier: 'all', subClauseIds: passFlags.map((_, i) => `c${i}`), }; const result = evaluateQuantifier(subResults, clause); const expected = passFlags.every(Boolean) ? 'pass' : 'fail'; assert.strictEqual(result.status, expected); } ), { numRuns: 200, seed: 55 } ); }); it('property: any quantifier passes iff at least one sub-clause passes', () => { fc.assert( fc.property( fc.array(fc.boolean(), { minLength: 1, maxLength: 20 }), (passFlags) => { const subResults = passFlags.map((pass, i) => ({ clauseId: `c${i}`, status: pass ? 'pass' as const : 'fail' as const, truth: 'determinate' as const, })); const clause: QuantifierClause = { clauseId: 'q1', clauseKind: 'quantifier.any', version: 1, quantifier: 'any', subClauseIds: passFlags.map((_, i) => `c${i}`), }; const result = evaluateQuantifier(subResults, clause); const expected = passFlags.some(Boolean) ? 'pass' : 'fail'; assert.strictEqual(result.status, expected); } ), { numRuns: 200, seed: 66 } ); }); it('property: none quantifier passes iff no sub-clause passes', () => { fc.assert( fc.property( fc.array(fc.boolean(), { minLength: 1, maxLength: 20 }), (passFlags) => { const subResults = passFlags.map((pass, i) => ({ clauseId: `c${i}`, status: pass ? 'pass' as const : 'fail' as const, truth: 'determinate' as const, })); const clause: QuantifierClause = { clauseId: 'q1', clauseKind: 'quantifier.none', version: 1, quantifier: 'none', subClauseIds: passFlags.map((_, i) => `c${i}`), }; const result = evaluateQuantifier(subResults, clause); const expected = !passFlags.some(Boolean) ? 'pass' : 'fail'; assert.strictEqual(result.status, expected); } ), { numRuns: 200, seed: 33 } ); }); it('property: proof generation preserves failure/success', () => { fc.assert( fc.property( fc.tuple(coordArb, coordArb, coordArb, coordArb), fc.tuple(coordArb, coordArb, coordArb, coordArb), gapArb, ([sL, sT, sR, sB], [rL, rT, rR, rB], minGap) => { if (sL >= sR || sT >= sB || rL >= rR || rT >= rB) return; const world = makeWorldFromRects(sL, sT, sR, sB, rL, rT, rR, rB); const clause = makeClause('relation.leftOf', { bounds: { minGap }, }); resetProofCounter(); const evalResult = evaluateLeftOf(world, clause); const proof = generateProof(evalResult, clause, world); assert.strictEqual(proof.outcome, evalResult.status); assert.strictEqual(proof.truth, evalResult.truth); assert.strictEqual(proof.clauseId, clause.clauseId); if (evalResult.status === 'fail') { assert.ok(proof.failedPredicate, 'Failed proof should have failedPredicate'); } else { assert.strictEqual(proof.failedPredicate, undefined); } } ), { numRuns: 300, seed: 99 } ); }); });