release: prepare v1.3.0

This commit is contained in:
Umutcan Edizsalan
2026-07-17 15:48:43 +03:00
parent 1d47408157
commit 4a2e9d658f
35 changed files with 3053 additions and 48 deletions
+108
View File
@@ -0,0 +1,108 @@
"""Fair benchmark v2 runner contract."""
from __future__ import annotations
import asyncio
import subprocess
import sys
from unittest.mock import AsyncMock, patch
import pytest
from benchmarks.adapters.autocad_mcp_pro import AutoCADMCPProAdapter
from benchmarks.adapters.base import BenchmarkAdapter, TaskResult
from benchmarks.run_competitors import run_tasks
from benchmarks.tasks_v2 import TaskSpec
class FakeAdapter(BenchmarkAdapter):
name = "fake"
async def setup(self, artifact_dir):
self.artifact_dir = artifact_dir
async def run_task(self, task):
if task.task_id == "slow":
await asyncio.sleep(0.2)
if task.task_id == "unsupported":
return TaskResult(task_id=task.task_id, status="unsupported", score=0.0)
return TaskResult(task_id=task.task_id, status="pass", score=100.0)
async def cleanup(self):
return None
@pytest.mark.asyncio
async def test_benchmark_timeout_does_not_stop_later_tasks(tmp_path):
tasks = [
TaskSpec("slow", "reliability", "Times out", 1.0),
TaskSpec("next", "reliability", "Still runs", 1.0),
]
report = await run_tasks(FakeAdapter(), tasks, tmp_path, timeout=0.01)
assert [item["status"] for item in report["results"]] == ["timeout", "pass"]
assert report["summary"]["attempted"] == 2
assert report["summary"]["passed"] == 1
assert report["git_sha"]
assert report["environment"]["python"]
@pytest.mark.asyncio
async def test_unsupported_tasks_count_as_zero_in_fixed_matrix_score(tmp_path):
tasks = [
TaskSpec("supported", "coverage", "Implemented", 1.0),
TaskSpec("unsupported", "coverage", "Not implemented", 9.0),
]
report = await run_tasks(FakeAdapter(), tasks, tmp_path)
assert report["summary"]["score"] == 10.0
assert report["summary"]["supported"] == 1
assert report["summary"]["coverage_percent"] == 10.0
def test_task_result_has_machine_readable_status():
result = TaskResult(
task_id="table",
status="unsupported",
score=0.0,
message="TABLE is unavailable",
)
assert result.to_dict() == {
"task_id": "table",
"status": "unsupported",
"score": 0.0,
"message": "TABLE is unavailable",
"duration_ms": 0.0,
"artifacts": [],
"metrics": {},
"stdout_summary": "",
"stderr_summary": "",
}
@pytest.mark.asyncio
async def test_reference_adapter_selects_real_com_backend_class(tmp_path):
with patch("backends.com_backend.ComBackend") as backend_class:
backend_class.return_value.connect = AsyncMock()
adapter = AutoCADMCPProAdapter(backend="com")
await adapter.setup(tmp_path)
backend_class.assert_called_once_with()
assert adapter.backend is backend_class.return_value
@pytest.mark.parametrize("args", [[], ["--help"]])
def test_correctness_suite_help_is_not_a_traceback(args):
completed = subprocess.run(
[sys.executable, "benchmarks/correctness_suite.py", *args],
capture_output=True,
text=True,
timeout=10,
)
assert completed.returncode == 0
assert "usage" in completed.stdout.lower()
+74
View File
@@ -0,0 +1,74 @@
from __future__ import annotations
from unittest.mock import patch
import pytest
from backends.base import CapabilityMap, FeatureCapability
def test_capability_types_serialize_stably():
caps = CapabilityMap(
backend="demo",
features={"table": FeatureCapability(True, "native")},
)
assert caps.to_dict() == {
"backend": "demo",
"features": {
"table": {"supported": True, "mode": "native", "reason": None},
},
}
@pytest.mark.asyncio
async def test_ezdxf_capabilities_are_machine_readable(backend):
caps = backend.capabilities().to_dict()
assert caps["backend"] == "ezdxf"
assert caps["features"]["table"] == {
"supported": True,
"mode": "composite",
"reason": None,
}
assert caps["features"]["mleader"]["mode"] == "composite"
assert caps["features"]["paper_space"]["supported"] is False
assert caps["features"]["solid_3d"]["supported"] is False
@pytest.mark.asyncio
async def test_ezdxf_render_capabilities_follow_optional_dependency(backend):
with patch("backends.ezdxf_backend.find_spec", return_value=None):
caps = backend.capabilities().to_dict()
assert caps["features"]["pdf"] == {
"supported": False,
"mode": None,
"reason": "optional_dependency_missing:matplotlib",
}
assert caps["features"]["png"]["supported"] is False
@pytest.mark.asyncio
async def test_system_capabilities_tool_uses_backend_map(backend):
import server
class Ctx:
lifespan_context = {"backend": backend}
result = await server.system_capabilities(ctx=Ctx())
assert result == backend.capabilities().to_dict()
@pytest.mark.asyncio
async def test_system_about_uses_package_version_and_capability_summary(backend):
import server
class Ctx:
lifespan_context = {"backend": backend}
result = await server.system_about(ctx=Ctx())
assert result["version"] == "1.3.0"
assert result["capabilities"]["table"]["mode"] == "composite"
+29
View File
@@ -11,6 +11,7 @@ Strategy:
from __future__ import annotations
import sys
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock, patch
@@ -19,6 +20,34 @@ import pytest
from backends.base import normalize_lineweight
from backends.com_backend import ComBackend
@pytest.mark.asyncio
async def test_active_document_switch_clears_com_document_state(monkeypatch):
backend = ComBackend()
documents = SimpleNamespace(Count=1)
app = SimpleNamespace(
Documents=documents,
ActiveDocument=SimpleNamespace(Name="First.dwg", FullName="C:/First.dwg"),
)
async def run_inline(function, *args, **kwargs):
return function(*args, **kwargs)
monkeypatch.setattr(backend, "_run", run_inline)
monkeypatch.setattr("backends.com_backend._acad_app", lambda: app)
await backend._ensure_document_state()
backend._plan_spec = object()
backend._preflight_result = object()
backend._gdt_datums_defined = {"A"}
app.ActiveDocument = SimpleNamespace(Name="Second.dwg", FullName="C:/Second.dwg")
await backend._ensure_document_state()
assert backend._plan_spec is None
assert backend._preflight_result is None
assert backend._gdt_datums_defined == set()
# ---------------------------------------------------------------------------
# normalize_lineweight — parametrized table
# ---------------------------------------------------------------------------
+139
View File
@@ -0,0 +1,139 @@
"""Release delivery contract: artifacts, hashes, validation and DXF parity."""
from __future__ import annotations
import hashlib
import json
import pytest
import server
from engineering.delivery import compare_drawing_snapshots, deliver_drawing, drawing_snapshot
from engineering.layers import ensure_engineering_layers, ensure_standard_linetypes
async def _clean_drawing(backend) -> None:
await ensure_standard_linetypes(backend)
await ensure_engineering_layers(backend)
await backend.entity_create_line(0, 0, 40, 0, layer="GEOMETRY")
@pytest.mark.asyncio
async def test_delivery_writes_hashed_manifest_and_reopen_parity(backend, tmp_path):
await _clean_drawing(backend)
result = await deliver_drawing(
backend,
tmp_path,
formats=["dxf"],
min_score=0,
strict_critique=False,
)
assert result.status == "success"
manifest_path = tmp_path / "manifest.json"
validation_path = tmp_path / "validation.json"
assert manifest_path.is_file()
assert validation_path.is_file()
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
dxf = next(item for item in manifest["artifacts"] if item["format"] == "dxf")
artifact_path = tmp_path / dxf["filename"]
assert dxf["status"] == "created"
assert dxf["sha256"] == hashlib.sha256(artifact_path.read_bytes()).hexdigest()
assert manifest["parity"]["ok"] is True
assert manifest["version"] == "1.3.0"
assert manifest["capabilities"]["backend"] == "ezdxf"
@pytest.mark.asyncio
async def test_delivery_keeps_artifacts_when_validation_fails(backend, tmp_path):
await _clean_drawing(backend)
await backend.construction_xline(0, 0, 45)
result = await deliver_drawing(
backend,
tmp_path,
formats=["dxf"],
min_score=100,
strict_critique=True,
)
assert result.status == "failed_validation"
assert (tmp_path / "drawing.dxf").is_file()
manifest = json.loads((tmp_path / "manifest.json").read_text(encoding="utf-8"))
assert manifest["status"] == "failed_validation"
assert any(issue["focus"] == "construction_left" for issue in manifest["critique"])
def test_snapshot_comparison_reports_type_and_bounds_drift():
source = {
"entity_count": 2,
"types": {"LINE": 2},
"layers": {"GEOMETRY": 2},
"extents_min": [0.0, 0.0],
"extents_max": [10.0, 5.0],
}
reopened = {
"entity_count": 2,
"types": {"LINE": 1, "CIRCLE": 1},
"layers": {"GEOMETRY": 2},
"extents_min": [0.0, 0.0],
"extents_max": [10.5, 5.0],
}
result = compare_drawing_snapshots(source, reopened)
assert result["ok"] is False
assert {item["field"] for item in result["differences"]} == {"types", "extents_max"}
@pytest.mark.asyncio
async def test_snapshot_parity_rejects_incomplete_entity_inventory(backend, monkeypatch):
await backend.entity_create_line(0, 0, 10, 0)
await backend.entity_create_circle(5, 5, 2)
original_entity_list = backend.entity_list
async def incomplete_entity_list(*args, **kwargs):
return (await original_entity_list(*args, **kwargs))[:1]
monkeypatch.setattr(backend, "entity_list", incomplete_entity_list)
source = await drawing_snapshot(backend)
reopened = dict(source, inventory_complete=True, listed_entity_count=2)
result = compare_drawing_snapshots(source, reopened)
assert source["entity_count"] == 2
assert source["listed_entity_count"] == 1
assert source["inventory_complete"] is False
assert result["ok"] is False
assert "source_inventory_complete" in {
item["field"] for item in result["differences"]
}
@pytest.mark.asyncio
async def test_delivery_rejects_unknown_format(backend, tmp_path):
await _clean_drawing(backend)
with pytest.raises(ValueError, match="Unsupported delivery format"):
await deliver_drawing(backend, tmp_path, formats=["step"])
@pytest.mark.asyncio
async def test_server_drawing_deliver_exposes_manifest_tool(backend, tmp_path):
await _clean_drawing(backend)
class Ctx:
lifespan_context = {"backend": backend}
result = await server.drawing_deliver(
output_dir=str(tmp_path),
formats=["dxf"],
min_score=0,
strict_critique=False,
ctx=Ctx(),
)
assert result["status"] == "success"
assert result["manifest_path"].endswith("manifest.json")
+166
View File
@@ -0,0 +1,166 @@
from __future__ import annotations
import pytest
from engineering.preflight import preflight_drawing
def _complete_requirements() -> dict:
return {
"units": "mm",
"part_type": "bracket",
"dimensions": {"width": 80.0, "height": 50.0, "thickness": 8.0},
"tolerance_policy": "ISO 2768-m",
}
def test_preflight_missing_production_fields_is_not_ready():
result = preflight_drawing("L bracket", requirements={})
assert result.ready is False
assert {q.code for q in result.questions} == {
"MISSING_UNITS",
"MISSING_PART_TYPE",
"MISSING_DIMENSIONS",
"MISSING_TOLERANCE_POLICY",
}
assert result.spec_hash.startswith("sha256:")
def test_preflight_normalizes_complete_spec_and_hash_is_deterministic():
first = preflight_drawing(
" L bracket ",
requirements=_complete_requirements(),
sheet_size="a3",
layer_set_id="MECH",
)
second = preflight_drawing(
"L bracket",
requirements={
"tolerance_policy": "ISO 2768-m",
"dimensions": {"thickness": 8, "height": 50, "width": 80},
"part_type": "bracket",
"units": "MM",
},
sheet_size="A3",
layer_set_id="mech",
)
assert first.ready is True
assert first.normalized_spec["requirements"]["units"] == "mm"
assert first.spec_hash == second.spec_hash
def test_preflight_detects_conflicting_constraints():
requirements = _complete_requirements()
requirements["constraints"] = [
{"field": "dimensions.width", "value": 80},
{"field": "dimensions.width", "value": 90},
]
result = preflight_drawing("L bracket", requirements=requirements)
assert result.ready is False
assert result.conflicts[0].code == "CONFLICTING_REQUIREMENT"
assert result.conflicts[0].field == "dimensions.width"
@pytest.mark.parametrize(
("kwargs", "code"),
[
({"sheet_size": "LETTER"}, "INVALID_SHEET_SIZE"),
({"scale": 0}, "INVALID_SCALE"),
({"view_count": 0}, "INVALID_VIEW_COUNT"),
({"dim_style": "guess"}, "INVALID_DIM_STYLE"),
({"layer_set_id": "custom"}, "INVALID_LAYER_SET"),
],
)
def test_preflight_rejects_invalid_planning_parameters(kwargs, code):
result = preflight_drawing(
"L bracket",
requirements=_complete_requirements(),
**kwargs,
)
assert result.ready is False
assert code in {conflict.code for conflict in result.conflicts}
def test_preflight_does_not_invent_values_when_assumptions_disabled():
result = preflight_drawing(
"L bracket",
requirements={"part_type": "bracket"},
allow_assumptions=False,
)
assert result.assumptions == []
assert "units" not in result.normalized_spec["requirements"]
@pytest.mark.asyncio
async def test_backend_plan_accepts_matching_ready_preflight(backend):
preflight = await backend.drawing_preflight(
"L bracket", requirements=_complete_requirements(),
)
plan = await backend.drawing_plan(
"L bracket",
requirements=preflight.normalized_spec["requirements"],
spec_hash=preflight.spec_hash,
)
assert plan.preflight_status == "ready"
assert plan.spec_hash == preflight.spec_hash
assert plan.requirements["units"] == "mm"
@pytest.mark.asyncio
async def test_backend_plan_rejects_not_ready_preflight_hash(backend):
preflight = await backend.drawing_preflight("L bracket", requirements={})
with pytest.raises(RuntimeError, match="not ready"):
await backend.drawing_plan("L bracket", spec_hash=preflight.spec_hash)
@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(),
)
with pytest.raises(RuntimeError, match="do not match"):
await backend.drawing_plan(
"Different part",
requirements=preflight.normalized_spec["requirements"],
spec_hash=preflight.spec_hash,
)
@pytest.mark.asyncio
async def test_legacy_plan_remains_compatible_and_marks_preflight_skipped(backend):
plan = await backend.drawing_plan("Legacy drawing")
assert plan.preflight_status == "skipped"
assert plan.spec_hash is None
assert plan.requirements == {}
@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(),
)
await backend.drawing_plan(
"First drawing",
requirements=preflight.normalized_spec["requirements"],
spec_hash=preflight.spec_hash,
)
backend._gdt_datums_defined = {"A"}
backend._gdt_datums_referenced = {"A", "B"}
await backend.drawing_new()
assert backend.get_plan_spec() is None
assert backend._preflight_result is None
assert backend._gdt_datums_defined == set()
assert backend._gdt_datums_referenced == set()
+139
View File
@@ -0,0 +1,139 @@
from __future__ import annotations
import pytest
from engineering.refiner import RepairRegistry, refine_drawing
pytestmark = pytest.mark.asyncio
async def test_transaction_stack_is_separate_from_drawing_undo(backend):
await backend.entity_create_line(0, 0, 10, 0)
await backend.transaction_begin()
undo = await backend.drawing_undo()
status = await backend.system_status()
assert undo["ok"] is False
assert status["transaction_depth"] == 1
await backend.transaction_rollback()
async def test_transaction_rollback_restores_document(backend):
await backend.entity_create_line(0, 0, 10, 0)
await backend.transaction_begin()
await backend.entity_create_circle(5, 5, 2)
await backend.transaction_rollback()
entities = await backend.entity_list(limit=100)
assert [entity.type for entity in entities] == ["LINE"]
async def test_refiner_does_not_mutate_when_transaction_cannot_start(backend, monkeypatch):
await backend.entity_create_line(0, 0, 20, 0)
await backend.entity_create_line(0, 0, 20, 0)
async def transaction_rejected():
return {"ok": False, "error": "A transaction is already active"}
async def unexpected_transaction_end():
raise AssertionError("refiner must not end a transaction it did not start")
monkeypatch.setattr(backend, "transaction_begin", transaction_rejected)
monkeypatch.setattr(backend, "transaction_commit", unexpected_transaction_end)
monkeypatch.setattr(backend, "transaction_rollback", unexpected_transaction_end)
result = await refine_drawing(backend, focus=["duplicate_entities"])
assert result.status == "transaction_unavailable"
assert result.rounds[0].transaction == "not_started"
assert len(await backend.entity_list(type_filter="LINE")) == 2
async def test_refiner_repairs_construction_and_duplicates(backend):
await backend.entity_create_line(0, 0, 20, 0)
await backend.entity_create_line(0, 0, 20, 0)
await backend.construction_xline(0, 0, 0)
result = await refine_drawing(
backend,
max_rounds=3,
min_score=95,
focus=["duplicate_entities", "construction_left"],
)
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"]
) == []
async def test_refiner_dry_run_does_not_mutate_drawing(backend):
await backend.entity_create_line(0, 0, 20, 0)
await backend.entity_create_line(0, 0, 20, 0)
result = await refine_drawing(
backend,
focus=["duplicate_entities"],
dry_run=True,
)
assert result.status == "dry_run"
assert len(await backend.entity_list(type_filter="LINE")) == 2
assert result.rounds[0].actions[0].status == "planned"
async def test_refiner_rolls_back_when_custom_repair_lowers_score(backend):
await backend.entity_create_line(0, 0, 20, 0)
await backend.entity_create_line(0, 0, 20, 0)
registry = RepairRegistry()
async def harmful_repair(active_backend, issue):
await active_backend.construction_xline(0, 0, 0)
return {"created": "construction"}
registry.register("duplicate_entities", harmful_repair)
before = len(await backend.entity_list(limit=100))
result = await refine_drawing(
backend,
focus=["duplicate_entities", "construction_left"],
registry=registry,
)
assert result.status == "rolled_back"
assert result.rounds[0].transaction == "rolled_back"
assert len(await backend.entity_list(limit=100)) == before
async def test_refiner_leaves_missing_gdt_datum_for_manual_resolution(backend):
backend._gdt_datums_referenced = {"A"}
result = await refine_drawing(backend, focus=["gdt"])
assert result.status == "manual_required"
assert result.remaining_issues[0]["focus"] == "gdt"
async def test_server_drawing_refine_returns_structured_payload(backend):
import server
class Ctx:
lifespan_context = {"backend": backend}
await backend.entity_create_line(0, 0, 20, 0)
await backend.entity_create_line(0, 0, 20, 0)
result = await server.drawing_refine(
max_rounds=3,
min_score=95,
focus=["duplicate_entities"],
ctx=Ctx(),
)
assert result["status"] == "threshold_met"
assert result["final_score"] == 100.0
+41
View File
@@ -0,0 +1,41 @@
from __future__ import annotations
import sys
import tomllib
from pathlib import Path
from unittest.mock import MagicMock
import server
def test_packaged_console_script_targets_callable_main():
metadata = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
assert metadata["project"]["scripts"]["autocad-mcp"] == "server:main"
assert callable(server.main)
def test_main_starts_stdio_transport(monkeypatch):
run = MagicMock()
monkeypatch.setattr(server.mcp, "run", run)
monkeypatch.setattr(sys, "argv", ["autocad-mcp"])
server.main()
run.assert_called_once_with()
def test_main_help_exits_without_starting_server(monkeypatch, capsys):
run = MagicMock()
monkeypatch.setattr(server.mcp, "run", run)
monkeypatch.setattr(sys, "argv", ["autocad-mcp", "--help"])
try:
server.main()
except SystemExit as exc:
assert exc.code == 0
else:
raise AssertionError("--help must exit without starting the server")
assert "usage:" in capsys.readouterr().out.lower()
run.assert_not_called()
+4 -3
View File
@@ -113,11 +113,12 @@ async def test_rollback_without_transaction_returns_error(backend):
async def test_transaction_begin_commit_roundtrip(backend):
begin = await backend.transaction_begin()
assert begin["ok"] is True
assert len(backend._undo_stack) == 1
assert len(backend._transaction_stack) == 1
assert len(backend._undo_stack) == 0
commit = await backend.transaction_commit()
assert commit["ok"] is True
assert len(backend._undo_stack) == 0
assert len(backend._transaction_stack) == 0
async def test_transaction_rollback_restores_snapshot(backend):
@@ -129,7 +130,7 @@ async def test_transaction_rollback_restores_snapshot(backend):
res = await backend.transaction_rollback()
assert res["ok"] is True
assert len(backend._undo_stack) == 0
assert len(backend._transaction_stack) == 0
# The line is gone after rollback.
listed = await backend.entity_list(type_filter="LINE")
+141
View File
@@ -0,0 +1,141 @@
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from backends.base import EntityInfo
from backends.com_backend import ComBackend
pytestmark = pytest.mark.asyncio
async def test_ezdxf_table_returns_composite_contract(backend):
result = await backend.entity_create_table(
10,
100,
rows=[["P-01", "4"], ["P-02", "2"]],
headers=["Part", "Qty"],
title="BOM",
layer="TEXT",
)
assert result.type == "TABLE"
assert result.layer == "TEXT"
assert result.properties["representation"] == "composite"
assert result.properties["logical_group_id"].startswith("table:")
assert len(result.properties["child_handles"]) >= 10
assert result.properties["bounds"] == {"min": [10.0, 72.0], "max": [30.0, 100.0]}
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"]
)
path = tmp_path / "table.dxf"
await backend.drawing_save_as(str(path), fmt="dxf")
await backend.drawing_open(str(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"}
}
assert set(result.properties["child_handles"]).issubset(handles)
assert {"Name", "Qty", "A", "1"}.issubset(texts)
async def test_table_rejects_ragged_rows_and_size_limit(backend):
with pytest.raises(RuntimeError, match="same number of columns"):
await backend.entity_create_table(0, 0, rows=[["A", "B"], ["C"]])
with pytest.raises(RuntimeError, match="200 rows"):
await backend.entity_create_table(0, 0, rows=[["A"]] * 201)
async def test_ezdxf_mleader_returns_composite_contract(backend):
result = await backend.leader_create_mleader(
[[0, 0], [10, 5], [30, 5]],
"SURFACE A",
layer="DIM",
)
assert result.type == "MLEADER"
assert result.properties["representation"] == "composite"
assert result.properties["logical_group_id"].startswith("mleader:")
assert result.properties["text"] == "SURFACE A"
assert len(result.properties["child_handles"]) == 3
async def test_mleader_requires_two_points(backend):
with pytest.raises(RuntimeError, match="at least two points"):
await backend.leader_create_mleader([[0, 0]], "note")
async def test_com_table_uses_native_add_table():
backend = ComBackend()
async def run_inline(func, *args, **kwargs):
return func(*args, **kwargs)
backend._run = run_inline
mspace = MagicMock()
table = MagicMock()
table.Handle = "A1"
mspace.AddTable.return_value = table
info = EntityInfo("A1", "ACAD_TABLE", "TEXT", 256, "ByLayer", True, {})
with (
patch("backends.com_backend._msp", return_value=mspace),
patch("backends.com_backend._apoint", side_effect=lambda *p: tuple(p)),
patch("backends.com_backend._entity_info", return_value=info),
patch("backends.com_backend._apply_entity_attrs"),
patch("backends.com_backend._regen"),
):
result = await backend.entity_create_table(
0, 50, [["A", "1"]], headers=["Name", "Qty"], title="BOM"
)
mspace.AddTable.assert_called_once()
assert mspace.AddTable.call_args[0][1:3] == (3, 2)
assert table.SetText.call_count == 6
assert result.type == "TABLE"
assert result.properties["representation"] == "native"
async def test_com_mleader_uses_native_add_mleader():
backend = ComBackend()
async def run_inline(func, *args, **kwargs):
return func(*args, **kwargs)
backend._run = run_inline
mspace = MagicMock()
leader = MagicMock()
leader.Handle = "B1"
mspace.AddMLeader.return_value = leader
info = EntityInfo("B1", "ACAD_MLEADER", "DIM", 256, "ByLayer", True, {})
with (
patch("backends.com_backend._msp", return_value=mspace),
patch("backends.com_backend._av", side_effect=lambda values: values),
patch("backends.com_backend._entity_info", return_value=info),
patch("backends.com_backend._apply_entity_attrs"),
patch("backends.com_backend._regen"),
):
result = await backend.leader_create_mleader([[0, 0], [20, 5]], "NOTE")
mspace.AddMLeader.assert_called_once_with([0.0, 0.0, 0.0, 20.0, 5.0, 0.0], 0)
assert leader.TextString == "NOTE"
assert result.type == "MLEADER"
assert result.properties["representation"] == "native"
async def test_server_registers_table_and_mleader_tools():
import server
names = {tool.name for tool in await server._registered_tools()}
assert "entity_create_table" in names
assert "leader_create_mleader" in names