mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 21:01:21 +00:00
ci: derive the browser-smoke shortlist from the diff
Issue: #241 User-Visible: no
This commit is contained in:
+329
@@ -0,0 +1,329 @@
|
||||
commit c8e9597228143530b98e9a984139dc1b0c193d0e
|
||||
Author: Sergey Matyunin <s.matyunin@justbusiness.site>
|
||||
Date: Fri Aug 21 16:47:27 2026 +0300
|
||||
|
||||
fix: resolve chain segment thickness in one place
|
||||
|
||||
Issue: #234
|
||||
User-Visible: yes
|
||||
|
||||
diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts
|
||||
index a3d21d6..48e3e87 100755
|
||||
--- a/src/houseplan-card.ts
|
||||
+++ b/src/houseplan-card.ts
|
||||
@@ -158,7 +158,7 @@ import {
|
||||
} from './plan-snap-overlay';
|
||||
import {
|
||||
atomizeWallSegments, buildWallFaceGraph, findNewWallFacesInGraphs,
|
||||
- normalizeUnifiedWallTool, wallChainSegments,
|
||||
+ normalizeUnifiedWallTool, wallChainSegments, chainSegmentCms,
|
||||
type WallFaceGraph, type WallGraphFace, type WallGraphSourceSegment,
|
||||
} from './wall-face-graph';
|
||||
import {
|
||||
@@ -2617,7 +2617,9 @@ class HouseplanCard extends LitElement {
|
||||
if (draft) {
|
||||
this._activeDraftId = draft.id;
|
||||
this._path = draft.points.map((point) => [...point]);
|
||||
- this._draftSegmentCms = draft.segments.map((segment) => segment.cm);
|
||||
+ this._draftSegmentCms = this._adoptDraftCms(
|
||||
+ this._path, draft.segments.map((segment: any) => segment.cm), draft.id,
|
||||
+ );
|
||||
this._resumeDraftBySpace[this._space] = draft.id;
|
||||
} else {
|
||||
this._activeDraftId = null;
|
||||
@@ -6574,7 +6576,11 @@ class HouseplanCard extends LitElement {
|
||||
return true;
|
||||
}
|
||||
const segments = wallChainSegments(
|
||||
- this._path, this._draftSegmentCms, DRAW_WALL_DEFAULT_CM,
|
||||
+ this._path,
|
||||
+ chainSegmentCms(
|
||||
+ this._path.length - 1, this._draftSegmentCms,
|
||||
+ this._drawWallCm, DRAW_WALL_DEFAULT_CM,
|
||||
+ ),
|
||||
);
|
||||
const segmentCount = segments.length;
|
||||
if ((sp.partitions || []).length + segmentCount > MAX_PARTITIONS) {
|
||||
@@ -7268,9 +7274,13 @@ class HouseplanCard extends LitElement {
|
||||
this._path = endHit.reverse
|
||||
? [...endHit.draft.points].reverse().map((p) => [...p])
|
||||
: endHit.draft.points.map((p) => [...p]);
|
||||
- this._draftSegmentCms = endHit.reverse
|
||||
- ? [...endHit.draft.segments].reverse().map((s) => s.cm)
|
||||
- : endHit.draft.segments.map((s) => s.cm);
|
||||
+ this._draftSegmentCms = this._adoptDraftCms(
|
||||
+ this._path,
|
||||
+ endHit.reverse
|
||||
+ ? [...endHit.draft.segments].reverse().map((s) => s.cm)
|
||||
+ : endHit.draft.segments.map((s) => s.cm),
|
||||
+ endHit.draft.id,
|
||||
+ );
|
||||
return;
|
||||
}
|
||||
this._activeDraftId = null;
|
||||
@@ -7289,8 +7299,16 @@ class HouseplanCard extends LitElement {
|
||||
return;
|
||||
}
|
||||
if (!this._canAppendRoomDraftPoint()) return;
|
||||
+ // Точка и толщина её отрезка пишутся вместе (#234). Раньше запись жила в
|
||||
+ // отдельном методе, который молча выходил при невалидном поле толщины, и
|
||||
+ // тогда `_draftSegmentCms` становился короче числа отрезков: превью
|
||||
+ // показывало текущее поле, а запись — 15 см. Инвариант читается прямо
|
||||
+ // здесь, а не выводится из двух проверок в разных местах.
|
||||
+ const cm = this._drawWallCm;
|
||||
+ if (cm == null) { this._showPhysicalRange(100); return; }
|
||||
const beforePath = this._path.map((point) => [...point]);
|
||||
this._path = [...this._path, pt];
|
||||
+ this._draftSegmentCms = [...this._draftSegmentCms, cm];
|
||||
this._persistActiveDraftSegment();
|
||||
this._offerWallFaces(beforePath);
|
||||
}
|
||||
@@ -7395,8 +7413,10 @@ class HouseplanCard extends LitElement {
|
||||
sp.room_drafts.push(saved);
|
||||
this._activeDraftId = id;
|
||||
this._resumeDraftBySpace[this._space] = id;
|
||||
- this._draftSegmentCms = persistedSegments.map((s: any) => Number(s.cm));
|
||||
this._path = persistedPoints;
|
||||
+ this._draftSegmentCms = this._adoptDraftCms(
|
||||
+ persistedPoints, persistedSegments.map((s: any) => Number(s.cm)), id,
|
||||
+ );
|
||||
this._physicalSel = null;
|
||||
this._recordGeometry(this._t('history.draft_merge'), before);
|
||||
this._saveConfig();
|
||||
@@ -7406,12 +7426,35 @@ class HouseplanCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
- /** Persist every completed draft segment immediately. */
|
||||
+ /**
|
||||
+ * Thickness array adopted from storage, brought to the length of the path.
|
||||
+ *
|
||||
+ * A record written before #234 may be shorter than the path: the resolver
|
||||
+ * fills the gaps by the same rule the preview and the writers use, so a
|
||||
+ * resumed draft cannot carry a hidden 15 cm into the next save. Reported to
|
||||
+ * the console rather than to the user: the person did not cause it and cannot
|
||||
+ * fix it.
|
||||
+ */
|
||||
+ private _adoptDraftCms(path: readonly (readonly number[])[], recorded: readonly (number | null | undefined)[], id?: string): number[] {
|
||||
+ const count = Math.max(0, path.length - 1);
|
||||
+ const resolved = chainSegmentCms(count, recorded, this._drawWallCm, DRAW_WALL_DEFAULT_CM);
|
||||
+ if (recorded.length !== count) {
|
||||
+ console.debug(
|
||||
+ `[houseplan] draft ${id ?? '?'}: восстановлено толщин ${count - recorded.length} (#234)`,
|
||||
+ );
|
||||
+ }
|
||||
+ return resolved;
|
||||
+ }
|
||||
+
|
||||
+ /**
|
||||
+ * Persist every completed draft segment immediately.
|
||||
+ *
|
||||
+ * The thickness of the new segment is already recorded by the caller (#234):
|
||||
+ * this method must not decide whether to record it, or the array and the path
|
||||
+ * drift apart the moment the toolbar field is mid-edit.
|
||||
+ */
|
||||
private _persistActiveDraftSegment(): void {
|
||||
if (this._path.length < 2 || !this._curSpaceCfg) return;
|
||||
- const cm = this._drawWallCm;
|
||||
- if (cm == null) return;
|
||||
- this._draftSegmentCms = [...this._draftSegmentCms, cm];
|
||||
const before = this._geometrySnapshot();
|
||||
const sp = this._curSpaceCfg as any;
|
||||
sp.room_drafts ||= [];
|
||||
@@ -12383,10 +12426,17 @@ class HouseplanCard extends LitElement {
|
||||
return validCm(model.room_drafts.find((item) => item.id === draftId)?.segments[index]?.cm)
|
||||
?? DRAW_WALL_DEFAULT_CM;
|
||||
}
|
||||
+ // Толщина отрезка активной цепочки решается тем же резолвером (#234):
|
||||
+ // именно это значение подсвечивает инструмент «Толщина», и расхождение с
|
||||
+ // записью здесь было тем способом, которым дефект и обнаружился.
|
||||
+ const resolved = chainSegmentCms(
|
||||
+ Math.max(0, activePath.length - 1), activeCms,
|
||||
+ this._drawWallCm, DRAW_WALL_DEFAULT_CM,
|
||||
+ );
|
||||
for (let i = 0; i + 1 < activePath.length; i++) {
|
||||
const a = activePath[i], b = activePath[i + 1];
|
||||
if (distToSegment(point, [a[0], a[1], b[0], b[1]]) <= epsilon) {
|
||||
- return validCm(activeCms[i]) ?? DRAW_WALL_DEFAULT_CM;
|
||||
+ return resolved[i] ?? DRAW_WALL_DEFAULT_CM;
|
||||
}
|
||||
}
|
||||
return DRAW_WALL_DEFAULT_CM;
|
||||
@@ -12441,7 +12491,11 @@ class HouseplanCard extends LitElement {
|
||||
const partitions: Array<{ a: number[]; b: number[]; cm: number }> = [];
|
||||
if (!accepted.length) {
|
||||
partitions.push(...wallChainSegments(
|
||||
- batch.activePath, batch.activeCms, DRAW_WALL_DEFAULT_CM,
|
||||
+ batch.activePath,
|
||||
+ chainSegmentCms(
|
||||
+ batch.activePath.length - 1, batch.activeCms,
|
||||
+ this._drawWallCm, DRAW_WALL_DEFAULT_CM,
|
||||
+ ),
|
||||
));
|
||||
} else {
|
||||
const consumed = new Set(accepted.flatMap((decision) => decision.candidate.atomKeys));
|
||||
@@ -12656,8 +12710,12 @@ class HouseplanCard extends LitElement {
|
||||
// Draw-session wall thickness: apply to new edges only; keep neighbour cm
|
||||
// on shared stretches. Split naming does not use the Draw field.
|
||||
if (!wasSplit) {
|
||||
- const edgeCms = [...this._draftSegmentCms, this._closingWallCm || this._drawWallCm || DRAW_WALL_DEFAULT_CM];
|
||||
- const cm = edgeCms[0] || this._drawWallCm;
|
||||
+ const edgeCms = chainSegmentCms(
|
||||
+ verts.length,
|
||||
+ [...this._draftSegmentCms, this._closingWallCm ?? undefined],
|
||||
+ this._drawWallCm, DRAW_WALL_DEFAULT_CM,
|
||||
+ );
|
||||
+ const cm = edgeCms[0];
|
||||
if (cm != null) {
|
||||
this._cfgEpoch++; // the new room must be in the model before keying
|
||||
const openCuts = this._openCuts();
|
||||
@@ -12681,7 +12739,7 @@ class HouseplanCard extends LitElement {
|
||||
return distToSegment(mid, [a[0], a[1], b[0], b[1]]) <= this._gridPitch * 0.02;
|
||||
});
|
||||
if (source >= 0) next = setWallThickness(
|
||||
- next, iv.a, iv.b, edgeCms[source] || cm,
|
||||
+ next, iv.a, iv.b, edgeCms[source],
|
||||
this._wallKeyPitch, NORM_W,
|
||||
);
|
||||
}
|
||||
@@ -12759,7 +12817,9 @@ class HouseplanCard extends LitElement {
|
||||
if (!draft) { delete this._resumeDraftBySpace[this._space]; return; }
|
||||
this._activeDraftId = id;
|
||||
this._path = draft.points.map((p) => [...p]);
|
||||
- this._draftSegmentCms = draft.segments.map((s) => s.cm);
|
||||
+ this._draftSegmentCms = this._adoptDraftCms(
|
||||
+ this._path, draft.segments.map((s: any) => s.cm), draft.id,
|
||||
+ );
|
||||
this._clearPlanSnapHover();
|
||||
}
|
||||
|
||||
@@ -13029,8 +13089,14 @@ class HouseplanCard extends LitElement {
|
||||
return;
|
||||
}
|
||||
const before = this._geometrySnapshot();
|
||||
- const cms = [...this._draftSegmentCms,
|
||||
- this._closingWallCm || this._drawWallCm || DRAW_WALL_DEFAULT_CM];
|
||||
+ // Замкнутый контур: отрезков столько же, сколько вершин, и последний —
|
||||
+ // закрывающий. Его известное значение подаётся резолверу как запись, всё
|
||||
+ // остальное решает единое правило (#234).
|
||||
+ const cms = chainSegmentCms(
|
||||
+ verts.length,
|
||||
+ [...this._draftSegmentCms, this._closingWallCm ?? undefined],
|
||||
+ this._drawWallCm, DRAW_WALL_DEFAULT_CM,
|
||||
+ );
|
||||
sp.partitions ||= [];
|
||||
const seed = Date.now().toString(36);
|
||||
for (let i = 0; i < verts.length; i++) {
|
||||
@@ -13039,7 +13105,7 @@ class HouseplanCard extends LitElement {
|
||||
id: `partition-${seed}-${i}`,
|
||||
a: [a[0] / NORM_W, a[1] / NORM_W],
|
||||
b: [b[0] / NORM_W, b[1] / NORM_W],
|
||||
- cm: cms[i] || DRAW_WALL_DEFAULT_CM,
|
||||
+ cm: cms[i],
|
||||
});
|
||||
}
|
||||
if (this._activeDraftId && Array.isArray(sp.room_drafts)) {
|
||||
@@ -18544,15 +18610,17 @@ class HouseplanCard extends LitElement {
|
||||
if (this._cursorPt) return [...path, this._cursorPt];
|
||||
return path.length >= 2 ? path : null;
|
||||
})();
|
||||
+ // Превью берёт толщины из того же резолвера, что и запись (#234). Раньше
|
||||
+ // здесь была вторая формула, и расходились они ровно на пропуске: на экране
|
||||
+ // текущее поле, в конфиге — 15 см.
|
||||
const previewHalfDepths = previewPts
|
||||
- ? previewPts.slice(0, -1).map((_, i) => {
|
||||
- const cm = Number(this._draftSegmentCms[i]) > 0
|
||||
- ? Number(this._draftSegmentCms[i])
|
||||
- : this._contourClosed && i === previewPts.length - 2
|
||||
- ? (this._closingWallCm || drawCm || DRAW_WALL_DEFAULT_CM)
|
||||
- : (drawCm || DRAW_WALL_DEFAULT_CM);
|
||||
- return wallCmToUnits(cm, this._cellCm, this._gridPitch) / 2;
|
||||
- })
|
||||
+ ? chainSegmentCms(
|
||||
+ previewPts.length - 1,
|
||||
+ this._contourClosed
|
||||
+ ? [...this._draftSegmentCms, this._closingWallCm ?? undefined]
|
||||
+ : this._draftSegmentCms,
|
||||
+ drawCm, DRAW_WALL_DEFAULT_CM,
|
||||
+ ).map((cm) => wallCmToUnits(cm, this._cellCm, this._gridPitch) / 2)
|
||||
: [];
|
||||
const previewD = previewPts
|
||||
? drawWallPreviewD(
|
||||
diff --git a/src/wall-face-graph.ts b/src/wall-face-graph.ts
|
||||
index 202cf92..380ab63 100644
|
||||
--- a/src/wall-face-graph.ts
|
||||
+++ b/src/wall-face-graph.ts
|
||||
@@ -55,10 +55,60 @@ export function normalizeUnifiedWallTool(value: unknown): unknown {
|
||||
}
|
||||
|
||||
/** Immutable open-chain projection used by explicit finish and full rejection. */
|
||||
+/**
|
||||
+ * Thickness of every segment in a chain — the single answer to that question.
|
||||
+ *
|
||||
+ * Issue #234: five call sites decided it independently and disagreed in three
|
||||
+ * different ways. The preview filled a gap with the toolbar field, the two
|
||||
+ * partition writers with a hard-coded 15 cm, the room writer with the first
|
||||
+ * edge's value. So a chain drawn at 30 cm was shown at 30 and stored at 15, and
|
||||
+ * the owner discovered it much later by hovering a wall. Two formulas for one
|
||||
+ * meaning always drift; there is exactly one here now.
|
||||
+ *
|
||||
+ * A missing record inherits the previous segment of the same chain, then the
|
||||
+ * toolbar field, then the default (owner's decision 2026-08-21): that is what
|
||||
+ * the person saw on screen while drawing, and a global default is not.
|
||||
+ *
|
||||
+ * Strictly positive is the validity boundary. The previous `wallChainSegments`
|
||||
+ * accepted a recorded zero, which cannot be drawn through the UI (1..100 cm,
|
||||
+ * `docs/WALL-THICKNESS.md`) but can sit in an old draft.
|
||||
+ */
|
||||
+export function chainSegmentCms(
|
||||
+ segmentCount: number,
|
||||
+ recorded: readonly (number | null | undefined)[] | null | undefined,
|
||||
+ activeCm: number | null | undefined,
|
||||
+ defaultCm: number,
|
||||
+): number[] {
|
||||
+ const count = Number.isFinite(segmentCount) && segmentCount > 0
|
||||
+ ? Math.floor(segmentCount) : 0;
|
||||
+ const valid = (value: unknown): number | null =>
|
||||
+ typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
|
||||
+ // `defaultCm` — ответственность вызывающего: он передаёт
|
||||
+ // DRAW_WALL_DEFAULT_CM. Константа сюда не импортируется намеренно — этот
|
||||
+ // модуль не зависит ни от чего, и второе место, где живёт число 15, было бы
|
||||
+ // ровно тем дублированием, которое задача и убирает. Невалидный default —
|
||||
+ // дефект вызывающего, поэтому он приводится к минимальной допустимой
|
||||
+ // толщине (1 см, docs/WALL-THICKNESS.md), а не к выдуманному значению.
|
||||
+ const fallbackTail = valid(activeCm) ?? valid(defaultCm) ?? 1;
|
||||
+ const out: number[] = [];
|
||||
+ let previous: number | null = null;
|
||||
+ for (let i = 0; i < count; i++) {
|
||||
+ const own = valid(recorded?.[i]);
|
||||
+ const cm = own ?? previous ?? fallbackTail;
|
||||
+ out.push(cm);
|
||||
+ previous = cm;
|
||||
+ }
|
||||
+ return out;
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * Drawable segments of a chain. Thickness arrives already resolved (#234): this
|
||||
+ * function no longer owns a fallback of its own, because owning one is how the
|
||||
+ * disagreement started.
|
||||
+ */
|
||||
export function wallChainSegments(
|
||||
path: readonly (readonly number[])[],
|
||||
cms: readonly number[],
|
||||
- defaultCm: number,
|
||||
): WallChainSegment[] {
|
||||
const result: WallChainSegment[] = [];
|
||||
for (let i = 0; i + 1 < path.length; i++) {
|
||||
@@ -66,8 +116,9 @@ export function wallChainSegments(
|
||||
const b = path[i + 1];
|
||||
if (!finitePoint(a) || !finitePoint(b)
|
||||
|| Math.hypot(b[0] - a[0], b[1] - a[1]) <= Number.EPSILON) continue;
|
||||
- const cm = Number.isFinite(cms[i]) && cms[i] >= 0 ? cms[i] : defaultCm;
|
||||
- result.push({ a: [a[0], a[1]], b: [b[0], b[1]], cm });
|
||||
+ // The resolver guarantees a positive number per index; a caller that skips
|
||||
+ // it is a defect, so the value is used as given rather than re-defaulted.
|
||||
+ result.push({ a: [a[0], a[1]], b: [b[0], b[1]], cm: cms[i] });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
commit abfaae3e38b86769c39692d62dd146424ad0255c
|
||||
Author: Matysh <s.matyunin@justbusiness.site>
|
||||
Date: Sat Aug 22 09:45:15 2026 +0300
|
||||
|
||||
feat: measure resize labels between wall faces
|
||||
|
||||
Issue: #233
|
||||
User-Visible: yes
|
||||
|
||||
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
|
||||
index a9ae244..be221a9 100644
|
||||
--- a/docs/CHANGELOG.md
|
||||
+++ b/docs/CHANGELOG.md
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
+- Resizing a room now reports the distance between wall faces, the number a tape
|
||||
+ measure gives: a 300 cm span between 15 cm walls reads 285 cm. The area label
|
||||
+ was already computed from the floor, so one bubble used to carry two
|
||||
+ conventions and neither number could be checked against anything. A passage or
|
||||
+ a side open to the next room keeps its full length, because there is no face to
|
||||
+ measure from. Area still subtracts columns and partitions, so length times
|
||||
+ length may differ from it — that gap is furniture in the room, not a change of
|
||||
+ ruler ([#233](https://github.com/Matysh/houseplan-card/issues/233)).
|
||||
+
|
||||
- A chain of walls now stores the thickness it was drawn with. A segment whose
|
||||
thickness was not recorded — which happened while the toolbar field was being
|
||||
edited between segments — used to be previewed at the drawn value and saved at
|
||||
diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md
|
||||
index 3adaa08..c4d57ef 100644
|
||||
--- a/docs/USER-GUIDE.md
|
||||
+++ b/docs/USER-GUIDE.md
|
||||
@@ -295,7 +295,7 @@ Other operations edit existing geometry:
|
||||
|---|---|
|
||||
| Merge | Joins adjacent rooms; a dialog chooses the surviving identity, name and area |
|
||||
| Split | Cuts a room from one wall to another; the larger part keeps the original room |
|
||||
-| Resize | Moves a wall with shared geometry or scales a room by corner handles |
|
||||
+| Resize | Moves a wall with shared geometry or scales a room by corner handles. Live labels report **inner** dimensions — the clear distance between wall faces, the number a tape measure gives — and the room area |
|
||||
| Thickness | Changes one physical span or every wall of a room |
|
||||
| Delete room | Deletes only the selected room after confirmation |
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
diff --git a/src/wall-thickness.ts b/src/wall-thickness.ts
|
||||
index 1111111..2222222 100644
|
||||
--- a/src/wall-thickness.ts
|
||||
+++ b/src/wall-thickness.ts
|
||||
@@ -2508,7 +2508,7 @@
|
||||
-export function openingInnerFaceOffsetFromIndex(
|
||||
+export function openingInnerFaceOffsetFromIndex(
|
||||
index: OpeningWallIndex,
|
||||
- const piece = centrePiece(selected);
|
||||
+ const piece = tunnelFacePath(selected);
|
||||
@@ -0,0 +1,114 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { selectSmokes, parseDiff, symbolTable } from '../scripts/smoke-select.mjs';
|
||||
import { SMOKE_LINKS, registeredSmokes } from '../scripts/smoke-links.mjs';
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const fixture = (name) =>
|
||||
readFileSync(join(repoRoot, 'test/fixtures/smoke-select', name), 'utf8');
|
||||
|
||||
// Выборка считается по настоящему корпусу смоков и настоящей таблице символов:
|
||||
// фикстура — только дифф. Тест поэтому краснеет и когда ломается инструмент, и
|
||||
// когда контракт переименовали, не обновив связи. Второе — тоже дефект.
|
||||
const names = (selection) => selection.direct.map((entry) => entry.smoke);
|
||||
const strongNames = (selection) => selection.direct
|
||||
.filter((entry) => entry.strong).map((entry) => entry.smoke);
|
||||
|
||||
test('#234: выборка находит все четыре смока контрольного случая (#241)', () => {
|
||||
const selection = selectSmokes(fixture('234-chain-thickness.diff'), { root: repoRoot });
|
||||
const recommended = new Set([
|
||||
...strongNames(selection),
|
||||
...selection.registered.map((entry) => entry.smoke),
|
||||
]);
|
||||
for (const smoke of [
|
||||
'smoke_wall_chain_thickness.mjs',
|
||||
'smoke_draw_wall_thickness.mjs',
|
||||
'smoke_wall_thickness_transition.mjs',
|
||||
'smoke_wall_junctions.mjs',
|
||||
]) {
|
||||
assert.ok(recommended.has(smoke), `${smoke} не попал в выборку по диффу #234`);
|
||||
}
|
||||
// Именно тот смок, на котором #234 потерял регресс, и именно по имени поля:
|
||||
// если связь начнёт находиться «вообще как-нибудь», проверка обесценится.
|
||||
const junctions = selection.direct.find((e) => e.smoke === 'smoke_wall_junctions.mjs');
|
||||
assert.ok(junctions.symbols.includes('_draftSegmentCms'));
|
||||
// И не превращается в полный прогон: смысл выборки в том, что она меньше матрицы.
|
||||
assert.ok(recommended.size < selection.smokeCount / 4,
|
||||
`выборка ${recommended.size} из ${selection.smokeCount} — это уже полная матрица`);
|
||||
});
|
||||
|
||||
test('#234: переход толщин держится на зарегистрированной связи, а не на совпадении', () => {
|
||||
const selection = selectSmokes(fixture('234-chain-thickness.diff'), { root: repoRoot });
|
||||
// Смок не называет ни одного изменённого символа — если он вдруг окажется в
|
||||
// прямых совпадениях, значит реестр перестал быть нагруженным, и мутант его
|
||||
// удаления ничего не докажет.
|
||||
assert.ok(!names(selection).includes('smoke_wall_thickness_transition.mjs'),
|
||||
'смок перехода нашёлся по совпадению — реестр надо пересобрать заново');
|
||||
const entry = selection.registered.find(
|
||||
(candidate) => candidate.smoke === 'smoke_wall_thickness_transition.mjs',
|
||||
);
|
||||
assert.ok(entry, 'зарегистрированная связь #234 пропала');
|
||||
assert.ok(entry.symbols.includes('chainSegmentCms'));
|
||||
assert.ok(entry.because.join(' ').length > 40, 'связь без объяснения — суеверие');
|
||||
});
|
||||
|
||||
test('только документация: выборка пуста и говорит почему (#241)', () => {
|
||||
const selection = selectSmokes(fixture('docs-only.diff'), { root: repoRoot });
|
||||
assert.equal(selection.noExecutableDiff, true);
|
||||
assert.deepEqual(selection.direct, []);
|
||||
assert.deepEqual(selection.registered, []);
|
||||
// «Нечего выбирать» и «неопределённость» — разные ответы, и путать их нельзя.
|
||||
assert.equal(selection.unproven, false);
|
||||
assert.ok(selection.files.length > 0, 'файлы в диффе всё же были');
|
||||
});
|
||||
|
||||
test('связь не доказана — это неопределённость, а не «проверок не нужно» (#241)', () => {
|
||||
const selection = selectSmokes(fixture('unproven.diff'), { root: repoRoot });
|
||||
assert.equal(selection.noExecutableDiff, false);
|
||||
assert.equal(selection.unproven, true);
|
||||
assert.equal(strongNames(selection).length, 0);
|
||||
assert.ok(selection.unseen.includes('openingInnerFaceOffsetFromIndex'),
|
||||
'символ без смока обязан быть назван');
|
||||
});
|
||||
|
||||
test('таблица символов не берёт одиночные английские слова (#241)', () => {
|
||||
const table = symbolTable(repoRoot);
|
||||
for (const noise of ['floor', 'value', 'index', 'return', 'length', 'edit']) {
|
||||
assert.ok(!table.has(noise), `«${noise}» попал в таблицу символов и вернёт шум`);
|
||||
}
|
||||
for (const real of ['chainSegmentCms', '_draftSegmentCms', 'innerEdgeSpan']) {
|
||||
assert.ok(table.has(real), `${real} не распознан как символ проекта`);
|
||||
}
|
||||
});
|
||||
|
||||
test('parseDiff читает только исполняемый frontend (#241)', () => {
|
||||
const table = new Set(['chainSegmentCms']);
|
||||
const parsed = parseDiff([
|
||||
'diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md',
|
||||
'+chainSegmentCms упомянут в документации',
|
||||
'diff --git a/src/wall-face-graph.ts b/src/wall-face-graph.ts',
|
||||
'+export function chainSegmentCms(',
|
||||
].join('\n'), table);
|
||||
assert.deepEqual(parsed.executable, ['src/wall-face-graph.ts']);
|
||||
assert.deepEqual(parsed.symbols, ['chainSegmentCms'], 'упоминание в docs не символ диффа');
|
||||
});
|
||||
|
||||
test('каждая запись реестра объясняет себя и указывает на существующий смок (#241)', () => {
|
||||
for (const link of SMOKE_LINKS) {
|
||||
assert.ok(link.symbols.length, 'связь без символов не сработает никогда');
|
||||
assert.ok(link.because && link.because.length > 40, 'связь без объяснения — суеверие');
|
||||
for (const smoke of link.smokes) {
|
||||
assert.match(smoke, /^smoke_.*\.mjs$/);
|
||||
assert.ok(
|
||||
readFileSync(join(repoRoot, 'demo', smoke), 'utf8').length > 0,
|
||||
`${smoke} в реестре, но файла нет`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Пустой набор изменённых символов не должен давать связей.
|
||||
assert.deepEqual(registeredSmokes([]), []);
|
||||
});
|
||||
Reference in New Issue
Block a user