Files
autocad-mcp-pro/tests/test_sprint4_server.py
T
Umutcan Edizsalan 32a68be919 fix: correctness, cross-backend parity & enforced quality gate (audit Sprints 2-4)
Audit-driven fixes across the dual backend, the premium workflow, and the
engineering layer. 318 tests, ruff-clean.

Quality gate (the moat):
- drawing_finalize now runs the premium ISO-128 critique in addition to the
  8-step validator; leftover construction, non-ISO lineweights, untrimmed
  corners, duplicates and dim overlap block the gate (strict_critique fails on
  warnings too).
- dim_overlap critique no longer a no-op (dim reference point + bbox fallback);
  dim/construction layers resolve from the active layer set (iso13567 too).

Stability / correctness:
- view_screenshot & drawing_export_pdf render headless via Agg (no more
  worker-thread SIGSEGV).
- gear tooth profile no longer self-overlaps at high tooth counts; section view
  no longer duplicates bore lines; validator keyway heuristic fixed.
- ezdxf aligned/angular dim API, full-circle polar array duplicate already
  covered by Sprint 1; this bundle adds save-format-from-extension,
  entity_offset side selection, ARC length, properties parity (bounding_box /
  rotation / char_height), mtext rotation.

COM robustness (mocked-COM tested):
- CoUninitialize on teardown; transaction commit/rollback try/finally + UNDO via
  the CMDACTIVE guard; system_run_lisp guard + USERS1 value capture;
  set_variable type coercion; HWND via Application.HWND; BlockInfo.description.

Security:
- HTTP remote-bind guard fires on every launch path (incl. `fastmcp run`);
  StaticTokenVerifier wired into FastMCP; AutoLISP allowlist bypass-vector tests.

Hygiene: linetype-on-demand in _apply_attrs, honest view_zoom returns, snap-on-arc
sweep handling, point_tangent acos clamp, dynamic tool_groups / tool count,
word-boundary SPUR/HELICAL, undo-stack locking.

Deferred: R22 (undo/transaction stack isolation) lands with the I1 refiner (1.2),
which depends on it.
2026-06-19 08:36:58 +03:00

144 lines
5.2 KiB
Python

"""Regression tests for the Sprint-4 server.py fixes.
* R20 — _registered_tool_count() uses the public FastMCP accessor
(mcp._list_tools) and returns a real positive count, never -1/None.
* R15 — system_about's tool_groups is derived dynamically from each tool's
tags: it now includes the engineering/premium groups and no longer
misfiles entity_delete_many under entity_creation.
* N8 — drawing_plan docstring no longer claims the PlanSpec is "replayed by
drawing_critique".
These tests drive server.py's underlying async functions directly with a
minimal fake ctx (lifespan_context + async info/warning), the same pattern the
other sprint test files use. They are ezdxf-backend-agnostic.
"""
from __future__ import annotations
import pytest
pytestmark = pytest.mark.asyncio
class _Ctx:
"""Minimal async context mimicking fastmcp.Context for direct tool calls."""
def __init__(self, backend):
self.lifespan_context = {"backend": backend}
async def info(self, message):
pass
async def warning(self, message):
pass
# ── 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
count = await server._registered_tool_count()
assert count is not None # never the old sentinel state
assert isinstance(count, int)
assert count != -1 # the old silent-failure value must never surface
assert count > 50 # the server registers dozens of tools
async def test_registered_tools_returns_objects_with_name_and_tags():
import server
tools = await server._registered_tools()
assert len(tools) > 50
# Every entry is a real tool object usable for tag-based grouping.
for t in tools:
assert getattr(t, "name", None)
# tags is a set (possibly empty) on FunctionTool
assert hasattr(t, "tags")
# ── R15 — system_about tool_groups is dynamic + correct ──────────────────────
async def test_system_about_includes_engineering_group(backend):
import server
about = await server.system_about(ctx=_Ctx(backend))
groups = about["tool_groups"]
# An engineering tool must appear somewhere in the breakdown (the old
# static dict omitted all engineering tools entirely).
all_listed = {name for names in groups.values() for name in names}
assert "gear_draw_section_aa" in all_listed
# The engineering group itself is present and non-empty.
assert groups.get("engineering")
assert "gear_draw_section_aa" in groups["engineering"]
async def test_system_about_includes_premium_group(backend):
import server
about = await server.system_about(ctx=_Ctx(backend))
groups = about["tool_groups"]
all_listed = {name for names in groups.values() for name in names}
# Premium meta-tools (e.g. drawing_plan) used to be missing from the dict.
assert "drawing_plan" in all_listed
assert groups.get("premium")
async def test_entity_delete_many_not_under_entity_creation(backend):
import server
about = await server.system_about(ctx=_Ctx(backend))
groups = about["tool_groups"]
# The bug: entity_delete_many was hardcoded under entity_creation.
assert "entity_delete_many" not in groups.get("entity_creation", [])
# It is tagged {"entity", "modify"} so it belongs with modification.
assert "entity_delete_many" in groups.get("entity_modification", [])
async def test_system_about_total_tools_matches_count(backend):
import server
about = await server.system_about(ctx=_Ctx(backend))
count = await server._registered_tool_count()
# total_tools is reported dynamically (never hardcoded) and agrees with
# the count helper.
assert about.get("total_tools") == count
assert about["total_tools"] > 50
# The sum of all grouped tools equals the total (every tool buckets once).
grouped = sum(len(v) for v in about["tool_groups"].values())
assert grouped == about["total_tools"]
async def test_drawing_close_appears_in_groups(backend):
import server
about = await server.system_about(ctx=_Ctx(backend))
all_listed = {name for names in about["tool_groups"].values() for name in names}
# drawing_close was omitted by the old static dict.
assert "drawing_close" in all_listed
# ── R20 — system_status reports a real tool_count, never -1 ──────────────────
async def test_system_status_reports_real_tool_count(backend):
import server
status = await server.system_status(ctx=_Ctx(backend))
# tool_count is present and a real positive number (omitted only if unknown).
assert status.get("tool_count") is not None
assert status["tool_count"] != -1
assert status["tool_count"] > 50
# ── N8 — drawing_plan docstring corrected ────────────────────────────────────
async def test_drawing_plan_docstring_not_misleading():
import server
doc = server.drawing_plan.__doc__ or ""
# The corrected docstring must not claim the plan is replayed as a critique.
assert "replayed by `drawing_critique`" not in doc
assert "stored on the backend" in doc