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:
Umutcan Edizsalan
2026-07-23 15:14:51 +03:00
co-authored by Cursor
parent 4a2e9d658f
commit 9ec6c4a989
43 changed files with 785 additions and 376 deletions
+4 -1
View File
@@ -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
+11 -4
View File
@@ -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)
+10 -4
View File
@@ -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
+6 -2
View File
@@ -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)
+1
View File
@@ -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)
+8 -3
View File
@@ -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}
+51 -13
View File
@@ -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
+11 -4
View File
@@ -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
+15 -5
View File
@@ -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)
+12 -3
View File
@@ -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)
+4
View File
@@ -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)
+3 -1
View File
@@ -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
View File
@@ -1,6 +1,5 @@
"""Tests for ezdxf backend — no AutoCAD required."""
import pytest
pytestmark = pytest.mark.asyncio
+16 -1
View File
@@ -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
+15 -4
View File
@@ -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 = []."""
+2
View File
@@ -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)."""
+6 -3
View File
@@ -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",
+1 -3
View File
@@ -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):
+4
View File
@@ -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")')
+30 -9
View File
@@ -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 == []
+10 -5
View File
@@ -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)
+6
View File
@@ -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
+8 -4
View File
@@ -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()
+4
View File
@@ -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
+4
View File
@@ -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
+40 -8
View File
@@ -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})
+2 -6
View File
@@ -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)