write_workbook builds the user's takeoff workbook from a result: Metraj (rows with direct, continuity and unassigned lengths split, the allowance as its own column, routed / unroutable / grand totals), Ozet (by service, diameter and room), Kontrol (unassigned diameters and unroutable runs with the reason) and Metodoloji, generated from the result: sources, the length source and why, the scale-check verdict with its statistics, the units, every rule and tolerance, and every warning. Headers and sentences in Turkish, English or Russian. CSV always, one file per sheet; XLSX with openpyxl, otherwise the xlsx_write refusal naming the office extra while the CSV files are still written. pipe_takeoff reads the P&ID and the layout once each (take_snapshot), computes in a worker thread and never modifies either drawing. Tagged plant and analysis; the plant pack is the integration task's. Alias record in English, Turkish and Russian. Inventory regenerated; README snapshot 242 tools (237 advertised); registry snapshot analysis: 13. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
"""pipe_takeoff over the MCP wire, on the synthetic plant pair - never on a client drawing."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from fastmcp import Client
|
|
|
|
import server
|
|
from tests.fixtures.plant_pair import build_plant_pair
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
CSV_NAMES = [
|
|
"takeoff_metraj.csv",
|
|
"takeoff_ozet.csv",
|
|
"takeoff_kontrol.csv",
|
|
"takeoff_metodoloji.csv",
|
|
]
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def client(monkeypatch):
|
|
"""In-memory client on the headless engine - never the operator's AutoCAD."""
|
|
monkeypatch.setenv("AUTOCAD_MCP_BACKEND", "ezdxf")
|
|
async with Client(server.mcp) as connected:
|
|
status = (await connected.call_tool("system_status", {})).structured_content or {}
|
|
assert status.get("backend") == "ezdxf", "these tests must never touch live AutoCAD"
|
|
yield connected
|
|
|
|
|
|
@pytest.fixture
|
|
def plant(tmp_path):
|
|
return build_plant_pair(tmp_path)
|
|
|
|
|
|
async def test_pipe_takeoff_measures_on_the_layout_and_writes_the_workbook(client, plant, tmp_path):
|
|
output = tmp_path / "takeoff.xlsx"
|
|
arguments = {
|
|
"pid_path": plant["pid"],
|
|
"layout_path": plant["layout"],
|
|
"output": str(output),
|
|
"lang": "tr",
|
|
}
|
|
result = (await client.call_tool("pipe_takeoff", arguments)).structured_content
|
|
assert result["length_source"] == "layout"
|
|
assert result["scale"]["verdict"] == plant["scale_verdict"]
|
|
files = result["files"]
|
|
assert [Path(p).name for p in files["csv"]] == CSV_NAMES
|
|
assert all(Path(p).exists() for p in files["csv"])
|
|
if files["xlsx"] is None:
|
|
assert files["refused"]["capability"] == "xlsx_write"
|
|
else:
|
|
assert Path(files["xlsx"]) == output
|
|
assert output.exists()
|
|
|
|
|
|
async def test_pid_lengths_on_the_schematic_pid_are_refused_without_force(client, plant):
|
|
arguments = {"pid_path": plant["pid"], "layout_path": plant["layout"], "length_source": "pid"}
|
|
result = await client.call_tool("pipe_takeoff", arguments, raise_on_error=False)
|
|
assert result.is_error is True
|
|
text = result.content[0].text
|
|
assert "the scale check calls this P&ID schematic" in text
|
|
assert "force=True" in text
|
|
|
|
|
|
async def test_force_measures_on_the_pid_and_marks_it_unverified(client, plant):
|
|
arguments = {
|
|
"pid_path": plant["pid"],
|
|
"layout_path": plant["layout"],
|
|
"length_source": "pid",
|
|
"force": True,
|
|
}
|
|
result = (await client.call_tool("pipe_takeoff", arguments)).structured_content
|
|
assert (result["length_source"], result["scale_verified"]) == ("pid", False)
|
|
assert result["files"] is None # no output named, nothing written
|
|
|
|
|
|
async def test_an_unknown_language_is_refused_before_anything_is_read(client, tmp_path):
|
|
arguments = {"pid_path": str(tmp_path / "never_read.dxf"), "lang": "de"}
|
|
result = await client.call_tool("pipe_takeoff", arguments, raise_on_error=False)
|
|
assert result.is_error is True
|
|
assert "lang: 'de' unknown; choose from tr, en, ru" in result.content[0].text
|
|
|
|
|
|
async def test_the_drawings_are_never_modified(client, plant, tmp_path):
|
|
before = {key: Path(plant[key]).read_bytes() for key in ("pid", "layout")}
|
|
arguments = {
|
|
"pid_path": plant["pid"],
|
|
"layout_path": plant["layout"],
|
|
"output": str(tmp_path / "t.xlsx"),
|
|
}
|
|
await client.call_tool("pipe_takeoff", arguments)
|
|
assert {key: Path(plant[key]).read_bytes() for key in ("pid", "layout")} == before
|
|
|
|
|
|
async def test_the_tool_says_it_writes_a_file_and_destroys_nothing(client):
|
|
tools = {tool.name: tool for tool in await client.list_tools()}
|
|
annotations = tools["pipe_takeoff"].annotations
|
|
assert annotations.readOnlyHint is False
|
|
assert annotations.destructiveHint is False
|