fix: the first write of a newer wall model is not an outdated client (#319)

A stale client can only echo the stored model_version, never raise it. The
'unchanged wall catalogue' refusal now applies only when the submitted model
is not above the stored one; the first v9 write over a v8 document with an
orphan open_span/open_to legitimately drops the legacy projection without
touching the catalogue and passes. The regression pair fixture is produced
by the real writers (stored: v1.68.0-beta.2, sent: current migration) and is
pinned on the frontend byte-for-byte so it cannot drift.

Issue: #319
User-Visible: yes
This commit is contained in:
Codex
2026-08-26 18:20:15 +03:00
parent 3d31d65735
commit 3ab6fa9f8a
6 changed files with 580 additions and 1 deletions
+39
View File
@@ -223,6 +223,45 @@ def test_stale_client_echoing_v8_catalog_gets_the_named_error() -> None:
validate_wall_model_transition(non_structural, previous)
def test_first_write_of_a_newer_model_is_not_outdated_even_without_catalog_change() -> None:
"""#319: a client can only echo the stored model_version, never raise it.
The pair fixture is generated by real writers: `stored` by the
v1.68.0-beta.2 (model v8) writer with one orphan open_span, `sent` by the
current initial migration — the catalogue is byte-identical, the orphan
span is dropped. Before the fix this exact pair raised
WallModelClientOutdatedError and wedged every structural write forever.
"""
fixture = json.loads((
Path(__file__).parents[1] / "test" / "fixtures"
/ "319-orphan-span-migration.json"
).read_text(encoding="utf-8"))
stored, sent = fixture["stored"], fixture["sent"]
assert stored["model_version"] == 8 and sent["model_version"] == 9
assert stored["spaces"][0]["wall_segments"] == sent["spaces"][0]["wall_segments"]
assert "open_spans" in stored["spaces"][0]
assert "open_spans" not in sent["spaces"][0]
# AC1: the pure migration write passes.
validate_wall_model_transition(copy.deepcopy(sent), stored)
# AC2: the same write with the user's first draft (the first click of the
# Walls tool, independent geometry per #314) passes too.
with_draft = copy.deepcopy(sent)
with_draft["spaces"][0].setdefault("room_drafts", []).append({
"id": "d-319", "points": [[0.7, 0.7], [0.8, 0.7]],
"segments": [{"id": "seg-d-319", "cm": 20}],
})
validate_wall_model_transition(with_draft, stored)
# AC3: the same-version echo (9 → 9) with changed contours and an
# unchanged catalogue keeps the named refusal.
echoed = copy.deepcopy(sent)
echoed["spaces"][0]["rooms"][0]["poly"][0][0] += 0.01
with pytest.raises(WallModelClientOutdatedError, match="unchanged wall catalogue"):
validate_wall_model_transition(echoed, copy.deepcopy(sent))
def test_current_wall_model_independent_geometry_does_not_require_contour_catalog_change() -> None:
"""Drafts, partitions, columns and hosted openings own their identity (#314)."""
previous, _ = commit_wall_segment_model(_config({