From 81f38689bb0b79656f28e0878a0174de1f5c1d6d Mon Sep 17 00:00:00 2001 From: Matysh Date: Sat, 22 Aug 2026 10:12:59 +0300 Subject: [PATCH] test: prove the atomic profile on a genuinely split edge (#233 r1 M1,M2) Issue: #233 User-Visible: no --- scripts/mutation-gate.mjs | 14 ++++++++++++ test/wall-thickness.test.mjs | 42 +++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs index 3e714b9d..12aae0fe 100644 --- a/scripts/mutation-gate.mjs +++ b/scripts/mutation-gate.mjs @@ -108,6 +108,20 @@ export const MUTANTS = [ replace: ' const span = centre;', }], }, + { + id: 'inner-span-ignores-neighbour-thickness', + guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs ' + + '&& node --test --test-name-pattern="innerEdgeSpan measures between wall faces" ' + + 'test/wall-thickness.test.mjs', + because: 'an edge is cut by the inner faces of its NEIGHBOURS, not by its own thickness: a ' + + 'thin side between two thick walls loses 2x15, not 2x7.5, and reading own thickness at ' + + 'both ends gives a number no tape measure confirms (#233 AC2)', + patches: [{ + file: 'src/wall-thickness.ts', + find: ' const o = Math.max(0, Number(offsets[edge]) || 0);', + replace: ' const o = own;', + }], + }, { id: 'inner-span-shortens-a-passage', guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs ' diff --git a/test/wall-thickness.test.mjs b/test/wall-thickness.test.mjs index 903c0cb4..133cc3cc 100644 --- a/test/wall-thickness.test.mjs +++ b/test/wall-thickness.test.mjs @@ -1531,22 +1531,40 @@ test('innerEdgeSpan handles a diagonal edge and degenerate input (#233)', () => }); test('ownEdgeOffsets reads the atomic profile, not a whole-edge lookup (#233)', () => { - // AC6b: половина ребра задана толщиной, половина нет. thicknessCmAt по целому - // ребру вернул бы 0 (test выше «exact-parent fallback does not leak»), а - // атомарный профиль отдаёт толщину участка под серединой ребра. + // AC6b, находка H2: толщина записана только на ЧАСТЬ ребра. Именно на таком + // ребре `thicknessCmAt` по целому ребру возвращает 0 — наивный источник + // толщин молча перестал бы сокращать подпись, и тест обязан это различать + // (M1 код-ревью r1: прежняя редакция задавала толщину на всё ребро, где оба + // источника отвечают одинаково, и наивную реализацию пропускала). const rooms = [{ id: 'r', poly: [[0, 0], [1, 0], [1, 1], [0, 1]] }]; - const walls = setWallThickness([], [0, 0], [1, 0], 20, pitch); - assert.equal(thicknessCmAt(walls, [0, 0], [1, 0], pitch), 20); + const split = setWallThickness([], [0, 0], [0.6, 0], 20, pitch); + assert.equal(thicknessCmAt(split, [0, 0], [1, 0], pitch), 0, + 'запрос по целому ребру против частичной толщины даёт 0 — это и есть H2'); + assert.equal(thicknessCmAt(split, [0, 0], [0.6, 0], pitch), 20, + 'а по самому участку толщина находится'); // Полигон здесь в нормализованных координатах, поэтому coordScale = 1: // с NORM_W допуск поиска ключа стал бы больше самой комнаты и толщина // «протекла» бы на противоположное ребро — проверено прогоном. - const offsets = ownEdgeOffsets(rooms, 'r', walls, [], pitch, 5, GRID_PITCH, 1); - assert.equal(offsets.length, 4); - assert.ok(offsets[0] > 0, 'ребро со стеной получает половинную глубину'); - assert.deepEqual(offsets.slice(1), [0, 0, 0], 'рёбра без записи остаются нулевыми'); - // Тот же ответ, что у канонического источника профиля. - assert.deepEqual(offsets, insetOffsetsForRoom(rooms, 'r', walls, [], pitch, 5, GRID_PITCH, 1)); + const halfDepth = wallCmToUnits(20, 5, GRID_PITCH) / 2; + const viaProfile = ownEdgeOffsets(rooms, 'r', split, [], pitch, 5, GRID_PITCH, 1); + assert.equal(viaProfile.length, 4, 'по одному числу на ребро своего полигона'); + assert.equal(viaProfile[0], halfDepth, + 'участок под серединой ребра отдаёт половинную глубину, а не 0'); + assert.deepEqual(viaProfile.slice(1), [0, 0, 0], 'рёбра без записи остаются нулевыми'); + // Разница с наивным источником именно здесь: 0 против половинной глубины. + // Дальше эту разницу видит `innerEdgeSpan` — с нулём сокращения не будет + // вовсе (AC3), с профилем соседнее ребро сократится (AC1, отдельные тесты + // выше; смешивать здесь нельзя — полигон нормализован, а профиль в единицах + // рендера). + assert.notEqual(viaProfile[0], thicknessCmAt(split, [0, 0], [1, 0], pitch)); + + // Неразрезанное ребро: тот же ответ, что у канонического источника профиля. + const whole = setWallThickness([], [0, 0], [1, 0], 20, pitch); + assert.deepEqual( + ownEdgeOffsets(rooms, 'r', whole, [], pitch, 5, GRID_PITCH, 1), + insetOffsetsForRoom(rooms, 'r', whole, [], pitch, 5, GRID_PITCH, 1), + ); // Комната без стен: все нули — вызывающий покажет осевую длину. assert.deepEqual(ownEdgeOffsets(rooms, 'r', [], [], pitch, 5, GRID_PITCH, 1), [0, 0, 0, 0]); - assert.equal(ownEdgeOffsets(rooms, 'missing', walls, [], pitch, 5, GRID_PITCH, 1), null); + assert.equal(ownEdgeOffsets(rooms, 'missing', whole, [], pitch, 5, GRID_PITCH, 1), null); });