Files
houseplan-card/test/switch-cycle-guard.test.mjs
T
Claude ff4c69ac28 test(perf): judge the #735 switch cycle by floor-cache build counters (#769)
The #735 guard compared cache sizes around the warmed twelve-switch cycle.
Once an LRU is full a cold build evicts one entry and every size stays: a
union planted cold in the full pool (8/8) changed no field of the snapshot and
the guard stayed green. Its `openingWallIndex` field read
`card._openingWallIndexCache ? 1 : 0` of a Map and was always 1.

The card now counts real builds of each floor-geometry cache in its miss
branch (`_floorCacheBuilds`: wall union, inner contours, clean floors via a
`cleanFloorForRoom` hook, opening wall index, light barriers, Glow clips,
physical bodies, opening tunnels, light physical bodies) — never on a hit, a
recency refresh, a resize seed or alias, or a clear. One pure decision
(demo/performance/switch-cycle-guard.mjs) judges the window for the benchmark,
the floor-cache smoke and the unit tests: any build of a pooled family fails
and names the family and its growth; the single-slot families may rebuild once
per switch; 2.5D keeps `_isoStructuralBuildCount`. A base bundle without the
counters reads null and is not judged; a candidate must expose them. Each row
reports `switchCycleBuilds` with the size snapshot as a diagnostic (the real
index size there). The budgeted cacheEntries/cacheGrowth keep the index as the
presence flag their `openingWallIndex: 1` ceilings were set on: no budget moves.

Counted run, one sample per profile on the dev base with the counters: every
large-house* profile and isometric-stage3-dense-v1 build 0 in every judged
family, 12/12 physical bodies and opening tunnels, nothing in 2.5D; no family
moved to the reported list. The same runner against a dev bundle reports
`supported: false` and passes.

AC7: demo/smoke_floor_geometry_cache.mjs fills the pool to 8 and drops one
floor's union; a four-switch cycle builds exactly that union (+1, an eviction,
every size the same) and the decision names "wall union" — the old size
comparison sees nothing. AC1 of the smoke also reads the counters. Registry
mutant: a counter that skips the evicting miss (caught by the smoke, checked
by hand); browser inventory 245/200.

Issue: #769
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-07 05:21:45 +03:00

110 lines
5.2 KiB
JavaScript

