mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 15:09:30 +00:00
test: обход последовательностей правок с инвариантами после каждого жеста
Все прежние гейты проверяют снимок модели. Дефекты геометрии рождаются в редактировании: #289, #296 и #298 прошли решётку, кладку, роли, ключи и аудит ручек, потому что такая геометрия снимок не портит — она портит следующий жест. demo/smoke_edit_walk.mjs расшатывает реальный план продуктовыми жестами (_rszEdgeDown/_rszMove/_rszUp, _confirmRoomDelete, optimizePlans) по фиксированному семени и после каждого шага судит конфиг в node. Второго представления редактора не появляется — принцип #292. Подшаговый шум остаётся наблюдением, а не нарушением: координата пишется девятью знаками, 304/240 = 1.266666667, отклонение 8e-8 шага неустранимо форматом хранения и уйдёт на этапе 1 ADR #282. Судится только «вне сетки». Таблица KNOWN работает в обе стороны: обход падает и когда находок больше, и когда меньше. Молча позеленевший гейт не сообщает о починке — так partition-mt2on9ou-0 прожил в плане владельца от беты 9 до rc.1. Новый инвариант checkHiddenObstacles: перегородка на стене комнаты, незакрытый контур на стене комнаты, черновик, который не может стать комнатой (#296). Найдено сразу, в пяти прогонах из шести — на первом жесте: #298 (ресайз уводит конец записи толщины мимо решётки и мимо ребра), #299 («Оптимизировать» и удаление комнаты сливают записи через границу роли). Issue: #297 User-Visible: no
This commit is contained in:
@@ -5,8 +5,9 @@ import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
checkMixedRoleRecords, checkReferences, checkWallKeys, checkWallRecordsPreserved,
|
||||
keyMidpoint, latticeProfile, nearAxisProfile, readModel, wallKey,
|
||||
checkHiddenObstacles, checkMixedRoleRecords, checkReferences, checkWallKeys,
|
||||
checkWallRecordsPreserved, keyMidpoint, latticeProfile, nearAxisProfile,
|
||||
readModel, wallKey,
|
||||
} from '../scripts/model-invariants.mjs';
|
||||
import { wallKey as productWallKey } from '../test-build/wall-thickness.js';
|
||||
import { GRID_STEP_N } from '../test-build/space-geometry.js';
|
||||
@@ -550,3 +551,102 @@ test('#290 near-axis audit deduplicates shared room-owner copies', () => {
|
||||
const profile = nearAxisProfile({ spaces: [{ id: 'near', ...fixture }] });
|
||||
assert.deepEqual(profile, { total: 1, spaces: [{ spaceId: 'near', count: 1 }] });
|
||||
});
|
||||
|
||||
// #296/#297. Геометрия, которая ничего не рисует, но выключает ручки ресайза.
|
||||
// Проверка нужна ровно потому, что все прежние гейтики её не видят: перегородка
|
||||
// под стеной комнаты не портит ни ключи, ни роли, ни решётку, ни кладку.
|
||||
const hidden = (space) => checkHiddenObstacles({ spaces: [{ id: 'sp1', cell_cm: 1, ...space }] });
|
||||
const roomAt = (x0, y0, x1, y1) => ({ id: 'r1', poly: rect(x0, y0, x1, y1) });
|
||||
|
||||
test('перегородка под стеной комнаты — нарушение (#296)', () => {
|
||||
const step = GRID_STEP_N;
|
||||
const found = hidden({
|
||||
rooms: [roomAt(0, 0, 40 * step, 30 * step)],
|
||||
// Ровно нижнее ребро комнаты: на плане неотличимо от стены.
|
||||
partitions: [{ id: 'p1', a: [0, 0], b: [40 * step, 0], cm: 20 }],
|
||||
});
|
||||
assert.equal(found.length, 1);
|
||||
assert.equal(found[0].kind, 'partition_over_room_wall');
|
||||
});
|
||||
|
||||
test('перегородка, лежащая на части стены, — тоже нарушение (#296)', () => {
|
||||
const step = GRID_STEP_N;
|
||||
const found = hidden({
|
||||
rooms: [roomAt(0, 0, 40 * step, 30 * step)],
|
||||
partitions: [{ id: 'p1', a: [10 * step, 0], b: [25 * step, 0], cm: 20 }],
|
||||
});
|
||||
assert.equal(found.length, 1);
|
||||
assert.equal(found[0].kind, 'partition_over_room_wall');
|
||||
});
|
||||
|
||||
test('перегородка поперёк комнаты законна и не ловится (#296)', () => {
|
||||
// Единственный смысл существования перегородки: разделить комнату внутри.
|
||||
// Инвариант, кричащий на это, отключат в первую неделю.
|
||||
const step = GRID_STEP_N;
|
||||
const found = hidden({
|
||||
rooms: [roomAt(0, 0, 40 * step, 30 * step)],
|
||||
partitions: [{ id: 'p1', a: [20 * step, 0], b: [20 * step, 30 * step], cm: 20 }],
|
||||
});
|
||||
assert.deepEqual(found, []);
|
||||
});
|
||||
|
||||
test('касание углом перекрытием не считается (#296)', () => {
|
||||
const step = GRID_STEP_N;
|
||||
const found = hidden({
|
||||
rooms: [roomAt(0, 0, 40 * step, 30 * step)],
|
||||
// Продолжение нижнего ребра за угол: общего с ребром — ровно точка.
|
||||
partitions: [{ id: 'p1', a: [40 * step, 0], b: [60 * step, 0], cm: 20 }],
|
||||
});
|
||||
assert.deepEqual(found, []);
|
||||
});
|
||||
|
||||
test('черновик из двух точек не может стать комнатой (#296)', () => {
|
||||
const step = GRID_STEP_N;
|
||||
const found = hidden({
|
||||
rooms: [roomAt(0, 0, 40 * step, 30 * step)],
|
||||
room_drafts: [{
|
||||
id: 'd1', points: [[50 * step, 0], [70 * step, 0]], segments: [{ cm: 30 }],
|
||||
}],
|
||||
});
|
||||
assert.equal(found.length, 1);
|
||||
assert.equal(found[0].kind, 'unusable_draft');
|
||||
});
|
||||
|
||||
test('черновик из трёх точек в стороне от стен законен (#296)', () => {
|
||||
const step = GRID_STEP_N;
|
||||
const found = hidden({
|
||||
rooms: [roomAt(0, 0, 40 * step, 30 * step)],
|
||||
room_drafts: [{
|
||||
id: 'd1',
|
||||
points: [[50 * step, 0], [70 * step, 0], [70 * step, 20 * step]],
|
||||
segments: [{ cm: 30 }, { cm: 30 }],
|
||||
}],
|
||||
});
|
||||
assert.deepEqual(found, []);
|
||||
});
|
||||
|
||||
test('незакрытый контур на стене комнаты — нарушение (#296)', () => {
|
||||
const step = GRID_STEP_N;
|
||||
const found = hidden({
|
||||
rooms: [roomAt(0, 0, 40 * step, 30 * step)],
|
||||
room_drafts: [{
|
||||
id: 'd1',
|
||||
points: [[5 * step, 0], [35 * step, 0], [35 * step, 20 * step]],
|
||||
segments: [{ cm: 30 }, { cm: 30 }],
|
||||
}],
|
||||
});
|
||||
assert.equal(found.length, 1);
|
||||
assert.equal(found[0].kind, 'draft_over_room_wall');
|
||||
});
|
||||
|
||||
test('реальный план: перегородка поверх наружной стены закреплена числом (#296)', () => {
|
||||
// Долг фикстуры, а не «допустимо»: `partition-mt2on9ou-0` лежит на наружной
|
||||
// стене трёх комнат и выключает ресайз Мастера с/у и Гардеробной. Число здесь
|
||||
// обязано стать нулём вместе с закрытием #296 — иначе починка пройдёт молча.
|
||||
const debt = { 'real-plan-second-floor.json': 1, 'real-plan-first-floor.json': 0 };
|
||||
for (const [file, expected] of Object.entries(debt)) {
|
||||
const { config } = readModel(
|
||||
readFileSync(resolve(repoRoot, 'test/fixtures', file), 'utf8'));
|
||||
assert.equal(checkHiddenObstacles(config).length, expected, file);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user