v1.59.0-beta.8: audit follow-ups and inner-corner sun rays

This commit is contained in:
Matysh
2026-08-05 20:07:27 +03:00
parent 3ad4e9d803
commit e188f9d609
31 changed files with 619 additions and 204 deletions
+20
View File
@@ -141,6 +141,26 @@ describe('open-spans', () => {
assert.ok(Math.max(sg[1], sg[3]) <= 4 + 1e-6, JSON.stringify(sg));
});
it('clipOpenSpansToShared keeps both room-pair pieces after Split', () => {
const rooms = [
{ id: 'left_top', poly: [[0, 0], [5, 0], [5, 2], [0, 2]] },
{ id: 'left_bottom', poly: [[0, 2], [5, 2], [5, 4], [0, 4]] },
{ id: 'right', poly: [[5, 0], [10, 0], [10, 4], [5, 4]] },
];
const clipped = clipOpenSpansToShared(
[spanToEntry([5, 1], [5, 3], 1)], rooms, 1, eps,
);
const pieces = clipped.map((e) => entryToSeg(e, 1))
.map((sg) => [Math.min(sg[1], sg[3]), Math.max(sg[1], sg[3])]);
assert.deepEqual(pieces, [[1, 2], [2, 3]]);
const cfg = rooms.map(({ id }) => ({ id }));
syncOpenToFromCuts(cfg, rooms, clipped.map((e) => entryToSeg(e, 1)), eps);
assert.deepEqual(cfg[0].open_to, ['right']);
assert.deepEqual(cfg[1].open_to, ['right']);
assert.deepEqual(cfg[2].open_to, ['left_top', 'left_bottom']);
});
it('entry round-trip', () => {
const e = spanToEntry([100, 200], [100, 400], scale);
const sg = entryToSeg(e, scale);
+36
View File
@@ -0,0 +1,36 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { classifyValidateRuns } from '../scripts/release-gate.mjs';
test('release gate waits until an exact-SHA Validate exists and completes', () => {
assert.equal(classifyValidateRuns([]), 'wait');
assert.equal(classifyValidateRuns([{ status: 'queued', conclusion: null }]), 'wait');
assert.equal(classifyValidateRuns([
{ status: 'completed', conclusion: 'success' },
{ status: 'in_progress', conclusion: null },
]), 'wait');
});
test('release gate accepts only completed success runs', () => {
assert.equal(classifyValidateRuns([
{ status: 'completed', conclusion: 'success' },
{ status: 'completed', conclusion: 'success' },
]), 'success');
});
test('release gate fails closed for red, cancelled and skipped runs', () => {
for (const conclusion of ['failure', 'cancelled', 'timed_out', 'action_required', 'skipped', null]) {
assert.equal(
classifyValidateRuns([{ status: 'completed', conclusion }]),
'fail',
`completed/${conclusion} must withhold the asset`,
);
}
});
test('one red duplicate blocks a green duplicate for the same SHA', () => {
assert.equal(classifyValidateRuns([
{ status: 'completed', conclusion: 'success' },
{ status: 'completed', conclusion: 'failure' },
]), 'fail');
});
+18
View File
@@ -329,6 +329,24 @@ test('computeSunRays: evening west sun → west window', () => {
assert.deepEqual(rays.map((r) => r.openingId), ['wW']);
});
test('computeSunRays: a thick-wall ray starts at both room-side opening corners', () => {
const win = { id: 'wW', x: 100, y: 300, angle: 90, length: 80 };
const inner = {
r1: [[110, 110], [490, 110], [490, 490], [110, 490]],
};
// Oblique sunlight is intentional: the source must remain the full inner
// aperture instead of shrinking or sliding away from either jamb corner.
const [ray] = computeSunRays(ROOMS, [win], 240, 60, 0, inner, { wW: 20 });
assert.ok(ray, 'the west window is lit');
assert.ok(near(ray.a[0], 110) && near(ray.a[1], 260), 'first inner corner');
assert.ok(near(ray.b[0], 110) && near(ray.b[1], 340), 'second inner corner');
assert.ok(near(Math.hypot(ray.b[0] - ray.a[0], ray.b[1] - ray.a[1]), 80), 'full opening width');
for (const poly of ray.polys) for (const [x, y] of poly) {
assert.ok(x >= 110 - 1e-6 && x <= 490 + 1e-6, 'clipped to the clean-floor contour');
assert.ok(y >= 110 - 1e-6 && y <= 490 + 1e-6, 'clipped to the clean-floor contour');
}
});
test('grazing sun: the auditor\'s repro, fixed by a normal-axis fade (DEV-EB173-01)', () => {
// The report's browser probe: a WEST window 80 render units long, elevation
// 90 (so the nominal reach is 0.56 · 80 = 44.8 — «на 30 % короче»), azimuth
+9 -1
View File
@@ -9,7 +9,7 @@ import {
wallBodyRings, wallBodiesUnionPath, innerContourForRoom,
paperRoomShapesWithWalls, WALL_MIN_CM, WALL_MAX_CM, MITRE_LIMIT,
atomicPolyForRoom, insetOffsetsForRoom, wallIntervals, normalizeWallIntervals,
intervalCmAt,
intervalCmAt, wallBodyNeedsSolid, WALL_HATCH_MIN_PX,
} from '../test-build/wall-thickness.js';
import { polygonArea, paperRoomShapes } from '../test-build/logic.js';
import { GRID_PITCH } from '../test-build/space-geometry.js';
@@ -61,6 +61,14 @@ test('wallCmToUnits goes through cell_cm like every other length', () => {
closeTo(wallCmToUnits(10, 5, GRID_PITCH), 2 * GRID_PITCH);
});
test('thin-on-screen fallback policy is shared by both renderers', () => {
assert.equal(wallBodyNeedsSolid(2, 1), true);
assert.equal(wallBodyNeedsSolid(WALL_HATCH_MIN_PX, 1), false);
assert.equal(wallBodyNeedsSolid(2, 2), false);
assert.equal(wallBodyNeedsSolid(Number.NaN, 1), false);
assert.equal(wallBodyNeedsSolid(2, 0), false);
});
// ------------------------------- degrade / rekey ----------------------------
test('degradeWalls drops a key with no matching room edge', () => {