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.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user