fix(task-21): layout_create, layout_set_current and viewport_create mark the headless document dirty — the close refusal and the saved row now see them, and each gets an undo step
document_close's promise (never drop unsaved work unless told to) and the `saved` row of document_list are only as good as _DocState.dirty, and three headless mutators never set it: a new layout tab, the $TILEMODE / active layout write, and a paper-space VIEWPORT all changed the file yet closed without refusal, reported discarded_changes: False, and were absent on disk. COM refused the same sequence (Saved is AutoCAD's DBMOD), so the two engines disagreed. Each now calls _mark_dirty() after its write, which also pushes the undo snapshot every other mutator already has. The regression test runs the save -> mutate -> list/close/undo/redo -> close with save sequence for all three and checks the change reached the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
b1c93cdd2a
commit
31daac022f
@@ -2035,6 +2035,7 @@ class EzdxfBackend(AutoCADBackend):
|
||||
if existing is not None:
|
||||
return {"ok": False, "error": f"Layout already exists: {existing}"}
|
||||
doc.layouts.new(wanted)
|
||||
self._mark_dirty()
|
||||
return {"ok": True, "layout": wanted}
|
||||
|
||||
return await self._async(_sync)
|
||||
@@ -2056,6 +2057,10 @@ class EzdxfBackend(AutoCADBackend):
|
||||
# Stored in the canonical spelling so _msp and _resync_space
|
||||
# compare against one form.
|
||||
self._current_space = resolved
|
||||
# $TILEMODE and the active-layout binding are persisted state:
|
||||
# the file on disk differs from the document after this, so the
|
||||
# close refusal and the ``saved`` row must know about it.
|
||||
self._mark_dirty()
|
||||
return {"ok": True, "current": resolved}
|
||||
|
||||
return await self._async(_sync)
|
||||
@@ -2273,6 +2278,7 @@ class EzdxfBackend(AutoCADBackend):
|
||||
view_center_point=(view_center_x, view_center_y),
|
||||
view_height=view_height,
|
||||
)
|
||||
self._mark_dirty()
|
||||
return {
|
||||
"ok": True,
|
||||
"handle": viewport.dxf.handle,
|
||||
|
||||
@@ -25,6 +25,7 @@ import types
|
||||
import pytest
|
||||
from fastmcp import Client
|
||||
|
||||
import config
|
||||
import server
|
||||
from backends.ezdxf_backend import EzdxfBackend
|
||||
|
||||
@@ -334,6 +335,92 @@ async def test_close_unsaved_refuses_unless_discard(backend):
|
||||
}
|
||||
|
||||
|
||||
async def _create_layout(backend):
|
||||
assert (await backend.layout_create("A3-Sheet"))["ok"] is True
|
||||
|
||||
|
||||
async def _set_current_layout(backend):
|
||||
assert (await backend.layout_set_current("Layout1"))["ok"] is True
|
||||
|
||||
|
||||
async def _create_viewport(backend):
|
||||
result = await backend.viewport_create("Layout1", 150, 100, 200, 150, 0, 0, 1.0)
|
||||
assert result["ok"] is True
|
||||
|
||||
|
||||
def _layout_names(path):
|
||||
import ezdxf
|
||||
|
||||
return set(ezdxf.readfile(str(path)).layouts.names())
|
||||
|
||||
|
||||
def _layout1_has_viewport(path):
|
||||
import ezdxf
|
||||
|
||||
return any(
|
||||
e.dxftype() == "VIEWPORT" and e.dxf.id != 1
|
||||
for e in ezdxf.readfile(str(path)).layouts.get("Layout1")
|
||||
)
|
||||
|
||||
|
||||
def _tilemode(path):
|
||||
import ezdxf
|
||||
|
||||
return int(ezdxf.readfile(str(path)).header.get("$TILEMODE", 1))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mutate", "changed_on_disk"),
|
||||
[
|
||||
(_create_layout, lambda p: "A3-Sheet" in _layout_names(p)),
|
||||
(_set_current_layout, lambda p: _tilemode(p) == 0),
|
||||
(_create_viewport, _layout1_has_viewport),
|
||||
],
|
||||
ids=["layout_create", "layout_set_current", "viewport_create"],
|
||||
)
|
||||
async def test_layout_and_viewport_mutators_mark_the_document_unsaved(
|
||||
monkeypatch, tmp_path, mutate, changed_on_disk
|
||||
):
|
||||
"""The close refusal is only as good as the dirty flag behind it.
|
||||
|
||||
``layout_create``, ``layout_set_current`` and ``viewport_create`` change
|
||||
the file (a new layout tab, ``$TILEMODE`` plus the active layout, a
|
||||
VIEWPORT) but used to leave ``_DocState.dirty`` untouched, so
|
||||
``document_list`` reported ``saved: True``, ``document_close`` closed
|
||||
without refusing and reported ``discarded_changes: False`` while the work
|
||||
was gone — and the live engine, whose ``Saved`` is AutoCAD's DBMOD,
|
||||
refused the same sequence. Marking dirty also gives each an undo step,
|
||||
which is why undo is switched on here before the document exists (the
|
||||
S0 snapshot is seeded by ``drawing_new`` only when history is enabled).
|
||||
"""
|
||||
monkeypatch.setattr(config.settings, "ezdxf_undo_depth", 4)
|
||||
backend = EzdxfBackend()
|
||||
await backend.connect()
|
||||
try:
|
||||
await backend.drawing_new()
|
||||
path = tmp_path / "sheet.dxf"
|
||||
await backend.drawing_save(str(path))
|
||||
assert (await backend.document_list())[0]["saved"] is True
|
||||
assert not changed_on_disk(path)
|
||||
|
||||
await mutate(backend)
|
||||
|
||||
assert (await backend.document_list())[0]["saved"] is False
|
||||
with pytest.raises(ValueError, match="unsaved changes"):
|
||||
await backend.document_close(None)
|
||||
assert len(await backend.document_list()) == 1, "a refusal closes nothing"
|
||||
|
||||
# Every mutator pushes an undo snapshot; these must too.
|
||||
assert (await backend.drawing_undo())["undo_depth"] == 0
|
||||
assert (await backend.drawing_redo())["ok"] is True
|
||||
|
||||
result = await backend.document_close(None, save=True)
|
||||
assert result["saved"] is True and result["discarded_changes"] is False
|
||||
assert changed_on_disk(path)
|
||||
finally:
|
||||
await backend.disconnect()
|
||||
|
||||
|
||||
async def test_close_with_save_writes_the_file(backend, tmp_path):
|
||||
path = tmp_path / "saved.dxf"
|
||||
await backend.drawing_save(str(path))
|
||||
|
||||
Reference in New Issue
Block a user