Opening moves become row-honest: ← → within the row, clamped at edges

Eric asked what the ↑↓ buttons did — the honest answer was "reorder a
flat list that no longer exists, and fragment your wall diagram if
pushed across a row boundary" (A1, B1, A2 renders as THREE rows under
the letter-grouped layout). The useful half survives as ← → "move
within its row"; the endpoint clamps at row-letter boundaries so the
diagram can't fragment. Tests pin both the clamp and the legitimate
within-row swap; the acceptance flow's reorder step updated to match
the new semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-09 14:28:15 -04:00
co-authored by Claude Fable 5
parent 75162c3e2e
commit a23b97d2f9
4 changed files with 71 additions and 20 deletions
+42 -5
View File
@@ -268,16 +268,19 @@ def test_acceptance_full_furniture_flow_without_touching_json(tmp_path):
assert saved[0]["openings"][0]["label"] == "wide left"
assert saved[-1]["openings"][0].get("width_in") is None # virtual
# editable, deletable, reorderable
# editable, deletable, reorderable (within a row — moves clamp at
# row-letter boundaries so the wall diagram can't fragment)
first_cube = saved[0]["openings"][2]["id"]
second_cube = saved[0]["openings"][3]["id"]
web.post("/api/furniture/edit-opening", json={"id": first_cube, "zone": "kids"})
web.post("/api/furniture/move-opening", json={"id": first_cube, "direction": -1})
web.post(
"/api/furniture/delete-opening", json={"id": saved[0]["openings"][3]["id"]}
)
saved_mid = json.loads(cfg.furniture_path.read_text())["units"]
assert saved_mid[0]["openings"][2]["id"] == first_cube # clamped: row edge
web.post("/api/furniture/move-opening", json={"id": second_cube, "direction": -1})
web.post("/api/furniture/delete-opening", json={"id": first_cube})
saved2 = json.loads(cfg.furniture_path.read_text())["units"]
assert len(saved2[0]["openings"]) == 13
assert saved2[0]["openings"][1]["id"] == first_cube # moved up one
assert saved2[0]["openings"][2]["id"] == second_cube # swapped within row
def test_locate_flow_and_stored_in_refusal(tmp_path):
@@ -599,6 +602,40 @@ def test_second_grid_continues_row_letters(tmp_path):
assert len(set(labels)) == 6 # unique: label-addressed CSVs stay usable
def test_move_clamps_at_row_boundaries(tmp_path):
"""A move must never fragment a row: B1 cannot cross above A2 (the
renderer would draw A, B, A as three rows), but B2 and B1 can swap."""
web, cfg = _web(tmp_path, {})
web.post("/api/furniture/add-unit", json={"name": "Wall"})
web.post(
"/api/furniture/add-openings",
json={
"unit": "Wall",
"rows": 2,
"cols": 2,
"width_in": 13.0,
"height_in": 13.0,
"depth_in": 15.0,
},
)
def labels():
return [
o["label"] for o in web.get("/api/shelves").json()["units"][0]["openings"]
]
ids = {
o["label"]: o["id"]
for o in web.get("/api/shelves").json()["units"][0]["openings"]
}
# cross-row move clamps: B1 stays put
web.post("/api/furniture/move-opening", json={"id": ids["B1"], "direction": -1})
assert labels() == ["A1", "A2", "B1", "B2"]
# within-row move works: B2 left of B1
web.post("/api/furniture/move-opening", json={"id": ids["B2"], "direction": -1})
assert labels() == ["A1", "A2", "B2", "B1"]
def test_move_opening_boundaries_no_op(tmp_path):
web, cfg = _web(tmp_path, {})
web.post("/api/furniture/add-unit", json={"name": "Den"})