aps-agent/tests/golden/test_dxf_master_flow.py

212 lines
8.1 KiB
Python

from __future__ import annotations
import os
from copy import deepcopy
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from server.aps_domain.drawing_dxf import build_drawing_master_candidates, inspect_dxf
from server.gateway.app import create_app
from server.state.seed import seed_world
from tests.auth_provider import install_test_auth
from tests.external_data import external_dir
_DEFAULT_SAMPLE = external_dir("RUIYANG_DEMO_DIR", "ruiyang") / "5060102101-001-e(1).dxf"
def _sample_dxf() -> Path:
try:
import ezdxf # noqa: F401
except ImportError as exc:
pytest.skip(f"DXF 运行依赖未完整安装:{exc}; 需要 ezdxf 及其 fontTools 依赖")
path = Path(os.environ.get("APS_DXF_SAMPLE", str(_DEFAULT_SAMPLE)))
if not path.is_file():
pytest.skip(f"真实 DXF 样例不存在:{path};可通过 APS_DXF_SAMPLE 指定")
return path
def _master_snapshot(world: dict) -> dict:
return {
key: deepcopy(world.get(key, []))
for key in ("materials", "boms", "bom", "routing", "routings", "operations")
}
class _WorldStore:
def __init__(self, data: dict, tenant_uuid: str, world_key: str = "default") -> None:
self.data = data
self.world_key = world_key
self.tenant_uuid = tenant_uuid
def next_id(self, _kind: str) -> int:
return len(self.data.get("auditEvents") or []) + 1
def save(self) -> None:
pass
def _auth_client(monkeypatch: pytest.MonkeyPatch, tenant: str) -> TestClient:
install_test_auth(monkeypatch, tenant)
monkeypatch.setenv("APS_DRAWING_ALLOWED_ROOTS", str(_sample_dxf().parent))
client = TestClient(create_app())
login = client.post("/api/auth/login", json={"username": "planner", "password": "test"})
assert login.status_code == 200, login.text
return client
def _synthetic_dxf(path: Path, text: str) -> Path:
payload = (
"0\nSECTION\n2\nHEADER\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n"
"0\nTEXT\n8\n0\n10\n0.0\n20\n0.0\n40\n2.5\n1\n"
f"{text}\n0\nENDSEC\n0\nEOF\n"
)
path.write_text(payload, encoding="ascii")
return path
def _bind_project(monkeypatch: pytest.MonkeyPatch, work_dir: Path) -> None:
class FakeProjectStore:
def snapshot(self, include_messages: bool = False):
return {
"projects": [{"id": "p-dxf-batch", "name": "批量验收项目", "workDir": str(work_dir)}],
"sessions": [{"id": "s-dxf-batch", "projectId": "p-dxf-batch"}],
"files": [],
}
def active_world_key(self) -> str:
return "default"
def ensure_session(self, session_id: str, project_id: str | None = None, title: str = ""):
return {"id": session_id, "projectId": project_id or "p-dxf-batch", "title": title}
def require_active_write(self) -> None:
pass
monkeypatch.setattr("server.state.projects.get_project_store", lambda: FakeProjectStore())
def test_dxf_candidates_are_review_only_and_do_not_mutate_master_data():
world = seed_world()
before = _master_snapshot(world)
candidates = build_drawing_master_candidates(inspect_dxf(_sample_dxf()))
assert candidates["contractVersion"] == "drawing-master-candidates.v1"
assert candidates["status"] == "PENDING_REVIEW"
assert candidates["reviewRequired"] is True
assert candidates["materials"]
assert candidates["materials"][0]["code"] == "5060102101-001"
assert candidates["materials"][0]["reviewRequired"] is True
assert candidates["bomReferences"]
assert all(row["resolutionStatus"] == "UNRESOLVED" for row in candidates["bomReferences"])
assert all(row["quantity"] is None for row in candidates["bomReferences"])
assert candidates["routingOperations"]
assert all(row["standardTime"] is None and row["resourceCode"] is None
for row in candidates["routingOperations"])
assert _master_snapshot(world) == before, "解析和候选生成阶段不得直接写主数据"
def test_dxf_p2_confirmation_is_required_before_master_commit_contract(monkeypatch):
import server.gateway.app as gateway_module
app = create_app()
paths = app.openapi().get("paths", {})
stage_path = "/api/drawings/{drawing_id}/stage"
confirm_path = "/api/actions/confirm"
if stage_path not in paths or confirm_path not in paths:
pytest.skip(
"依赖未合并:需要 Drawing stage 接口接入现有 P2 /api/actions/confirm 通道,"
"并在确认前保持主数据不变、确认后原子提交变更集。"
)
store = _WorldStore(seed_world(), "tenant-dxf-master-flow", "personal-1001")
monkeypatch.setattr(
gateway_module,
"_drawing_project_store",
lambda project_id, write=False: store,
)
monkeypatch.setattr(gateway_module, "get_store", lambda: store)
_bind_project(monkeypatch, _sample_dxf().parent)
client = _auth_client(monkeypatch, "tenant-dxf-master-flow")
inspect_resp = client.post("/api/drawings/inspect", json={"path": str(_sample_dxf())})
assert inspect_resp.status_code == 200, inspect_resp.text
parsed = inspect_resp.json()["parsed"]
drawing_id = parsed["asset"]["id"]
before = _master_snapshot(store.data)
stage = client.post(f"/api/drawings/{drawing_id}/stage", json={})
assert stage.status_code == 200, stage.text
staged = stage.json()
assert staged["status"] in {"staged", "PENDING_REVIEW"}
assert staged.get("confirmId"), "候选必须生成 P2 确认卡"
assert staged.get("masterCommitted", False) is False
assert _master_snapshot(store.data) == before, "P2 暂存阶段不得直接写主数据"
confirm = client.post("/api/actions/confirm", json={
"confirmId": staged["confirmId"],
"approve": True,
})
assert confirm.status_code == 200, confirm.text
result = confirm.json()
assert "Drawing review applied" in result["message"]
world = store.data
assert any(row.get("code") == "5060102101-001" for row in world.get("materials") or [])
assert any(link.get("drawingId") == drawing_id for link in world.get("drawingLinks") or [])
asset = next(row for row in world.get("drawingAssets") or [] if row.get("id") == drawing_id)
assert asset.get("status") == "COMMITTED"
def test_batch_inspect_keeps_independent_candidate_change_sets(monkeypatch, tmp_path):
import server.gateway.app as gateway_module
p1 = _synthetic_dxf(tmp_path / "E2E-001-a.dxf", "E2E-DXF-ONE")
p2 = _synthetic_dxf(tmp_path / "E2E-002-a.dxf", "E2E-DXF-TWO")
_bind_project(monkeypatch, tmp_path)
install_test_auth(monkeypatch, "tenant-dxf-batch")
store = _WorldStore(seed_world(), "tenant-dxf-batch")
monkeypatch.setattr(
gateway_module,
"_drawing_project_store",
lambda project_id, write=False: store,
)
client = TestClient(create_app())
login = client.post("/api/auth/login", json={"username": "planner", "password": "test"})
assert login.status_code == 200, login.text
resp = client.post("/api/drawings/inspect-batch", json={
"projectId": "p-dxf-batch",
"paths": [str(p1), str(p2)],
})
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["count"] == 2
assert body["errors"] == []
assert len({row["id"] for row in body["drawings"]}) == 2
world = store.data
changes = world.get("drawingCandidates") or []
assets = world.get("drawingAssets") or []
assert len(changes) == 2
assert len(assets) == 2
assert len({row.get("drawingAssetId") for row in changes}) == 2
assert all(row.get("projectId") == "p-dxf-batch" for row in changes)
again = client.post("/api/drawings/inspect-batch", json={
"projectId": "p-dxf-batch",
"paths": [str(p1), str(p2)],
})
assert again.status_code == 200, again.text
world = store.data
changes = world.get("drawingCandidates") or []
assets = world.get("drawingAssets") or []
assert len(changes) == 2
assert len(assets) == 2
codes = [
{row.get("code") for row in (change.get("materials") or [])}
for change in changes
]
assert codes[0] != codes[1]