style: apply ruff format across the repository
ruff format was never run repo-wide; CI now enforces 'ruff format --check', so normalize every module once. No behavior changes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
4a2e9d658f
commit
9ec6c4a989
@@ -68,7 +68,10 @@ async def test_analysis_entity_stats(backend):
|
||||
stats = await backend.analysis_stats()
|
||||
assert isinstance(stats, dict)
|
||||
by_type = stats.get("by_type", stats)
|
||||
total = stats.get("total", stats.get("total_entities", sum(by_type.values()) if isinstance(by_type, dict) else 0))
|
||||
total = stats.get(
|
||||
"total",
|
||||
stats.get("total_entities", sum(by_type.values()) if isinstance(by_type, dict) else 0),
|
||||
)
|
||||
assert total >= 3
|
||||
|
||||
|
||||
|
||||
@@ -51,8 +51,12 @@ async def test_batch_modify_delete(backend):
|
||||
async def test_template_architectural_layers(backend):
|
||||
"""Architectural template should create standard layers."""
|
||||
layer_defs = [
|
||||
("WALLS", 7), ("DOORS", 3), ("WINDOWS", 4),
|
||||
("FURNITURE", 8), ("DIMENSIONS", 2), ("TEXT", 7),
|
||||
("WALLS", 7),
|
||||
("DOORS", 3),
|
||||
("WINDOWS", 4),
|
||||
("FURNITURE", 8),
|
||||
("DIMENSIONS", 2),
|
||||
("TEXT", 7),
|
||||
]
|
||||
for name, color in layer_defs:
|
||||
await backend.layer_create(name, color=color)
|
||||
@@ -67,8 +71,11 @@ async def test_template_architectural_layers(backend):
|
||||
async def test_template_mechanical_layers(backend):
|
||||
"""Mechanical template layers."""
|
||||
layer_defs = [
|
||||
("VISIBLE", 7), ("HIDDEN", 1), ("CENTER", 3),
|
||||
("DIMENSIONS", 2), ("SECTION", 5),
|
||||
("VISIBLE", 7),
|
||||
("HIDDEN", 1),
|
||||
("CENTER", 3),
|
||||
("DIMENSIONS", 2),
|
||||
("SECTION", 5),
|
||||
]
|
||||
for name, color in layer_defs:
|
||||
await backend.layer_create(name, color=color)
|
||||
|
||||
@@ -48,6 +48,7 @@ async def test_active_document_switch_clears_com_document_state(monkeypatch):
|
||||
assert backend._preflight_result is None
|
||||
assert backend._gdt_datums_defined == set()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# normalize_lineweight — parametrized table
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -107,7 +108,12 @@ def test_normalize_lineweight_non_numeric_passthrough():
|
||||
# _safe_send_command — CMDACTIVE polling / timeout / var-restore
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_mock_doc(cmdactive_sequence: list[int], modelspace_handles_pre: set[str] | None = None, modelspace_handles_post: set[str] | None = None) -> MagicMock:
|
||||
|
||||
def _make_mock_doc(
|
||||
cmdactive_sequence: list[int],
|
||||
modelspace_handles_pre: set[str] | None = None,
|
||||
modelspace_handles_post: set[str] | None = None,
|
||||
) -> MagicMock:
|
||||
"""Build a minimal mock AutoCAD document for _safe_send_command tests."""
|
||||
doc = MagicMock()
|
||||
|
||||
@@ -270,9 +276,9 @@ async def test_dimension_linear_uses_add_dim_rotated():
|
||||
mspace.AddDimRotated.assert_called_once()
|
||||
mspace.AddDimLinear.assert_not_called()
|
||||
args = mspace.AddDimRotated.call_args[0]
|
||||
assert args[0] == (0.0, 0.0) # ext line 1 origin
|
||||
assert args[1] == (100.0, 0.0) # ext line 2 origin
|
||||
assert args[2] == (50.0, 20.0) # dim line location
|
||||
assert args[0] == (0.0, 0.0) # ext line 1 origin
|
||||
assert args[1] == (100.0, 0.0) # ext line 2 origin
|
||||
assert args[2] == (50.0, 20.0) # dim line location
|
||||
assert args[3] == pytest.approx(0.0) # rotation in radians
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ class _Doc:
|
||||
|
||||
# ── NEW-com-set-variable-coercion ───────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_set_variable_coerces_string_to_sysvar_int_type(monkeypatch):
|
||||
app = MagicMock()
|
||||
app.GetVariable.return_value = 4159 # OSMODE current value is an int
|
||||
@@ -69,6 +70,7 @@ async def test_set_variable_coerces_string_to_sysvar_int_type(monkeypatch):
|
||||
|
||||
# ── R23 / N6 — run_lisp guard + value capture ───────────────────────────────
|
||||
|
||||
|
||||
async def test_run_lisp_refuses_when_cmdactive(monkeypatch):
|
||||
app = MagicMock()
|
||||
app.GetVariable.side_effect = lambda n: 1 if n == "CMDACTIVE" else 0
|
||||
@@ -90,12 +92,13 @@ async def test_run_lisp_captures_value_from_users1(monkeypatch):
|
||||
b = _backend_no_executor()
|
||||
out = await b.system_run_lisp("(+ 1 2)")
|
||||
|
||||
assert out["result"] == "3" # captured, not the bogus "nil"
|
||||
assert out["result"] == "3" # captured, not the bogus "nil"
|
||||
assert any("USERS1" in c for c in doc.sent) # value stashed out-of-band
|
||||
|
||||
|
||||
# ── R16 — transaction flag cleared even on error ────────────────────────────
|
||||
|
||||
|
||||
async def test_transaction_begin_sets_flag_and_marks_undo(monkeypatch):
|
||||
doc = _Doc()
|
||||
doc.StartUndoMark = MagicMock()
|
||||
@@ -136,10 +139,11 @@ async def test_transaction_rollback_uses_safe_send_command(monkeypatch):
|
||||
|
||||
# ── R13 — offset honors side_x/side_y and deletes the unused copy ───────────
|
||||
|
||||
|
||||
async def test_offset_picks_copy_nearest_side_point(monkeypatch):
|
||||
# ent.Offset(+d) -> copy centred at (0, +10); ent.Offset(-d) -> (0, -10).
|
||||
pos_copy = MagicMock(name="pos")
|
||||
pos_copy.GetBoundingBox.return_value = ((-1, 9), (1, 11)) # centre (0, 10)
|
||||
pos_copy.GetBoundingBox.return_value = ((-1, 9), (1, 11)) # centre (0, 10)
|
||||
neg_copy = MagicMock(name="neg")
|
||||
neg_copy.GetBoundingBox.return_value = ((-1, -11), (1, -9)) # centre (0, -10)
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ async def test_drawing_export_dxf(backend, tmp_path):
|
||||
result = await backend.drawing_export_dxf(export_path)
|
||||
assert result.get("ok") is True
|
||||
import os
|
||||
|
||||
assert os.path.exists(export_path)
|
||||
|
||||
|
||||
|
||||
@@ -33,9 +33,13 @@ async def test_set_units_cm_and_inch_codes(backend):
|
||||
|
||||
|
||||
async def test_set_numeric_settings(backend):
|
||||
res = await backend.drawing_settings({
|
||||
"dimscale": 2.0, "linear_precision": 3, "ltscale": 0.5,
|
||||
})
|
||||
res = await backend.drawing_settings(
|
||||
{
|
||||
"dimscale": 2.0,
|
||||
"linear_precision": 3,
|
||||
"ltscale": 0.5,
|
||||
}
|
||||
)
|
||||
assert res["ok"] is True
|
||||
snap = (await backend.drawing_settings())["settings"]
|
||||
assert snap["dimscale"] == 2.0
|
||||
@@ -57,6 +61,7 @@ async def test_bad_unit_value_is_reported(backend):
|
||||
|
||||
# ── server tool wiring ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class _FakeCtx:
|
||||
def __init__(self, backend):
|
||||
self.lifespan_context = {"backend": backend}
|
||||
|
||||
@@ -66,19 +66,41 @@ async def test_helical_gear_front_view_returns_metadata(backend):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
res = await draw_helical_gear_front_view(
|
||||
backend, module=3, teeth=24, helix_angle=20.0, hand="RH",
|
||||
backend,
|
||||
module=3,
|
||||
teeth=24,
|
||||
helix_angle=20.0,
|
||||
hand="RH",
|
||||
center=(0.0, 0.0),
|
||||
)
|
||||
expected_keys = {
|
||||
"outline", "pitch_circle", "base_circle", "outer_circle", "root_circle",
|
||||
"helix_symbol", "helix_label", "bore", "keyway", "metadata",
|
||||
"outline",
|
||||
"pitch_circle",
|
||||
"base_circle",
|
||||
"outer_circle",
|
||||
"root_circle",
|
||||
"helix_symbol",
|
||||
"helix_label",
|
||||
"bore",
|
||||
"keyway",
|
||||
"metadata",
|
||||
}
|
||||
assert expected_keys.issubset(res.keys())
|
||||
md = res["metadata"]
|
||||
for k in (
|
||||
"module", "teeth", "pressure_angle", "helix_angle", "hand", "center",
|
||||
"bore_diameter", "keyway_width", "keyway_depth",
|
||||
"pitch_radius", "outer_radius", "base_radius", "root_radius",
|
||||
"module",
|
||||
"teeth",
|
||||
"pressure_angle",
|
||||
"helix_angle",
|
||||
"hand",
|
||||
"center",
|
||||
"bore_diameter",
|
||||
"keyway_width",
|
||||
"keyway_depth",
|
||||
"pitch_radius",
|
||||
"outer_radius",
|
||||
"base_radius",
|
||||
"root_radius",
|
||||
):
|
||||
assert k in md, f"metadata missing key {k!r}"
|
||||
|
||||
@@ -88,8 +110,13 @@ async def test_helical_gear_with_bore_and_keyway_handles_present(backend):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
res = await draw_helical_gear_front_view(
|
||||
backend, module=3, teeth=24, helix_angle=20.0,
|
||||
bore_diameter=25.0, keyway_width=8.0, keyway_depth=4.0,
|
||||
backend,
|
||||
module=3,
|
||||
teeth=24,
|
||||
helix_angle=20.0,
|
||||
bore_diameter=25.0,
|
||||
keyway_width=8.0,
|
||||
keyway_depth=4.0,
|
||||
)
|
||||
assert res["bore"] is not None
|
||||
assert res["keyway"] is not None
|
||||
@@ -102,7 +129,11 @@ async def test_helical_gear_metadata_radii_correct(backend):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
res = await draw_helical_gear_front_view(
|
||||
backend, module=3, teeth=24, helix_angle=20.0, pressure_angle=20.0,
|
||||
backend,
|
||||
module=3,
|
||||
teeth=24,
|
||||
helix_angle=20.0,
|
||||
pressure_angle=20.0,
|
||||
)
|
||||
md = res["metadata"]
|
||||
assert math.isclose(md["pitch_radius"], 36.0, abs_tol=1e-6)
|
||||
@@ -116,12 +147,19 @@ async def test_section_aa_does_not_draw_helix_lines(backend):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
front = await draw_helical_gear_front_view(
|
||||
backend, module=3, teeth=24, helix_angle=20.0,
|
||||
bore_diameter=25.0, keyway_width=8.0, keyway_depth=4.0,
|
||||
backend,
|
||||
module=3,
|
||||
teeth=24,
|
||||
helix_angle=20.0,
|
||||
bore_diameter=25.0,
|
||||
keyway_width=8.0,
|
||||
keyway_depth=4.0,
|
||||
)
|
||||
section = await draw_gear_section_aa(
|
||||
backend, gear_metadata=front["metadata"],
|
||||
x_offset=200.0, face_width=40.0,
|
||||
backend,
|
||||
gear_metadata=front["metadata"],
|
||||
x_offset=200.0,
|
||||
face_width=40.0,
|
||||
)
|
||||
assert "helix" not in section
|
||||
assert "helix_symbol" not in section
|
||||
|
||||
@@ -37,8 +37,11 @@ async def test_draw_keyed_bore_returns_expected_handles(backend):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
res = await draw_keyed_bore(
|
||||
backend, center=(0.0, 0.0), bore_diameter=25.0,
|
||||
keyway_width=8.0, keyway_depth=4.0,
|
||||
backend,
|
||||
center=(0.0, 0.0),
|
||||
bore_diameter=25.0,
|
||||
keyway_width=8.0,
|
||||
keyway_depth=4.0,
|
||||
)
|
||||
for k in ("bore", "keyway_polyline", "centerline_h", "centerline_v"):
|
||||
assert k in res
|
||||
@@ -50,8 +53,12 @@ async def test_draw_keyway_section_geometry(backend):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
res = await draw_keyway_section(
|
||||
backend, center=(0.0, 0.0), bore_diameter=25.0, face_width=40.0,
|
||||
keyway_width=8.0, keyway_depth=4.0,
|
||||
backend,
|
||||
center=(0.0, 0.0),
|
||||
bore_diameter=25.0,
|
||||
face_width=40.0,
|
||||
keyway_width=8.0,
|
||||
keyway_depth=4.0,
|
||||
)
|
||||
assert "bore_top" in res
|
||||
assert "bore_bottom" in res
|
||||
|
||||
@@ -39,7 +39,8 @@ async def test_titleblock_outer_border_dimensions(backend):
|
||||
async def test_title_text_exact_match(backend):
|
||||
"""Title text in the drawing must equal metadata.title verbatim."""
|
||||
result = await apply_iso_a3_titleblock(
|
||||
backend, metadata=_meta(title="HELICAL GEAR"),
|
||||
backend,
|
||||
metadata=_meta(title="HELICAL GEAR"),
|
||||
)
|
||||
title_ent = await backend.entity_get(result["title_text"])
|
||||
assert title_ent.type == "TEXT"
|
||||
@@ -50,9 +51,16 @@ async def test_titleblock_creates_text_for_each_metadata_field(backend):
|
||||
"""value_texts must contain a handle for every metadata field."""
|
||||
result = await apply_iso_a3_titleblock(backend, metadata=_meta())
|
||||
expected_keys = {
|
||||
"drawing_no", "revision", "sheet",
|
||||
"part_no", "material", "scale",
|
||||
"drawn_by", "checked_by", "date", "company",
|
||||
"drawing_no",
|
||||
"revision",
|
||||
"sheet",
|
||||
"part_no",
|
||||
"material",
|
||||
"scale",
|
||||
"drawn_by",
|
||||
"checked_by",
|
||||
"date",
|
||||
"company",
|
||||
}
|
||||
assert set(result["value_texts"].keys()) == expected_keys
|
||||
# All handles must resolve to TEXT entities
|
||||
@@ -64,7 +72,9 @@ async def test_titleblock_creates_text_for_each_metadata_field(backend):
|
||||
async def test_titleblock_origin_offset(backend):
|
||||
"""Passing origin=(100,50) must shift the bbox by that amount."""
|
||||
result = await apply_iso_a3_titleblock(
|
||||
backend, metadata=_meta(), origin=(100.0, 50.0),
|
||||
backend,
|
||||
metadata=_meta(),
|
||||
origin=(100.0, 50.0),
|
||||
)
|
||||
bbox = result["bbox"]
|
||||
assert bbox["min"][0] == pytest.approx(100.0)
|
||||
|
||||
@@ -73,7 +73,11 @@ async def test_validator_catches_helical_spur_title(backend, tmp_path):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
await backend.entity_create_text(
|
||||
text="HELICAL SPUR GEAR", x=0, y=0, height=5, layer="TEXT",
|
||||
text="HELICAL SPUR GEAR",
|
||||
x=0,
|
||||
y=0,
|
||||
height=5,
|
||||
layer="TEXT",
|
||||
)
|
||||
await backend.drawing_save_as(str(tmp_path / "tb.dxf"))
|
||||
result = await DrawingValidator().run(backend, expected={"helix_angle": 20})
|
||||
@@ -86,7 +90,11 @@ async def test_validator_catches_fake_dimension_text(backend, tmp_path):
|
||||
await ensure_standard_linetypes(backend)
|
||||
await ensure_engineering_layers(backend)
|
||||
await backend.entity_create_text(
|
||||
text="Ø78", x=10, y=10, height=3.5, layer="DIM",
|
||||
text="Ø78",
|
||||
x=10,
|
||||
y=10,
|
||||
height=3.5,
|
||||
layer="DIM",
|
||||
)
|
||||
await backend.drawing_save_as(str(tmp_path / "fake.dxf"))
|
||||
result = await DrawingValidator().run(backend)
|
||||
@@ -124,7 +132,8 @@ async def test_validator_bore_missing_when_expected(backend, tmp_path):
|
||||
# No circle drawn at all
|
||||
await backend.drawing_save_as(str(tmp_path / "nobore.dxf"))
|
||||
result = await DrawingValidator().run(
|
||||
backend, expected={"must_have_bore": True},
|
||||
backend,
|
||||
expected={"must_have_bore": True},
|
||||
)
|
||||
assert "bore_missing" in _codes(result)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ def _close(a, b, tol=1e-6):
|
||||
|
||||
# ─── TRIM ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestTrim:
|
||||
async def test_trim_keep_left(self, backend):
|
||||
target = await backend.entity_create_line(0, 0, 100, 0)
|
||||
@@ -55,6 +56,7 @@ class TestTrim:
|
||||
|
||||
# ─── EXTEND ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestExtend:
|
||||
async def test_extend_auto_endpoint(self, backend):
|
||||
# Target ends at x=40; boundary is vertical at x=80.
|
||||
@@ -84,6 +86,7 @@ class TestExtend:
|
||||
|
||||
# ─── FILLET ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestFillet:
|
||||
async def test_fillet_90_degree_corner(self, backend):
|
||||
# L-shape: horizontal (0,0)-(100,0) + vertical (100,0)-(100,80).
|
||||
@@ -139,6 +142,7 @@ class TestFillet:
|
||||
|
||||
# ─── CHAMFER ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestChamfer:
|
||||
async def test_chamfer_symmetric(self, backend):
|
||||
l1 = await backend.entity_create_line(0, 0, 100, 0)
|
||||
|
||||
@@ -14,10 +14,11 @@ pytestmark = pytest.mark.asyncio
|
||||
|
||||
# ── entity_edit_text ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_edit_text_changes_content_keeps_handle(backend):
|
||||
t = await backend.entity_create_text("OLD LABEL", 0, 0, height=2.5, layer="TEXT")
|
||||
updated = await backend.entity_edit_text(t.handle, text="NEW LABEL")
|
||||
assert updated.handle == t.handle # handle preserved
|
||||
assert updated.handle == t.handle # handle preserved
|
||||
assert updated.properties["text"] == "NEW LABEL"
|
||||
|
||||
|
||||
@@ -44,6 +45,7 @@ async def test_edit_text_rejects_non_text(backend):
|
||||
|
||||
# ── entity_edit_geometry ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_edit_circle_radius_and_center(backend):
|
||||
c = await backend.entity_create_circle(0, 0, 10, layer="GEOMETRY")
|
||||
updated = await backend.entity_edit_geometry(c.handle, cx=5, cy=5, radius=20)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Tests for ezdxf backend — no AutoCAD required."""
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
+16
-1
@@ -46,15 +46,18 @@ def _reload_server_with_token(token: str | None):
|
||||
# Auth object construction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_auth_object_built_when_token_set():
|
||||
"""_auth is a StaticTokenVerifier when MCP_AUTH_TOKEN is non-empty."""
|
||||
with patch.dict("os.environ", {"MCP_AUTH_TOKEN": "secret-test-token-123"}):
|
||||
import config as cfg
|
||||
|
||||
old_token = cfg.settings.mcp_auth_token
|
||||
cfg.settings.mcp_auth_token = "secret-test-token-123"
|
||||
try:
|
||||
sys.modules.pop("server", None)
|
||||
import server as srv
|
||||
|
||||
assert srv._auth is not None
|
||||
assert isinstance(srv._auth, StaticTokenVerifier)
|
||||
finally:
|
||||
@@ -65,11 +68,13 @@ def test_auth_object_built_when_token_set():
|
||||
def test_auth_object_none_when_no_token():
|
||||
"""_auth is None when MCP_AUTH_TOKEN is empty (anonymous loopback mode)."""
|
||||
import config as cfg
|
||||
|
||||
old_token = cfg.settings.mcp_auth_token
|
||||
cfg.settings.mcp_auth_token = ""
|
||||
try:
|
||||
sys.modules.pop("server", None)
|
||||
import server as srv
|
||||
|
||||
assert srv._auth is None
|
||||
finally:
|
||||
cfg.settings.mcp_auth_token = old_token
|
||||
@@ -79,11 +84,13 @@ def test_auth_object_none_when_no_token():
|
||||
def test_mcp_receives_auth_when_set():
|
||||
"""The FastMCP instance's auth attribute matches the _auth object."""
|
||||
import config as cfg
|
||||
|
||||
old_token = cfg.settings.mcp_auth_token
|
||||
cfg.settings.mcp_auth_token = "another-secret"
|
||||
try:
|
||||
sys.modules.pop("server", None)
|
||||
import server as srv
|
||||
|
||||
# FastMCP stores auth as _auth_provider or similar; we check it via
|
||||
# the _auth module-level variable matching what was passed in.
|
||||
assert srv._auth is not None
|
||||
@@ -97,11 +104,13 @@ def test_mcp_receives_auth_when_set():
|
||||
# _validate_http_bind guard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_validate_http_bind_loopback_no_error():
|
||||
"""Loopback hosts pass validation regardless of token/allow_remote flags."""
|
||||
sys.modules.pop("server", None)
|
||||
import config as cfg
|
||||
import server as srv
|
||||
|
||||
old_remote = cfg.settings.allow_remote_http
|
||||
old_token = cfg.settings.mcp_auth_token
|
||||
cfg.settings.allow_remote_http = False
|
||||
@@ -121,6 +130,7 @@ def test_validate_http_bind_remote_without_flag_raises():
|
||||
sys.modules.pop("server", None)
|
||||
import config as cfg
|
||||
import server as srv
|
||||
|
||||
old = cfg.settings.allow_remote_http
|
||||
cfg.settings.allow_remote_http = False
|
||||
try:
|
||||
@@ -135,6 +145,7 @@ def test_validate_http_bind_remote_without_token_raises():
|
||||
sys.modules.pop("server", None)
|
||||
import config as cfg
|
||||
import server as srv
|
||||
|
||||
old_remote = cfg.settings.allow_remote_http
|
||||
old_token = cfg.settings.mcp_auth_token
|
||||
cfg.settings.allow_remote_http = True
|
||||
@@ -150,9 +161,11 @@ def test_validate_http_bind_remote_without_token_raises():
|
||||
def test_validate_http_bind_remote_with_token_logs_warning(caplog):
|
||||
"""Remote bind with both flag and token logs a warning but does not raise."""
|
||||
import logging
|
||||
|
||||
sys.modules.pop("server", None)
|
||||
import config as cfg
|
||||
import server as srv
|
||||
|
||||
old_remote = cfg.settings.allow_remote_http
|
||||
old_token = cfg.settings.mcp_auth_token
|
||||
cfg.settings.allow_remote_http = True
|
||||
@@ -160,7 +173,9 @@ def test_validate_http_bind_remote_with_token_logs_warning(caplog):
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING, logger="server"):
|
||||
srv._validate_http_bind("0.0.0.0") # should not raise
|
||||
assert any("non-loopback" in r.message.lower() or "0.0.0.0" in r.message for r in caplog.records)
|
||||
assert any(
|
||||
"non-loopback" in r.message.lower() or "0.0.0.0" in r.message for r in caplog.records
|
||||
)
|
||||
finally:
|
||||
cfg.settings.allow_remote_http = old_remote
|
||||
cfg.settings.mcp_auth_token = old_token
|
||||
|
||||
@@ -16,10 +16,13 @@ def _close(a, b, tol=1e-6):
|
||||
|
||||
# ─── drawing_plan ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestDrawingPlan:
|
||||
async def test_plan_returns_spec_with_intent(self, backend):
|
||||
plan = await backend.drawing_plan(
|
||||
"L-bracket 50x80", sheet_size="A4", scale=1.0,
|
||||
"L-bracket 50x80",
|
||||
sheet_size="A4",
|
||||
scale=1.0,
|
||||
)
|
||||
assert plan.intent == "L-bracket 50x80"
|
||||
assert plan.sheet_size == "A4"
|
||||
@@ -36,6 +39,7 @@ class TestDrawingPlan:
|
||||
|
||||
# ─── drawing_critique ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestDrawingCritique:
|
||||
async def test_critique_clean_drawing(self, backend):
|
||||
# Bootstrap layer set so layer_color/iso128 don't trigger.
|
||||
@@ -73,6 +77,7 @@ class TestDrawingCritique:
|
||||
|
||||
# ─── point_from_snap ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestPointFromSnap:
|
||||
async def test_snap_end_default(self, backend):
|
||||
line = await backend.entity_create_line(0, 0, 100, 0)
|
||||
@@ -122,6 +127,7 @@ class TestPointFromSnap:
|
||||
|
||||
# ─── construction layer ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestConstructionLayer:
|
||||
async def test_xline_creates_on_construction_layer(self, backend):
|
||||
xl = await backend.construction_xline(0, 0, 45)
|
||||
@@ -140,6 +146,7 @@ class TestConstructionLayer:
|
||||
|
||||
# ─── drawing_apply_iso_layers ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestApplyISOLayers:
|
||||
async def test_mech_layer_set(self, backend):
|
||||
result = await backend.drawing_apply_iso_layers("mech")
|
||||
@@ -170,13 +177,15 @@ class TestApplyISOLayers:
|
||||
|
||||
# ─── dimension_auto ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestDimensionAuto:
|
||||
async def test_chain_creates_one_dim_per_segment(self, backend):
|
||||
l1 = await backend.entity_create_line(0, 0, 30, 0)
|
||||
l2 = await backend.entity_create_line(30, 0, 70, 0)
|
||||
l3 = await backend.entity_create_line(70, 0, 100, 0)
|
||||
dims = await backend.dimension_auto(
|
||||
[l1.handle, l2.handle, l3.handle], style="chain",
|
||||
[l1.handle, l2.handle, l3.handle],
|
||||
style="chain",
|
||||
)
|
||||
assert len(dims) == 3
|
||||
|
||||
@@ -198,6 +207,7 @@ class TestDimensionAuto:
|
||||
|
||||
# ─── entity_select_smart ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestEntitySelectSmart:
|
||||
async def test_select_by_type(self, backend):
|
||||
await backend.entity_create_line(0, 0, 50, 0)
|
||||
@@ -214,13 +224,13 @@ class TestEntitySelectSmart:
|
||||
assert len(result) == 1
|
||||
|
||||
async def test_select_by_near(self, backend):
|
||||
await backend.entity_create_line(0, 0, 10, 0) # near origin
|
||||
await backend.entity_create_line(0, 0, 10, 0) # near origin
|
||||
await backend.entity_create_line(1000, 1000, 1010, 1000) # far
|
||||
result = await backend.entity_select_smart({"type": "LINE", "near": [0, 0, 50]})
|
||||
assert len(result) == 1
|
||||
|
||||
async def test_select_by_length_range(self, backend):
|
||||
await backend.entity_create_line(0, 0, 10, 0) # length 10
|
||||
await backend.entity_create_line(0, 0, 10, 0) # length 10
|
||||
await backend.entity_create_line(0, 50, 100, 50) # length 100
|
||||
result = await backend.entity_select_smart({"type": "LINE", "length_range": [50, 200]})
|
||||
assert len(result) == 1
|
||||
@@ -228,6 +238,7 @@ class TestEntitySelectSmart:
|
||||
|
||||
# ─── Integration: plan → draw → critique pipeline ──────────────────────────
|
||||
|
||||
|
||||
class TestPremiumPipeline:
|
||||
async def test_full_l_bracket_pipeline_clean(self, backend):
|
||||
"""plan → apply_layers → draw + fillet → clear construction → critique = []."""
|
||||
|
||||
@@ -13,6 +13,7 @@ pytestmark = pytest.mark.asyncio
|
||||
# point_intersection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestPointIntersection:
|
||||
async def test_line_line_perpendicular(self, backend):
|
||||
"""Horizontal and vertical lines cross at (50, 50)."""
|
||||
@@ -91,6 +92,7 @@ class TestPointIntersection:
|
||||
# point_tangent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestPointTangent:
|
||||
async def test_tangent_from_outside(self, backend):
|
||||
"""From (0, 0) to unit circle at (5, 0): tangent point is at (25/5, ±12/5)."""
|
||||
|
||||
@@ -100,7 +100,8 @@ def test_preflight_does_not_invent_values_when_assumptions_disabled():
|
||||
@pytest.mark.asyncio
|
||||
async def test_backend_plan_accepts_matching_ready_preflight(backend):
|
||||
preflight = await backend.drawing_preflight(
|
||||
"L bracket", requirements=_complete_requirements(),
|
||||
"L bracket",
|
||||
requirements=_complete_requirements(),
|
||||
)
|
||||
|
||||
plan = await backend.drawing_plan(
|
||||
@@ -125,7 +126,8 @@ async def test_backend_plan_rejects_not_ready_preflight_hash(backend):
|
||||
@pytest.mark.asyncio
|
||||
async def test_backend_plan_rejects_fields_that_do_not_match_preflight_hash(backend):
|
||||
preflight = await backend.drawing_preflight(
|
||||
"L bracket", requirements=_complete_requirements(),
|
||||
"L bracket",
|
||||
requirements=_complete_requirements(),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="do not match"):
|
||||
@@ -148,7 +150,8 @@ async def test_legacy_plan_remains_compatible_and_marks_preflight_skipped(backen
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_change_clears_plan_preflight_and_gdt_state(backend):
|
||||
preflight = await backend.drawing_preflight(
|
||||
"First drawing", requirements=_complete_requirements(),
|
||||
"First drawing",
|
||||
requirements=_complete_requirements(),
|
||||
)
|
||||
await backend.drawing_plan(
|
||||
"First drawing",
|
||||
|
||||
@@ -66,9 +66,7 @@ async def test_refiner_repairs_construction_and_duplicates(backend):
|
||||
assert result.status == "threshold_met"
|
||||
assert result.final_score == 100.0
|
||||
assert result.rounds[0].score_after > result.rounds[0].score_before
|
||||
assert await backend.drawing_critique(
|
||||
["duplicate_entities", "construction_left"]
|
||||
) == []
|
||||
assert await backend.drawing_critique(["duplicate_entities", "construction_left"]) == []
|
||||
|
||||
|
||||
async def test_refiner_dry_run_does_not_mutate_drawing(backend):
|
||||
|
||||
@@ -73,6 +73,7 @@ class TestValidatePath:
|
||||
monkeypatch.setenv("ALLOWED_PATHS", str(allowed_dir))
|
||||
|
||||
import config
|
||||
|
||||
config.settings = Settings()
|
||||
|
||||
blocked = tmp_path / "blocked" / "file.dxf"
|
||||
@@ -98,6 +99,7 @@ class TestValidatePath:
|
||||
monkeypatch.setenv("ALLOWED_PATHS", str(allowed_dir))
|
||||
|
||||
import config
|
||||
|
||||
config.settings = Settings()
|
||||
|
||||
result = validate_path(str(test_file))
|
||||
@@ -160,6 +162,7 @@ class TestSanitizeCommand:
|
||||
monkeypatch.setenv("DANGEROUS_COMMANDS_ENABLED", "true")
|
||||
|
||||
import config
|
||||
|
||||
config.settings = Settings()
|
||||
|
||||
result = sanitize_command("ERASE ALL")
|
||||
@@ -222,6 +225,7 @@ class TestSanitizeLisp:
|
||||
monkeypatch.setenv("DANGEROUS_COMMANDS_ENABLED", "true")
|
||||
|
||||
import config
|
||||
|
||||
config.settings = Settings()
|
||||
|
||||
result = sanitize_lisp('(startapp "cmd")')
|
||||
|
||||
@@ -32,6 +32,7 @@ class _FakeCtx:
|
||||
|
||||
# ── NEW-screenshot-1 ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_screenshot_renders_png_without_gui_thread_crash(backend):
|
||||
"""view_screenshot runs in a worker thread; the Agg figure must not crash and
|
||||
must return valid PNG bytes."""
|
||||
@@ -43,6 +44,7 @@ async def test_screenshot_renders_png_without_gui_thread_crash(backend):
|
||||
|
||||
# ── I15 — critique is part of the finalize gate ─────────────────────────────
|
||||
|
||||
|
||||
async def test_finalize_runs_critique_and_blocks_on_construction(backend, tmp_path):
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
@@ -68,6 +70,7 @@ async def test_finalize_runs_critique_and_blocks_on_construction(backend, tmp_pa
|
||||
|
||||
# ── R4 — dim_overlap critique fires ─────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_dim_overlap_critique_is_not_a_noop(backend):
|
||||
await backend.drawing_apply_iso_layers("mech")
|
||||
# Two dimensions measuring the same segment overlap exactly.
|
||||
@@ -80,6 +83,7 @@ async def test_dim_overlap_critique_is_not_a_noop(backend):
|
||||
|
||||
# ── N4 — dim / construction layers follow the active layer set ──────────────
|
||||
|
||||
|
||||
async def test_iso13567_dim_and_construction_layers_resolve(backend):
|
||||
await backend.drawing_apply_iso_layers("iso13567")
|
||||
assert backend._active_layer_set_id() == "iso13567"
|
||||
@@ -102,6 +106,7 @@ async def test_iso13567_dim_and_construction_layers_resolve(backend):
|
||||
|
||||
# ── NEW-AUTH-1 — bind guard on the run_async launch path ────────────────────
|
||||
|
||||
|
||||
async def test_run_async_invokes_bind_guard_for_http(monkeypatch):
|
||||
import server
|
||||
|
||||
@@ -121,8 +126,7 @@ async def test_run_async_skips_guard_for_stdio(monkeypatch):
|
||||
import server
|
||||
|
||||
seen = {}
|
||||
monkeypatch.setattr(server, "_validate_http_bind",
|
||||
lambda h: seen.setdefault("host", h))
|
||||
monkeypatch.setattr(server, "_validate_http_bind", lambda h: seen.setdefault("host", h))
|
||||
|
||||
async def fake_orig(*args, **kwargs):
|
||||
return "stdio-ran"
|
||||
@@ -135,6 +139,7 @@ async def test_run_async_skips_guard_for_stdio(monkeypatch):
|
||||
|
||||
# ── NEW-gear-roottooth — outline never dips inside the root circle ──────────
|
||||
|
||||
|
||||
async def test_gear_outline_stays_outside_root_circle():
|
||||
from engineering.gear import generate_full_gear_outline
|
||||
|
||||
@@ -144,19 +149,28 @@ async def test_gear_outline_stays_outside_root_circle():
|
||||
pts = generate_full_gear_outline(module, teeth, pa)
|
||||
radii = [math.hypot(x, y) for x, y in pts]
|
||||
# No point may sit inside the root circle (the self-overlap bug for z>=~42).
|
||||
assert min(radii) >= root_r - 1e-6, f"m={module} z={teeth}: min_r {min(radii)} < root_r {root_r}"
|
||||
assert min(radii) >= root_r - 1e-6, (
|
||||
f"m={module} z={teeth}: min_r {min(radii)} < root_r {root_r}"
|
||||
)
|
||||
|
||||
|
||||
# ── NEW-section-dupbore / NEW-validator-keyway / NEW-gear-baseconstr ─────────
|
||||
|
||||
|
||||
async def test_gear_front_view_base_circle_persists_and_keyway_validates(backend, tmp_path):
|
||||
from engineering.gear import draw_helical_gear_front_view
|
||||
from engineering.validator import DrawingValidator
|
||||
|
||||
await backend.drawing_apply_iso_layers("mech")
|
||||
res = await draw_helical_gear_front_view(
|
||||
backend, module=2, teeth=20, helix_angle=0, center=(0, 0),
|
||||
bore_diameter=20, keyway_width=6, keyway_depth=2.8,
|
||||
backend,
|
||||
module=2,
|
||||
teeth=20,
|
||||
helix_angle=0,
|
||||
center=(0, 0),
|
||||
bore_diameter=20,
|
||||
keyway_width=6,
|
||||
keyway_depth=2.8,
|
||||
)
|
||||
# NEW-gear-baseconstr: base circle is a persistent PHANTOM reference, not CONSTRUCTION.
|
||||
base = await backend.entity_get(res["base_circle"])
|
||||
@@ -167,7 +181,8 @@ async def test_gear_front_view_base_circle_persists_and_keyway_validates(backend
|
||||
# NEW-validator-keyway: the open 4-point keyway polyline is now recognised.
|
||||
await backend.drawing_save_as(str(tmp_path / "gear.dxf"))
|
||||
vr = await DrawingValidator().run(
|
||||
backend, expected={"must_have_bore": True, "must_have_keyway": True},
|
||||
backend,
|
||||
expected={"must_have_bore": True, "must_have_keyway": True},
|
||||
)
|
||||
assert "keyway_section_unverified" not in [f.code for f in vr.findings]
|
||||
|
||||
@@ -178,9 +193,15 @@ async def test_gear_section_does_not_duplicate_bore_lines(backend):
|
||||
await backend.drawing_apply_iso_layers("mech")
|
||||
await draw_gear_section_aa(
|
||||
backend,
|
||||
gear_metadata={"outer_radius": 22, "center": [0, 0], "bore_diameter": 20,
|
||||
"keyway_width": 6, "keyway_depth": 2.8},
|
||||
x_offset=100, face_width=30,
|
||||
gear_metadata={
|
||||
"outer_radius": 22,
|
||||
"center": [0, 0],
|
||||
"bore_diameter": 20,
|
||||
"keyway_width": 6,
|
||||
"keyway_depth": 2.8,
|
||||
},
|
||||
x_offset=100,
|
||||
face_width=30,
|
||||
)
|
||||
dups = await backend.drawing_critique(focus=["duplicate_entities"])
|
||||
assert dups == []
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Regression tests for the CI-verifiable (ezdxf / shared) Sprint-3 fixes.
|
||||
|
||||
* N2 — drawing_save_as derives the on-disk format from the path extension
|
||||
* N3 — ARC carries a `length` property; entity_select_smart length_range selects it
|
||||
* N5 — properties parity: ezdxf sets bounding_box; MTEXT carries char_height + rotation
|
||||
* NEW-dimauto-baseline-rotation — baseline dims run parallel to a rotated baseline
|
||||
* NEW-dimauto-ordinate-misnomer — ordinate dims are staggered, not stacked at one offset
|
||||
* N2 — drawing_save_as derives the on-disk format from the path extension
|
||||
* N3 — ARC carries a `length` property; entity_select_smart length_range selects it
|
||||
* N5 — properties parity: ezdxf sets bounding_box; MTEXT carries char_height + rotation
|
||||
* NEW-dimauto-baseline-rotation — baseline dims run parallel to a rotated baseline
|
||||
* NEW-dimauto-ordinate-misnomer — ordinate dims are staggered, not stacked at one offset
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,6 +27,7 @@ class _Ctx:
|
||||
|
||||
# ── N2 — format follows the file extension ──────────────────────────────────
|
||||
|
||||
|
||||
async def test_save_as_derives_format_from_extension(backend):
|
||||
import server
|
||||
|
||||
@@ -42,6 +43,7 @@ async def test_save_as_derives_format_from_extension(backend):
|
||||
|
||||
# ── N3 — ARC length + length_range selection ────────────────────────────────
|
||||
|
||||
|
||||
async def test_arc_has_length_and_is_selectable(backend):
|
||||
arc = await backend.entity_create_arc(0, 0, 10, 0, 90) # r=10, 90deg -> len ~15.708
|
||||
info = await backend.entity_get(arc.handle)
|
||||
@@ -55,6 +57,7 @@ async def test_arc_has_length_and_is_selectable(backend):
|
||||
|
||||
# ── N5 — cross-backend property parity ──────────────────────────────────────
|
||||
|
||||
|
||||
async def test_ezdxf_populates_bounding_box(backend):
|
||||
line = await backend.entity_create_line(0, 0, 30, 40)
|
||||
info = await backend.entity_get(line.handle)
|
||||
@@ -78,6 +81,7 @@ async def test_mtext_honors_caller_rotation(backend):
|
||||
|
||||
# ── NEW-dimauto-baseline-rotation ───────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_baseline_dim_runs_parallel_to_rotated_baseline(backend):
|
||||
ln = await backend.entity_create_line(0, 0, 40, 40) # 45-degree baseline
|
||||
dims = await backend.dimension_auto([ln.handle], style="baseline")
|
||||
@@ -90,6 +94,7 @@ async def test_baseline_dim_runs_parallel_to_rotated_baseline(backend):
|
||||
|
||||
# ── NEW-dimauto-ordinate-misnomer ───────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_ordinate_dims_are_staggered_not_overlapping(backend):
|
||||
l1 = await backend.entity_create_line(0, 0, 20, 10)
|
||||
l2 = await backend.entity_create_line(0, 0, 40, 30)
|
||||
|
||||
@@ -25,6 +25,7 @@ pytestmark = pytest.mark.asyncio
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _on_arc(pt, cx, cy, r, sa_deg, ea_deg, tol=1e-6):
|
||||
"""True if pt lies on the circle of radius r AND within the [sa, ea] sweep
|
||||
(degrees, CCW, with wraparound)."""
|
||||
@@ -41,10 +42,12 @@ def _on_arc(pt, cx, cy, r, sa_deg, ea_deg, tol=1e-6):
|
||||
# N7 — ARC snap angle-missing raises RuntimeError, not KeyError
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestN7ArcAngleMissing:
|
||||
async def _patch_entity_get(self, backend, info: EntityInfo):
|
||||
async def _fake(handle):
|
||||
return info
|
||||
|
||||
backend.entity_get = _fake # type: ignore[method-assign]
|
||||
|
||||
async def test_end_snap_missing_angles_raises_runtimeerror(self, backend):
|
||||
@@ -107,6 +110,7 @@ class TestN7ArcAngleMissing:
|
||||
# NEW-base-1 — point_tangent on/near the perimeter must not raise ValueError
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestNewBase1TangentClamp:
|
||||
async def test_from_point_exactly_on_circle_no_valueerror(self, backend):
|
||||
"""A from-point exactly on the perimeter (d == r) degenerates to the
|
||||
@@ -145,6 +149,7 @@ class TestNewBase1TangentClamp:
|
||||
# NEW-snap-quad-arc / NEW-snap-near-arc — snaps stay on the arc sweep
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestNewSnapArcSweep:
|
||||
async def test_quad_on_quarter_arc_stays_on_sweep(self, backend):
|
||||
"""Quarter arc 0..90 deg, r=10 at origin. Only the 0-deg and 90-deg
|
||||
@@ -208,6 +213,7 @@ class TestNewSnapArcSweep:
|
||||
# N8 — get_plan_spec accessor exposes the stored PlanSpec
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestN8GetPlanSpec:
|
||||
async def test_none_before_plan(self, backend):
|
||||
assert backend.get_plan_spec() is None
|
||||
|
||||
@@ -34,6 +34,7 @@ def _backend_no_executor() -> ComBackend:
|
||||
|
||||
# ── R28 — _find_autocad_hwnd prefers Application.HWND over EnumWindows ───────
|
||||
|
||||
|
||||
async def test_find_hwnd_prefers_application_hwnd(monkeypatch):
|
||||
"""The real main-frame handle from the COM app wins; EnumWindows is not
|
||||
consulted when the HWND read succeeds."""
|
||||
@@ -98,6 +99,7 @@ async def test_find_hwnd_returns_none_when_win32_unavailable(monkeypatch):
|
||||
|
||||
# ── S3 — block_list populates BlockInfo.description from .Comments ───────────
|
||||
|
||||
|
||||
class _Block:
|
||||
"""Minimal stand-in for an AutoCAD block definition."""
|
||||
|
||||
@@ -167,10 +169,12 @@ async def test_block_list_description_empty_when_comments_none(monkeypatch):
|
||||
|
||||
|
||||
async def test_block_list_skips_anonymous_layout_blocks(monkeypatch):
|
||||
doc = _doc_with_blocks([
|
||||
_Block("*Model_Space", comments="ignored"),
|
||||
_Block("GEAR", comments="spur gear"),
|
||||
])
|
||||
doc = _doc_with_blocks(
|
||||
[
|
||||
_Block("*Model_Space", comments="ignored"),
|
||||
_Block("GEAR", comments="spur gear"),
|
||||
]
|
||||
)
|
||||
monkeypatch.setattr(cb, "_acad_doc", lambda: doc)
|
||||
|
||||
b = _backend_no_executor()
|
||||
|
||||
@@ -21,6 +21,7 @@ pytestmark = pytest.mark.asyncio
|
||||
|
||||
# ── R24 — CENTER/HIDDEN linetype actually loaded + applied ──────────────────
|
||||
|
||||
|
||||
async def test_create_line_loads_center_linetype(backend):
|
||||
# Fresh doc: CENTER is not loaded yet.
|
||||
assert "CENTER" not in backend._doc.linetypes
|
||||
@@ -53,6 +54,7 @@ async def test_create_line_continuous_does_not_error(backend):
|
||||
|
||||
# ── S3 — block description surfaced from the definition ─────────────────────
|
||||
|
||||
|
||||
async def test_block_list_surfaces_description(backend):
|
||||
# Create a block from an entity, then stamp a description on its definition.
|
||||
seg = await backend.entity_create_line(0, 0, 10, 0)
|
||||
@@ -76,6 +78,7 @@ async def test_block_list_description_defaults_empty(backend):
|
||||
|
||||
# ── R26 / N9 — honest zoom payload shape ────────────────────────────────────
|
||||
|
||||
|
||||
async def test_zoom_extents_reports_not_applied(backend):
|
||||
res = await backend.view_zoom_extents()
|
||||
assert res["ok"] is True
|
||||
@@ -92,6 +95,7 @@ async def test_zoom_window_reports_not_applied(backend):
|
||||
|
||||
# ── NEW-undo-1 — undo/transaction stack behaviour under lock ────────────────
|
||||
|
||||
|
||||
async def test_undo_empty_stack_returns_error(backend):
|
||||
res = await backend.drawing_undo()
|
||||
assert res["ok"] is False
|
||||
|
||||
@@ -35,6 +35,7 @@ class _Ctx:
|
||||
|
||||
# ── R20 — tool count comes from the public accessor and is a real number ─────
|
||||
|
||||
|
||||
async def test_registered_tool_count_is_positive_never_minus_one():
|
||||
import server
|
||||
|
||||
@@ -59,6 +60,7 @@ async def test_registered_tools_returns_objects_with_name_and_tags():
|
||||
|
||||
# ── R15 — system_about tool_groups is dynamic + correct ──────────────────────
|
||||
|
||||
|
||||
async def test_system_about_includes_engineering_group(backend):
|
||||
import server
|
||||
|
||||
@@ -122,6 +124,7 @@ async def test_drawing_close_appears_in_groups(backend):
|
||||
|
||||
# ── R20 — system_status reports a real tool_count, never -1 ──────────────────
|
||||
|
||||
|
||||
async def test_system_status_reports_real_tool_count(backend):
|
||||
import server
|
||||
|
||||
@@ -134,6 +137,7 @@ async def test_system_status_reports_real_tool_count(backend):
|
||||
|
||||
# ── N8 — drawing_plan docstring corrected ────────────────────────────────────
|
||||
|
||||
|
||||
async def test_drawing_plan_docstring_not_misleading():
|
||||
import server
|
||||
|
||||
|
||||
@@ -37,7 +37,11 @@ async def test_spur_as_substring_does_not_false_match(backend, tmp_path):
|
||||
"""R27: 'SPURIOUS' contains 'spur' but must NOT trip a SPUR mismatch."""
|
||||
await _bootstrap(backend)
|
||||
await backend.entity_create_text(
|
||||
text="SPURIOUS HELICAL GEAR", x=0, y=0, height=5, layer="TEXT",
|
||||
text="SPURIOUS HELICAL GEAR",
|
||||
x=0,
|
||||
y=0,
|
||||
height=5,
|
||||
layer="TEXT",
|
||||
)
|
||||
await backend.drawing_save_as(str(tmp_path / "spurious.dxf"))
|
||||
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
|
||||
@@ -50,7 +54,11 @@ async def test_genuine_spur_title_flags_when_helical_expected(backend, tmp_path)
|
||||
"""R27: a real 'SPUR GEAR' title still flags the mismatch when helix declared."""
|
||||
await _bootstrap(backend)
|
||||
await backend.entity_create_text(
|
||||
text="SPUR GEAR", x=0, y=0, height=5, layer="TEXT",
|
||||
text="SPUR GEAR",
|
||||
x=0,
|
||||
y=0,
|
||||
height=5,
|
||||
layer="TEXT",
|
||||
)
|
||||
await backend.drawing_save_as(str(tmp_path / "spur.dxf"))
|
||||
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
|
||||
@@ -63,7 +71,11 @@ async def test_spur_word_in_titleblock_layer_flags(backend, tmp_path):
|
||||
"""R27: title-block-scoped scan — SPUR on TITLEBLOCK layer still flags."""
|
||||
await _bootstrap(backend)
|
||||
await backend.entity_create_text(
|
||||
text="SPUR GEAR", x=0, y=0, height=5, layer="TITLEBLOCK",
|
||||
text="SPUR GEAR",
|
||||
x=0,
|
||||
y=0,
|
||||
height=5,
|
||||
layer="TITLEBLOCK",
|
||||
)
|
||||
await backend.drawing_save_as(str(tmp_path / "spur_tb.dxf"))
|
||||
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
|
||||
@@ -74,7 +86,11 @@ async def test_substring_in_titleblock_layer_does_not_false_match(backend, tmp_p
|
||||
"""R27: 'SPURIOUS' on TITLEBLOCK layer must not trip a SPUR match either."""
|
||||
await _bootstrap(backend)
|
||||
await backend.entity_create_text(
|
||||
text="SPURIOUS NOTE", x=0, y=0, height=5, layer="TITLEBLOCK",
|
||||
text="SPURIOUS NOTE",
|
||||
x=0,
|
||||
y=0,
|
||||
height=5,
|
||||
layer="TITLEBLOCK",
|
||||
)
|
||||
await backend.drawing_save_as(str(tmp_path / "spurious_tb.dxf"))
|
||||
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
|
||||
@@ -87,7 +103,11 @@ async def test_helical_spur_combo_still_flags_invalid(backend, tmp_path):
|
||||
"""R27: both whole words present -> invalid combo error preserved."""
|
||||
await _bootstrap(backend)
|
||||
await backend.entity_create_text(
|
||||
text="HELICAL SPUR GEAR", x=0, y=0, height=5, layer="TEXT",
|
||||
text="HELICAL SPUR GEAR",
|
||||
x=0,
|
||||
y=0,
|
||||
height=5,
|
||||
layer="TEXT",
|
||||
)
|
||||
await backend.drawing_save_as(str(tmp_path / "combo.dxf"))
|
||||
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
|
||||
@@ -99,7 +119,11 @@ async def test_no_helix_expected_skips_title_check(backend, tmp_path):
|
||||
"""Gating preserved: without helix_angle, a SPUR title raises no title finding."""
|
||||
await _bootstrap(backend)
|
||||
await backend.entity_create_text(
|
||||
text="SPUR GEAR", x=0, y=0, height=5, layer="TEXT",
|
||||
text="SPUR GEAR",
|
||||
x=0,
|
||||
y=0,
|
||||
height=5,
|
||||
layer="TEXT",
|
||||
)
|
||||
await backend.drawing_save_as(str(tmp_path / "nohelix.dxf"))
|
||||
result = await DrawingValidator().run(backend)
|
||||
@@ -116,10 +140,18 @@ async def test_titleblock_layer_takes_precedence_over_text_layer(backend, tmp_pa
|
||||
"""
|
||||
await _bootstrap(backend)
|
||||
await backend.entity_create_text(
|
||||
text="HELICAL GEAR", x=0, y=0, height=5, layer="TITLEBLOCK",
|
||||
text="HELICAL GEAR",
|
||||
x=0,
|
||||
y=0,
|
||||
height=5,
|
||||
layer="TITLEBLOCK",
|
||||
)
|
||||
await backend.entity_create_text(
|
||||
text="SPUR TOOTH DETAIL NOTE", x=0, y=-20, height=3.5, layer="TEXT",
|
||||
text="SPUR TOOTH DETAIL NOTE",
|
||||
x=0,
|
||||
y=-20,
|
||||
height=3.5,
|
||||
layer="TEXT",
|
||||
)
|
||||
await backend.drawing_save_as(str(tmp_path / "scope.dxf"))
|
||||
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
|
||||
|
||||
@@ -29,9 +29,7 @@ async def test_ezdxf_table_returns_composite_contract(backend):
|
||||
|
||||
|
||||
async def test_ezdxf_table_survives_save_and_reopen(backend, tmp_path):
|
||||
result = await backend.entity_create_table(
|
||||
0, 30, rows=[["A", "1"]], headers=["Name", "Qty"]
|
||||
)
|
||||
result = await backend.entity_create_table(0, 30, rows=[["A", "1"]], headers=["Name", "Qty"])
|
||||
path = tmp_path / "table.dxf"
|
||||
await backend.drawing_save_as(str(path), fmt="dxf")
|
||||
await backend.drawing_open(str(path))
|
||||
@@ -39,9 +37,7 @@ async def test_ezdxf_table_survives_save_and_reopen(backend, tmp_path):
|
||||
entities = await backend.entity_list(limit=100)
|
||||
handles = {entity.handle for entity in entities}
|
||||
texts = {
|
||||
entity.properties.get("text")
|
||||
for entity in entities
|
||||
if entity.type in {"TEXT", "MTEXT"}
|
||||
entity.properties.get("text") for entity in entities if entity.type in {"TEXT", "MTEXT"}
|
||||
}
|
||||
assert set(result.properties["child_handles"]).issubset(handles)
|
||||
assert {"Name", "Qty", "A", "1"}.issubset(texts)
|
||||
|
||||
Reference in New Issue
Block a user