// #769: the #735 switch-cycle guard judges build counters, not cache sizes.
// The decision is the one pure function the benchmark serialises into the page
// and the floor-cache smoke uses (demo/performance/switch-cycle-guard.mjs).
import assert from 'node:assert/strict';
import test from 'node:test';
import {
floorCacheBuilds, floorCacheSnapshot, judgeSwitchCycle,
} from '../demo/performance/switch-cycle-guard.mjs';
const counters = (overrides = {}) => ({
wallUnion: 3, innerContour: 60, cleanFloor: 60, openingWallIndex: 3, lightBarrier: 3,
glowClip: 3, physicalBodies: 5, openingTunnel: 5, lightPhysicalBodies: 3, ...overrides,
});
/** The guard before #769: a failure is any grown size. */
const legacySizeGuard = (before, after) => Object.keys(after)
.filter((key) => after[key] > before[key]).map((key) => `${key} +${after[key] - before[key]}`);
test('#769 AC6: a warm cycle passes; single-slot families may rebuild once per switch', () => {
const verdict = judgeSwitchCycle({
before: counters(),
after: counters({ physicalBodies: 17, openingTunnel: 17 }),
switches: 12,
});
assert.deepEqual(verdict, {
ok: true, supported: true, failures: [],
builds: {
wallUnion: 0, innerContour: 0, cleanFloor: 0, openingWallIndex: 0, lightBarrier: 0,
glowClip: 0, physicalBodies: 12, openingTunnel: 12, lightPhysicalBodies: 0,
},
});
});
test('#769 AC6: any build of a pooled family fails and names the family', () => {
for (const [family, label] of [
['wallUnion', 'wall union'], ['innerContour', 'inner contour'], ['cleanFloor', 'clean floor'],
['openingWallIndex', 'opening wall index'], ['lightBarrier', 'light barrier'], ['glowClip', 'glow clip'],
]) {
const before = counters();
const verdict = judgeSwitchCycle({ before, after: counters({ [family]: before[family] + 2 }), switches: 12 });
assert.equal(verdict.ok, false, family);
assert.deepEqual(verdict.failures, [`${label} +2`]);
}
});
test('#769 AC6: a single-slot family is judged by the number of switches only', () => {
const before = counters();
assert.equal(judgeSwitchCycle({ before, after: counters({ physicalBodies: 5 + 12 }), switches: 12 }).ok, true);
const over = judgeSwitchCycle({ before, after: counters({ physicalBodies: 5 + 13 }), switches: 12 });
assert.equal(over.ok, false);
assert.deepEqual(over.failures, ['physical bodies +13 (more than 12 floor switches)']);
assert.deepEqual(
judgeSwitchCycle({ before, after: counters({ lightPhysicalBodies: 3 + 13 }), switches: 12 }).failures,
['light physical bodies +13 (more than 12 floor switches)'],
);
});
test('#769 AC6: a base without counters is not judged; a candidate without one family is', () => {
for (const [before, after] of [[null, null], [null, counters()], [counters(), null]]) {
const verdict = judgeSwitchCycle({ before, after, switches: 12 });
assert.deepEqual(verdict, { ok: true, supported: false, builds: null, failures: [] });
}
const { glowClip: _glowClip, ...partial } = counters();
const verdict = judgeSwitchCycle({ before: partial, after: partial, switches: 12 });
assert.equal(verdict.ok, false);
assert.deepEqual(verdict.failures, ['glow clip counter is missing']);
assert.equal(floorCacheBuilds({}), null, 'an old bundle reads null');
assert.deepEqual(floorCacheBuilds({ _floorCacheBuilds: counters() }), counters());
});
test('#769 AC6: the 2.5D structural counter is still judged', () => {
const verdict = judgeSwitchCycle({
before: counters(), after: counters(), switches: 12, isoBefore: 4, isoAfter: 5,
});
assert.deepEqual(verdict.failures, ['isoStructuralBuilds +1']);
assert.equal(judgeSwitchCycle({
before: null, after: null, switches: 12, isoBefore: null, isoAfter: 7,
}).ok, true, 'a base without the 2.5D counter is not judged');
});
test('#769 AC7: a cold build in a full LRU keeps every size and still fails', () => {
// A full union pool: the cold floor's union is built and one entry is
// evicted, so the pool stays at 8; the floor's contours and clean floors
// were warm. Sizes before and after are identical.
const sizes = {
cleanFloor: 60, glowClip: 3, wallUnion: 1, wallUnionPool: 8, innerContour: 60,
openingTunnel: 1, openingWallIndex: 3, isoGeometry: 0, planSnapGeometry: 0, wallFaceGraph: 0,
};
assert.deepEqual(legacySizeGuard(sizes, { ...sizes }), [], 'the size guard saw nothing');
const verdict = judgeSwitchCycle({
before: counters(), after: counters({ wallUnion: 4, physicalBodies: 9, openingTunnel: 9 }), switches: 4,
});
assert.equal(verdict.ok, false);
assert.deepEqual(verdict.failures, ['wall union +1']);
});
test('#769: the size snapshot reads the real size of the opening wall index', () => {
const card = {
_cleanFloorCache: new Map([[1, 1]]), _glowClipCache: new Map(),
_wallUnionCache: { key: 'k' }, _wallUnionPool: new Map([[1, 1], [2, 2]]),
_innerContourCache: new Map(), _openingTunnelCache: null,
_openingWallIndexCache: new Map([[1, 1], [2, 2], [3, 3]]),
};
assert.deepEqual(floorCacheSnapshot(card), {
cleanFloor: 1, glowClip: 0, wallUnion: 1, wallUnionPool: 2, innerContour: 0,
openingTunnel: 0, openingWallIndex: 3, isoGeometry: 0, planSnapGeometry: 0, wallFaceGraph: 0,
});
});