226 lines
12 KiB
Python
226 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from app.services.autonomous_cdsl_generation import (
|
|
AutonomousCdslGenerationRunner,
|
|
parse_modeling_plan,
|
|
)
|
|
from app.services.storage import WorkspaceStore
|
|
from pathlib import Path
|
|
from app.settings import ProviderConfig, ProviderModel, Settings
|
|
|
|
|
|
def _plan():
|
|
return parse_modeling_plan(
|
|
"""
|
|
[STEP step_3]
|
|
goal: 两端夹紧槽
|
|
relationship: 左右槽属于同一夹紧结构,但分别作用于左右轴套
|
|
[ACTION left_clamp_slot]
|
|
step: step_3
|
|
title: 左轴套夹紧槽
|
|
target: left_hub
|
|
operation: extrude_cut_blind
|
|
[ACTION right_clamp_slot]
|
|
step: step_3
|
|
title: 右轴套夹紧槽
|
|
target: right_hub
|
|
operation: extrude_cut_blind
|
|
depends_on: left_clamp_slot
|
|
""",
|
|
checklist=["两端夹紧槽"],
|
|
runtime_atomic_ids={"extrude_cut_blind"},
|
|
)
|
|
|
|
|
|
def test_related_targets_stay_in_one_step_with_independent_actions():
|
|
plan = _plan()
|
|
assert [item["action_id"] for item in plan["steps"][0]["actions"]] == [
|
|
"left_clamp_slot",
|
|
"right_clamp_slot",
|
|
]
|
|
assert plan["steps"][0]["relationship"] == "左右槽属于同一夹紧结构,但分别作用于左右轴套"
|
|
assert plan["actions"][1]["depends_on"] == ["left_clamp_slot"]
|
|
|
|
|
|
def test_soft_action_headings_are_indexed_without_forcing_machine_fields():
|
|
plan = parse_modeling_plan(
|
|
"## Step 3 - 两端夹紧槽\n### Action 3a - 左槽\n切左槽\n### Action 3b - 右槽\n切右槽",
|
|
checklist=["两端夹紧槽"],
|
|
runtime_atomic_ids={"extrude_cut_blind"},
|
|
)
|
|
assert [item["title"] for item in plan["steps"][0]["actions"]] == ["左槽", "右槽"]
|
|
assert plan["steps"][0]["actions"][1]["depends_on"] == ["step_1_action_1"]
|
|
|
|
|
|
def test_action_progress_advances_within_step_before_next_step(tmp_path):
|
|
provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),))
|
|
settings = Settings(task_root=tmp_path / "tasks", conversation_root=tmp_path / "conversations", library_root=Path("backend/cdsl_library"), engine_root=Path("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,))
|
|
store = WorkspaceStore(settings)
|
|
runner = AutonomousCdslGenerationRunner(settings, store, AsyncMock())
|
|
state = {
|
|
"modeling_plan_enforced": True,
|
|
"modeling_plan_status": "approved",
|
|
"modeling_plan": _plan(),
|
|
"active_plan_step_id": "step_3",
|
|
"active_plan_action_id": "left_clamp_slot",
|
|
"plan_action_status": {"left_clamp_slot": "pending", "right_clamp_slot": "pending"},
|
|
"plan_feature_status": {},
|
|
"plan_step_status": {"step_3": "pending"},
|
|
}
|
|
runner._mark_plan_progress(state, {
|
|
"plan_step_id": "step_3",
|
|
"plan_action_id": "left_clamp_slot",
|
|
"atomic_ids": ["extrude_cut_blind"],
|
|
})
|
|
assert state["active_plan_step_id"] == "step_3"
|
|
assert state["active_plan_action_id"] == "right_clamp_slot"
|
|
assert state["plan_action_status"]["left_clamp_slot"] == "complete"
|
|
assert state["plan_action_status"]["right_clamp_slot"] == "pending"
|
|
assert state["plan_step_status"]["step_3"] == "pending"
|
|
|
|
|
|
def test_action_operation_mismatch_is_explicit():
|
|
provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),))
|
|
settings = Settings(task_root=Path("/tmp/cdsl-cad-plan-action-test/tasks"), conversation_root=Path("/tmp/cdsl-cad-plan-action-test/conversations"), library_root=Path("backend/cdsl_library"), engine_root=Path("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,))
|
|
store = WorkspaceStore(settings)
|
|
runner = AutonomousCdslGenerationRunner(settings, store, AsyncMock())
|
|
state = {
|
|
"modeling_plan_enforced": True,
|
|
"modeling_plan_status": "approved",
|
|
"modeling_plan": _plan(),
|
|
"active_plan_step_id": "step_3",
|
|
"active_plan_action_id": "left_clamp_slot",
|
|
"plan_action_status": {"left_clamp_slot": "pending", "right_clamp_slot": "pending"},
|
|
}
|
|
action = runner._plan_current_action(state)
|
|
assert action is not None
|
|
assert action["operation"] == "extrude_cut_blind"
|
|
|
|
|
|
def test_enforced_submit_schema_exposes_action_id():
|
|
from app.services.autonomous_cdsl_generation import _operation_contract_payload
|
|
from app.services.cdsl_authoring_schema import operation_contract_hash
|
|
from app.services.engine_service import load_engine
|
|
|
|
provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),))
|
|
root = Path("/tmp/cdsl-cad-plan-action-schema")
|
|
settings = Settings(task_root=root / "tasks", conversation_root=root / "conversations", library_root=Path("backend/cdsl_library"), engine_root=Path("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,))
|
|
store = WorkspaceStore(settings)
|
|
task = store.ensure_task(None, "schema")
|
|
contract = _operation_contract_payload(load_engine(settings), "extrude_cut_blind")
|
|
state = {"modeling_plan_enforced": True, "modeling_plan_mode": "indexed", "active_operation_contract": contract, "pending_operation_revision": "", "pending_operation_contract_hash": operation_contract_hash("extrude_cut_blind", contract["canonical_fragment_schema"])}
|
|
tool = AutonomousCdslGenerationRunner(settings, store, AsyncMock())._canonical_submit_tool(task, state)
|
|
assert tool is not None
|
|
assert "plan_action_id" in tool["function"]["parameters"]["properties"]
|
|
assert "plan_action_id" in tool["function"]["parameters"]["required"]
|
|
|
|
|
|
def test_multiple_outer_profiles_request_plan_action_split_without_schema_loop(tmp_path):
|
|
from app.services.engine_service import load_engine
|
|
|
|
provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),))
|
|
settings = Settings(task_root=tmp_path / "tasks", conversation_root=tmp_path / "conversations", library_root=Path("backend/cdsl_library"), engine_root=Path("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,))
|
|
store = WorkspaceStore(settings)
|
|
task = store.ensure_task(None, "left and right slots")
|
|
task_id = str(task["task_id"])
|
|
store.write_requirements_document(task_id, "# Frozen\nCreate left and right slots.")
|
|
runner = AutonomousCdslGenerationRunner(settings, store, AsyncMock())
|
|
engine = load_engine(settings)
|
|
plan = parse_modeling_plan(
|
|
"## Step 1 - Slots\n### Action 1a - Left slot\n### Action 1b - Right slot",
|
|
checklist=["left and right slots"],
|
|
runtime_atomic_ids={"extrude_cut_blind"},
|
|
)
|
|
state = {
|
|
"modeling_plan_enforced": True,
|
|
"modeling_plan_status": "approved",
|
|
"modeling_plan": plan,
|
|
"active_plan_step_id": "step_1",
|
|
"active_plan_action_id": "step_1_action_1",
|
|
"plan_step_status": {"step_1": "pending"},
|
|
"plan_action_status": {"step_1_action_1": "pending", "step_1_action_2": "pending"},
|
|
"candidate_attempts_by_head": {},
|
|
}
|
|
|
|
asyncio.run(runner._execute_tool(
|
|
task_id, "left and right slots", state, engine, "get_cdsl_operation_contract",
|
|
{"atomic_id": "extrude_cut_blind"},
|
|
))
|
|
fragment = {
|
|
"sketch": {
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {
|
|
"type": "analytic_contours",
|
|
"contours": [
|
|
{"role": "outer", "closed": True, "segments": [{"type": "circle", "center": [-10, 0], "radius_mm": 2}]},
|
|
{"role": "outer", "closed": True, "segments": [{"type": "circle", "center": [10, 0], "radius_mm": 2}]},
|
|
],
|
|
},
|
|
},
|
|
"feature": {"atomic_id": "extrude_cut_blind", "params": {"distance_mm": 5}},
|
|
}
|
|
events, progressed = asyncio.run(runner._execute_tool(
|
|
task_id, "left and right slots", state, engine, "submit_cdsl_fragment",
|
|
{
|
|
"plan_step_id": "step_1",
|
|
"plan_action_id": "step_1_action_1",
|
|
"batch_goal": "Create both slots.",
|
|
"fragment": fragment,
|
|
},
|
|
))
|
|
|
|
assert progressed is False
|
|
assert events[0][1]["diagnostic"]["code"] == "CDSL_PROFILE_MULTIPLE_OUTERS"
|
|
assert state["modeling_plan_status"] == "revise"
|
|
assert state["candidate_action_required"]["reason"] == "plan_action_split_required"
|
|
assert state["format_correction"] == {}
|
|
assert not state.get("schema_retry_counts")
|
|
assert (store.read_task(task_id) or {}).get("active_revision") == ""
|
|
|
|
|
|
def test_reviewer_action_count_can_require_plan_revision_without_server_semantic_rules(tmp_path):
|
|
from app.services.engine_service import load_engine
|
|
|
|
provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),))
|
|
settings = Settings(task_root=tmp_path / "tasks", conversation_root=tmp_path / "conversations", library_root=Path("backend/cdsl_library"), engine_root=Path("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,))
|
|
store = WorkspaceStore(settings)
|
|
task = store.ensure_task(None, "left and right slots")
|
|
task_id = str(task["task_id"])
|
|
store.write_requirements_document(task_id, "# Frozen\nCreate left and right slots.")
|
|
store.write_completion_checklist(task_id, "- [ ] left and right slots")
|
|
runner = AutonomousCdslGenerationRunner(settings, store, AsyncMock())
|
|
state = {
|
|
"modeling_plan_enforced": True,
|
|
"modeling_plan_status": "missing",
|
|
"modeling_plan_review_attempts": 0,
|
|
}
|
|
reviewer_revise = {
|
|
"schema_version": "cad.modeling-plan-review.v1",
|
|
"verdict": "revise",
|
|
"confidence": 0.9,
|
|
"issues": [{"type": "action_granularity", "step_id": "step_1", "message": "The two independently located slots need separate actions."}],
|
|
"coverage": [{"requirement": "left and right slots", "step_id": "step_1", "status": "covered", "evidence": "The step names both slots."}],
|
|
"step_checks": [{"step_id": "step_1", "status": "pass", "notes": "The related slots remain together."}],
|
|
"action_checks": [{"step_id": "step_1", "status": "fail", "notes": "The plan has one action but requires two.", "required_action_count": 2}],
|
|
}
|
|
with patch("app.services.autonomous_cdsl_generation.review_modeling_plan", AsyncMock(return_value=reviewer_revise)):
|
|
events, progressed = asyncio.run(runner._execute_tool(
|
|
task_id,
|
|
"left and right slots",
|
|
state,
|
|
load_engine(settings),
|
|
"write_modeling_plan",
|
|
{"plan_text": "## Step 1 - Clamp slots\nCreate the left slot and right slot."},
|
|
))
|
|
|
|
assert progressed is True
|
|
assert state["modeling_plan_status"] == "revise"
|
|
assert "action_granularity_issues" not in state["modeling_plan"]
|
|
review_event = next(payload for name, payload in events if name == "modeling_plan_review")
|
|
assert review_event["review"]["verdict"] == "revise"
|
|
assert review_event["review"]["action_checks"][0]["status"] == "fail"
|
|
assert [item["function"]["name"] for item in runner._author_tools(store.read_task(task_id) or {}, requirements_frozen=True, state=state)] == ["write_modeling_plan"]
|