v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
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>): 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 }
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user