integrate ganjihong engine refactor

This commit is contained in:
2026-09-09 18:41:56 +08:00
parent cf9cd0366a
commit 21a5185926
116 changed files with 3596 additions and 12259 deletions
+80
View File
@@ -0,0 +1,80 @@
from pathlib import Path
from tempfile import TemporaryDirectory
from app.cad_agent.adapters.sqlite_repository import SqliteTaskRepository
from app.cad_agent.domain.state import transition
from app.services.agent_service import AgentService
def test_result_payload_contains_all_published_artifacts() -> None:
payload = AgentService._result_payload("cad_abcdefghijkl", {
"lifecycle": "completed",
"published_revision": "rev_prefix",
"revisions": [{
"revision_id": "rev_prefix",
"cdsl_path": "revisions/rev_prefix/model.cdsl.json",
"step_path": "revisions/rev_prefix/model.step",
"glb_path": "revisions/rev_prefix/model.glb",
"report_path": "revisions/rev_prefix/rebuild-report.json",
}],
})
assert payload == {
"taskId": "cad_abcdefghijkl",
"revisionId": "rev_prefix",
"cdslPath": "revisions/rev_prefix/model.cdsl.json",
"stepPath": "revisions/rev_prefix/model.step",
"glbPath": "revisions/rev_prefix/model.glb",
"reportPath": "revisions/rev_prefix/rebuild-report.json",
"summary": "CDSL CAD model",
"referenceIds": [],
"engine": "cdsl_only",
"lifecycle": "completed",
}
def test_best_effort_projection_retains_prefix_artifact_paths() -> None:
with TemporaryDirectory() as temporary:
repository = SqliteTaskRepository(Path(temporary) / "state.sqlite3")
state = repository.create_task("cad_abcdefghijkl", "make a plate")
state = transition(state, "analysis_written", requirements_path="documents/requirements-analysis.json")
assert repository.compare_and_swap(state)
state = transition(state, "authoring_written", authoring_path="documents/authoring-cdsl-attempt-01.json")
assert repository.compare_and_swap(state)
state = transition(state, "compiled", runtime_cdsl_path="documents/runtime-cdsl-attempt-01.json")
assert repository.compare_and_swap(state)
state = transition(state, "repair_required", active_revision="rev_prefix")
assert repository.compare_and_swap(state, events=[{
"event": "build_failed",
"revision_id": "rev_prefix",
"paths": {
"model.cdsl.json": "revisions/rev_prefix/model.cdsl.json",
"model.step": "revisions/rev_prefix/model.step",
"model.glb": "revisions/rev_prefix/model.glb",
"rebuild-report.json": "revisions/rev_prefix/rebuild-report.json",
},
}])
state = transition(state, "repair_started", repair_count=1)
assert repository.compare_and_swap(state)
state = transition(state, "repair_required")
assert repository.compare_and_swap(state)
state = transition(state, "repair_started", repair_count=2)
assert repository.compare_and_swap(state)
state = transition(state, "publish_best_effort")
assert repository.compare_and_swap(state, events=[{"event": "repair_budget_exhausted"}])
state = transition(state, "published")
assert repository.compare_and_swap(state)
projection = repository.get_task_projection("cad_abcdefghijkl")
assert projection is not None
result = AgentService._result_payload("cad_abcdefghijkl", projection)
assert result is not None
assert result["revisionId"] == "rev_prefix"
assert result["stepPath"] == "revisions/rev_prefix/model.step"
def test_result_payload_rejects_incomplete_artifact_sets() -> None:
assert AgentService._result_payload("cad_abcdefghijkl", {
"published_revision": "rev_prefix",
"revisions": [{"revision_id": "rev_prefix", "step_path": "revisions/rev_prefix/model.step"}],
}) is None
-158
View File
@@ -1,158 +0,0 @@
from __future__ import annotations
import asyncio
import json
from pathlib import Path
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "backend"))
from app.cad_agent.adapters.author_guidance import FileAuthorGuidance # noqa: E402
from app.cad_agent.application.workflow import ModelIdentity, WorkflowConfig, WorkflowCoordinator # noqa: E402
from app.cad_agent.domain.errors import ErrorCode, WorkflowError # noqa: E402
from app.cad_agent.domain.state import TaskPhase, TaskState # noqa: E402
GUIDANCE_ROOT = ROOT / "backend" / "agent" / "skills" / "cdsl-author-guidance"
PROFILE = ROOT / "backend" / "engine" / "cdsl_engine" / "profile_schema.json"
def atomic_ids() -> tuple[str, ...]:
return tuple(json.loads(PROFILE.read_text(encoding="utf-8"))["operation_contracts"])
class _Repository:
def __init__(self, state: TaskState) -> None:
self.state = state
self.usage_records: list[dict] = []
def get_state(self, _task_id: str) -> TaskState:
return self.state
def ledger_events(self, _task_id: str) -> list[dict]:
return []
def record_usage(self, _task_id: str, payload: dict) -> None:
self.usage_records.append(payload)
def record_tool_audit(self, _task_id: str, _payload: dict) -> None:
pass
class _Artifacts:
def read_source_requirements(self, _task_id: str) -> str:
return "Create a symmetric mounting plate."
def read_json(self, *_args: object) -> None:
return None
class _Runtime:
def supported_atomic_ids(self) -> tuple[str, ...]:
return atomic_ids()
class AuthorGuidanceTests(unittest.TestCase):
def test_manifest_covers_every_runtime_atomic_and_keeps_coordinate_core_at_minimum_budget(self) -> None:
guidance = FileAuthorGuidance(GUIDANCE_ROOT, max_chars=1_200)
covered: set[str] = set()
for atomic_id in atomic_ids():
selection = guidance.select(
phase=TaskPhase.FEATURE_PENDING,
atomic_id=atomic_id,
repair_required=False,
supported_atomic_ids=atomic_ids(),
)
self.assertTrue(selection.enabled, selection.fallback_reason)
self.assertIn("00-author-contract", selection.section_ids)
self.assertIn("03-coordinate-system-and-datums", selection.section_ids)
self.assertLessEqual(len(selection.content), 1_200)
self.assertIn("世界坐标", selection.content)
covered.update(section_id for section_id in selection.section_ids if section_id.startswith("op-"))
self.assertEqual(covered, {"op-extrude-add", "op-extrude-cut", "op-loft", "op-revolve", "op-hole", "op-reference", "op-pattern", "op-finish", "op-sphere", "op-primitives", "op-thread", "op-bend", "op-gear"})
def test_phase_repair_and_budget_selection_are_stable(self) -> None:
guidance = FileAuthorGuidance(GUIDANCE_ROOT, max_chars=3_600)
planning = guidance.select(
phase=TaskPhase.COMPILING_FEATURE_PLAN,
atomic_id="",
repair_required=False,
supported_atomic_ids=atomic_ids(),
)
repair = guidance.select(
phase=TaskPhase.AWAITING_ACTION,
atomic_id="fillet",
repair_required=True,
supported_atomic_ids=atomic_ids(),
)
self.assertEqual(planning.section_ids[:2], ("00-author-contract", "03-coordinate-system-and-datums"))
self.assertIn("02-parameters-and-derived-dimensions", planning.section_ids)
self.assertEqual(repair.section_ids[:3], ("00-author-contract", "03-coordinate-system-and-datums", "op-finish"))
self.assertIn("10-repair-and-best-effort", repair.section_ids)
def test_disabled_missing_and_invalid_corpus_fall_back_without_authoring_failure(self) -> None:
common = {
"phase": TaskPhase.FEATURE_PENDING,
"atomic_id": "extrude_add_blind",
"repair_required": False,
"supported_atomic_ids": atomic_ids(),
}
self.assertEqual(FileAuthorGuidance(GUIDANCE_ROOT, enabled=False).select(**common).fallback_reason, "guidance_disabled")
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
self.assertEqual(FileAuthorGuidance(root).select(**common).fallback_reason, "guidance_load_failed:FileNotFoundError")
(root / "manifest.json").write_text("{}", encoding="utf-8")
self.assertEqual(FileAuthorGuidance(root).select(**common).fallback_reason, "guidance_load_failed:ValueError")
def test_author_context_receives_guidance_but_keeps_the_existing_tool_instruction(self) -> None:
state = TaskState("cad_123456abcdef", TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT, 1)
workflow = WorkflowCoordinator(
WorkflowConfig(max_turns=8, format_error_limit=2),
_Repository(state),
_Artifacts(),
_Runtime(),
object(),
object(),
object(),
object(),
FileAuthorGuidance(GUIDANCE_ROOT),
)
messages, selection = workflow._author_context(state.task_id, [])
system = str(messages[0]["content"])
self.assertTrue(selection.enabled)
self.assertIn("Use exactly one offered structured tool call", system)
self.assertIn("Coordinate System And Datums", system)
self.assertIn("世界坐标", system)
def test_invalid_author_tool_call_retains_guidance_usage_metadata(self) -> None:
state = TaskState("cad_123456abcdef", TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT, 1)
repository = _Repository(state)
class _Models:
async def call_tool(self, **_kwargs: object) -> dict:
return {"tool_calls": [], "usage": {"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4}}
workflow = WorkflowCoordinator(
WorkflowConfig(max_turns=8, format_error_limit=2),
repository,
_Artifacts(),
_Runtime(),
_Models(),
object(),
object(),
object(),
FileAuthorGuidance(GUIDANCE_ROOT),
)
tool = {"type": "function", "function": {"name": "write_requirements_document", "parameters": {"type": "object"}}}
result = asyncio.run(workflow._author_turn(state.task_id, ModelIdentity("provider", "model"), [tool], []))
self.assertIsInstance(result, WorkflowError)
self.assertEqual(result.code, ErrorCode.AUTHOR_FORMAT_INVALID)
self.assertEqual(repository.usage_records[0]["guidance_enabled"], True)
self.assertIn("03-coordinate-system-and-datums", repository.usage_records[0]["guidance_section_ids"])
self.assertEqual(repository.usage_records[0]["retry_reason"], "invalid_tool_call")
if __name__ == "__main__":
unittest.main()
+81
View File
@@ -0,0 +1,81 @@
import math
import pytest
from app.cad_agent.application.authoring_contract import AuthoringDocument
from app.cad_agent.application.authoring_compiler import AuthoringCompiler, AuthoringCompileError
def test_model_cannot_submit_runtime_identity():
with pytest.raises(ValueError, match="AUTHOR_FORBIDDEN_FIELD"):
AuthoringDocument.model_validate({"feature_id": "feature_001", "bodies": []})
@pytest.mark.parametrize("field", ["task_id", "revision_id", "stable_id", "selector_tokens", "selector token", "host_face", "mirror_plane"])
def test_model_cannot_submit_any_internal_identity_variant(field):
with pytest.raises(ValueError, match="AUTHOR_FORBIDDEN_FIELD"):
AuthoringDocument.model_validate({field: "internal", "bodies": []})
def test_authoring_rejects_non_mm_and_non_finite_values():
with pytest.raises(ValueError):
AuthoringDocument.model_validate({"units": "in", "bodies": []})
with pytest.raises(ValueError, match="non-finite"):
AuthoringDocument.model_validate({"bodies": [{"name": "main", "features": [{"name": "base", "operation": "box_add", "params": {"length_mm": math.nan}}]}]})
@pytest.mark.parametrize("sketch", [
{
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
"profiles": [{"type": "circle", "diameter_mm": 20}],
},
{
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
"profile": {"type": "circle", "radius_mm": 10},
},
])
def test_authoring_rejects_runtime_or_ambiguous_sketch_shapes(sketch):
with pytest.raises(ValueError):
AuthoringDocument.model_validate({
"bodies": [{"name": "main", "features": [{
"name": "base", "operation": "extrude_add_blind", "params": {"distance_mm": 8}, "sketch": sketch,
}]}],
})
def test_authoring_rejects_selector_fields_that_are_not_declarative_source_intent():
with pytest.raises(ValueError):
AuthoringDocument.model_validate({
"bodies": [{"name": "main", "features": [{
"name": "base", "operation": "cylinder_add", "params": {},
"selectors": [{"kind": "face", "source": "other.top_planar_face", "role": "host_face"}],
}]}],
})
def test_compiler_assigns_deterministic_ids_and_orders_dependencies():
document = {"bodies": [{"name": "main", "features": [
{"name": "hole", "operation": "hole_blind", "depends_on": ["base"]},
{"name": "base", "operation": "extrude_add_blind"},
]}]}
runtime, audit = AuthoringCompiler(lambda operation: {"atomic_id": operation}).compile(document)
assert [item["id"] for item in runtime["features"]] == ["feature_002", "feature_001"]
assert runtime["features"][1]["depends_on"] == ["feature_002"]
assert audit["feature_ids"] == {"hole": "feature_001", "base": "feature_002"}
def test_compiler_rejects_unknown_operation():
with pytest.raises(AuthoringCompileError) as error:
AuthoringCompiler(lambda _: (_ for _ in ()).throw(ValueError("unknown"))).compile({
"bodies": [{"name": "main", "features": [{"name": "base", "operation": "bad"}]}]
})
assert error.value.code == "OPERATION_UNSUPPORTED"
def test_compiler_preserves_the_forbidden_field_error_code():
with pytest.raises(AuthoringCompileError) as error:
AuthoringCompiler(lambda operation: {"atomic_id": operation}).compile({
"bodies": [{"name": "main", "features": [{"name": "base", "operation": "box_add", "params": {}}]}],
"feature_id": "feature_001",
})
assert error.value.code == "AUTHOR_FORBIDDEN_FIELD"
+219
View File
@@ -0,0 +1,219 @@
import math
from pathlib import Path
from tempfile import TemporaryDirectory
from app.cad_agent.adapters.artifact_store import FileArtifactStore
from app.cad_agent.adapters.runtime import ProfileCadRuntime
from app.cad_agent.ports import AdapterUnavailable
from app.cad_agent.application.single_stage import SingleStageExecutor
from app.services.engine_service import validate_cdsl, validate_cdsl_shape
from app.settings import get_settings
def _base_feature() -> dict:
return {
"name": "base",
"operation": "extrude_add_blind",
"params": {"distance_mm": 8, "result_mode": "new_body"},
"sketch": {
"workplane": {
"origin_mm": [0, 0, 0],
"x_dir": [1, 0, 0],
"normal": [0, 0, 1],
},
"profile": {
"type": "polygon",
"vertices": [[-40, -25], [40, -25], [40, 25], [-40, 25]],
},
},
}
def _authoring(*features: dict) -> dict:
return {
"schema_version": "cad.author.v1",
"units": "mm",
"bodies": [{"name": "main", "features": list(features)}],
}
def test_authoring_compiles_to_runtime_cdsl_and_publishes_one_revision() -> None:
runtime = ProfileCadRuntime(get_settings())
authoring = _authoring(_base_feature())
runtime_cdsl, audit = runtime.compile_authoring(authoring)
assert runtime_cdsl["schema"] == "cad.runtime.v1"
assert runtime_cdsl["bodies"] == [{"id": "body_001", "name": "main"}]
assert runtime_cdsl["features"][0]["id"] == "feature_001"
assert audit["feature_ids"] == {"base": "feature_001"}
validate_cdsl_shape(runtime_cdsl, runtime.engine)
validate_cdsl(runtime_cdsl, runtime.engine)
with TemporaryDirectory() as temporary:
artifacts = FileArtifactStore(Path(temporary) / "artifacts")
task_id = "cad_abcdefghijkl"
artifacts.initialize_task(task_id, "make a rectangular plate")
result = SingleStageExecutor(None, artifacts, runtime).execute(task_id, authoring)
assert result["status"] == "completed"
revision = str(result["revision_id"])
assert result["executed_feature_ids"] == ["feature_001"]
for relative in ("model.step", "model.glb", "model.cdsl.json", "rebuild-report.json", "renders/render-manifest.json"):
assert artifacts.artifact_path(task_id, f"revisions/{revision}/{relative}").is_file()
def test_selector_failure_keeps_the_successful_prefix() -> None:
runtime = ProfileCadRuntime(get_settings())
invalid_fillet = {
"name": "bad_fillet",
"operation": "fillet",
"depends_on": ["base"],
"params": {"radius_mm": 1},
# extrude.end is a face output. Using it for an edge-only operation
# is a deliberate selector-kind failure, not a geometry fallback.
"selectors": [{"kind": "edge", "source": "base.top_planar_face"}],
}
runtime_cdsl, _audit = runtime.compile_authoring(_authoring(_base_feature(), invalid_fillet))
with TemporaryDirectory() as temporary:
built, diagnostics = runtime.rebuild_best_effort(
runtime_cdsl, temporary, "cad_abcdefghijkl", "stage_selector_failure",
)
assert built["executed_feature_ids"] == ["feature_001"]
assert diagnostics[0]["feature_id"] == "feature_002"
assert diagnostics[0]["code"] == "SELECTOR_KIND_MISMATCH"
def test_cylinder_cap_selector_is_compiled_into_hole_host_face_and_executes() -> None:
runtime = ProfileCadRuntime(get_settings())
base = {
"name": "base_flange",
"operation": "cylinder_add",
"params": {
"radius_mm": 30,
"height_mm": 10,
"axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
},
}
bore = {
"name": "center_bore",
"operation": "hole_wizard",
"params": {
"hole_type": "simple",
"diameter_mm": 12,
"depth_mm": 10,
"end_condition": {"type": "through_all", "solidworks_code": 1},
"positions": [{"mm": [0, 0, 10]}],
},
"selectors": [{"kind": "face", "source": "base_flange.top_planar_face", "match": "unique"}],
}
runtime_cdsl, _audit = runtime.compile_authoring(_authoring(base, bore))
compiled_bore = runtime_cdsl["features"][1]
assert compiled_bore["depends_on"] == ["feature_001"]
assert "selectors" not in compiled_bore
assert compiled_bore["params"]["host_face"] == {
"kind": "face",
"output_role": "cylinder.end",
"owner_feature_id": "feature_001",
"source": "runtime_snapshot",
"confidence": 1.0,
"match_mode": "unique",
}
with TemporaryDirectory() as temporary:
built, diagnostics = runtime.rebuild_best_effort(
runtime_cdsl, temporary, "cad_abcdefghijkl", "stage_cylinder_host",
)
assert diagnostics == []
assert built["executed_feature_ids"] == ["feature_001", "feature_002"]
def test_authoring_circle_diameter_is_lowered_to_runtime_radius() -> None:
runtime = ProfileCadRuntime(get_settings())
authoring = _authoring({
"name": "round_base",
"operation": "extrude_add_blind",
"params": {"distance_mm": 8, "result_mode": "new_body"},
"sketch": {
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
"profile": {"type": "circle", "diameter_mm": 20, "center_mm": [0, 0]},
},
})
compiled, _audit = runtime.compile_authoring(authoring)
assert compiled["geometry"]["sketches"][0]["profile"] == {
"type": "circle", "center": [0.0, 0.0], "radius_mm": 10.0,
}
def test_flange_bolt_host_is_built_before_source_topology_is_replaced() -> None:
"""An exposed base cap remains a valid bolt host before later fusions/cuts."""
runtime = ProfileCadRuntime(get_settings())
circle_sketch = lambda z, normal, diameter: {
"workplane": {"origin_mm": [0, 0, z], "x_dir": [1, 0, 0], "normal": normal},
"profile": {"type": "circle", "diameter_mm": diameter, "center_mm": [0, 0]},
}
bolt_positions = [
{"mm": [43 * math.cos(math.radians(angle)), 43 * math.sin(math.radians(angle)), 12]}
for angle in range(0, 360, 45)
]
authoring = _authoring(
{
"name": "base_flange", "operation": "cylinder_add",
"params": {"radius_mm": 60, "height_mm": 12, "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]}},
},
{
"name": "rear_shallow_pad", "operation": "extrude_add_blind", "depends_on": ["bolt_holes"],
"params": {"distance_mm": 4, "result_mode": "fuse", "reverse": False},
"sketch": circle_sketch(0, [0, 0, -1], 105),
},
{
"name": "rear_guide_boss", "operation": "extrude_add_blind", "depends_on": ["rear_shallow_pad"],
"params": {"distance_mm": 12, "result_mode": "fuse", "reverse": False},
"sketch": circle_sketch(0, [0, 0, -1], 38),
},
{
"name": "bolt_holes", "operation": "hole_wizard",
"params": {
"hole_type": "counterbore", "diameter_mm": 8, "depth_mm": 12,
"end_condition": {"type": "through_all_both", "solidworks_code": 7},
"counterbore": {"diameter_mm": 10, "depth_mm": 4}, "positions": bolt_positions,
},
"selectors": [{"kind": "face", "source": "base_flange.top_planar_face", "match": "unique"}],
},
{
"name": "front_hub_boss", "operation": "extrude_add_blind", "depends_on": ["bolt_holes"],
"params": {"distance_mm": 14, "result_mode": "fuse", "reverse": False},
"sketch": circle_sketch(12, [0, 0, 1], 56),
},
{
"name": "center_through_cut", "operation": "extrude_cut_through", "depends_on": ["front_hub_boss", "rear_guide_boss"],
"params": {"end_condition": {"type": "through_all_both", "solidworks_code": 7}},
"sketch": circle_sketch(0, [0, 0, 1], 32),
},
)
compiled, audit = runtime.compile_authoring(authoring)
assert audit["implicit_selector_dependencies"] == {"bolt_holes": ["base_flange"]}
with TemporaryDirectory() as temporary:
rebuilt = runtime.rebuild(compiled, temporary, "cad_abcdefghijkl", "flange_source_order")
assert rebuilt["health"]["feature_count"] == 6
def test_service_failure_during_prefix_export_is_not_reclassified_as_a_model_error(monkeypatch) -> None:
runtime = ProfileCadRuntime(get_settings())
runtime_cdsl, _audit = runtime.compile_authoring(_authoring(_base_feature()))
def unavailable(*_args, **_kwargs):
raise AdapterUnavailable("renderer unavailable")
monkeypatch.setattr(runtime, "rebuild", unavailable)
with TemporaryDirectory() as temporary:
try:
runtime.rebuild_best_effort(runtime_cdsl, temporary, "cad_abcdefghijkl", "stage_service_failure")
except AdapterUnavailable as error:
assert "renderer unavailable" in str(error)
else:
raise AssertionError("service failure was incorrectly converted into a model repair diagnostic")
File diff suppressed because it is too large Load Diff
@@ -1,41 +1,17 @@
"""#2 draft 假接受陷阱:extrudeParams.draft 必须被显式拒绝,而不是静默忽略。
"""Extrude draft contracts distinguish executable and unsupported extents.
中文说明
--------
这个文件在测试什么(issue #2「draft 被 schema 接受但 runtime 未执行」的回归测试):
1. 背景:三方合同错位——
- 机器契约 cdsl_schema.json(extrudeParams,约 101 行)允许
"draft": {"type": "object"},且是空 object(无任何子字段约束),
即"文档格式假接受";
- 人读契约 profile_schema.json 的 extrude_add_blind /
extrude_add_two_sided / extrude_cut_blind 均**未**声明 draft
为 optional_params;
- runtime(build123d_adapter.extrude 仅调 Solid.extrude,无锥形
拉伸)也完全没有 draft 实现,遇到 draft 就静默忽略。
- 隐患:importer(translator.py 已解析 SolidWorks 的
draft_angle_rad / reverse_draft_angle_rad)一旦把拔模角写进
CDSL params,runtime 会静默产出**无拔模角的直壁实体**——
注塑件/压铸件丢失脱模斜度,脱模卡死、分型面配合错误,且全程
无警告(与 #1 y_dir / #3 revolve.reverse 同族的静默错误)。
CADFS lowering now maps a single-sided blind draft to
``Build123dGeometryAdapter.extrude_taper``. The CDSL machine schema uses
``angle_deg`` plus ``pull_direction`` for that executable contract.
2. 修复策略:因为 build123d 内核没有锥形拉伸能力、且人读契约未声明
draft,正确的合同是"显式拒绝"而不是"实现拔模"——
capabilities.py 对携带 draft 的 extrude 特征报 unsupported_draft
blocker(与 unsupported_extent 同模式)。schema 字段保留(文档格式
契约,importer 未来可能产出),能力层明确划界。
3. 本测试套件把"draft 必须显式拒绝"固定下来:
- 主契约:带 draft 的 extrude 特征 → analyze 报 unsupported_draft
blocker,runtime_eligible=False;
- 回归护栏:不带 draft 的 extrude 特征 → 仍 runtime_eligible;
- 文档格式契约:带 draft 的文档仍能通过 cdsl_schema.json(拒绝
发生在能力层,不是 schema 层);
- 覆盖:extrude_add_blind / extrude_add_two_sided / extrude_cut_blind
三种原子都报同一 blocker(同一检查全类生效);
- 端到端:rebuild_cdsl(strict)带 draft → 抛 ValueError(大声失败),
analyze_document → runtime_eligible=False 且不 built(批量重建
不被静默污染)。
A two-sided or non-blind draft has no defined neutral-plane semantics in
the current CDSL runtime. It must remain in the input document but stop
with ``unsupported_draft_extent`` before execution. Non-canonical draft
fields are protocol errors and are rejected by the machine schema.
4. sys.path 说明:把 backend/engine 加入搜索路径,是为了直接 import
cdsl_engine 包做端到端测试(与 test_engine_revolve_reverse.py 风格
@@ -97,15 +73,16 @@ def _rectangle(minimum: list[float], maximum: list[float]) -> dict:
def _extrude_cdsl(*, atomic_id: str = "extrude_add_blind", with_draft: bool = True) -> dict:
"""构造最小 extrude 文档。
- with_draft=True 时 params 携带 draft 对象(任意非空 object 即可,
因为 cdsl_schema.json 对 draft 没有子字段约束);
- with_draft=True 时 params 携带可执行的单侧盲向 draft 对象;
- with_draft=False 时完全不写 draft 字段(回归护栏用)。
- atomic_id 可切换 extrude_add_blind / extrude_add_two_sided /
extrude_cut_blind 三种原子,验证同一 blocker 检查对全类生效。
"""
params: dict = {"distance_mm": 10.0}
if atomic_id == "extrude_add_two_sided":
params["reverse_distance_mm"] = 10.0
if with_draft:
params["draft"] = {"angle_deg": 5.0, "direction": "toward_sketch"}
params["draft"] = {"angle_deg": 5.0, "pull_direction": True}
return {
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
"part_id": "draft-contract", "meta": {"unit": "mm"},
@@ -116,7 +93,7 @@ def _extrude_cdsl(*, atomic_id: str = "extrude_add_blind", with_draft: bool = Tr
}]},
"features": [{
"id": "base_add", "atomic_id": atomic_id, "depends_on": [], "sketch_id": "base",
"params": params,
"params": params, "execution_status": "supported",
}],
}
@@ -134,20 +111,13 @@ def _validate_against_cdsl_schema(doc: dict) -> None:
class ExtrudeDraftContractTests(unittest.TestCase):
"""draft 假接受陷阱「文档格式-能力边界-运行时」三方合同的回归测试。"""
def test_draft_extrude_is_explicitly_blocked(self) -> None:
"""主契约:带 draft 的 extrude 特征必须被显式拒绝,而不是静默通过。
修复前(当前):capabilities 对 extrude 只检查 end_condition,
draft 字段完全无人过问 → analyze 报 runtime_eligible=True,
rebuild 静默产出直壁实体 → 本测试红灯。
修复后:capabilities 报 unsupported_draft blocker →
runtime_eligible=False → 绿灯。
"""
def test_blind_draft_extrude_is_runtime_eligible(self) -> None:
"""A canonical single-sided blind draft reaches the taper executor."""
analysis = analyze_cdsl(_extrude_cdsl(with_draft=True))
self.assertFalse(analysis.runtime_eligible)
self.assertTrue(analysis.runtime_eligible)
result = next(item for item in analysis.feature_results if item.feature_id == "base_add")
self.assertIn("unsupported_draft", [blocker.code for blocker in result.blockers])
self.assertNotIn("unsupported_draft_extent", [blocker.code for blocker in result.blockers])
def test_draft_free_extrude_stays_eligible(self) -> None:
"""回归护栏:不带 draft 的 extrude 特征仍必须 runtime_eligible。
@@ -160,52 +130,38 @@ class ExtrudeDraftContractTests(unittest.TestCase):
self.assertTrue(analysis.runtime_eligible)
def test_draft_passes_machine_schema(self) -> None:
"""文档格式契约:带 draft 的文档仍能通过 cdsl_schema.json 校验。
cdsl_schema.json(extrudeParams)保留 draft 字段,拒绝发生在
能力层(capabilities),不是 schema 层。这条测试锁死"schema 允许
+ 能力拒绝"的分层职责,防止未来把 schema 改过头(删掉字段后
importer 未来产出 draft 会直接被 schema 打回,失去可诊断性)。
"""
"""Canonical draft input is a valid CDSL document for each extrude form."""
_validate_against_cdsl_schema(_extrude_cdsl(atomic_id="extrude_add_blind", with_draft=True))
_validate_against_cdsl_schema(_extrude_cdsl(atomic_id="extrude_add_two_sided", with_draft=True))
_validate_against_cdsl_schema(_extrude_cdsl(atomic_id="extrude_cut_blind", with_draft=True))
def test_draft_blocks_every_extrude_atomic(self) -> None:
"""覆盖:三种 extrude 原子都报同一个 unsupported_draft blocker。
def test_two_sided_draft_is_explicitly_blocked(self) -> None:
analysis = analyze_cdsl(_extrude_cdsl(atomic_id="extrude_add_two_sided", with_draft=True))
draft 检查挂在 _SKETCH_ATOM_PREFIXES(extrude_/revolve_)公共入口,
必须对 extrude_add_blind / extrude_add_two_sided / extrude_cut_blind
同时生效,而不是只修了某一个。
"""
for atomic_id in ("extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind"):
with self.subTest(atomic_id=atomic_id):
analysis = analyze_cdsl(_extrude_cdsl(atomic_id=atomic_id, with_draft=True))
result = next(item for item in analysis.feature_results if item.feature_id == "base_add")
self.assertIn("unsupported_draft", [blocker.code for blocker in result.blockers])
self.assertFalse(analysis.runtime_eligible)
result = next(item for item in analysis.feature_results if item.feature_id == "base_add")
self.assertIn("unsupported_draft_extent", [blocker.code for blocker in result.blockers])
def test_draft_rebuild_fails_loudly_not_silently(self) -> None:
"""端到端:draft 文档的重建必须大声失败,而不是静默产出直壁实体。
def test_blind_draft_rebuilds_and_batch_reports_an_artifact(self) -> None:
"""The supported branch builds a STEP artifact through both entry points."""
with tempfile.TemporaryDirectory() as directory:
out_step = Path(directory) / "part.step"
rebuild_cdsl(_extrude_cdsl(with_draft=True), out_step)
self.assertTrue(out_step.exists())
修复前:rebuild_cdsl(strict)对 draft 视而不见 → 正常返回实体,
volume > 0,但几何是**没有拔模角的直壁**——静默错误。
修复后:rebuild_cdsl(strict)因 runtime_eligible=False 抛
ValueError(feature is not runtime eligible: unsupported_draft);
analyze_document 报告 runtime_eligible=False 且不 built——
批量重建不会被静默污染。
"""
# 1) strict 重建直接抛错(大声失败)。
with self.assertRaisesRegex(ValueError, "unsupported_draft"):
with tempfile.TemporaryDirectory() as directory:
rebuild_cdsl(_extrude_cdsl(with_draft=True), Path(directory) / "part.step")
# 2) 批量层 analyze_document 报告不可执行且不产出 STEP。
cdsl = _extrude_cdsl(with_draft=True)
with tempfile.TemporaryDirectory() as directory:
out_step = Path(directory) / "part.step"
report = _analyze_inline(cdsl, out_step)
self.assertFalse(report["runtime_eligible"])
self.assertFalse(report.get("built", False))
self.assertTrue(out_step.exists())
self.assertTrue(report["runtime_eligible"])
self.assertTrue(report["built"])
def test_noncanonical_draft_is_rejected_by_machine_schema(self) -> None:
cdsl = _extrude_cdsl(with_draft=True)
cdsl["features"][0]["params"]["draft"] = {"angle_deg": 5.0, "direction": "toward_sketch"}
with self.assertRaises(jsonschema.ValidationError):
_validate_against_cdsl_schema(cdsl)
def _analyze_inline(cdsl: dict, out_step: Path) -> dict:
@@ -916,7 +916,6 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
self.assertEqual(resolved.record.record_id, "sweep:end")
self.assertEqual(resolved.record.output_roles, ("sweep.end",))
self.assertEqual(evidence["output_role_status"], "unique_result_snapshot")
registry.replace_body_topology("later", "body:later", [
TopologyRecord("later:end", "face", "later", "body:later", {"center_mm": [0, 0, 10]}, object()),
])
@@ -926,6 +925,103 @@ class EngineRuntimeFoundationTests(unittest.TestCase):
self.assertEqual(stale.status, "not_found")
self.assertEqual(stale.diagnostic.code, "selector_output_role_not_found")
def test_provenance_selector_uses_exact_lineage_not_geometry_successors(self) -> None:
registry = TopologyRegistry()
source = object()
exact_result = object()
geometrically_similar = object()
registry.replace_body_topology("base", "body:base", [
TopologyRecord("base:face", "face", "base", "body:base", {"center_mm": [0, 0, 0]}, source),
])
registry.replace_body_topology("later", "body:later", [
TopologyRecord("later:exact", "face", "later", "body:later", {"center_mm": [10, 0, 0]}, exact_result),
TopologyRecord("later:similar", "face", "later", "body:later", {"center_mm": [0, 0, 0]}, geometrically_similar),
], topology_delta=TopologyDelta("transform", (
TopologyDeltaRelation("modified", "face", source, (exact_result,)),
)))
selector = {
"kind": "face", "owner_feature_id": "base", "stable_id": "base:face",
"source": "runtime_snapshot", "confidence": 1.0,
"geometry": {"center_mm": [0, 0, 0]},
"selector_intent": {
"version": "1.0", "kind": "face", "query_family": "SWEPT_FACE",
"source_query": {"ast": {}, "featurescript_version": "1511"},
"derivation_policy": {"allowed": ["continuation"], "multiplicity": "one"},
"evidence": "kernel_history",
},
}
resolution = registry.resolve(selector, active_body_id="body:later")
self.assertEqual(resolution.status, "resolved")
self.assertEqual(resolution.record.record_id, "later:exact")
self.assertEqual(registry.lineage()[0].derivation, "continuation")
def test_provenance_selector_rejects_non_unique_fragment(self) -> None:
registry = TopologyRegistry()
source = object()
first = object()
second = object()
registry.replace_body_topology("base", "body:base", [
TopologyRecord("base:edge", "edge", "base", "body:base", {"center_mm": [0, 0, 0]}, source),
])
registry.replace_body_topology("fillet", "body:fillet", [
TopologyRecord("fillet:first", "edge", "fillet", "body:fillet", {"center_mm": [0, 0, 0]}, first),
TopologyRecord("fillet:second", "edge", "fillet", "body:fillet", {"center_mm": [1, 0, 0]}, second),
], topology_delta=TopologyDelta("fillet", (
TopologyDeltaRelation("modified", "edge", source, (first, second)),
)))
resolution = registry.resolve({
"kind": "edge", "owner_feature_id": "base", "stable_id": "base:edge",
"source": "runtime_snapshot", "confidence": 1.0,
"selector_intent": {
"version": "1.0", "kind": "edge", "query_family": "SWEPT_EDGE",
"source_query": {"ast": {}, "featurescript_version": "1511"},
"derivation_policy": {"allowed": ["fragment"], "multiplicity": "one"},
"evidence": "kernel_history",
},
}, active_body_id="body:fillet")
self.assertEqual(resolution.status, "ambiguous")
self.assertEqual(resolution.diagnostic.code, "selector_relation_non_unique")
def test_provenance_selector_returns_all_proven_fragments(self) -> None:
registry = TopologyRegistry()
source = object()
first = object()
second = object()
registry.replace_body_topology("base", "body:base", [
TopologyRecord("base:edge", "edge", "base", "body:base", {}, source),
])
registry.replace_body_topology("fillet", "body:fillet", [
TopologyRecord("fillet:first", "edge", "fillet", "body:fillet", {}, first),
TopologyRecord("fillet:second", "edge", "fillet", "body:fillet", {}, second),
], topology_delta=TopologyDelta("fillet", (
TopologyDeltaRelation("modified", "edge", source, (first, second)),
)))
resolution = registry.resolve({
"kind": "edge", "owner_feature_id": "base", "stable_id": "base:edge",
"source": "runtime_snapshot", "confidence": 1.0,
"selector_intent": {
"version": "1.0", "kind": "edge", "query_family": "SWEPT_EDGE",
"source_query": {"ast": {}, "featurescript_version": "1511"},
"derivation_policy": {"allowed": ["fragment"], "multiplicity": "all_fragments"},
"evidence": "kernel_history",
},
}, active_body_id="body:fillet")
self.assertEqual(resolution.status, "resolved")
self.assertIsNone(resolution.record)
self.assertEqual([record.record_id for record in resolution.records], ["fillet:first", "fillet:second"])
def test_boolean_section_edges_are_recorded_as_intersection_lineage(self) -> None:
registry = TopologyRegistry()
section_edge = object()
registry.replace_body_topology("boolean", "body:boolean", [
TopologyRecord("boolean:section", "edge", "boolean", "body:boolean", {}, section_edge),
], topology_delta=TopologyDelta("intersect", section_values=(section_edge,)))
delta = registry.topology_deltas()[0]
lineage = delta["lineage"][0]
self.assertEqual(lineage["derivation"], "intersection")
self.assertEqual(lineage["result_record_ids"], ["boolean:section"])
self.assertTrue(delta["relations"][0]["section_edge"])
def test_shell_offset_role_source_selects_one_exact_builder_relation(self) -> None:
registry = TopologyRegistry()
extrude_start = object()
-357
View File
@@ -1,357 +0,0 @@
from __future__ import annotations
from pathlib import Path
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "backend"))
from app.cad_agent.domain.feature_plan import FeaturePlan, FeatureScheduler, node_hash, plan_hash, validate_feature_plan
from app.cad_agent.adapters.artifact_store import FileArtifactStore
from app.cad_agent.adapters.runtime import ProfileCadRuntime
from app.cad_agent.adapters.sqlite_repository import SqliteTaskRepository
from app.cad_agent.adapters.verifier import RegistryVerifierExecutor
from app.cad_agent.application.action_handlers import ActionCommandHandler
from app.cad_agent.application.llm_contracts import CompiledRequirementsSpec, MarkdownDocument
from app.cad_agent.application.requirements import RequirementsCommandHandler
from app.cad_agent.application.results import Accepted, Rejected
from app.cad_agent.domain.state import TaskPhase, transition
from app.cad_agent.domain.verifier_registry import default_registry
from app.settings import ProviderConfig, ProviderModel, Settings
def runtime_settings(root: Path) -> Settings:
provider = ProviderConfig("test", "Test", "https://test.invalid/v1", "key", (ProviderModel("test-model"),))
return Settings(
task_root=root / "tasks", conversation_root=root / "conversations",
library_root=ROOT / "backend" / "cdsl_library", engine_root=ROOT / "backend" / "engine" / "cdsl_engine",
llm_base_url=provider.base_url, llm_api_key=provider.api_key, llm_model="test-model", llm_timeout_s=1,
default_provider_id="test", providers=(provider,),
)
def contract() -> dict[str, object]:
return {
"requirements": [
{
"requirement_id": "req_001",
"acceptance_claims": [
{"claim_id": "claim_base", "verification_mode": "deterministic"},
{"claim_id": "claim_visual", "verification_mode": "visual"},
],
},
{
"requirement_id": "req_002",
"acceptance_claims": [{"claim_id": "claim_bore", "verification_mode": "deterministic"}],
},
{
"requirement_id": "req_003",
"acceptance_claims": [{"claim_id": "claim_pattern", "verification_mode": "deterministic"}],
},
],
}
def initial_plan() -> FeaturePlan:
return FeaturePlan.model_validate({
"schema_version": "cad.v3.2.feature-plan.v1",
"parent_plan_hash": "",
"replaces_node_ids": [],
"nodes": [
{"node_id": "base", "priority": 10, "intent": "Create the base.", "atomic_id": "extrude_add_blind", "depends_on": [], "claim_ids": ["claim_base"], "expected_change": "One base solid."},
{"node_id": "bore", "priority": 20, "intent": "Cut the bore.", "atomic_id": "hole_blind", "depends_on": ["base"], "claim_ids": ["claim_bore"], "expected_change": "One through bore."},
{"node_id": "pattern", "priority": 30, "intent": "Add the pattern.", "atomic_id": "hole_blind", "depends_on": ["base"], "claim_ids": ["claim_pattern"], "expected_change": "Mounting holes."},
],
"final_claim_ids": ["claim_visual"],
})
class FeaturePlanTests(unittest.TestCase):
def test_plan_requires_exact_claim_ownership(self) -> None:
plan = initial_plan()
self.assertEqual(validate_feature_plan(plan, contract(), {"extrude_add_blind", "hole_blind"}), [])
broken = plan.model_copy(deep=True)
broken.nodes[1].claim_ids = ["claim_base"]
messages = [item["message"] for item in validate_feature_plan(broken, contract(), {"extrude_add_blind", "hole_blind"})]
self.assertTrue(any("already owned" in message for message in messages))
self.assertTrue(any("has no owner" in message for message in messages))
def test_visual_claim_repeated_on_a_node_is_removed_before_persisting_the_plan(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
repository = SqliteTaskRepository(root / "state.sqlite3")
artifacts = FileArtifactStore(root / "tasks")
handler = RequirementsCommandHandler(repository, artifacts, default_registry(), atomic_ids=lambda: ("extrude_add_blind", "hole_blind"))
task_id = "cad_123456abcdef"
repository.create_task(task_id, "Create a part.")
artifacts.initialize_task(task_id, "Create a part.")
self.assertIsInstance(handler.submit_requirements_document(task_id, MarkdownDocument(markdown="Create a part."), invocation_id="requirements"), Accepted)
self.assertIsInstance(handler.submit_completion_target(task_id, MarkdownDocument(markdown="- [ ] A base and a visual edge treatment."), invocation_id="target"), Accepted)
compiled = CompiledRequirementsSpec.model_validate({"requirements": [{"assumptions": [], "acceptance_claims": [
{"claim_kind": "single_connected_body", "expected": {}},
{"claim_kind": "visual", "expected": {"description": "A visible edge treatment."}},
]}]})
self.assertIsInstance(handler.submit_compiled_spec(task_id, compiled, invocation_id="compile"), Accepted)
submitted = FeaturePlan.model_validate({
"schema_version": "cad.v3.2.feature-plan.v1", "parent_plan_hash": "", "replaces_node_ids": [],
"nodes": [{"node_id": "base", "priority": 10, "intent": "Create the base.", "atomic_id": "extrude_add_blind", "depends_on": [], "claim_ids": ["claim_001", "claim_002"], "expected_change": "One base solid."}],
"final_claim_ids": ["claim_002"],
})
accepted = handler.submit_feature_plan(task_id, submitted, invocation_id="plan")
self.assertIsInstance(accepted, Accepted)
state = repository.get_state(task_id)
persisted = artifacts.read_json(task_id, state.feature_plan_path)
self.assertEqual(persisted["nodes"][0]["claim_ids"], ["claim_001"])
self.assertEqual(persisted["final_claim_ids"], ["claim_002"])
def test_unowned_global_health_claim_is_bound_to_the_unique_root_add_feature(self) -> None:
plan = FeaturePlan.model_validate({
"schema_version": "cad.v3.2.feature-plan.v1", "parent_plan_hash": "", "replaces_node_ids": [],
"nodes": [
{"node_id": "base", "priority": 10, "intent": "Create the base.", "atomic_id": "extrude_add_blind", "depends_on": [], "claim_ids": ["claim_bore"], "expected_change": "One base solid."},
{"node_id": "finish", "priority": 20, "intent": "Finish the part.", "atomic_id": "hole_blind", "depends_on": ["base"], "claim_ids": ["claim_pattern"], "expected_change": "One bore."},
],
"final_claim_ids": ["claim_visual"],
})
health_contract = contract()
health_contract["requirements"][0]["acceptance_claims"][0]["claim_kind"] = "single_connected_body"
normalized = RequirementsCommandHandler._assign_unowned_global_health_claims(plan, health_contract)
self.assertEqual(normalized.nodes[0].claim_ids, ["claim_bore", "claim_base"])
self.assertEqual(validate_feature_plan(normalized, health_contract, {"extrude_add_blind", "hole_blind"}), [])
def test_scheduler_uses_ready_nodes_and_fixed_priority(self) -> None:
plan = initial_plan()
scheduler = FeatureScheduler(plan, [])
self.assertEqual(scheduler.next_ready().node_id, "base")
base = plan.nodes[0]
events = [{
"event": "feature_node_verified", "node_id": "base", "node_hash": node_hash(base),
"feature_id": "feature_001", "revision_id": "rev_001",
}]
scheduler = FeatureScheduler(plan, events)
self.assertEqual(scheduler.statuses()["base"], "done")
# Both bore and pattern are ready; priority decides deterministically.
self.assertEqual(scheduler.next_ready().node_id, "bore")
self.assertEqual(scheduler.feature_ids(), {"base": "feature_001"})
def test_revision_cannot_change_done_node_and_replaces_failed_subgraph(self) -> None:
previous = initial_plan()
base = previous.nodes[0]
bore = previous.nodes[1]
events = [
{"event": "feature_node_verified", "node_id": "base", "node_hash": node_hash(base), "feature_id": "feature_001", "revision_id": "rev_001"},
{"event": "feature_node_failed", "node_id": "bore", "node_hash": node_hash(bore), "failure_class": "engine_build", "attempt": 2, "terminal": True},
]
completed = FeatureScheduler(previous, events).completed_node_hashes()
revision = FeaturePlan.model_validate({
"schema_version": "cad.v3.2.feature-plan.v1",
"parent_plan_hash": plan_hash(previous),
"replaces_node_ids": ["bore"],
"nodes": [
base.model_dump(mode="json"),
{"node_id": "bore_revised", "priority": 20, "intent": "Cut the bore with revised operation.", "atomic_id": "hole_blind", "depends_on": ["base"], "claim_ids": ["claim_bore"], "expected_change": "One through bore."},
previous.nodes[2].model_dump(mode="json"),
],
"final_claim_ids": ["claim_visual"],
})
self.assertEqual(
validate_feature_plan(revision, contract(), {"extrude_add_blind", "hole_blind"}, previous_plan=previous, completed_node_hashes=completed, required_replacements={"bore"}),
[],
)
changed = revision.model_copy(deep=True)
changed.nodes[0].intent = "Changed completed node."
self.assertTrue(any("completed node 'base' was modified" in item["message"] for item in validate_feature_plan(changed, contract(), {"extrude_add_blind", "hole_blind"}, previous_plan=previous, completed_node_hashes=completed, required_replacements={"bore"})))
def test_materialized_feature_uses_direct_dag_dependencies_not_previous_history_item(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
runtime = ProfileCadRuntime(runtime_settings(Path(temporary)))
base = {
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "dag_test",
"geometry": {"sketches": [{
"id": "sketch_001", "workplane": {"origin_mm": [0, 0, 0], "normal": [0, 0, 1], "x_dir": [1, 0, 0]},
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 10},
}]},
"features": [
{"id": "feature_001", "atomic_id": "extrude_add_blind", "params": {"distance_mm": 5}, "depends_on": [], "sketch_id": "sketch_001"},
{"id": "feature_002", "atomic_id": "reference_plane", "params": {"plane": {"origin_mm": [0, 0, 5], "normal": [0, 0, 1], "x_dir": [1, 0, 0]}}, "depends_on": ["feature_001"]},
],
}
contract = runtime.operation_contract("reference_axis")
document, audit = runtime.materialize_fragment(
base,
{"feature": {"atomic_id": "reference_axis", "params": {"axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]}}}},
contract,
{},
runtime.reference_tokens(base),
depends_on_feature_ids=("feature_001",),
)
self.assertEqual(document["features"][-1]["depends_on"], ["feature_001"])
self.assertEqual(audit["depends_on_feature_ids"], ["feature_001"])
def test_required_through_cut_gets_a_server_recorded_exit_allowance(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
runtime = ProfileCadRuntime(runtime_settings(Path(temporary)))
base = {
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "through_cut",
"geometry": {"sketches": [{
"id": "sketch_001",
"workplane": {"origin_mm": [0, 0, 0], "normal": [0, 0, 1], "x_dir": [1, 0, 0]},
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 10},
}]},
"features": [{"id": "feature_001", "atomic_id": "extrude_add_blind", "params": {"distance_mm": 5}, "depends_on": [], "sketch_id": "sketch_001"}],
}
document, audit = runtime.materialize_fragment(
base,
{
"sketch": {
"workplane": {"origin_mm": [0, 0, 5], "normal": [0, 0, 1], "x_dir": [1, 0, 0]},
"profile": {"type": "circle", "center": [0, 0], "radius_mm": 2},
},
"feature": {"atomic_id": "extrude_cut_blind", "params": {"distance_mm": 5, "reverse": True}},
},
runtime.operation_contract("extrude_cut_blind"),
{"body": {"kind": "body", "geometry": {"bbox_mm": [-10, -10, 0, 10, 10, 5]}}},
runtime.reference_tokens(base),
require_through=True,
depends_on_feature_ids=("feature_001",),
)
self.assertAlmostEqual(document["features"][-1]["params"]["distance_mm"], 5.01)
self.assertEqual(audit["server_normalizations"][0]["submitted_mm"], 5.0)
self.assertEqual(audit["server_normalizations"][0]["reason"], "required through-cut exit allowance")
def test_verified_node_publishes_without_a_render_bundle_or_candidate_review(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
registry = default_registry()
runtime = ProfileCadRuntime(runtime_settings(root))
repository = SqliteTaskRepository(root / "state.sqlite3")
artifacts = FileArtifactStore(root / "tasks")
requirements = RequirementsCommandHandler(repository, artifacts, registry, atomic_ids=runtime.supported_atomic_ids)
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(registry))
task_id = "cad_123456abcdef"
repository.create_task(task_id, "Create a disk.")
artifacts.initialize_task(task_id, "Create a disk.")
self.assertIsInstance(requirements.submit_requirements_document(task_id, MarkdownDocument(markdown="Create a disk."), invocation_id="requirements"), Accepted)
self.assertIsInstance(requirements.submit_completion_target(task_id, MarkdownDocument(markdown="- [ ] One connected disk with 20 mm diameter and 5 mm thickness."), invocation_id="target"), Accepted)
compiled = CompiledRequirementsSpec.model_validate({"requirements": [{"assumptions": [], "acceptance_claims": [
{"claim_kind": "single_connected_body", "expected": {}},
{"claim_kind": "outer_cylindrical_surface", "expected": {"diameter_mm": 20, "axial_span_mm": 5, "tolerance_mm": 0.1}},
]}]})
self.assertIsInstance(requirements.submit_compiled_spec(task_id, compiled, invocation_id="compile"), Accepted)
plan = FeaturePlan.model_validate({
"schema_version": "cad.v3.2.feature-plan.v1", "parent_plan_hash": "", "replaces_node_ids": [],
"nodes": [{"node_id": "base", "priority": 10, "intent": "Create disk.", "atomic_id": "extrude_add_blind", "depends_on": [], "claim_ids": ["claim_001", "claim_002"], "expected_change": "One disk solid."}],
"final_claim_ids": [],
})
self.assertIsInstance(requirements.submit_feature_plan(task_id, plan, invocation_id="plan"), Accepted)
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.SCHEDULING_FEATURE)
self.assertIsInstance(actions.schedule_next_feature(task_id), Accepted)
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.FEATURE_PENDING)
result = actions.submit_feature_fragment(task_id, {
"sketch": {"workplane": {"origin_mm": [0, 0, 0], "normal": [0, 0, 1], "x_dir": [1, 0, 0]}, "profile": {"type": "circle", "center": [0, 0], "radius_mm": 10}},
"feature": {"atomic_id": "extrude_add_blind", "params": {"distance_mm": 5}},
}, invocation_id="fragment")
self.assertIsInstance(result, Accepted)
state = repository.get_state(task_id)
self.assertEqual(state.phase, TaskPhase.SCHEDULING_FEATURE)
self.assertEqual(state.active_revision, "rev_001")
self.assertFalse((artifacts.task_dir(task_id) / "revisions" / "rev_001" / "renders").exists())
events = repository.ledger_events(task_id)
self.assertTrue(any(event.get("event") == "feature_node_verified" for event in events))
self.assertFalse(any(event.get("event") == "candidate_built" for event in events))
final_gate = actions.schedule_next_feature(task_id)
self.assertIsInstance(final_gate, Accepted)
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.FINAL_VALIDATION)
self.assertTrue(any(event.get("event") == "feature_plan_complete" for event in repository.ledger_events(task_id)))
def test_feature_plan_schema_binds_plan_revision_lineage_to_state(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
runtime = ProfileCadRuntime(runtime_settings(root))
repository = SqliteTaskRepository(root / "state.sqlite3")
artifacts = FileArtifactStore(root / "tasks")
requirements = RequirementsCommandHandler(repository, artifacts, default_registry(), atomic_ids=runtime.supported_atomic_ids)
task_id = "cad_123456abcdef"
repository.create_task(task_id, "Create a disk.")
artifacts.initialize_task(task_id, "Create a disk.")
self.assertIsInstance(requirements.submit_requirements_document(task_id, MarkdownDocument(markdown="Create a disk."), invocation_id="requirements"), Accepted)
self.assertIsInstance(requirements.submit_completion_target(task_id, MarkdownDocument(markdown="- [ ] One disk."), invocation_id="target"), Accepted)
compiled = CompiledRequirementsSpec.model_validate({"requirements": [{"assumptions": [], "acceptance_claims": [
{"claim_kind": "single_connected_body", "expected": {}},
]}]})
self.assertIsInstance(requirements.submit_compiled_spec(task_id, compiled, invocation_id="compile"), Accepted)
plan = FeaturePlan.model_validate({
"schema_version": "cad.v3.2.feature-plan.v1", "parent_plan_hash": "", "replaces_node_ids": [],
"nodes": [{"node_id": "base", "priority": 10, "intent": "Create disk.", "atomic_id": "extrude_add_blind", "depends_on": [], "claim_ids": ["claim_001"], "expected_change": "One disk solid."}],
"final_claim_ids": [],
})
self.assertIsInstance(requirements.submit_feature_plan(task_id, plan, invocation_id="plan"), Accepted)
scheduled = transition(repository.get_state(task_id), "feature_scheduled")
self.assertTrue(repository.compare_and_swap(scheduled))
replanning = transition(repository.get_state(task_id), "feature_replan")
self.assertTrue(repository.compare_and_swap(replanning))
schema = requirements.feature_plan_schema(task_id)
properties = schema["properties"]
self.assertEqual(properties["parent_plan_hash"]["enum"], [plan_hash(plan)])
self.assertEqual(properties["replaces_node_ids"]["minItems"], 0)
self.assertEqual(properties["replaces_node_ids"]["maxItems"], 0)
node_schema = schema["$defs"]["FeatureNode"]["properties"]
self.assertEqual(node_schema["claim_ids"]["items"], {"enum": ["claim_001"]})
self.assertEqual(properties["final_claim_ids"]["items"], {"enum": []})
def test_pending_assigned_claim_rejects_the_node_checkpoint(self) -> None:
class PendingAssignedClaimVerifier:
def evaluate(self, claims: list[dict[str, object]], _facts: dict[str, object]) -> list[dict[str, object]]:
return [
{
"claim_id": str(claim.get("claim_id") or ""),
"claim_kind": str(claim.get("claim_kind") or ""),
"deterministic": True,
"status": "pending" if claim.get("claim_id") == "claim_002" else "pass",
"evidence": {"reason": "target geometry has not been introduced"},
}
for claim in claims
]
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
runtime = ProfileCadRuntime(runtime_settings(root))
repository = SqliteTaskRepository(root / "state.sqlite3")
artifacts = FileArtifactStore(root / "tasks")
requirements = RequirementsCommandHandler(repository, artifacts, default_registry(), atomic_ids=runtime.supported_atomic_ids)
actions = ActionCommandHandler(repository, artifacts, runtime, PendingAssignedClaimVerifier())
task_id = "cad_123456abcdef"
repository.create_task(task_id, "Create a disk.")
artifacts.initialize_task(task_id, "Create a disk.")
self.assertIsInstance(requirements.submit_requirements_document(task_id, MarkdownDocument(markdown="Create a disk."), invocation_id="requirements"), Accepted)
self.assertIsInstance(requirements.submit_completion_target(task_id, MarkdownDocument(markdown="- [ ] One disk."), invocation_id="target"), Accepted)
compiled = CompiledRequirementsSpec.model_validate({"requirements": [{"assumptions": [], "acceptance_claims": [
{"claim_kind": "single_connected_body", "expected": {}},
{"claim_kind": "outer_cylindrical_surface", "expected": {"diameter_mm": 20, "tolerance_mm": 0.1}},
]}]})
self.assertIsInstance(requirements.submit_compiled_spec(task_id, compiled, invocation_id="compile"), Accepted)
plan = FeaturePlan.model_validate({
"schema_version": "cad.v3.2.feature-plan.v1", "parent_plan_hash": "", "replaces_node_ids": [],
"nodes": [{"node_id": "base", "priority": 10, "intent": "Create disk.", "atomic_id": "extrude_add_blind", "depends_on": [], "claim_ids": ["claim_001", "claim_002"], "expected_change": "One disk solid."}],
"final_claim_ids": [],
})
self.assertIsInstance(requirements.submit_feature_plan(task_id, plan, invocation_id="plan"), Accepted)
self.assertIsInstance(actions.schedule_next_feature(task_id), Accepted)
result = actions.submit_feature_fragment(task_id, {
"sketch": {"workplane": {"origin_mm": [0, 0, 0], "normal": [0, 0, 1], "x_dir": [1, 0, 0]}, "profile": {"type": "circle", "center": [0, 0], "radius_mm": 10}},
"feature": {"atomic_id": "extrude_add_blind", "params": {"distance_mm": 5}},
}, invocation_id="fragment")
self.assertIsInstance(result, Rejected)
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.FEATURE_PENDING)
events = repository.ledger_events(task_id)
failure = next(event for event in reversed(events) if event.get("event") == "feature_node_failed")
self.assertEqual(failure["failure_class"], "node_validation")
self.assertTrue(any(item.get("claim_id") == "claim_002" and item.get("status") == "pending" for item in failure["blockers"]))
self.assertFalse(any(event.get("event") == "feature_node_verified" for event in events))
if __name__ == "__main__":
unittest.main()
@@ -1,71 +0,0 @@
from __future__ import annotations
from pathlib import Path
import sys
import unittest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "backend"))
from app.cad_agent.evals.live import _fixture, compare_guidance_reports # noqa: E402
def _result(*, scenario: str, repetition: int, author_calls: int, context_chars: int, schema_rejections: int, failure_layer: str = "") -> dict:
return {
"scenario": scenario,
"repetition": repetition,
"outcome": "passed",
"revision_ids": ["revision_001"],
"projection": {"phase": "COMPLETED"},
"checks": {"deterministic_claims_pass": True},
"schema_rejection_count": schema_rejections,
"failure_attribution": {"layer": failure_layer} if failure_layer else None,
"scenario_budget": {"max_author_turns": 8, "max_reviewer_turns": 2, "max_total_calls": 10, "max_total_tokens": 1000},
"usage": {"records": [{"context_chars": context_chars} for _ in range(author_calls)]},
}
def _report(results: list[dict]) -> dict:
return {
"author": {"provider": "author", "model": "model"},
"reviewer": {"provider": "reviewer", "model": "review"},
"runtime_profile_sha256": "a" * 64,
"operation_contracts": [{"atomic_id": "extrude_add_blind", "contract_hash": "b" * 64}],
"author_guidance": {"enabled": False, "max_chars": 3600},
"results": results,
}
class LiveGuidanceComparisonTests(unittest.TestCase):
def test_fixture_accepts_multiple_stable_scenarios_in_fixture_order(self) -> None:
selected = _fixture("comprehensive", ["l_bracket", "circular_flange_pcd"])
self.assertEqual([item["id"] for item in selected], ["circular_flange_pcd", "l_bracket"])
def test_comparison_enforces_matched_budget_and_quality_gates(self) -> None:
control = _report([
_result(scenario="part_a", repetition=1, author_calls=10, context_chars=1000, schema_rejections=2, failure_layer="cdsl_expression"),
_result(scenario="part_a", repetition=2, author_calls=10, context_chars=1000, schema_rejections=1),
])
treatment = _report([
_result(scenario="part_a", repetition=1, author_calls=11, context_chars=1300, schema_rejections=0),
_result(scenario="part_a", repetition=2, author_calls=11, context_chars=1300, schema_rejections=0),
])
treatment["author_guidance"]["enabled"] = True
comparison = compare_guidance_reports(control, treatment)
self.assertEqual(comparison["status"], "passed")
self.assertTrue(comparison["gates"]["median_author_calls_within_ten_percent"])
self.assertTrue(comparison["gates"]["model_or_cdsl_failure_improved"])
def test_comparison_excludes_explicit_unsupported_runtime_capability(self) -> None:
control = _report([_result(scenario="part_a", repetition=1, author_calls=10, context_chars=1000, schema_rejections=1)])
treatment = _report([_result(scenario="part_a", repetition=1, author_calls=10, context_chars=1200, schema_rejections=0)])
treatment["author_guidance"]["enabled"] = True
treatment["results"][0]["ledger"] = [{"operation_failures": [{"message": "unsupported_draft"}]}]
comparison = compare_guidance_reports(control, treatment)
self.assertEqual(comparison["treatment"]["eligible_runs"], 0)
self.assertEqual(comparison["excluded_capability_gaps"]["treatment"], [{"scenario": "part_a", "repetition": 1}])
if __name__ == "__main__":
unittest.main()
@@ -9,7 +9,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "backend"))
from app.services.review_renderer import render_section # noqa: E402
from app.services.render_bundle import render_section # noqa: E402
from app.settings import ProviderConfig, ProviderModel, Settings # noqa: E402
@@ -32,7 +32,7 @@ def settings(root: Path) -> Settings:
)
class ReviewRendererTests(unittest.TestCase):
class RenderBundleTests(unittest.TestCase):
def test_section_uses_build123d_part_operation_and_emits_a_png(self) -> None:
import build123d as b3d
-21
View File
@@ -58,24 +58,3 @@ class SettingsModelSelectionTests(unittest.TestCase):
self.assertEqual(resolved_provider.id, "alternate")
self.assertEqual(resolved_model.id, "alternate-first")
def test_reviewer_cannot_be_the_same_author_model(self) -> None:
provider = ProviderConfig("openai", "OpenAI", "https://example.invalid/v1", "key", (ProviderModel("gpt-5.5", vision=True),))
settings = Settings(
task_root=ROOT / "tmp-tasks",
conversation_root=ROOT / "tmp-conversations",
library_root=ROOT / "backend" / "cdsl_library",
engine_root=ROOT / "backend" / "engine" / "cdsl_engine",
llm_base_url=provider.base_url,
llm_api_key=provider.api_key,
llm_model="gpt-5.5",
llm_timeout_s=1,
default_provider_id="openai",
providers=(provider,),
review_provider_id="openai",
review_model_id="gpt-5.5",
)
author_provider, author_model = settings.resolve_model(None, None)
with self.assertRaisesRegex(ValueError, "must differ"):
settings.resolve_independent_review_model(author_provider, author_model)
+21
View File
@@ -0,0 +1,21 @@
from app.cad_agent.application.single_stage import SingleStageExecutor
class Artifacts:
def __init__(self): self.writes = []
def write_json_once(self, *_args): self.writes.append(_args); return "x"
def start_staging_revision(self, *_args):
return type("Stage", (), {"stage_id": "stage", "output_dir": "/tmp"})()
def write_stage_json(self, *_args): return "x"
def publish_staging_revision(self, *_args): return {"model.step": "x.step"}
class Runtime:
def compile_authoring(self, value): return ({"features": [{"id": "feature_001"}]}, {"feature_ids": {"base": "feature_001"}})
def rebuild_best_effort(self, *_args): return ({"executed_feature_ids": ["feature_001"]}, [])
def test_single_stage_publishes_compiled_document():
result = SingleStageExecutor(None, Artifacts(), Runtime()).execute("cad_abcdefghijkl", {"bodies": []})
assert result["status"] == "completed"
assert result["executed_feature_ids"] == ["feature_001"]
+27
View File
@@ -0,0 +1,27 @@
from app.cad_agent.evals.single_stage import summarize
def test_single_stage_summary_reports_protocol_quality_and_cost() -> None:
report = summarize([
{
"attempts": [{"schema_valid": True, "executable": False}],
"published_revision": "rev_1",
"requirement_targets": [{"status": "pass"}, {"status": "pending"}],
"usage": {"records": [{"prompt_tokens": 10, "completion_tokens": 2}]},
"duration_ms": 15,
},
{
"attempts": [{"schema_valid": False, "executable": False}],
"requirement_targets": [{"status": "fail"}],
"usage": {"records": [{"prompt_tokens": 3, "completion_tokens": 1}]},
"duration_ms": 5,
},
])
assert report["first_pass_schema_rate"] == 0.5
assert report["first_pass_executable_rate"] == 0.0
assert report["final_executable_rate"] == 0.5
assert report["requirement_targets"] == {"pass": 1, "fail": 1, "pending": 1, "not_applicable": 0}
assert report["calls"] == 2
assert report["prompt_tokens"] == 13
assert report["completion_tokens"] == 3
assert report["duration_ms"] == 20
+180
View File
@@ -0,0 +1,180 @@
import asyncio
from pathlib import Path
from tempfile import TemporaryDirectory
from app.cad_agent.adapters.artifact_store import FileArtifactStore
from app.cad_agent.adapters.sqlite_repository import SqliteTaskRepository
from app.cad_agent.application.workflow import ModelIdentity, WorkflowConfig, WorkflowCoordinator
from app.cad_agent.application.authoring_compiler import AuthoringCompileError
from app.cad_agent.domain.errors import ErrorCode
from app.cad_agent.domain.state import TaskPhase, transition
class Runtime:
def supported_atomic_ids(self): return ("extrude_add_blind",)
def operation_contract(self, _operation):
return {"atomic_id": "extrude_add_blind", "fragment_shape": {"sketch": "required", "selector_tokens": "forbidden"}, "selector_policy": {"slot": None, "token_kind": None, "min_items": 0, "max_items": 0}, "reference_policy": {"mode": "none"}, "author_params_schema": {"type": "object", "properties": {"distance_mm": {"type": "number", "exclusiveMinimum": 0}}, "required": ["distance_mm"], "additionalProperties": False}}
class Executor:
def __init__(self, artifacts): self.artifacts = artifacts
def compile(self, _task, _authoring, *, repair_count):
runtime, audit = {"features": []}, {}
runtime_path, audit_path = "documents/runtime.json", "documents/audit.json"
self.artifacts.write_json_once(_task, runtime_path, runtime)
self.artifacts.write_json_once(_task, audit_path, audit)
return {"runtime": runtime, "compile_audit": audit, "runtime_path": runtime_path, "audit_path": audit_path, "digest": "x"}
def build(self, _task, _authoring, _runtime, _audit, *, repair_count, digest=""):
return {"status": "completed", "revision_id": "rev_1", "paths": {"model.step": "revisions/rev_1/model.step"}, "executed_feature_ids": ["feature_001"], "diagnostics": []}
class Models:
def __init__(self): self.calls = 0
async def call_tool(self, **kwargs):
self.calls += 1
if kwargs["required_tool_name"] == "analyze_requirements":
payload = {"explicit_requirements": ["plate"], "assumptions": [], "acceptance_targets": [], "manual_targets": []}
else:
payload = {"bodies": [{"name": "main", "features": [{"name": "base", "operation": "extrude_add_blind", "params": {"distance_mm": 8}, "sketch": {"workplane": {"origin_mm": [0,0,0], "x_dir": [1,0,0], "normal": [0,0,1]}, "profile": {"type": "circle", "diameter_mm": 20}}}]}]}
import json
return {"tool_calls": [{"function": {"name": kwargs["required_tool_name"], "arguments": json.dumps(payload)}}], "usage": {}}
class InvalidAuthorModels(Models):
def __init__(self):
super().__init__()
self.author_calls = 0
async def call_tool(self, **kwargs):
if kwargs["required_tool_name"] == "analyze_requirements":
return await super().call_tool(**kwargs)
self.calls += 1
self.author_calls += 1
import json
return {
"tool_calls": [{"function": {
"name": "write_authoring_cdsl",
"arguments": json.dumps({"feature_id": "feature_001", "bodies": []}),
}}],
"usage": {},
}
def test_workflow_uses_two_author_calls_and_publishes():
with TemporaryDirectory() as temporary:
repository = SqliteTaskRepository(Path(temporary) / "state.sqlite3")
artifacts = FileArtifactStore(Path(temporary) / "artifacts")
models = Models()
workflow = WorkflowCoordinator(WorkflowConfig(), repository, artifacts, Runtime(), models, Executor(artifacts))
workflow.create_task("cad_abcdefghijkl", "make a plate")
events = asyncio.run(_collect(workflow))
assert models.calls == 2
assert events[-1][0] == "task_terminal"
assert repository.get_state("cad_abcdefghijkl").phase.value == "COMPLETED"
def test_service_retry_returns_to_the_persisted_stage_without_new_authoring_call():
with TemporaryDirectory() as temporary:
repository = SqliteTaskRepository(Path(temporary) / "state.sqlite3")
artifacts = FileArtifactStore(Path(temporary) / "artifacts")
workflow = WorkflowCoordinator(WorkflowConfig(), repository, artifacts, Runtime(), Models(), Executor(artifacts))
workflow.create_task("cad_abcdefghijkl", "make a plate")
created = repository.get_state("cad_abcdefghijkl")
assert created is not None
authoring = transition(created, "analysis_written", requirements_path="documents/requirements-analysis.json")
assert repository.compare_and_swap(authoring)
failed = transition(authoring, "failed", error=ErrorCode.STORAGE_FAILURE)
assert failed.phase == TaskPhase.FAILED
assert failed.retry_from_phase == TaskPhase.AUTHORING_CDSL
assert repository.compare_and_swap(failed)
assert workflow.resume("cad_abcdefghijkl")
resumed = repository.get_state("cad_abcdefghijkl")
assert resumed is not None
assert resumed.phase == TaskPhase.AUTHORING_CDSL
assert resumed.retry_from_phase is None
def test_authoring_schema_failures_use_at_most_two_repairs():
with TemporaryDirectory() as temporary:
repository = SqliteTaskRepository(Path(temporary) / "state.sqlite3")
artifacts = FileArtifactStore(Path(temporary) / "artifacts")
models = InvalidAuthorModels()
workflow = WorkflowCoordinator(WorkflowConfig(), repository, artifacts, Runtime(), models, Executor(artifacts))
workflow.create_task("cad_abcdefghijkl", "make a plate")
asyncio.run(_collect(workflow))
state = repository.get_state("cad_abcdefghijkl")
assert state is not None
assert state.phase == TaskPhase.FAILED
assert state.repair_count == 2
assert models.author_calls == 3
def test_author_operation_context_exposes_server_injected_selector_contract():
context = WorkflowCoordinator._author_operation_contract({
"fragment_shape": {"sketch": "forbidden", "selector_tokens": "required"},
"selector_policy": {
"slot": "params.host_face", "token_kind": "face", "min_items": 1, "max_items": 1,
},
"author_params_schema": {"type": "object", "properties": {}, "additionalProperties": False},
})
assert context["sketch"] == "forbidden"
assert context["selector"] == {
"required": True,
"kind": "face",
"min_items": 1,
"max_items": 1,
"destination": "params.host_face",
"source_syntax": "<local_feature_name>.<output_role>",
}
assert context["authoring_sketch_template"] is None
def test_schema_repair_hint_contains_the_exact_authoring_sketch_form():
assert "diameter_mm" in WorkflowCoordinator._repair_hint(
"AUTHOR_SCHEMA_INVALID", "bodies.0.features.0.sketch.profile",
)
def test_repair_can_change_unexecuted_features_after_compile_failure():
previous = {"bodies": [{"name": "main", "features": [
{"name": "base", "operation": "box_add", "params": {}},
{"name": "bad_hole", "operation": "hole_wizard", "params": {}},
]}]}
replacement = {"bodies": [{"name": "main", "features": [
{"name": "base", "operation": "box_add", "params": {}},
{"name": "bad_hole", "operation": "hole_wizard", "params": {"diameter_mm": 8}},
]}]}
WorkflowCoordinator._validate_repair_document(
previous,
replacement,
{"diagnostics": [{"code": "SELECTOR_NOT_FOUND", "path": "features.bad_hole.selectors"}]},
{"feature_ids": {"base": "feature_001", "bad_hole": "feature_002"}},
)
def test_repair_cannot_change_executed_feature_without_targeted_diagnostic():
previous = {"bodies": [{"name": "main", "features": [
{"name": "base", "operation": "box_add", "params": {"height_mm": 8}},
]}]}
replacement = {"bodies": [{"name": "main", "features": [
{"name": "base", "operation": "box_add", "params": {"height_mm": 9}},
]}]}
try:
WorkflowCoordinator._validate_repair_document(
previous,
replacement,
{"diagnostics": [], "executed_feature_ids": ["feature_001"]},
{"feature_ids": {"base": "feature_001"}},
)
except AuthoringCompileError as error:
assert error.code == "AUTHOR_SCHEMA_INVALID"
else:
raise AssertionError("an executed feature was changed without a diagnostic target")
async def _collect(workflow):
return [item async for item in workflow.run(task_id="cad_abcdefghijkl", author=ModelIdentity("p", "m"))]