3837 lines
230 KiB
Python
3837 lines
230 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import ast
|
|
from dataclasses import replace
|
|
from hashlib import sha256
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "backend"))
|
|
|
|
from app.cad_agent.adapters.artifact_store import FileArtifactStore
|
|
from app.cad_agent.adapters.event_publisher import IdempotentInProcessPublisher
|
|
from app.cad_agent.adapters.runtime import ProfileCadRuntime, RuntimeAdapterError
|
|
from app.cad_agent.adapters.sqlite_repository import SqliteTaskRepository
|
|
from app.cad_agent.adapters.structured_llm import StructuredModelError, StructuredModelGateway, StructuredTransportError
|
|
from app.cad_agent.adapters.verifier import RegistryVerifierExecutor
|
|
from app.cad_agent.application.action_handlers import ActionCommandHandler
|
|
from app.cad_agent.application.outbox import OutboxDispatcher
|
|
from app.cad_agent.application.results import Accepted, Rejected, Waiting
|
|
from app.cad_agent.application.capabilities import conformance_tools
|
|
from app.cad_agent.application.llm_contracts import ( # noqa: E402
|
|
CandidateReview,
|
|
ClaimCoverage,
|
|
FinalReview,
|
|
GeometryConclusion,
|
|
NextAction,
|
|
RequirementsDraftBatch,
|
|
RequirementsPatchBatch,
|
|
RequirementsReview,
|
|
RollbackCheckpoint,
|
|
canonical_json_object,
|
|
canonical_validate_schema,
|
|
canonical_validate,
|
|
candidate_review_schema,
|
|
final_review_schema,
|
|
geometry_conclusion_schema,
|
|
operation_contract_request_schema,
|
|
requirements_draft_schema,
|
|
rollback_checkpoint_schema,
|
|
topology_request_schema,
|
|
)
|
|
from app.cad_agent.evals.live import _acceptance_coverage, _capability_block_reason, _failure_attribution, _fixture, _run_checks, _safe_artifact_manifest
|
|
from app.cad_agent.evals.token_baseline import (
|
|
BASELINE_SCHEMA_VERSION,
|
|
author_request_identity,
|
|
compare_token_baseline,
|
|
request_sha256,
|
|
validate_token_baseline_provenance,
|
|
)
|
|
from app.cad_agent.application.requirements_review import RequirementsCommandHandler
|
|
from app.cad_agent.application.workflow import ModelIdentity, WorkflowConfig, WorkflowCoordinator
|
|
from app.cad_agent.domain.errors import ErrorCode, WorkflowError
|
|
from app.cad_agent.domain.operation_contract import (
|
|
SEMANTIC_PREFLIGHT_NAMES,
|
|
OperationContractError,
|
|
fragment_schema,
|
|
validate_fragment,
|
|
validate_operation_contract,
|
|
)
|
|
from app.cad_agent.domain.state import PendingAction, TaskPhase, TaskState, legal_transitions, retry_resume_event, transition
|
|
from app.cad_agent.domain.verifier_registry import default_registry
|
|
from app.models.contracts import ChatMessage
|
|
from app.services.agent_service import AgentService
|
|
from app.services.library import CdslLibrary
|
|
from app.services.storage import WorkspaceStore
|
|
from app.settings import ProviderConfig, ProviderModel, Settings
|
|
|
|
|
|
def settings(root: Path) -> Settings:
|
|
provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-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 service_settings(root: Path) -> Settings:
|
|
author = ProviderConfig("author", "Author", "https://author.invalid/v1", "author-key", (ProviderModel("author-model"),))
|
|
reviewer = ProviderConfig("reviewer", "Reviewer", "https://reviewer.invalid/v1", "reviewer-key", (ProviderModel("reviewer-model", vision=True),))
|
|
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=author.base_url,
|
|
llm_api_key=author.api_key,
|
|
llm_model="author-model",
|
|
llm_timeout_s=1,
|
|
default_provider_id="author",
|
|
providers=(author, reviewer),
|
|
review_provider_id="reviewer",
|
|
review_model_id="reviewer-model",
|
|
)
|
|
|
|
|
|
def schema_example(schema: object) -> object:
|
|
"""Build a smallest valid fixture from a closed operation-param schema."""
|
|
if not isinstance(schema, dict):
|
|
raise AssertionError("Schema fixture must be an object")
|
|
if "const" in schema:
|
|
return schema["const"]
|
|
if isinstance(schema.get("enum"), list) and schema["enum"]:
|
|
return schema["enum"][0]
|
|
for branch_name in ("oneOf", "anyOf"):
|
|
branches = schema.get(branch_name)
|
|
if isinstance(branches, list) and branches:
|
|
return schema_example(branches[0])
|
|
kind = schema.get("type")
|
|
if kind == "object":
|
|
properties = schema.get("properties") if isinstance(schema.get("properties"), dict) else {}
|
|
required = schema.get("required") if isinstance(schema.get("required"), list) else []
|
|
return {str(name): schema_example(properties[name]) for name in required if name in properties}
|
|
if kind == "array":
|
|
count = int(schema.get("minItems") or 0)
|
|
return [schema_example(schema.get("items") or {}) for _ in range(count)]
|
|
if kind == "integer":
|
|
lower = schema.get("minimum", schema.get("exclusiveMinimum", 0))
|
|
return int(lower) + (1 if "exclusiveMinimum" in schema else 0)
|
|
if kind == "number":
|
|
lower = schema.get("minimum", schema.get("exclusiveMinimum", 0))
|
|
return float(lower) + (1.0 if "exclusiveMinimum" in schema else 0.0)
|
|
if kind == "boolean":
|
|
return False
|
|
if kind == "string":
|
|
return "x" * max(1, int(schema.get("minLength") or 0))
|
|
if kind == "null":
|
|
return None
|
|
raise AssertionError(f"No generic fixture for schema type: {kind!r}")
|
|
|
|
|
|
class V3ProtocolTests(unittest.TestCase):
|
|
def _ready_action(self, root: Path) -> tuple[SqliteTaskRepository, FileArtifactStore, ActionCommandHandler, str, PendingAction]:
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
registry = default_registry()
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(registry))
|
|
task_id = "cad_123456abcdef"
|
|
state = repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
contract = {
|
|
"schema_version": "cad.requirements-contract.v1", "task_id": task_id, "contract_hash": "test-contract",
|
|
"requirements": [{"requirement_id": "req_001", "draft_id": "draft_001", "source_ids": ["src_001"], "statement": "One solid", "assumptions": [], "acceptance_claims": [{"claim_id": "claim_001", "claim_kind": "solid_count_equals", "expected": {"value": 1}}]}],
|
|
}
|
|
artifacts.write_requirements_contract(task_id, contract)
|
|
contract_path = artifacts.write_requirements_contract(task_id, contract, invocation_id="test_contract")
|
|
for event in ("draft_updated", "requirements_finalized", "requirements_approved"):
|
|
state = transition(state, event, requirements_contract_path=contract_path if event == "requirements_approved" else None)
|
|
self.assertTrue(repository.compare_and_swap(state))
|
|
operation = runtime.operation_contract("extrude_add_blind")
|
|
action = PendingAction("act_004", state.working_head, "Create base", ("req_001",), "extrude_add_blind", "Add one base", operation["contract_hash"], "candidate-key")
|
|
state = transition(state, "action_proposed", pending_action=action)
|
|
self.assertTrue(repository.compare_and_swap(state))
|
|
return repository, artifacts, actions, task_id, action
|
|
|
|
def test_canonical_dto_rejects_extra_fields_without_side_effect(self) -> None:
|
|
result = canonical_validate(
|
|
json.dumps({"working_head": "cad_123456abcdef:root:v1", "intent": "Create base", "requirement_ids": ["req_001"], "atomic_id": "extrude_add_blind", "expected_change": "Add base", "plan_step_id": "old"}),
|
|
NextAction,
|
|
)
|
|
self.assertEqual(getattr(result, "code", None), ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
self.assertTrue(any(error["path"] == "/plan_step_id" for error in result.field_errors))
|
|
|
|
def test_domain_transition_table_covers_every_legal_and_illegal_edge(self) -> None:
|
|
table = legal_transitions()
|
|
events = {event for _phase, event in table}
|
|
for (phase, event), target in table.items():
|
|
state = TaskState(
|
|
"cad_123456abcdef", phase, 7,
|
|
retry_from_phase=target if phase == TaskPhase.WAITING_RETRY and event.startswith("resume_") else None,
|
|
)
|
|
advanced = transition(state, event)
|
|
self.assertEqual(advanced.phase, target, f"{phase} --{event}-->")
|
|
self.assertEqual(advanced.version, state.version + 1)
|
|
for phase in TaskPhase:
|
|
for event in events:
|
|
if (phase, event) in table:
|
|
continue
|
|
with self.assertRaises(ValueError, msg=f"{phase} must reject {event}"):
|
|
transition(TaskState("cad_123456abcdef", phase, 7), event)
|
|
|
|
def test_waiting_retry_resumes_the_persisted_source_phase(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry()))
|
|
workflow = WorkflowCoordinator(WorkflowConfig(1, 1), repository, artifacts, runtime, None, None, requirements, actions)
|
|
task_id = "cad_123456abcdef"
|
|
initial = repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
|
|
waiting = transition(initial, "waiting_retry", error=ErrorCode.AUTHOR_TRANSPORT_UNAVAILABLE)
|
|
self.assertEqual(waiting.retry_from_phase, TaskPhase.DRAFTING_REQUIREMENTS)
|
|
self.assertTrue(repository.compare_and_swap(waiting))
|
|
self.assertTrue(workflow.resume(task_id))
|
|
resumed = repository.get_state(task_id)
|
|
self.assertEqual(resumed.phase, TaskPhase.DRAFTING_REQUIREMENTS)
|
|
self.assertIsNone(resumed.retry_from_phase)
|
|
|
|
reviewing = transition(resumed, "requirements_finalized")
|
|
self.assertTrue(repository.compare_and_swap(reviewing))
|
|
waiting_review = transition(reviewing, "waiting_retry", error=ErrorCode.REVIEW_SERVICE_UNAVAILABLE)
|
|
self.assertEqual(retry_resume_event(waiting_review), "resume_reviewing_requirements")
|
|
self.assertTrue(repository.compare_and_swap(waiting_review))
|
|
self.assertTrue(workflow.resume(task_id))
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.REVIEWING_REQUIREMENTS)
|
|
|
|
def test_waiting_retry_without_a_persisted_source_is_not_guessed(self) -> None:
|
|
state = TaskState("cad_123456abcdef", TaskPhase.WAITING_RETRY, 7)
|
|
self.assertIsNone(retry_resume_event(state))
|
|
|
|
def test_waiting_for_user_cannot_be_resumed_without_new_user_input(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry()))
|
|
workflow = WorkflowCoordinator(WorkflowConfig(1, 1), repository, artifacts, runtime, None, None, requirements, actions)
|
|
task_id = "cad_123456abcdef"
|
|
initial = repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
reviewing = transition(initial, "requirements_finalized")
|
|
self.assertTrue(repository.compare_and_swap(reviewing))
|
|
waiting = transition(reviewing, "waiting_for_user", error=ErrorCode.WAITING_FOR_USER)
|
|
self.assertTrue(repository.compare_and_swap(waiting))
|
|
self.assertFalse(workflow.resume(task_id))
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.WAITING_FOR_USER)
|
|
|
|
def test_waiting_for_user_terminal_exposes_persisted_review_question(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry()))
|
|
workflow = WorkflowCoordinator(WorkflowConfig(1, 1), repository, artifacts, runtime, None, None, requirements, actions)
|
|
task_id = "cad_123456abcdef"
|
|
initial = workflow.create_task(task_id, "Create a plate.")
|
|
reviewing = transition(initial, "requirements_finalized")
|
|
self.assertTrue(repository.compare_and_swap(reviewing))
|
|
question = "Should the spacing be corrected to 45 degrees or should the count change?"
|
|
review_path = artifacts.write_requirements_review(task_id, {
|
|
"schema_version": "cad.requirements-review.v2",
|
|
"decision": "waiting_for_user",
|
|
"findings": [{"draft_id": "draft_001", "source_ids": ["src_001"], "finding_type": "ambiguous_conflict", "description": "Count and spacing conflict.", "question": question}],
|
|
}, invocation_id="inv_waiting")
|
|
waiting = transition(
|
|
reviewing,
|
|
"waiting_for_user",
|
|
error=ErrorCode.WAITING_FOR_USER,
|
|
requirements_review_path=review_path,
|
|
)
|
|
self.assertTrue(repository.compare_and_swap(waiting))
|
|
|
|
async def collect() -> list[tuple[str, dict[str, object]]]:
|
|
return [item async for item in workflow.run(task_id=task_id, author=None, reviewer=None)] # type: ignore[arg-type]
|
|
|
|
events = asyncio.run(collect())
|
|
self.assertEqual(events, [("task_terminal", {
|
|
"taskId": task_id,
|
|
"lifecycle": "waiting_for_user",
|
|
"revisionId": "",
|
|
"code": ErrorCode.WAITING_FOR_USER.value,
|
|
"message": f"Requirements need a user decision. {question}",
|
|
"questions": [question],
|
|
"reviewPath": review_path,
|
|
"blockerType": "requirements_ambiguity",
|
|
"userActionRequired": True,
|
|
"issues": ["Count and spacing conflict."],
|
|
"unresolved": [{"draftId": "draft_001", "reasonCode": "ambiguous_conflict", "question": question}],
|
|
})])
|
|
|
|
def test_requirements_review_limit_is_a_non_user_failure(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry()))
|
|
workflow = WorkflowCoordinator(WorkflowConfig(1, 1), repository, artifacts, runtime, None, None, requirements, actions)
|
|
task_id = "cad_123456abcdef"
|
|
initial = workflow.create_task(task_id, "Create a plate.")
|
|
issue = "The requested local position has no available deterministic verifier."
|
|
failed = transition(initial, "failed", error=ErrorCode.REQUIREMENTS_REVIEW_NOT_CONVERGED)
|
|
self.assertTrue(repository.compare_and_swap(failed, events=[{
|
|
"event": "requirements_review_limit_reached",
|
|
"code": ErrorCode.REQUIREMENTS_REVIEW_NOT_CONVERGED.value,
|
|
"message": "Requirements review did not converge within the configured author-revision limit.",
|
|
"findings": [{"description": issue}],
|
|
}]))
|
|
|
|
payload = workflow._projected_terminal(task_id, failed)
|
|
|
|
self.assertEqual(payload["lifecycle"], "failed")
|
|
self.assertFalse(payload["userActionRequired"])
|
|
self.assertEqual(payload["issues"], [issue])
|
|
self.assertEqual(payload["code"], ErrorCode.REQUIREMENTS_REVIEW_NOT_CONVERGED.value)
|
|
|
|
def test_waiting_review_limit_reply_resumes_the_same_task_with_clarification(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
configured = service_settings(root)
|
|
service = AgentService(configured, WorkspaceStore(configured), CdslLibrary(configured))
|
|
task_id = "cad_123456abcdef"
|
|
state = service.v3.workflow.create_task(task_id, "Create a flange.")
|
|
reviewing = transition(state, "requirements_finalized")
|
|
self.assertTrue(service.v3.repository.compare_and_swap(reviewing))
|
|
question = "Should the flange use four holes or six holes?"
|
|
review_path = service.v3.artifacts.write_requirements_review(task_id, {
|
|
"schema_version": "cad.requirements-review.v2",
|
|
"decision": "waiting_for_user",
|
|
"findings": [{"draft_id": "draft_001", "source_ids": ["src_001"], "finding_type": "ambiguous_conflict", "description": "Two hole counts are stated.", "question": question}],
|
|
}, invocation_id="inv_limit")
|
|
waiting = transition(reviewing, "waiting_for_user", error=ErrorCode.WAITING_FOR_USER, requirements_review_path=review_path)
|
|
self.assertTrue(service.v3.repository.compare_and_swap(waiting, events=[{"event": "requirements_waiting_for_user", "review_path": review_path, "questions": [question]}]))
|
|
conversation = service.store.ensure_conversation("conv_123456abcdef", task_id)
|
|
message = ChatMessage.model_validate({"id": "user_1", "role": "user", "parts": [{"type": "text", "text": "允许"}]})
|
|
|
|
async def fake_run(*, task_id: str, **_kwargs: object):
|
|
self.assertEqual(task_id, "cad_123456abcdef")
|
|
yield "task_terminal", {"taskId": task_id, "lifecycle": "waiting_for_user", "message": "Still needs review."}
|
|
|
|
service.v3.workflow.run = fake_run # type: ignore[method-assign]
|
|
|
|
async def collect() -> bytes:
|
|
return b"".join([chunk async for chunk in service.stream([message], conversation["conversation_id"], task_id)])
|
|
|
|
with patch("app.services.agent_service.verify_model_capability", AsyncMock(return_value={"supported": True})):
|
|
output = asyncio.run(collect()).decode("utf-8")
|
|
self.assertIn("已应用补充说明并恢复 CAD 任务", output)
|
|
self.assertNotIn("event: cad_error", output)
|
|
self.assertEqual([item.name for item in configured.task_root.iterdir()], [task_id])
|
|
resumed = service.v3.repository.get_state(task_id)
|
|
self.assertEqual(resumed.phase, TaskPhase.DRAFTING_REQUIREMENTS)
|
|
clarification = next(event for event in service.v3.repository.ledger_events(task_id) if event.get("event") == "user_clarification_received")
|
|
payload = service.v3.artifacts.read_json(task_id, clarification["clarification_path"])
|
|
self.assertEqual(payload["text"], "允许")
|
|
|
|
def test_service_retry_resume_checks_capabilities_and_preserves_source_phase(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
configured = service_settings(root)
|
|
service = AgentService(configured, WorkspaceStore(configured), CdslLibrary(configured))
|
|
task_id = "cad_123456abcdef"
|
|
state = service.v3.workflow.create_task(task_id, "Create a plate.")
|
|
reviewing = transition(state, "requirements_finalized")
|
|
self.assertTrue(service.v3.repository.compare_and_swap(reviewing))
|
|
waiting = transition(reviewing, "waiting_retry", error=ErrorCode.REVIEW_SERVICE_UNAVAILABLE)
|
|
self.assertTrue(service.v3.repository.compare_and_swap(waiting))
|
|
|
|
async def consume_without_provider_calls(*_args: object) -> None:
|
|
await asyncio.sleep(0)
|
|
|
|
service._consume_discarding = consume_without_provider_calls # type: ignore[method-assign]
|
|
capability = AsyncMock(return_value={"supported": True})
|
|
with patch("app.services.agent_service.verify_model_capability", capability):
|
|
projection = asyncio.run(service.resume_retry(task_id))
|
|
|
|
self.assertEqual(capability.await_count, 2)
|
|
self.assertEqual((projection or {}).get("phase"), TaskPhase.REVIEWING_REQUIREMENTS.value)
|
|
resumed = service.v3.repository.get_state(task_id)
|
|
self.assertEqual(resumed.phase, TaskPhase.REVIEWING_REQUIREMENTS)
|
|
self.assertIsNone(resumed.retry_from_phase)
|
|
|
|
def test_service_retry_resume_rejects_waiting_for_user_before_provider_preflight(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
configured = service_settings(root)
|
|
service = AgentService(configured, WorkspaceStore(configured), CdslLibrary(configured))
|
|
task_id = "cad_123456abcdef"
|
|
state = service.v3.workflow.create_task(task_id, "Create a plate.")
|
|
reviewing = transition(state, "requirements_finalized")
|
|
self.assertTrue(service.v3.repository.compare_and_swap(reviewing))
|
|
waiting = transition(reviewing, "waiting_for_user", error=ErrorCode.WAITING_FOR_USER)
|
|
self.assertTrue(service.v3.repository.compare_and_swap(waiting))
|
|
|
|
capability = AsyncMock(return_value={"supported": True})
|
|
with patch("app.services.agent_service.verify_model_capability", capability):
|
|
with self.assertRaisesRegex(ValueError, "Only a WAITING_RETRY"):
|
|
asyncio.run(service.resume_retry(task_id))
|
|
|
|
capability.assert_not_awaited()
|
|
|
|
def test_service_cancel_persists_terminal_state_and_preserves_checkpoint(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
configured = service_settings(root)
|
|
service = AgentService(configured, WorkspaceStore(configured), CdslLibrary(configured))
|
|
task_id = "cad_123456abcdef"
|
|
initial = service.v3.workflow.create_task(task_id, "Create a plate.")
|
|
checkpoint = transition(initial, "requirements_finalized")
|
|
self.assertTrue(service.v3.repository.compare_and_swap(checkpoint))
|
|
|
|
projection = asyncio.run(service.cancel(task_id))
|
|
|
|
state = service.v3.repository.get_state(task_id)
|
|
self.assertEqual(state.phase, TaskPhase.CANCELLED)
|
|
self.assertEqual(state.last_error, ErrorCode.CANCELLED)
|
|
self.assertEqual((projection or {}).get("lifecycle"), "cancelled")
|
|
self.assertTrue(any(event.get("event") == "task_cancelled" for event in service.v3.repository.ledger_events(task_id)))
|
|
self.assertEqual(service.v3.repository.running_task_ids(), [])
|
|
|
|
def test_cancelled_workflow_is_terminal_and_never_reclassified_as_internal_failure(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry()))
|
|
workflow = WorkflowCoordinator(WorkflowConfig(1, 1), repository, artifacts, runtime, None, None, requirements, actions)
|
|
task_id = "cad_123456abcdef"
|
|
initial = workflow.create_task(task_id, "Create a plate.")
|
|
cancelled = transition(initial, "cancelled", error=ErrorCode.CANCELLED)
|
|
self.assertTrue(repository.compare_and_swap(cancelled))
|
|
|
|
async def collect() -> list[tuple[str, dict[str, object]]]:
|
|
return [item async for item in workflow.run(task_id=task_id, author=None, reviewer=None)] # type: ignore[arg-type]
|
|
|
|
events = asyncio.run(collect())
|
|
self.assertEqual(events[-1][0], "task_terminal")
|
|
self.assertEqual(events[-1][1]["lifecycle"], "cancelled")
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.CANCELLED)
|
|
|
|
def test_cancelled_state_rejects_failure_and_retry_transitions(self) -> None:
|
|
cancelled = TaskState("cad_123456abcdef", TaskPhase.CANCELLED, 4)
|
|
for event in ("failed", "waiting_retry", "cancelled"):
|
|
with self.assertRaisesRegex(ValueError, "Illegal v3 transition"):
|
|
transition(cancelled, event, error=ErrorCode.CANCELLED)
|
|
|
|
def test_task_creation_freezes_document_attachment_as_a_stable_source_block(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
configured = service_settings(root)
|
|
service = AgentService(configured, WorkspaceStore(configured), CdslLibrary(configured))
|
|
conversation = service.store.ensure_conversation("conv_123456abcdef")
|
|
document = b"Outer diameter is 120 mm.\nCenter bore is through."
|
|
relative_path, _ = service.store.write_conversation_upload(conversation["conversation_id"], "flange.txt", document)
|
|
extracted_path = relative_path + ".txt"
|
|
service.store.conversation_attachment_path(conversation["conversation_id"], extracted_path).write_text(
|
|
document.decode("utf-8"), encoding="utf-8"
|
|
)
|
|
attachment = {
|
|
"id": "upload_flange", "conversation_id": conversation["conversation_id"], "name": "flange.txt",
|
|
"kind": "document", "path": relative_path, "mime": "text/plain", "size": len(document),
|
|
"sha256": sha256(document).hexdigest(), "extracted_path": extracted_path,
|
|
}
|
|
conversation = service.store.add_conversation_attachment(conversation["conversation_id"], attachment)
|
|
|
|
task_id = "cad_123456abcdef"
|
|
blocks = service._task_source_blocks(conversation, "Create a flange.\n\nUse the attached dimensions.")
|
|
service.v3.workflow.create_task(task_id, "Create a flange.\n\nUse the attached dimensions.", source_blocks=blocks)
|
|
|
|
source_index = service.v3.artifacts.read_json(task_id, "documents/source-index.json") or {}
|
|
self.assertEqual(source_index["sources"]["src_001"], "Create a flange.")
|
|
self.assertEqual(source_index["sources"]["src_002"], "Use the attached dimensions.")
|
|
self.assertEqual(source_index["sources"]["src_003"], document.decode("utf-8"))
|
|
self.assertEqual(source_index["attachment_blocks"]["src_003"]["sha256"], sha256(document).hexdigest())
|
|
self.assertIn(document.decode("utf-8"), service.v3.artifacts.read_source_requirements(task_id))
|
|
|
|
service.store.conversation_attachment_path(conversation["conversation_id"], extracted_path).write_text(
|
|
"changed after task creation", encoding="utf-8"
|
|
)
|
|
self.assertEqual(
|
|
(service.v3.artifacts.read_json(task_id, "documents/source-index.json") or {})["sources"]["src_003"],
|
|
document.decode("utf-8"),
|
|
)
|
|
|
|
def test_startup_resume_requires_author_and_reviewer_capabilities(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
configured = service_settings(root)
|
|
service = AgentService(configured, WorkspaceStore(configured), CdslLibrary(configured))
|
|
service.v3.workflow.create_task("cad_123456abcdef", "Create a plate.")
|
|
capability = AsyncMock(side_effect=[{"supported": True}, {"supported": False}])
|
|
with patch("app.services.agent_service.verify_model_capability", capability):
|
|
asyncio.run(service.resume_running_tasks())
|
|
|
|
self.assertEqual(capability.await_count, 2)
|
|
self.assertEqual(service._autonomous_runs, {})
|
|
state = service.v3.repository.get_state("cad_123456abcdef")
|
|
self.assertEqual(state.phase, TaskPhase.FAILED)
|
|
self.assertEqual(state.last_error, ErrorCode.MODEL_STRUCTURED_OUTPUT_UNSUPPORTED)
|
|
|
|
def test_domain_and_application_do_not_import_adapter_or_io_implementations(self) -> None:
|
|
package = ROOT / "backend" / "app" / "cad_agent"
|
|
forbidden_modules = {
|
|
"sqlite3", "pathlib", "os", "shutil",
|
|
"app.cad_agent.adapters", "app.services.engine_service", "app.services.review_renderer",
|
|
}
|
|
for layer in ("domain", "application"):
|
|
for path in (package / layer).glob("*.py"):
|
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
imported: set[str] = set()
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
imported.update(alias.name for alias in node.names)
|
|
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
imported.add(node.module)
|
|
for module in imported:
|
|
self.assertFalse(
|
|
any(module == forbidden or module.startswith(forbidden + ".") for forbidden in forbidden_modules),
|
|
f"{path.relative_to(ROOT)} imports forbidden implementation module {module}",
|
|
)
|
|
|
|
def test_dynamic_requirements_schema_rejects_unknown_claim_fields(self) -> None:
|
|
schema = requirements_draft_schema(default_registry().expected_one_of_schema(), ["src_001"])
|
|
error = canonical_validate_schema(json.dumps({
|
|
"items": [{
|
|
"source_ids": ["src_001"], "statement": "One solid", "assumptions": [],
|
|
"acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1, "unexpected": True}}],
|
|
}],
|
|
}), schema)
|
|
self.assertIsNotNone(error)
|
|
self.assertEqual(error.code if error else None, ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
|
|
def test_requirement_claims_reject_vacuous_geometry_tolerances(self) -> None:
|
|
registry = default_registry()
|
|
bbox_errors = registry.validate_expected(
|
|
"bbox_dimension_mm",
|
|
{"axis": "z", "value": 10, "tolerance_mm": 1.01},
|
|
)
|
|
bore_errors = registry.validate_expected(
|
|
"through_cylindrical_bore",
|
|
{"diameter_mm": 12, "count": 3, "tolerance_mm": 5},
|
|
)
|
|
coaxial_errors = registry.validate_expected(
|
|
"coaxial",
|
|
{"record_ids": ["topology_a", "topology_b"], "tolerance": 0.1},
|
|
)
|
|
self.assertTrue(any(error["path"] == "/tolerance_mm" for error in bbox_errors))
|
|
self.assertTrue(any(error["path"] == "/tolerance_mm" for error in bore_errors))
|
|
self.assertTrue(any(error["path"] == "/tolerance" for error in coaxial_errors))
|
|
|
|
def test_dynamic_requirements_schema_rejects_unknown_source_before_write(self) -> None:
|
|
schema = requirements_draft_schema(default_registry().expected_one_of_schema(), ["src_001"])
|
|
error = canonical_validate_schema(json.dumps({
|
|
"items": [{
|
|
"source_ids": ["src_unknown"], "statement": "One solid", "assumptions": [],
|
|
"acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}],
|
|
}],
|
|
}), schema)
|
|
self.assertEqual(error.code if error else None, ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
|
|
def test_provider_conformance_rejects_tool_arguments_that_violate_schema(self) -> None:
|
|
gateway = StructuredModelGateway(settings(Path(tempfile.gettempdir())))
|
|
|
|
async def invalid_response(**_kwargs: object) -> dict[str, object]:
|
|
return {
|
|
"tool_calls": [{"function": {"name": "probe", "arguments": "{}"}}],
|
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
|
|
}
|
|
|
|
gateway.call_tool = invalid_response # type: ignore[method-assign]
|
|
report = asyncio.run(gateway.conformance(
|
|
provider_id="test",
|
|
model_id="test-model",
|
|
tools=[{"type": "function", "function": {"name": "probe", "parameters": {"type": "object", "properties": {"value": {"type": "integer"}}, "required": ["value"], "additionalProperties": False}}}],
|
|
))
|
|
self.assertFalse(report["supported"])
|
|
self.assertIn("violate schema", report["failures"][0]["message"])
|
|
self.assertFalse(report["probe_unavailable"])
|
|
|
|
def test_provider_conformance_stops_after_transport_failure(self) -> None:
|
|
gateway = StructuredModelGateway(settings(Path(tempfile.gettempdir())))
|
|
calls = 0
|
|
|
|
async def unavailable(**_kwargs: object) -> dict[str, object]:
|
|
nonlocal calls
|
|
calls += 1
|
|
raise StructuredTransportError("Provider transport unavailable after 3 attempts")
|
|
|
|
gateway.call_tool = unavailable # type: ignore[method-assign]
|
|
report = asyncio.run(gateway.conformance(
|
|
provider_id="test",
|
|
model_id="test-model",
|
|
tools=[
|
|
{"type": "function", "function": {"name": "probe_one", "parameters": {"type": "object"}}},
|
|
{"type": "function", "function": {"name": "probe_two", "parameters": {"type": "object"}}},
|
|
],
|
|
))
|
|
|
|
self.assertEqual(calls, 1)
|
|
self.assertFalse(report["supported"])
|
|
self.assertTrue(report["probe_unavailable"])
|
|
self.assertEqual([failure["tool"] for failure in report["failures"]], ["probe_one"])
|
|
|
|
def test_live_eval_main_persists_unexpected_preflight_failure(self) -> None:
|
|
"""A failed conformance preflight must still produce a release-gate report."""
|
|
import app.cad_agent.evals.live as live
|
|
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
|
|
async def unexpected(*_args: object, **_kwargs: object) -> dict[str, object]:
|
|
raise RuntimeError("conformance bootstrap broke")
|
|
|
|
arguments = type("Arguments", (), {
|
|
"suite": "smoke", "require_live": True, "allow_skip": False,
|
|
})()
|
|
with patch.object(live, "BACKEND_ROOT", root), patch.object(live, "_arguments", return_value=arguments), patch.object(live, "_run", unexpected):
|
|
self.assertEqual(live.main(), 2)
|
|
|
|
reports = list((root / "live-evals").glob("*/report.json"))
|
|
self.assertEqual(len(reports), 1)
|
|
report = json.loads(reports[0].read_text(encoding="utf-8"))
|
|
self.assertEqual(report["status"], "LIVE_EVAL_BLOCKED")
|
|
self.assertIn("RuntimeError: conformance bootstrap broke", report["error"])
|
|
self.assertEqual(report["failure_layer"], "configuration_or_network")
|
|
|
|
def test_provider_protocol_error_is_not_reclassified_as_author_format_error(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
|
|
class ProviderRejectedGateway:
|
|
async def call_tool(self, **_kwargs: object) -> dict[str, object]:
|
|
raise StructuredModelError("Provider rejected structured request (400)")
|
|
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=1, format_error_limit=1), repository, artifacts, runtime,
|
|
ProviderRejectedGateway(), None, RequirementsCommandHandler(repository, artifacts, default_registry()),
|
|
ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry())),
|
|
)
|
|
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
author = type("Model", (), {"provider_id": "test", "model_id": "test-model"})()
|
|
return [item async for item in workflow.run(task_id=task_id, author=author, reviewer=None)]
|
|
|
|
events = asyncio.run(run())
|
|
state = repository.get_state(task_id)
|
|
self.assertEqual(state.phase, TaskPhase.WAITING_RETRY)
|
|
self.assertEqual(state.last_error, ErrorCode.AUTHOR_TRANSPORT_UNAVAILABLE)
|
|
self.assertEqual(events[-1][1]["code"], ErrorCode.AUTHOR_TRANSPORT_UNAVAILABLE.value)
|
|
|
|
def test_runtime_contract_error_fails_once_without_author_format_retry(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry()))
|
|
workflow = WorkflowCoordinator(WorkflowConfig(max_turns=4, format_error_limit=4), repository, artifacts, runtime, None, None, requirements, actions)
|
|
|
|
terminal = workflow._model_rejection_or_service_failure(
|
|
task_id,
|
|
repository.get_state(task_id), # type: ignore[arg-type]
|
|
"propose_next_action",
|
|
WorkflowError(ErrorCode.RUNTIME_CONTRACT_INVALID, "profile registry is invalid"),
|
|
{},
|
|
[],
|
|
)
|
|
|
|
self.assertIsNotNone(terminal)
|
|
self.assertEqual(terminal[1]["code"], ErrorCode.RUNTIME_CONTRACT_INVALID.value) # type: ignore[index]
|
|
state = repository.get_state(task_id)
|
|
self.assertEqual(state.phase, TaskPhase.FAILED)
|
|
self.assertEqual(state.last_error, ErrorCode.RUNTIME_CONTRACT_INVALID)
|
|
self.assertFalse(any(event.get("event") == "failed_author_format" for event in repository.ledger_events(task_id)))
|
|
|
|
def test_runtime_precondition_rejection_returns_to_action_selection_without_format_failure(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=4, format_error_limit=1), repository, artifacts, actions.runtime,
|
|
None, None, RequirementsCommandHandler(repository, artifacts, default_registry()), actions,
|
|
)
|
|
state = repository.get_state(task_id)
|
|
self.assertEqual(state.phase, TaskPhase.ACTION_PENDING)
|
|
feedback: list[dict[str, object]] = []
|
|
terminal = workflow._model_rejection_or_service_failure(
|
|
task_id,
|
|
state,
|
|
"submit_cdsl_fragment",
|
|
WorkflowError(
|
|
ErrorCode.RUNTIME_PRECONDITION_FAILED,
|
|
"cut depth 12 mm must exceed measured host-body thickness 12 mm",
|
|
),
|
|
{},
|
|
feedback,
|
|
)
|
|
after = repository.get_state(task_id)
|
|
self.assertIsNone(terminal)
|
|
self.assertEqual(after.phase, TaskPhase.AWAITING_ACTION)
|
|
self.assertEqual(after.active_revision, state.active_revision)
|
|
self.assertIsNone(after.pending_action)
|
|
self.assertEqual(after.last_error, ErrorCode.RUNTIME_PRECONDITION_FAILED)
|
|
self.assertIn("must exceed measured host-body thickness", str(feedback[0]["content"]))
|
|
events = repository.ledger_events(task_id)
|
|
self.assertTrue(any(event.get("event") == "runtime_precondition_rejected" for event in events))
|
|
self.assertFalse(any(event.get("event") == "failed_author_format" for event in events))
|
|
|
|
def test_repeated_exact_fragment_is_rejected_without_creating_a_stage(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=8, format_error_limit=2), repository, artifacts, actions.runtime,
|
|
None, None, RequirementsCommandHandler(repository, artifacts, default_registry()), actions,
|
|
)
|
|
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": "arc", "start": [1, 0], "end": [0, 1], "center": [0, 0], "radius_mm": 2}],
|
|
}]},
|
|
},
|
|
"feature": {"atomic_id": "extrude_add_blind", "params": {"distance_mm": 5}},
|
|
}
|
|
first_state = repository.get_state(task_id)
|
|
first = actions.submit_cdsl_fragment(task_id, fragment, invocation_id="fragment_1")
|
|
self.assertIsInstance(first, Rejected)
|
|
self.assertEqual(first.error.code, ErrorCode.RUNTIME_PRECONDITION_FAILED)
|
|
self.assertIsNone(workflow._model_rejection_or_service_failure(
|
|
task_id, first_state, "submit_cdsl_fragment", first.error, {}, [],
|
|
))
|
|
selection_state = repository.get_state(task_id)
|
|
proposal = NextAction(
|
|
working_head=selection_state.working_head, intent="Try a corrected base", requirement_ids=["req_001"],
|
|
atomic_id="extrude_add_blind", expected_change="Create the base solid",
|
|
)
|
|
self.assertIsInstance(actions.propose_next_action(task_id, proposal, invocation_id="proposal_2"), Accepted)
|
|
|
|
with patch.object(artifacts, "start_candidate_stage") as start_stage:
|
|
duplicate = actions.submit_cdsl_fragment(task_id, fragment, invocation_id="fragment_2")
|
|
|
|
self.assertIsInstance(duplicate, Rejected)
|
|
self.assertTrue(duplicate.error.details["duplicate_fragment"])
|
|
start_stage.assert_not_called()
|
|
|
|
def test_third_failure_class_terminates_with_no_progress_limit(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=8, format_error_limit=2), repository, artifacts, actions.runtime,
|
|
None, None, RequirementsCommandHandler(repository, artifacts, default_registry()), actions,
|
|
)
|
|
terminal = None
|
|
for attempt in range(3):
|
|
state = repository.get_state(task_id)
|
|
error = WorkflowError(ErrorCode.RUNTIME_PRECONDITION_FAILED, "profile is degenerate", details={
|
|
"active_revision": state.active_revision, "atomic_id": "extrude_add_blind",
|
|
"fragment_hash": f"fragment-{attempt}", "failure_exact_fingerprint": f"exact-{attempt}",
|
|
"normalized_error_code": ErrorCode.RUNTIME_PRECONDITION_FAILED.value,
|
|
})
|
|
terminal = workflow._model_rejection_or_service_failure(
|
|
task_id, state, "submit_cdsl_fragment", error, {}, [],
|
|
)
|
|
if attempt < 2:
|
|
self.assertIsNone(terminal)
|
|
selection = repository.get_state(task_id)
|
|
proposal = NextAction(
|
|
working_head=selection.working_head, intent=f"Alternative {attempt}", requirement_ids=["req_001"],
|
|
atomic_id="extrude_add_blind", expected_change="Create the base solid",
|
|
)
|
|
self.assertIsInstance(actions.propose_next_action(task_id, proposal, invocation_id=f"proposal_{attempt}"), Accepted)
|
|
|
|
self.assertEqual(terminal[1]["code"], ErrorCode.NO_PROGRESS_LIMIT.value)
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.FAILED)
|
|
|
|
def test_author_call_budget_fails_without_calling_model_and_preserves_checkpoint(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, action = self._ready_action(Path(temporary))
|
|
|
|
class UnexpectedAuthor:
|
|
async def call_tool(self, **_kwargs: object) -> dict[str, object]:
|
|
raise AssertionError("the author budget must be checked before a provider call")
|
|
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=8, format_error_limit=1, max_author_turns=0),
|
|
repository,
|
|
artifacts,
|
|
actions.runtime,
|
|
UnexpectedAuthor(),
|
|
None,
|
|
RequirementsCommandHandler(repository, artifacts, default_registry()),
|
|
actions,
|
|
)
|
|
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
identity = ModelIdentity("test", "test-model")
|
|
return [item async for item in workflow.run(task_id=task_id, author=identity, reviewer=identity)]
|
|
|
|
events = asyncio.run(run())
|
|
after = repository.get_state(task_id)
|
|
self.assertEqual(after.phase, TaskPhase.FAILED)
|
|
self.assertEqual(after.last_error, ErrorCode.CALL_BUDGET_EXHAUSTED)
|
|
self.assertIsNone(after.pending_action)
|
|
self.assertEqual(repository.usage_summary(task_id)["calls"], 0)
|
|
self.assertEqual(events[-1][1]["code"], ErrorCode.CALL_BUDGET_EXHAUSTED.value)
|
|
self.assertFalse(events[-1][1]["userActionRequired"])
|
|
self.assertFalse(any(name in {"action_selection", "candidate_result"} for name, _payload in events))
|
|
budget_event = next(event for event in repository.ledger_events(task_id) if event.get("event") == "call_budget_exhausted")
|
|
self.assertEqual(budget_event["next_actor"], "author")
|
|
self.assertEqual(budget_event["author_calls"], 0)
|
|
|
|
def test_reviewer_call_budget_fails_and_preserves_candidate_stage(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
stage = artifacts.start_candidate_stage(task_id, "budget-stage-key", {
|
|
"candidate_id": "candidate_123456789012",
|
|
"action_id": action.action_id,
|
|
})
|
|
artifacts.write_stage_json(task_id, stage.stage_id, "candidate.json", {
|
|
"candidate_id": "candidate_123456789012",
|
|
"stage_id": stage.stage_id,
|
|
"action_id": action.action_id,
|
|
"claim_results": [{
|
|
"claim_id": "claim_001",
|
|
"claim_kind": "solid_count_equals",
|
|
"deterministic": True,
|
|
"status": "pass",
|
|
}],
|
|
})
|
|
building = transition(
|
|
state,
|
|
"candidate_started",
|
|
candidate_id="candidate_123456789012",
|
|
candidate_stage_id=stage.stage_id,
|
|
)
|
|
reviewing = transition(
|
|
building,
|
|
"candidate_built",
|
|
candidate_id="candidate_123456789012",
|
|
candidate_stage_id=stage.stage_id,
|
|
)
|
|
self.assertTrue(repository.compare_and_swap(building))
|
|
self.assertTrue(repository.compare_and_swap(reviewing))
|
|
|
|
class UnexpectedReviewer:
|
|
async def review(self, **_kwargs: object) -> dict[str, object]:
|
|
raise AssertionError("the reviewer budget must be checked before a provider call")
|
|
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=8, format_error_limit=1, max_reviewer_turns=0),
|
|
repository,
|
|
artifacts,
|
|
actions.runtime,
|
|
None,
|
|
UnexpectedReviewer(),
|
|
RequirementsCommandHandler(repository, artifacts, default_registry()),
|
|
actions,
|
|
)
|
|
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
identity = ModelIdentity("test", "test-model")
|
|
return [item async for item in workflow.run(task_id=task_id, author=identity, reviewer=identity)]
|
|
|
|
events = asyncio.run(run())
|
|
after = repository.get_state(task_id)
|
|
self.assertEqual(after.phase, TaskPhase.FAILED)
|
|
self.assertEqual(after.last_error, ErrorCode.CALL_BUDGET_EXHAUSTED)
|
|
self.assertEqual(after.candidate_stage_id, "")
|
|
self.assertTrue(Path(stage.output_dir).is_dir())
|
|
self.assertEqual(repository.usage_summary(task_id)["calls"], 0)
|
|
self.assertEqual(events[-1][1]["code"], ErrorCode.CALL_BUDGET_EXHAUSTED.value)
|
|
self.assertFalse(events[-1][1]["userActionRequired"])
|
|
budget_event = next(event for event in repository.ledger_events(task_id) if event.get("event") == "call_budget_exhausted")
|
|
self.assertEqual(budget_event["next_actor"], "reviewer")
|
|
self.assertEqual(budget_event["reviewer_calls"], 0)
|
|
|
|
def test_unexpected_author_gateway_error_fails_once_as_internal(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
|
|
class BrokenGateway:
|
|
async def call_tool(self, **_kwargs: object) -> dict[str, object]:
|
|
raise RuntimeError("unexpected adapter bug")
|
|
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=4, format_error_limit=4), repository, artifacts, runtime,
|
|
BrokenGateway(), None, RequirementsCommandHandler(repository, artifacts, default_registry()),
|
|
ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry())),
|
|
)
|
|
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
author = type("Model", (), {"provider_id": "test", "model_id": "test-model"})()
|
|
return [item async for item in workflow.run(task_id=task_id, author=author, reviewer=None)]
|
|
|
|
events = asyncio.run(run())
|
|
state = repository.get_state(task_id)
|
|
self.assertEqual(state.phase, TaskPhase.FAILED)
|
|
self.assertEqual(state.last_error, ErrorCode.FAILED_INTERNAL)
|
|
self.assertEqual(events[-1][1]["code"], ErrorCode.FAILED_INTERNAL.value)
|
|
self.assertFalse(any(event.get("event") == "failed_author_format" for event in repository.ledger_events(task_id)))
|
|
|
|
def test_responses_gateway_normalizes_input_tools_and_function_output(self) -> None:
|
|
provider = ProviderConfig("openai", "OpenAI", "https://example.invalid/v1", "test-key", (ProviderModel("test-model", vision=True),), api_style="responses")
|
|
gateway = StructuredModelGateway(settings(Path(tempfile.gettempdir())))
|
|
payload = gateway._payload(
|
|
provider,
|
|
"test-model",
|
|
[{"role": "system", "content": "Follow the contract."}, {"role": "user", "content": [{"type": "text", "text": "Review this."}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,AA=="}}]}],
|
|
[{"type": "function", "function": {"name": "submit", "description": "Submit", "parameters": {"type": "object", "additionalProperties": False}}}],
|
|
"submit",
|
|
)
|
|
self.assertEqual(payload["input"][0]["role"], "developer")
|
|
self.assertEqual(payload["input"][1]["content"][1]["type"], "input_image")
|
|
self.assertEqual(payload["tools"][0]["name"], "submit")
|
|
self.assertNotIn("strict", payload["tools"][0])
|
|
self.assertEqual(payload["tool_choice"], {"type": "function", "name": "submit"})
|
|
response = gateway._normalized_response(provider, {"output": [{"type": "function_call", "name": "submit", "arguments": "{}"}], "usage": {"input_tokens": 7, "output_tokens": 3}})
|
|
self.assertEqual(response["tool_calls"], [{"function": {"name": "submit", "arguments": "{}"}}])
|
|
self.assertEqual(response["usage"]["total_tokens"], 10)
|
|
|
|
def test_capability_probe_exposes_required_selector_as_author_input(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
parameters = fragment_schema(runtime.operation_contract("hole_counterbore"), selector_tokens=["sel_conformance"])
|
|
gateway = StructuredModelGateway(settings(Path(tempfile.gettempdir())))
|
|
messages = gateway._conformance_messages({
|
|
"type": "function",
|
|
"function": {"name": "conformance_hole_counterbore", "parameters": parameters},
|
|
})
|
|
instruction = str(messages[0]["content"])
|
|
selector = parameters["properties"]["feature"]["properties"]["selector_tokens"]
|
|
self.assertIn("Required author input", str(selector["description"]))
|
|
self.assertIn("sel_conformance", instruction)
|
|
self.assertIn('"selector_tokens":["sel_conformance"]', instruction)
|
|
self.assertIn("must not be moved into params", instruction)
|
|
|
|
def test_capability_probe_names_required_candidate_review_decision(self) -> None:
|
|
gateway = StructuredModelGateway(settings(Path(tempfile.gettempdir())))
|
|
messages = gateway._conformance_messages({
|
|
"type": "function",
|
|
"function": {
|
|
"name": "review_candidate",
|
|
"parameters": candidate_review_schema("candidate_conformance", "cad_conformance:root:v1", ["claim_001"]),
|
|
},
|
|
})
|
|
instruction = str(messages[0]["content"])
|
|
self.assertIn("provide a verdict and claim_coverage", instruction)
|
|
self.assertIn("every required top-level and nested field", instruction)
|
|
|
|
def test_model_gateway_rejects_any_unnamed_or_multi_tool_request(self) -> None:
|
|
gateway = StructuredModelGateway(settings(Path(tempfile.gettempdir())))
|
|
provider = ProviderConfig("openai", "OpenAI", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),))
|
|
tool = {"type": "function", "function": {"name": "submit", "parameters": {"type": "object", "additionalProperties": False}}}
|
|
with self.assertRaisesRegex(StructuredModelError, "exactly one named tool"):
|
|
gateway._payload(provider, "test-model", [], [tool], "")
|
|
with self.assertRaisesRegex(StructuredModelError, "exactly one named tool"):
|
|
gateway._payload(provider, "test-model", [], [tool, tool], "submit")
|
|
|
|
def test_provider_conformance_covers_fixed_and_representative_dynamic_contracts(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
tools = conformance_tools(runtime)
|
|
names = {str((tool.get("function") or {}).get("name") or "") for tool in tools}
|
|
self.assertEqual(
|
|
{f"conformance_{atomic_id}" for atomic_id in runtime.supported_atomic_ids()},
|
|
{name for name in names if name.startswith("conformance_")},
|
|
)
|
|
self.assertTrue({
|
|
"submit_requirements_draft_batch", "patch_requirements_draft", "finalize_requirements_draft",
|
|
"review_requirements", "propose_next_action", "inspect_topology",
|
|
"get_cdsl_operation_contract", "review_candidate", "complete_task", "review_final",
|
|
"record_geometry_conclusion", "rollback_checkpoint",
|
|
}.issubset(names))
|
|
|
|
def test_live_fixture_can_select_one_stable_comprehensive_scenario(self) -> None:
|
|
selected = _fixture("comprehensive", "circular_flange_pcd")
|
|
self.assertEqual([item["id"] for item in selected], ["circular_flange_pcd"])
|
|
with self.assertRaisesRegex(ValueError, "Unknown scenario"):
|
|
_fixture("comprehensive", "unknown_scenario")
|
|
|
|
def test_comprehensive_fixture_is_bound_to_the_target_document_hash(self) -> None:
|
|
fixture_path = ROOT / "backend" / "app" / "cad_agent" / "evals" / "fixtures" / "comprehensive.json"
|
|
fixture = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
source = ROOT / str(fixture["source_document"])
|
|
self.assertEqual(fixture["source_document_sha256"], sha256(source.read_bytes()).hexdigest())
|
|
|
|
def test_live_capability_probe_separates_transport_failure_from_schema_unsupported(self) -> None:
|
|
self.assertEqual(
|
|
_capability_block_reason({"failures": [{"message": "Provider transport unavailable after 3 attempts"}]}),
|
|
("MODEL_CAPABILITY_PROBE_UNAVAILABLE", "configuration_or_network"),
|
|
)
|
|
self.assertEqual(
|
|
_capability_block_reason({"failures": [{"message": "Provider arguments violate schema: required property"}]}),
|
|
("MODEL_STRUCTURED_OUTPUT_UNSUPPORTED", "model_format_or_decision"),
|
|
)
|
|
|
|
def test_live_failure_attribution_covers_contract_engine_visual_and_timeout_layers(self) -> None:
|
|
def attribution(*, outcome: str, checks: dict[str, bool], code: str = "") -> dict[str, object] | None:
|
|
events = [{"payload": {"code": code}}] if code else []
|
|
return _failure_attribution(checks=checks, outcome=outcome, events=events, projection={})
|
|
|
|
self.assertEqual(
|
|
attribution(outcome="failed", checks={"expected_terminal_phase": False}, code="RUNTIME_CONTRACT_INVALID")["layer"],
|
|
"v3_contract_or_verifier",
|
|
)
|
|
self.assertEqual(
|
|
attribution(outcome="failed", checks={"expected_terminal_phase": False}, code="CANDIDATE_BUILD_FAILED")["layer"],
|
|
"engine_execution",
|
|
)
|
|
self.assertEqual(
|
|
attribution(outcome="failed", checks={"expected_terminal_phase": False}, code="CANDIDATE_REVIEW_REJECTED")["layer"],
|
|
"independent_visual_review",
|
|
)
|
|
self.assertEqual(
|
|
attribution(outcome="failed", checks={"expected_terminal_phase": False}, code="LIVE_EVAL_TIMEOUT")["layer"],
|
|
"configuration_or_network",
|
|
)
|
|
self.assertEqual(
|
|
attribution(outcome="validation_capability_gap", checks={"acceptance_contract_coverage": False})["reason_code"],
|
|
"VALIDATION_CAPABILITY_GAP",
|
|
)
|
|
self.assertIsNone(_failure_attribution(checks={"expected_terminal_phase": True}, outcome="passed", events=[], projection={}))
|
|
|
|
def test_release_gate_checks_audits_claims_and_immutable_manifest(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
revision = root / "revisions" / "rev_001"
|
|
render_dir = revision / "renders"
|
|
render_dir.mkdir(parents=True)
|
|
evidence = {
|
|
"candidate.json": b"{}",
|
|
"candidate-review.json": b"{}",
|
|
"model.cdsl.json": b"{}",
|
|
"model.step": b"step",
|
|
"model.glb": b"glb",
|
|
"model.topology.json": b"{}",
|
|
"rebuild-report.json": b"{}",
|
|
"renders/render-manifest.json": b"{}",
|
|
"renders/contact-sheet.jpg": b"jpeg",
|
|
}
|
|
for relative, contents in evidence.items():
|
|
path = revision / relative
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(contents)
|
|
(revision / "manifest.json").write_text(json.dumps({
|
|
"files": {relative: sha256(contents).hexdigest() for relative, contents in evidence.items()},
|
|
}), encoding="utf-8")
|
|
(root / "requirements-contract.json").write_text("{}", encoding="utf-8")
|
|
final_review = root / "reviews" / "final" / "rev_001" / "final-review.json"
|
|
final_review.parent.mkdir(parents=True)
|
|
final_review.write_text("{}", encoding="utf-8")
|
|
scenario = {
|
|
"expected_phase": "COMPLETED", "max_total_tokens": 10, "max_total_calls": 2,
|
|
"max_author_turns": 1, "max_reviewer_turns": 1,
|
|
"required_claim_kinds": ["solid_count_equals"], "required_atomic_ids": ["extrude_add_blind"],
|
|
}
|
|
digest = sha256(b"arguments").hexdigest()
|
|
(root / "actions").mkdir()
|
|
(root / "actions" / "action-ledger.jsonl").write_text('{"sequence":1}\n', encoding="utf-8")
|
|
ledger = [
|
|
{"event": "accepted", "revision_id": "rev_001", "actual_atomic_id": "extrude_add_blind"},
|
|
{"event": "completed", "claim_results": [{"claim_id": "claim_001", "deterministic": True, "status": "pass"}]},
|
|
]
|
|
projection = {"phase": "COMPLETED", "active_revision": "rev_001", "action_ledger_summary": ledger}
|
|
usage = {"prompt_tokens": 3, "completion_tokens": 2, "records": [{"raw_arguments_hash": digest}, {"role": "reviewer", "raw_arguments_hash": digest}]}
|
|
contract = {"requirements": [{"acceptance_claims": [{"claim_kind": "solid_count_equals"}]}]}
|
|
self.assertTrue(all(_run_checks(scenario, projection, usage, contract, [], root, ledger).values()))
|
|
semantic_alternative = {
|
|
**scenario,
|
|
"required_atomic_ids": [],
|
|
"required_any_atomic_id_groups": [["extrude_add_blind", "revolve_add"]],
|
|
}
|
|
alternative_ledger = [
|
|
{"event": "accepted", "revision_id": "rev_001", "actual_atomic_id": "revolve_add"},
|
|
{"event": "completed", "claim_results": [{"claim_id": "claim_001", "deterministic": True, "status": "pass"}]},
|
|
]
|
|
alternative_checks = _run_checks(semantic_alternative, projection, usage, contract, [], root, alternative_ledger)
|
|
self.assertTrue(alternative_checks["required_operations"])
|
|
self.assertTrue(alternative_checks["required_operation_alternative"])
|
|
audits = [{
|
|
"raw_arguments_hash": digest,
|
|
"canonical_schema_valid": True,
|
|
"single_allowed_call": True,
|
|
"state_binding": {"phase": "AWAITING_ACTION", "binding_valid": True},
|
|
}]
|
|
self.assertTrue(_run_checks(scenario, projection, usage, contract, [], root, ledger, audits)["raw_argument_audit"])
|
|
audits[0]["state_binding"]["binding_valid"] = False
|
|
self.assertFalse(_run_checks(scenario, projection, usage, contract, [], root, ledger, audits)["raw_argument_audit"])
|
|
self.assertFalse(_run_checks(scenario, projection, usage, contract, [{"payload": {"result": {"code": "AUTHOR_FORMAT_INVALID"}}}], root, ledger)["no_schema_or_decision_rejections"])
|
|
(revision / "model.glb").unlink()
|
|
self.assertFalse(_run_checks(scenario, projection, usage, contract, [], root, ledger)["required_artifact_evidence"])
|
|
|
|
def test_comprehensive_prompt_fixture_covers_all_document_cases_and_declares_gaps(self) -> None:
|
|
scenarios = _fixture("comprehensive")
|
|
supported_claim_kinds = set(default_registry().claim_kinds)
|
|
supported_atomic_ids = set(ProfileCadRuntime(settings(Path(tempfile.gettempdir()))).supported_atomic_ids())
|
|
source_document = ROOT / "docs" / "cad-agent-v3-comprehensive-prompt-test-target.md"
|
|
document_requests = [
|
|
columns[3].strip()
|
|
for line in source_document.read_text(encoding="utf-8").splitlines()
|
|
if line.startswith("| ")
|
|
for columns in [line.split("|")]
|
|
if len(columns) >= 5 and columns[1].strip().isdigit()
|
|
]
|
|
self.assertEqual(len(scenarios), 20)
|
|
self.assertEqual([str(scenario["request"]) for scenario in scenarios], document_requests)
|
|
self.assertEqual(
|
|
[scenario["id"] for scenario in scenarios],
|
|
[
|
|
"rectangular_mounting_plate", "circular_flange_pcd", "square_flange", "counterbored_mounting_plate",
|
|
"countersunk_cover_plate", "obround_slot_plate", "t_slot_test_block", "rounded_rectangular_pocket",
|
|
"two_level_pocket_plate", "cross_drilled_valve_block", "double_hole_linkage_arm", "three_hole_linkage",
|
|
"l_bracket", "ribbed_l_bracket", "u_bearing_support", "double_lug_mount", "stepped_shaft",
|
|
"keyed_stepped_shaft", "flanged_sleeve", "chamfered_bushing",
|
|
],
|
|
)
|
|
for scenario in scenarios:
|
|
self.assertEqual(scenario["units"], "mm")
|
|
self.assertEqual(scenario["expected_phase"], "COMPLETED")
|
|
self.assertTrue(scenario["request"])
|
|
self.assertTrue(scenario["required_claim_kinds"])
|
|
self.assertTrue(scenario["required_claims"])
|
|
self.assertGreaterEqual(
|
|
scenario["max_total_calls"],
|
|
scenario["max_author_turns"] + scenario["max_reviewer_turns"],
|
|
)
|
|
self.assertGreater(scenario["max_wall_seconds"], 0)
|
|
self.assertGreater(scenario["max_total_tokens"], 0)
|
|
self.assertTrue(set(scenario["required_claim_kinds"]).issubset(supported_claim_kinds))
|
|
self.assertTrue({claim["claim_kind"] for claim in scenario["required_claims"]}.issubset(supported_claim_kinds))
|
|
required_operations = {
|
|
str(atomic_id)
|
|
for atomic_id in [
|
|
*(scenario.get("required_atomic_ids") or ()),
|
|
*(scenario.get("required_any_atomic_ids") or ()),
|
|
*(
|
|
atomic_id
|
|
for group in scenario.get("required_any_atomic_id_groups") or ()
|
|
if isinstance(group, list)
|
|
for atomic_id in group
|
|
),
|
|
]
|
|
}
|
|
self.assertTrue(required_operations.issubset(supported_atomic_ids))
|
|
for group in scenario.get("required_any_atomic_id_groups") or ():
|
|
self.assertIsInstance(group, list)
|
|
self.assertTrue(group)
|
|
gap_ids = [str(gap.get("id") or "") for gap in scenario["validation_capability_gaps"]]
|
|
self.assertEqual(len(gap_ids), len(set(gap_ids)), f"{scenario['id']} repeats a capability gap")
|
|
self.assertTrue(all(gap_ids))
|
|
self.assertTrue(all(str(gap.get("description") or "") for gap in scenario["validation_capability_gaps"]))
|
|
|
|
linkage = next(scenario for scenario in scenarios if scenario["id"] == "three_hole_linkage")
|
|
assessment = _acceptance_coverage(
|
|
linkage,
|
|
{"requirements": [{"acceptance_claims": linkage["required_claims"]}]},
|
|
)
|
|
self.assertEqual(assessment["missing_claims"], [])
|
|
self.assertFalse(assessment["complete"])
|
|
self.assertEqual(
|
|
{gap["id"] for gap in assessment["validation_capability_gaps"]},
|
|
{"linkage_outer_circles"},
|
|
)
|
|
wrong_thickness = _acceptance_coverage(
|
|
linkage,
|
|
{"requirements": [{"acceptance_claims": [
|
|
*[claim for claim in linkage["required_claims"] if claim["claim_kind"] != "bbox_rank_dimension_mm"],
|
|
{"claim_kind": "bbox_rank_dimension_mm", "expected": {"rank": "minimum", "value": 12}},
|
|
]}]},
|
|
)
|
|
self.assertIn(
|
|
{"claim_kind": "bbox_rank_dimension_mm", "expected": {"rank": "minimum", "value": 10}},
|
|
wrong_thickness["missing_claims"],
|
|
)
|
|
|
|
def test_release_fixture_has_oracle_claims_for_each_scenario(self) -> None:
|
|
scenarios = _fixture("release")
|
|
registry = default_registry()
|
|
self.assertEqual([scenario["id"] for scenario in scenarios], [
|
|
"rectangular_plate", "simple_flange", "ribbed_mounting_plate", "selector_finish",
|
|
])
|
|
for scenario in scenarios:
|
|
claims = scenario.get("required_claims")
|
|
self.assertIsInstance(claims, list)
|
|
self.assertTrue(claims, scenario["id"])
|
|
for claim in claims:
|
|
self.assertIsInstance(claim, dict)
|
|
self.assertEqual(registry.validate_expected(str(claim.get("claim_kind") or ""), claim.get("expected")), [])
|
|
|
|
rectangular = scenarios[0]
|
|
self.assertEqual(
|
|
_acceptance_coverage(rectangular, {"requirements": [{"acceptance_claims": rectangular["required_claims"]}]}),
|
|
{
|
|
"required_claim_kinds": ["bbox_dimension_mm", "solid_count_equals"],
|
|
"covered_claim_kinds": ["bbox_dimension_mm", "solid_count_equals"],
|
|
"required_claims": rectangular["required_claims"],
|
|
"missing_claims": [],
|
|
"validation_capability_gaps": [],
|
|
"complete": True,
|
|
},
|
|
)
|
|
|
|
def test_ranked_bbox_dimension_is_orientation_independent(self) -> None:
|
|
registry = default_registry()
|
|
expected = {"rank": "minimum", "value": 10, "tolerance_mm": 0.1}
|
|
self.assertEqual(
|
|
registry.evaluate("bbox_rank_dimension_mm", expected, {"health": {"bbox_mm": {"dimensions": [140, 10, 32]}}})["status"],
|
|
"pass",
|
|
)
|
|
self.assertEqual(
|
|
registry.evaluate("bbox_rank_dimension_mm", expected, {"health": {"bbox_mm": {"dimensions": [140, 12, 32]}}})["status"],
|
|
"fail",
|
|
)
|
|
|
|
def test_bore_chain_and_circular_pattern_verifiers_prove_arrangement(self) -> None:
|
|
registry = default_registry()
|
|
|
|
def cylinder(
|
|
record_id: str,
|
|
centre: list[float],
|
|
radius: float = 6,
|
|
axis_origin: list[float] | None = None,
|
|
axis_direction: list[float] | None = None,
|
|
) -> dict[str, object]:
|
|
geometry: dict[str, object] = {
|
|
"surface_type": "cylinder",
|
|
"radius_mm": radius,
|
|
"center_mm": centre,
|
|
"cylinder_role": "inner",
|
|
"through": True,
|
|
}
|
|
if axis_origin is not None:
|
|
geometry["axis_origin_mm"] = axis_origin
|
|
if axis_direction is not None:
|
|
geometry["axis_direction"] = axis_direction
|
|
return {
|
|
"record_id": record_id,
|
|
"geometry": geometry,
|
|
}
|
|
|
|
chain_facts = {"topology": {"records": [
|
|
cylinder("bore_1", [0, 0, 0]),
|
|
cylinder("bore_2", [60, 0, 0]),
|
|
cylinder("bore_3", [140, 0, 0]),
|
|
]}}
|
|
chain_expected = {"diameter_mm": 12, "adjacent_distances_mm": [60, 80], "tolerance_mm": 0.1}
|
|
self.assertEqual(
|
|
registry.evaluate(
|
|
"collinear_through_bore_chain",
|
|
chain_expected,
|
|
{"topology": {"records": [cylinder("base_outer", [0, 0, 0], 16)]}},
|
|
)["status"],
|
|
"pending",
|
|
)
|
|
self.assertEqual(
|
|
registry.evaluate(
|
|
"collinear_through_bore_chain",
|
|
chain_expected,
|
|
{"topology": {"records": chain_facts["topology"]["records"][:2]}},
|
|
)["status"],
|
|
"pending",
|
|
)
|
|
forward_chain = registry.evaluate("collinear_through_bore_chain", chain_expected, chain_facts)
|
|
self.assertEqual(forward_chain["status"], "pass")
|
|
self.assertEqual(forward_chain["evidence"]["expected_orientation"], "forward")
|
|
reverse_chain_facts = {"topology": {"records": list(reversed(chain_facts["topology"]["records"]))}}
|
|
reversed_chain = registry.evaluate("collinear_through_bore_chain", chain_expected, reverse_chain_facts)
|
|
self.assertEqual(reversed_chain["status"], "pass")
|
|
self.assertEqual(reversed_chain["evidence"]["adjacent_distances_mm"], [80.0, 60.0])
|
|
self.assertEqual(reversed_chain["evidence"]["expected_orientation"], "reversed")
|
|
chain_facts["topology"]["records"][1]["geometry"]["center_mm"] = [60, 0.2, 0] # type: ignore[index]
|
|
self.assertEqual(registry.evaluate("collinear_through_bore_chain", chain_expected, chain_facts)["status"], "fail")
|
|
|
|
mixed_host_chain_facts = {"topology": {"records": [
|
|
cylinder("bore_top_1", [0, 0, 5], axis_origin=[0, 0, 10], axis_direction=[0, 0, -1]),
|
|
cylinder("bore_bottom", [60, 0, 5], axis_origin=[60, 0, 0], axis_direction=[0, 0, 1]),
|
|
cylinder("bore_top_2", [140, 0, 5], axis_origin=[140, 0, 10], axis_direction=[0, 0, -1]),
|
|
]}}
|
|
mixed_host_chain = registry.evaluate("collinear_through_bore_chain", chain_expected, mixed_host_chain_facts)
|
|
self.assertEqual(mixed_host_chain["status"], "pass", mixed_host_chain)
|
|
self.assertEqual(mixed_host_chain["evidence"]["adjacent_distances_mm"], [60.0, 80.0])
|
|
|
|
pattern_facts = {"topology": {"records": [
|
|
cylinder("hole_1", [10, 0, 0], 1),
|
|
cylinder("hole_2", [0, 10, 0], 1),
|
|
cylinder("hole_3", [-10, 0, 0], 1),
|
|
cylinder("hole_4", [0, -10, 0], 1),
|
|
]}}
|
|
pattern_expected = {"diameter_mm": 2, "count": 4, "pitch_radius_mm": 10, "tolerance_mm": 0.1}
|
|
self.assertEqual(registry.evaluate("circular_hole_pattern", pattern_expected, pattern_facts)["status"], "pass")
|
|
|
|
pattern_facts["topology"]["records"][1]["geometry"]["center_mm"] = [5, 8.6602540378, 0] # type: ignore[index]
|
|
pattern_facts["topology"]["records"][3]["geometry"]["center_mm"] = [-5, -8.6602540378, 0] # type: ignore[index]
|
|
self.assertEqual(registry.evaluate("circular_hole_pattern", pattern_expected, pattern_facts)["status"], "fail")
|
|
|
|
concentric_facts = {"topology": {"records": [
|
|
*[
|
|
{
|
|
**record,
|
|
"geometry": {
|
|
**record["geometry"],
|
|
"axis_origin_mm": record["geometry"]["center_mm"], "axis_direction": [0, 0, 1],
|
|
},
|
|
}
|
|
for record in [
|
|
cylinder("hole_1", [10, 0, 0], 1),
|
|
cylinder("hole_2", [0, 10, 0], 1),
|
|
cylinder("hole_3", [-10, 0, 0], 1),
|
|
cylinder("hole_4", [0, -10, 0], 1),
|
|
]
|
|
],
|
|
cylinder("central_bore", [0, 0, 0], 4),
|
|
]}}
|
|
concentric_facts["topology"]["records"][-1]["geometry"].update({"axis_origin_mm": [0, 0, 0], "axis_direction": [0, 0, 1]}) # type: ignore[index]
|
|
concentric_expected = {"diameter_mm": 2, "count": 4, "pitch_radius_mm": 10, "concentric_bore_diameter_mm": 8, "tolerance_mm": 0.1}
|
|
self.assertEqual(registry.evaluate("circular_hole_pattern", concentric_expected, concentric_facts)["status"], "pass")
|
|
for record in concentric_facts["topology"]["records"][:4]:
|
|
record["geometry"]["axis_origin_mm"][0] += 1 # type: ignore[index]
|
|
self.assertEqual(registry.evaluate("circular_hole_pattern", concentric_expected, concentric_facts)["status"], "fail")
|
|
|
|
def test_operation_bore_verifier_counts_from_the_parent_checkpoint(self) -> None:
|
|
"""Adding a second single-position hole must not be compared to zero."""
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
_repository, _artifacts, actions, _task_id, _action = self._ready_action(Path(temporary))
|
|
contract = actions.runtime.operation_contract("hole_wizard")
|
|
action = PendingAction(
|
|
"act_005", "cad_123456abcdef:rev_001:v1", "Add another bore", ("req_001",),
|
|
"hole_wizard", "Add one 12 mm bore", contract["contract_hash"], "operation-bore-key",
|
|
)
|
|
|
|
def bore(record_id: str, x: float, *, through: bool = False) -> dict[str, object]:
|
|
return {
|
|
"record_id": record_id,
|
|
"kind": "face",
|
|
"geometry": {
|
|
"surface_type": "cylinder", "cylinder_role": "inner", "radius_mm": 6,
|
|
"center_mm": [x, 0, 0], "axis_direction": [0, 0, 1], "through": through,
|
|
},
|
|
}
|
|
|
|
cdsl = {"features": [{"atomic_id": "hole_wizard", "params": {"diameter_mm": 12, "positions": [{"mm": [60, 0, 0]}]}}]}
|
|
parent_facts = {"topology": {"records": [bore("bore_1", 0)]}}
|
|
rebuilt = {"health": {}, "topology": {"records": [bore("bore_1", 0), bore("bore_2", 60)]}, "report": {}}
|
|
results = actions._operation_candidate_results(
|
|
action, contract, cdsl, rebuilt, parent_facts=parent_facts, require_through=False,
|
|
)
|
|
|
|
self.assertEqual(results[0]["claim_kind"], "cylindrical_bore")
|
|
self.assertEqual(results[0]["status"], "pass", results)
|
|
self.assertEqual(results[0]["evidence"]["parent_matching_count"], 1)
|
|
self.assertEqual(results[0]["evidence"]["expected_increment"], 1)
|
|
self.assertEqual(results[0]["evidence"]["expected_total_count"], 2)
|
|
|
|
no_delta = actions._operation_candidate_results(
|
|
action, contract, cdsl,
|
|
{"health": {}, "topology": {"records": [bore("bore_1", 0)]}, "report": {}},
|
|
parent_facts=parent_facts,
|
|
require_through=False,
|
|
)
|
|
self.assertEqual(no_delta[0]["status"], "pending", no_delta)
|
|
|
|
through_contract = actions.runtime.operation_contract("hole_blind")
|
|
through_action = PendingAction(
|
|
"act_006", "cad_123456abcdef:rev_001:v2", "Add another through bore", ("req_001",),
|
|
"hole_blind", "Add one through 12 mm bore", through_contract["contract_hash"], "operation-through-bore-key",
|
|
)
|
|
through_cdsl = {"features": [{"atomic_id": "hole_blind", "params": {"diameter_mm": 12, "positions": [{"mm": [60, 0, 0]}]}}]}
|
|
through_parent = {"topology": {"records": [bore("through_bore_1", 0, through=True)]}}
|
|
through_rebuilt = {"health": {}, "topology": {"records": [bore("through_bore_1", 0, through=True), bore("through_bore_2", 60, through=True)]}, "report": {}}
|
|
through_results = actions._operation_candidate_results(
|
|
through_action, through_contract, through_cdsl, through_rebuilt,
|
|
parent_facts=through_parent, require_through=True,
|
|
)
|
|
self.assertTrue(all(item["status"] == "pass" for item in through_results), through_results)
|
|
self.assertTrue(all(item["evidence"]["expected_total_count"] == 2 for item in through_results), through_results)
|
|
|
|
def test_bore_count_claims_reject_extra_matching_holes_but_defer_incomplete_patterns(self) -> None:
|
|
registry = default_registry()
|
|
records = [
|
|
{
|
|
"record_id": f"bore_{index}",
|
|
"geometry": {
|
|
"surface_type": "cylinder", "radius_mm": 4, "center_mm": [index * 10, 0, 0],
|
|
"cylinder_role": "inner", "through": True,
|
|
},
|
|
}
|
|
for index in range(5)
|
|
]
|
|
expected = {"diameter_mm": 8, "count": 4, "tolerance_mm": 0.1}
|
|
self.assertEqual(
|
|
registry.evaluate("through_cylindrical_bore", expected, {"topology": {"records": records[:3]}})["status"],
|
|
"pending",
|
|
)
|
|
self.assertEqual(
|
|
registry.evaluate("through_cylindrical_bore", expected, {"topology": {"records": records}})["status"],
|
|
"fail",
|
|
)
|
|
|
|
def test_bore_axis_verifiers_prove_coaxiality_and_orthogonal_intersection(self) -> None:
|
|
registry = default_registry()
|
|
|
|
def bore(record_id: str, diameter: float, origin: list[float], direction: list[float]) -> dict[str, object]:
|
|
return {
|
|
"record_id": record_id,
|
|
"geometry": {
|
|
"surface_type": "cylinder", "radius_mm": diameter / 2,
|
|
"axis_origin_mm": origin, "axis_direction": direction,
|
|
"cylinder_role": "inner", "through": True,
|
|
},
|
|
}
|
|
|
|
coaxial = [
|
|
bore("lug_a", 16, [0, 0, 0], [1, 0, 0]),
|
|
bore("lug_b", 16, [20, 0, 0], [1, 0, 0]),
|
|
]
|
|
coaxial_expected = {"diameter_mm": 16, "count": 2, "tolerance_mm": 0.01}
|
|
self.assertEqual(
|
|
registry.evaluate("coaxial_through_bore_group", coaxial_expected, {"topology": {"records": coaxial}})["status"],
|
|
"pass",
|
|
)
|
|
coaxial[1]["geometry"]["axis_origin_mm"] = [20, 0.02, 0] # type: ignore[index]
|
|
self.assertEqual(
|
|
registry.evaluate("coaxial_through_bore_group", coaxial_expected, {"topology": {"records": coaxial}})["status"],
|
|
"fail",
|
|
)
|
|
|
|
crossing = [
|
|
bore("longitudinal", 20, [0, 0, 0], [1, 0, 0]),
|
|
bore("transverse", 12, [0, 0, 0], [0, 1, 0]),
|
|
]
|
|
crossing_expected = {"first_diameter_mm": 20, "second_diameter_mm": 12, "first_axis": "x", "second_axis": "y", "tolerance_mm": 0.01}
|
|
self.assertEqual(
|
|
registry.evaluate("orthogonal_intersecting_through_bores", crossing_expected, {"topology": {"records": crossing}})["status"],
|
|
"pass",
|
|
)
|
|
crossing[1]["geometry"]["axis_origin_mm"] = [0, 0, 0.02] # type: ignore[index]
|
|
self.assertEqual(
|
|
registry.evaluate("orthogonal_intersecting_through_bores", crossing_expected, {"topology": {"records": crossing}})["status"],
|
|
"fail",
|
|
)
|
|
crossing[1]["geometry"]["axis_origin_mm"] = [0, 0, 0] # type: ignore[index]
|
|
crossing[1]["geometry"]["axis_direction"] = [0, 0, 1] # type: ignore[index]
|
|
self.assertEqual(
|
|
registry.evaluate("orthogonal_intersecting_through_bores", crossing_expected, {"topology": {"records": crossing}})["status"],
|
|
"fail",
|
|
)
|
|
|
|
def test_subtractive_operation_volume_verifier_rejects_a_noop_cut(self) -> None:
|
|
registry = default_registry()
|
|
self.assertEqual(
|
|
registry.evaluate(
|
|
"volume_decreased",
|
|
{},
|
|
{"parent_health": {"volume_mm3": 1200}, "health": {"volume_mm3": 1200}},
|
|
)["status"],
|
|
"fail",
|
|
)
|
|
self.assertEqual(
|
|
registry.evaluate(
|
|
"volume_decreased",
|
|
{},
|
|
{"parent_health": {"volume_mm3": 1200}, "health": {"volume_mm3": 1199.5}},
|
|
)["status"],
|
|
"pass",
|
|
)
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
extrude_cut_contract = runtime.operation_contract("extrude_cut_blind")
|
|
self.assertIn("volume_decreased", extrude_cut_contract["candidate_verifiers"])
|
|
self.assertIn("volume_decreased", runtime.operation_contract("revolve_cut")["candidate_verifiers"])
|
|
_repository, _artifacts, actions, _task_id, action = self._ready_action(root)
|
|
cut_action = replace(
|
|
action,
|
|
atomic_id="extrude_cut_blind",
|
|
contract_hash=str(extrude_cut_contract["contract_hash"]),
|
|
)
|
|
results = actions._operation_candidate_results(
|
|
cut_action,
|
|
extrude_cut_contract,
|
|
{"features": [{"atomic_id": "extrude_cut_blind", "params": {"distance_mm": 1}}]},
|
|
{"health": {"volume_mm3": 1200}, "topology": {}, "report": {}},
|
|
parent_facts={"health": {"volume_mm3": 1200}},
|
|
require_through=False,
|
|
)
|
|
self.assertEqual(
|
|
next(item for item in results if item["claim_kind"] == "volume_decreased")["status"],
|
|
"fail",
|
|
)
|
|
|
|
def test_live_artifact_manifest_excludes_source_content_and_hashes_cad_evidence(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
(root / "actions").mkdir()
|
|
(root / "actions" / "action-ledger.jsonl").write_text('{"event":"accepted"}\n', encoding="utf-8")
|
|
(root / "revisions" / "rev_001").mkdir(parents=True)
|
|
(root / "revisions" / "rev_001" / "model.cdsl.json").write_text("{}", encoding="utf-8")
|
|
(root / "documents").mkdir()
|
|
(root / "documents" / "source-index.json").write_text('{"sources":{"src_001":"secret source"}}', encoding="utf-8")
|
|
(root / "source-requirements.md").write_text("secret source", encoding="utf-8")
|
|
|
|
manifest = _safe_artifact_manifest(root)
|
|
|
|
self.assertEqual([item["path"] for item in manifest["files"]], ["actions/action-ledger.jsonl", "revisions/rev_001/model.cdsl.json"])
|
|
self.assertTrue(all(len(item["sha256"]) == 64 for item in manifest["files"]))
|
|
|
|
def test_rejected_provider_tool_calls_keep_per_call_audit_evidence(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
configured = service_settings(root)
|
|
service = AgentService(configured, WorkspaceStore(configured), CdslLibrary(configured))
|
|
task_id = "cad_123456abcdef"
|
|
service.v3.workflow.create_task(task_id, "Create a plate.")
|
|
service.v3.workflow._record_rejected_tool_calls(
|
|
task_id,
|
|
actor="author",
|
|
expected_tool="submit_requirements_draft_batch",
|
|
tool_calls=[
|
|
{"function": {"name": "wrong_tool", "arguments": "{}"}},
|
|
{"function": {"name": "another_wrong_tool", "arguments": "{\"extra\":true}"}},
|
|
],
|
|
schema={"type": "object", "additionalProperties": False},
|
|
)
|
|
|
|
audits = service.v3.repository.tool_audits(task_id)
|
|
self.assertEqual([audit["returned_tool"] for audit in audits], ["wrong_tool", "another_wrong_tool"])
|
|
self.assertTrue(all(audit["single_allowed_call"] is False for audit in audits))
|
|
self.assertTrue(all(audit["state_binding"]["binding_valid"] is False for audit in audits))
|
|
self.assertEqual(audits[0]["raw_arguments_hash"], sha256(b"{}").hexdigest())
|
|
|
|
def test_token_baseline_requires_matching_provenance_and_proves_median_reduction(self) -> None:
|
|
root = Path(tempfile.gettempdir())
|
|
identity = author_request_identity(settings(root).providers[0], settings(root).providers[0].models[0])
|
|
scenarios = [{"id": "plate", "request": "Create a plate."}]
|
|
baseline = {
|
|
"schema_version": BASELINE_SCHEMA_VERSION,
|
|
"protocol_version": "2.0",
|
|
"measurement": "prompt_tokens",
|
|
"author": identity,
|
|
"runtime_profile_sha256": "profile-current",
|
|
"scenarios": [{
|
|
"scenario": "plate", "request_sha256": request_sha256("Create a plate."),
|
|
"repetitions": [
|
|
{"repetition": 1, "author_metric": 120, "plan_review_metric": 80, "completed": True, "final_review_passed": True, "deterministic_claims_pass": True},
|
|
{"repetition": 2, "author_metric": 130, "plan_review_metric": 70, "completed": True, "final_review_passed": True, "deterministic_claims_pass": True},
|
|
{"repetition": 3, "author_metric": 110, "plan_review_metric": 90, "completed": True, "final_review_passed": True, "deterministic_claims_pass": True},
|
|
],
|
|
}],
|
|
}
|
|
results = [
|
|
{
|
|
"scenario": "plate", "repetition": repetition,
|
|
"usage": {"records": [{"prompt_tokens": 100, "usage_available": True}, {"role": "reviewer", "prompt_tokens": 500, "usage_available": True}]},
|
|
"projection": {"phase": "COMPLETED"}, "terminal": {"lifecycle": "completed"},
|
|
"checks": {"deterministic_claims_pass": True},
|
|
}
|
|
for repetition in range(1, 4)
|
|
]
|
|
comparison = compare_token_baseline(
|
|
baseline, scenarios=scenarios, v3_results=results,
|
|
author_identity=identity, runtime_profile_hash="profile-current",
|
|
)
|
|
self.assertTrue(all(comparison["checks"].values()))
|
|
self.assertEqual(comparison["baseline_median_metric"], 200)
|
|
self.assertEqual(comparison["v3_median_author_metric"], 100)
|
|
self.assertEqual(comparison["median_metric_reduction"], 0.5)
|
|
|
|
mismatched = {**baseline, "author": {**identity, "model": "another-model"}}
|
|
self.assertTrue(validate_token_baseline_provenance(
|
|
mismatched, scenarios=scenarios, author_identity=identity,
|
|
runtime_profile_hash="profile-current",
|
|
))
|
|
rejected = compare_token_baseline(
|
|
mismatched, scenarios=scenarios, v3_results=results,
|
|
author_identity=identity, runtime_profile_hash="profile-current",
|
|
)
|
|
self.assertFalse(rejected["checks"]["baseline_valid"])
|
|
self.assertFalse(rejected["checks"]["median_metric_reduction"])
|
|
|
|
def test_dynamic_operation_schema_has_exact_shape_and_snapshot_enum(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
contract = runtime.operation_contract("hole_blind")
|
|
schema = fragment_schema(contract, selector_tokens=["sel_current"])
|
|
self.assertEqual(schema["properties"]["feature"]["properties"]["selector_tokens"]["items"], {"enum": ["sel_current"]})
|
|
self.assertNotIn("sketch", schema["properties"])
|
|
invalid = {
|
|
"sketch": {"workplane": {}, "profile": {}},
|
|
"feature": {"atomic_id": "hole_blind", "selector_tokens": ["sel_old"], "params": {"diameter_mm": 10, "depth_mm": 4, "positions": [{"mm": [0, 0, 0]}]}},
|
|
}
|
|
self.assertTrue(validate_fragment(contract, invalid, selector_tokens=["sel_current"]))
|
|
|
|
def test_runtime_materialization_revalidates_current_contract_and_selector_bounds(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
contract = runtime.operation_contract("hole_blind")
|
|
selectors = {
|
|
"sel_host": {
|
|
"kind": "face",
|
|
"selector": {"kind": "face", "stable_id": "face_top"},
|
|
"geometry": {
|
|
"surface_type": "plane",
|
|
"center_mm": [0, 0, 10],
|
|
"normal": [0, 0, 1],
|
|
"bbox_mm": [-5, -5, 10, 5, 5, 10],
|
|
},
|
|
},
|
|
}
|
|
fragment = {
|
|
"feature": {
|
|
"atomic_id": "hole_blind",
|
|
"selector_tokens": [],
|
|
"params": {"diameter_mm": 2, "depth_mm": 4, "positions": [{"mm": [0, 0, 10]}]},
|
|
},
|
|
}
|
|
with self.assertRaisesRegex(RuntimeAdapterError, "active operation schema"):
|
|
runtime.materialize_fragment(None, fragment, contract, selectors, {})
|
|
|
|
stale = dict(contract)
|
|
stale["contract_hash"] = "stale"
|
|
with self.assertRaisesRegex(RuntimeAdapterError, "current verified registry entry"):
|
|
runtime.materialize_fragment(None, {**fragment, "feature": {**fragment["feature"], "selector_tokens": ["sel_host"]}}, stale, selectors, {})
|
|
|
|
def test_selector_bound_hole_materializes_a_replay_stable_host_frame(self) -> None:
|
|
"""A patterned hole must not resolve its source face after that cut splits it."""
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
base = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "patterned-hole",
|
|
"geometry": {"sketches": [{
|
|
"id": "sketch_001",
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "polygon", "vertices": [[-5, -5], [5, -5], [5, 5], [-5, 5]]},
|
|
}]},
|
|
"features": [{
|
|
"id": "feature_001", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"params": {"distance_mm": 10}, "sketch_id": "sketch_001",
|
|
}],
|
|
}
|
|
first = runtime.rebuild(base, str(root / "base"), "cad_123456abcdef", "candidate_base")
|
|
tokens = runtime.selector_tokens(first["topology"])
|
|
host_token = next(
|
|
token for token, value in tokens.items()
|
|
if value["kind"] == "face"
|
|
and value["geometry"].get("surface_type") == "plane"
|
|
and float(value["geometry"].get("normal", [0, 0, 0])[2]) > 0.9
|
|
)
|
|
hole_fragment = {
|
|
"feature": {
|
|
"atomic_id": "hole_blind", "selector_tokens": [host_token],
|
|
"params": {"diameter_mm": 2, "depth_mm": 12, "positions": [{"mm": [0, 0, 10]}]},
|
|
},
|
|
}
|
|
with_hole, _audit = runtime.materialize_fragment(
|
|
base, hole_fragment, runtime.operation_contract("hole_blind"), tokens, {}, require_through=True,
|
|
)
|
|
host = with_hole["features"][-1]["params"]["host_face"]
|
|
self.assertEqual(set(host), {"frame"})
|
|
self.assertAlmostEqual(with_hole["features"][-1]["params"]["positions"][0]["mm"][2], 0.0)
|
|
with_hole["features"].append({
|
|
"id": "feature_003", "atomic_id": "pattern_linear", "depends_on": ["feature_002"],
|
|
"params": {
|
|
"source_feature_ids": ["feature_002"], "direction_1": [1, 0, 0],
|
|
"spacing_1_mm": 4, "pattern_count_1": 2,
|
|
},
|
|
})
|
|
patterned = runtime.rebuild(with_hole, str(root / "patterned"), "cad_123456abcdef", "candidate_patterned")
|
|
self.assertEqual(patterned["health"]["solid_count"], 1)
|
|
self.assertAlmostEqual(patterned["health"]["volume_mm3"], 1000 - 2 * math.pi * 10, places=5)
|
|
|
|
def test_selector_bound_hole_wizard_materializes_against_engine_schema(self) -> None:
|
|
"""The author contract must contain every CDSL-required Hole Wizard field."""
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
base = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "wizard-host",
|
|
"geometry": {"sketches": [{
|
|
"id": "sketch_001",
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "polygon", "vertices": [[-5, -5], [5, -5], [5, 5], [-5, 5]]},
|
|
}]},
|
|
"features": [{
|
|
"id": "feature_001", "atomic_id": "extrude_add_blind", "depends_on": [],
|
|
"params": {"distance_mm": 10}, "sketch_id": "sketch_001",
|
|
}],
|
|
}
|
|
first = runtime.rebuild(base, str(root / "base"), "cad_123456abcdef", "candidate_base")
|
|
tokens = runtime.selector_tokens(first["topology"])
|
|
host_token = next(
|
|
token for token, value in tokens.items()
|
|
if value["kind"] == "face"
|
|
and value["geometry"].get("surface_type") == "plane"
|
|
and float(value["geometry"].get("normal", [0, 0, 0])[2]) > 0.9
|
|
)
|
|
fragment = {
|
|
"feature": {
|
|
"atomic_id": "hole_wizard", "selector_tokens": [host_token],
|
|
"params": {
|
|
"hole_type": "simple", "diameter_mm": 2, "depth_mm": 12,
|
|
"end_condition": {"type": "blind", "solidworks_code": 0},
|
|
"positions": [{"mm": [0, 0, 10]}],
|
|
},
|
|
},
|
|
}
|
|
materialized, _audit = runtime.materialize_fragment(
|
|
base, fragment, runtime.operation_contract("hole_wizard"), tokens, {}, require_through=True,
|
|
)
|
|
self.assertEqual(materialized["features"][-1]["params"]["end_condition"], {"type": "blind", "solidworks_code": 0})
|
|
|
|
def test_operation_contract_rejects_inconsistent_server_selector_and_reference_bindings(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
selector_mismatch = runtime.operation_contract("hole_blind")
|
|
selector_mismatch["server_injected_paths"] = []
|
|
with self.assertRaisesRegex(OperationContractError, "injection paths disagree"):
|
|
validate_operation_contract(selector_mismatch)
|
|
self.assertEqual(
|
|
WorkflowCoordinator._selector_tokens_for_contract(
|
|
runtime.operation_contract("hole_blind"),
|
|
{"sel_face": {"kind": "face"}, "sel_edge": {"kind": "edge"}},
|
|
),
|
|
["sel_face"],
|
|
)
|
|
|
|
reference_mismatch = runtime.operation_contract("pattern_linear")
|
|
reference_mismatch["author_params_schema"]["required"].remove("source_feature_ids")
|
|
with self.assertRaisesRegex(OperationContractError, "required author array"):
|
|
validate_operation_contract(reference_mismatch)
|
|
|
|
def test_author_context_exposes_claim_coverage_and_compact_model_facts_without_source_text(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(root)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
workflow = WorkflowCoordinator(WorkflowConfig(1, 1), repository, artifacts, runtime, None, None, requirements, actions)
|
|
|
|
context = workflow._author_context(task_id, [])
|
|
payload = json.loads(str(context[1]["content"]))
|
|
self.assertEqual(payload["model_summary"], {"revision_id": "", "available": False})
|
|
self.assertEqual(payload["claim_coverage"][0]["claim_id"], "claim_001")
|
|
self.assertNotIn("source_requirements", payload)
|
|
|
|
def test_author_context_retains_claim_targets_and_measured_bore_facts(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository, artifacts, actions, task_id, action = self._ready_action(root)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
state = repository.get_state(task_id)
|
|
contract = {
|
|
"schema_version": "cad.requirements-contract.v1",
|
|
"task_id": task_id,
|
|
"contract_hash": "bore-context-contract",
|
|
"requirements": [{
|
|
"requirement_id": "req_001",
|
|
"draft_id": "draft_001",
|
|
"source_ids": ["src_001"],
|
|
"statement": "Three collinear bores",
|
|
"assumptions": [],
|
|
"acceptance_claims": [{
|
|
"claim_id": "claim_001",
|
|
"claim_kind": "through_cylindrical_bore",
|
|
"expected": {"diameter_mm": 12, "count": 3},
|
|
}],
|
|
}],
|
|
}
|
|
contract_path = artifacts.write_requirements_contract(task_id, contract, invocation_id="bore_contract")
|
|
artifacts.write_json_once(task_id, "revisions/rev_001/rebuild-report.json", {
|
|
"health": {"solid_count": 1, "bbox_mm": {"dimensions": [32, 140, 10]}},
|
|
})
|
|
artifacts.write_json_once(task_id, "revisions/rev_001/model.topology.json", {
|
|
"snapshot_id": "snapshot_bores",
|
|
"records": [{
|
|
"kind": "face",
|
|
"geometry": {
|
|
"surface_type": "cylinder",
|
|
"cylinder_role": "inner",
|
|
"radius_mm": 6,
|
|
"axis_origin_mm": [0, -70, 10],
|
|
"through": True,
|
|
},
|
|
}],
|
|
})
|
|
state = transition(state, "candidate_started", candidate_id="candidate_123456789012", candidate_stage_id="stage_1234567890abcdef12")
|
|
self.assertTrue(repository.compare_and_swap(state))
|
|
state = transition(state, "candidate_built")
|
|
self.assertTrue(repository.compare_and_swap(state))
|
|
state = transition(
|
|
state,
|
|
"candidate_accepted",
|
|
active_revision="rev_001",
|
|
requirements_contract_path=contract_path,
|
|
repair_required=False,
|
|
)
|
|
self.assertTrue(repository.compare_and_swap(state))
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, runtime, None, None, requirements, actions)
|
|
|
|
payload = json.loads(workflow._author_context(task_id, [])[1]["content"])
|
|
self.assertEqual(
|
|
payload["requirements"][0]["acceptance_claims"],
|
|
[{"claim_id": "claim_001", "claim_kind": "through_cylindrical_bore", "expected": {"diameter_mm": 12, "count": 3}}],
|
|
)
|
|
self.assertEqual(
|
|
payload["model_summary"]["inner_cylindrical_bores"],
|
|
[{"diameter_mm": 12.0, "axis_origin_mm": [0.0, -70.0, 10.0], "through": True}],
|
|
)
|
|
|
|
def test_repair_context_exposes_compact_rejected_candidate_facts(self) -> None:
|
|
"""Repair turns retain measured failure evidence after staging clears."""
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository, artifacts, actions, task_id, action = self._ready_action(root)
|
|
state = repository.get_state(task_id)
|
|
stage = artifacts.start_candidate_stage(task_id, action.idempotency_key, {"schema_version": "test"})
|
|
candidate_id = "candidate_123456789012"
|
|
artifacts.write_stage_json(task_id, stage.stage_id, "candidate.json", {
|
|
"candidate_id": candidate_id,
|
|
"actual_atomic_id": "extrude_add_blind",
|
|
"health": {
|
|
"solid_count": 3,
|
|
"feature_count": 1,
|
|
"bbox_mm": {"dimensions": [80, 50, 8]},
|
|
},
|
|
"claim_results": [{
|
|
"claim_id": "claim_001",
|
|
"claim_kind": "solid_count_equals",
|
|
"deterministic": True,
|
|
"status": "fail",
|
|
"evidence": {"actual": 3, "expected": 1},
|
|
}],
|
|
"operation_verifier_results": [{
|
|
"claim_id": "operation_act_004_cylindrical_bore",
|
|
"claim_kind": "cylindrical_bore",
|
|
"deterministic": True,
|
|
"status": "pending",
|
|
"evidence": {
|
|
"parent_matching_count": 1,
|
|
"expected_increment": 1,
|
|
"expected_total_count": 2,
|
|
},
|
|
}],
|
|
})
|
|
artifacts.write_stage_json(task_id, stage.stage_id, "candidate-review.json", {
|
|
"evidence": ["render_top: the requested pocket opens at the right edge instead of being centred."],
|
|
"issues": ["No material was removed from the centred pocket region."],
|
|
})
|
|
building = transition(state, "candidate_started", candidate_id=candidate_id, candidate_stage_id=stage.stage_id)
|
|
self.assertTrue(repository.compare_and_swap(building))
|
|
rejected = transition(
|
|
building,
|
|
"candidate_rejected",
|
|
candidate_id="",
|
|
candidate_stage_id="",
|
|
repair_required=True,
|
|
error=ErrorCode.CLAIM_VERIFICATION_FAILED,
|
|
)
|
|
self.assertTrue(repository.compare_and_swap(rejected, events=[{
|
|
"event": "candidate_rejected",
|
|
"candidate_id": candidate_id,
|
|
"stage_id": stage.stage_id,
|
|
"action_id": action.action_id,
|
|
"working_head": action.working_head,
|
|
}]))
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, runtime, None, None, requirements, actions)
|
|
|
|
payload = json.loads(workflow._author_context(task_id, [])[1]["content"])
|
|
diagnostic = payload["repair_diagnostics"][0]
|
|
self.assertEqual(diagnostic["candidate_id"], candidate_id)
|
|
self.assertEqual(diagnostic["atomic_id"], "extrude_add_blind")
|
|
self.assertEqual(diagnostic["measured"], {"solid_count": 3, "feature_count": 1, "bbox_dimensions_mm": [80, 50, 8]})
|
|
self.assertEqual(diagnostic["failed_claims"], [{
|
|
"claim_id": "claim_001",
|
|
"claim_kind": "solid_count_equals",
|
|
"status": "fail",
|
|
"evidence": {"actual": 3, "expected": 1},
|
|
}])
|
|
self.assertEqual(diagnostic["operation_blockers"], [{
|
|
"claim_id": "operation_act_004_cylindrical_bore",
|
|
"claim_kind": "cylindrical_bore",
|
|
"status": "pending",
|
|
"evidence": {
|
|
"parent_matching_count": 1,
|
|
"expected_increment": 1,
|
|
"expected_total_count": 2,
|
|
},
|
|
}])
|
|
self.assertEqual(diagnostic["review_evidence"], ["render_top: the requested pocket opens at the right edge instead of being centred."])
|
|
self.assertEqual(diagnostic["review_issues"], ["No material was removed from the centred pocket region."])
|
|
self.assertNotIn("fragment", diagnostic)
|
|
self.assertIn("fragment_schema", payload["operation_contract"])
|
|
|
|
def test_hole_tool_and_topology_payload_declare_world_space_positions(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
hole_contract = actions.runtime.operation_contract("hole_blind")
|
|
hole_action = PendingAction(
|
|
action.action_id,
|
|
state.working_head,
|
|
"Add one bore",
|
|
action.requirement_ids,
|
|
"hole_blind",
|
|
"Add a 12 mm bore",
|
|
str(hole_contract["contract_hash"]),
|
|
action.idempotency_key,
|
|
)
|
|
# Construct the context helper against an immutable pending action;
|
|
# this test concerns the generated tool contract wording only.
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, actions.runtime, None, None, RequirementsCommandHandler(repository, artifacts, default_registry()), actions)
|
|
state_with_hole = replace(state, pending_action=hole_action)
|
|
tools = workflow._action_tools(task_id, state_with_hole, {"contract", "topology"})
|
|
self.assertIn("absolute world-space mm point", tools[0]["function"]["description"])
|
|
self.assertEqual(workflow._topology_payload(task_id, state_with_hole, None, 1)["coordinate_system"], "world_mm")
|
|
|
|
def test_canonical_arguments_reject_non_finite_numbers_before_schema_validation(self) -> None:
|
|
rejected = canonical_json_object('{"value": NaN}')
|
|
self.assertIsInstance(rejected, WorkflowError)
|
|
self.assertEqual(rejected.code, ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
|
|
def test_hole_position_preflight_requires_selected_host_plane_and_bounds(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
contract = runtime.operation_contract("hole_blind")
|
|
selectors = {
|
|
"sel_host": {
|
|
"kind": "face",
|
|
"selector": {"kind": "face", "stable_id": "face_top"},
|
|
"geometry": {
|
|
"surface_type": "plane",
|
|
"center_mm": [0, 0, 10],
|
|
"normal": [0, 0, 1],
|
|
"bbox_mm": [-5, -5, 10, 5, 5, 10],
|
|
},
|
|
},
|
|
}
|
|
outside_plane = {
|
|
"feature": {
|
|
"atomic_id": "hole_blind",
|
|
"selector_tokens": ["sel_host"],
|
|
"params": {"diameter_mm": 2, "depth_mm": 4, "positions": [{"mm": [0, 0, 9]}]},
|
|
},
|
|
}
|
|
with self.assertRaisesRegex(RuntimeAdapterError, "not on the selected host plane"):
|
|
runtime.materialize_fragment(None, outside_plane, contract, selectors, {})
|
|
|
|
outside_bounds = {
|
|
"feature": {
|
|
"atomic_id": "hole_blind",
|
|
"selector_tokens": ["sel_host"],
|
|
"params": {"diameter_mm": 2, "depth_mm": 4, "positions": [{"mm": [6, 0, 10]}]},
|
|
},
|
|
}
|
|
with self.assertRaisesRegex(RuntimeAdapterError, "outside the selected host-face bounds"):
|
|
runtime.materialize_fragment(None, outside_bounds, contract, selectors, {})
|
|
|
|
selectors["sel_host"]["geometry"]["boundary_loops_mm"] = [
|
|
[[-5, -5, 10], [5, -5, 10], [5, 5, 10], [-5, 5, 10]],
|
|
[[-1, -1, 10], [1, -1, 10], [1, 1, 10], [-1, 1, 10]],
|
|
]
|
|
inside_existing_opening = {
|
|
"feature": {
|
|
"atomic_id": "hole_blind", "selector_tokens": ["sel_host"],
|
|
"params": {"diameter_mm": 2, "depth_mm": 4, "positions": [{"mm": [0, 0, 10]}]},
|
|
},
|
|
}
|
|
with self.assertRaisesRegex(RuntimeAdapterError, "effective boundary"):
|
|
runtime.materialize_fragment(None, inside_existing_opening, contract, selectors, {})
|
|
|
|
def test_analytic_arc_preflight_rejects_off_circle_and_degenerate_arcs(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
contract = runtime.operation_contract("extrude_add_blind")
|
|
|
|
def fragment(arc: dict[str, object]) -> dict[str, object]:
|
|
return {
|
|
"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": [arc]}]},
|
|
},
|
|
"feature": {"atomic_id": "extrude_add_blind", "params": {"distance_mm": 5}},
|
|
}
|
|
|
|
with self.assertRaisesRegex(RuntimeAdapterError, "endpoints are not on the declared circle"):
|
|
runtime.materialize_fragment(None, fragment({
|
|
"type": "arc", "start": [1, 0], "end": [0, 1], "center": [0, 0], "radius_mm": 2,
|
|
}), contract, {}, {})
|
|
with self.assertRaisesRegex(RuntimeAdapterError, "is degenerate"):
|
|
runtime.materialize_fragment(None, fragment({
|
|
"type": "arc", "start": [1, 0], "end": [1, 0], "center": [0, 0], "radius_mm": 1,
|
|
}), contract, {}, {})
|
|
|
|
def test_hole_materialization_rejects_unusable_host_frame_before_engine(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
contract = runtime.operation_contract("hole_blind")
|
|
selectors = {"sel_host": {
|
|
"kind": "face", "selector": {"kind": "face", "stable_id": "face_bad"},
|
|
"geometry": {"surface_type": "plane", "center_mm": [0, 0, None], "normal": [0, 0, 1], "bbox_mm": [-5, -5, 0, 5, 5, 0]},
|
|
}}
|
|
fragment = {"feature": {
|
|
"atomic_id": "hole_blind", "selector_tokens": ["sel_host"],
|
|
"params": {"diameter_mm": 2, "depth_mm": 4, "positions": [{"mm": [0, 0, 0]}]},
|
|
}}
|
|
|
|
with self.assertRaisesRegex(RuntimeAdapterError, "observable planar face"):
|
|
runtime.materialize_fragment(None, fragment, contract, selectors, {})
|
|
|
|
def test_revolve_axis_preflight_requires_axis_to_lie_on_sketch_plane(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
contract = runtime.operation_contract("revolve_add")
|
|
fragment = {
|
|
"sketch": {
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 1},
|
|
},
|
|
"feature": {
|
|
"atomic_id": "revolve_add",
|
|
"params": {"angle_deg": 180, "axis": {"origin_mm": [0, 0, 1], "direction": [1, 0, 0]}},
|
|
},
|
|
}
|
|
with self.assertRaisesRegex(RuntimeAdapterError, "revolve axis is not on the sketch plane"):
|
|
runtime.materialize_fragment(None, fragment, contract, {}, {})
|
|
|
|
def test_every_runtime_operation_has_closed_valid_and_invalid_dynamic_fragments(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
for atomic_id in runtime.supported_atomic_ids():
|
|
with self.subTest(atomic_id=atomic_id):
|
|
contract = runtime.operation_contract(atomic_id)
|
|
params = schema_example(contract["author_params_schema"])
|
|
self.assertIsInstance(params, dict)
|
|
if contract["reference_policy"]["mode"] == "snapshot_bound":
|
|
slot = str(contract["reference_policy"]["slot"]).removeprefix("params.")
|
|
params[slot] = ["ref_current"]
|
|
feature = {"atomic_id": atomic_id, "params": params}
|
|
if contract["fragment_shape"]["selector_tokens"] == "required":
|
|
feature["selector_tokens"] = ["sel_current"]
|
|
fragment: dict[str, object] = {"feature": feature}
|
|
if contract["fragment_shape"]["sketch"] == "required":
|
|
fragment["sketch"] = {
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 1},
|
|
}
|
|
self.assertEqual(validate_fragment(contract, fragment, selector_tokens=["sel_current"], reference_tokens=["ref_current"]), [])
|
|
|
|
malformed = json.loads(json.dumps(fragment))
|
|
malformed["feature"]["atomic_id"] = "wrong_atomic"
|
|
self.assertTrue(validate_fragment(contract, malformed, selector_tokens=["sel_current"], reference_tokens=["ref_current"]))
|
|
if contract["fragment_shape"]["sketch"] == "required":
|
|
missing_sketch = json.loads(json.dumps(fragment))
|
|
del missing_sketch["sketch"]
|
|
self.assertTrue(validate_fragment(contract, missing_sketch, selector_tokens=["sel_current"], reference_tokens=["ref_current"]))
|
|
else:
|
|
extra_sketch = json.loads(json.dumps(fragment))
|
|
extra_sketch["sketch"] = {"workplane": {}, "profile": {}}
|
|
self.assertTrue(validate_fragment(contract, extra_sketch, selector_tokens=["sel_current"], reference_tokens=["ref_current"]))
|
|
if contract["fragment_shape"]["selector_tokens"] == "required":
|
|
stale_selector = json.loads(json.dumps(fragment))
|
|
stale_selector["feature"]["selector_tokens"] = ["sel_stale"]
|
|
self.assertTrue(validate_fragment(contract, stale_selector, selector_tokens=["sel_current"], reference_tokens=["ref_current"]))
|
|
if contract["reference_policy"]["mode"] == "snapshot_bound":
|
|
stale_reference = json.loads(json.dumps(fragment))
|
|
slot = str(contract["reference_policy"]["slot"]).removeprefix("params.")
|
|
stale_reference["feature"]["params"][slot] = ["ref_stale"]
|
|
self.assertTrue(validate_fragment(contract, stale_reference, selector_tokens=["sel_current"], reference_tokens=["ref_current"]))
|
|
|
|
def test_hole_wizard_dynamic_schema_requires_positions_and_variant_details(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
contract = runtime.operation_contract("hole_wizard")
|
|
schema = fragment_schema(contract, selector_tokens=["sel_host"])
|
|
|
|
missing_positions = {
|
|
"feature": {
|
|
"atomic_id": "hole_wizard", "selector_tokens": ["sel_host"],
|
|
"params": {
|
|
"hole_type": "simple", "diameter_mm": 3, "depth_mm": 5,
|
|
"end_condition": {"type": "blind", "solidworks_code": 0},
|
|
},
|
|
},
|
|
}
|
|
self.assertTrue(validate_fragment(contract, missing_positions, selector_tokens=["sel_host"]))
|
|
|
|
missing_end_condition = {
|
|
"feature": {
|
|
"atomic_id": "hole_wizard", "selector_tokens": ["sel_host"],
|
|
"params": {
|
|
"hole_type": "simple", "diameter_mm": 3, "depth_mm": 5,
|
|
"positions": [{"mm": [0, 0, 0]}],
|
|
},
|
|
},
|
|
}
|
|
self.assertTrue(validate_fragment(contract, missing_end_condition, selector_tokens=["sel_host"]))
|
|
|
|
missing_variant_details = {
|
|
"feature": {
|
|
"atomic_id": "hole_wizard", "selector_tokens": ["sel_host"],
|
|
"params": {
|
|
"hole_type": "countersink", "diameter_mm": 3, "depth_mm": 5,
|
|
"end_condition": {"type": "blind", "solidworks_code": 0},
|
|
"positions": [{"mm": [0, 0, 0]}],
|
|
},
|
|
},
|
|
}
|
|
self.assertTrue(validate_fragment(contract, missing_variant_details, selector_tokens=["sel_host"]))
|
|
|
|
valid_countersink = {
|
|
"feature": {
|
|
"atomic_id": "hole_wizard", "selector_tokens": ["sel_host"],
|
|
"params": {
|
|
"hole_type": "countersink", "diameter_mm": 3, "depth_mm": 5,
|
|
"end_condition": {"type": "blind", "solidworks_code": 0},
|
|
"positions": [{"mm": [0, 0, 0]}],
|
|
"countersink": {"diameter_mm": 5, "angle_rad": 1.2},
|
|
},
|
|
},
|
|
}
|
|
self.assertEqual(validate_fragment(contract, valid_countersink, selector_tokens=["sel_host"]), [])
|
|
|
|
def test_materialized_cdsl_schema_drift_is_a_runtime_contract_error(self) -> None:
|
|
error = ActionCommandHandler._runtime_error(
|
|
ValueError("CDSL schema violation at $.features[1].params: 'end_condition' is a required property")
|
|
)
|
|
self.assertEqual(error.code, ErrorCode.RUNTIME_CONTRACT_INVALID)
|
|
|
|
def test_materialized_profile_resolution_failure_is_a_recoverable_precondition_error(self) -> None:
|
|
error = ActionCommandHandler._runtime_error(
|
|
RuntimeAdapterError(
|
|
"RUNTIME_PRECONDITION_FAILED: materialized fragment is not executable by the engine: "
|
|
"CDSL engine runtime preflight failed: feature feature_001: profile_resolution_failed"
|
|
)
|
|
)
|
|
self.assertEqual(error.code, ErrorCode.RUNTIME_PRECONDITION_FAILED)
|
|
|
|
def test_runtime_contract_registry_rejects_unknown_preflights_and_exposes_only_registered_verifiers(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
verifier_kinds = set(default_registry().claim_kinds)
|
|
declared_preflights: set[str] = set()
|
|
for atomic_id in runtime.supported_atomic_ids():
|
|
contract = runtime.operation_contract(atomic_id)
|
|
declared_preflights.update(contract["semantic_preflight"])
|
|
self.assertTrue(set(contract["candidate_verifiers"]).issubset(verifier_kinds), atomic_id)
|
|
self.assertTrue(declared_preflights.issubset(SEMANTIC_PREFLIGHT_NAMES))
|
|
|
|
invalid = runtime.operation_contract("extrude_add_blind")
|
|
invalid["semantic_preflight"] = ["unknown_preflight"]
|
|
with self.assertRaisesRegex(OperationContractError, "preflight"):
|
|
validate_operation_contract(invalid)
|
|
|
|
def test_engine_materializes_metadata_from_the_author_contract_registry(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
from cdsl_engine.operation_contracts import materialized_feature_contracts
|
|
|
|
materialized_contracts = materialized_feature_contracts(runtime._profile)
|
|
self.assertEqual(set(materialized_contracts), set(runtime.supported_atomic_ids()))
|
|
self.assertEqual(
|
|
materialized_contracts["hole_blind"]["required_params"],
|
|
["diameter_mm", "depth_mm", "positions", "host_face"],
|
|
)
|
|
self.assertTrue(materialized_contracts["extrude_add_blind"]["requires_sketch"])
|
|
|
|
def test_selectorless_extrude_cut_can_preflight_a_through_bore_from_active_body(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
base = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "cut-proof",
|
|
"geometry": {"sketches": [{
|
|
"id": "sketch_001",
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 60},
|
|
}]},
|
|
"features": [{
|
|
"id": "feature_001", "atomic_id": "extrude_add_blind", "params": {"distance_mm": 12},
|
|
"depends_on": [], "sketch_id": "sketch_001",
|
|
}],
|
|
}
|
|
base_result = runtime.rebuild(base, str(root / "base"), "cad_123456abcdef", "rev_001")
|
|
selectors = runtime.selector_tokens(base_result["topology"])
|
|
contract = runtime.operation_contract("extrude_cut_blind")
|
|
fragment = {
|
|
"sketch": {
|
|
"workplane": {"origin_mm": [0, 0, 12], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 20},
|
|
},
|
|
"feature": {"atomic_id": "extrude_cut_blind", "params": {"distance_mm": 14, "reverse": True}},
|
|
}
|
|
|
|
cdsl, _audit = runtime.materialize_fragment(base, fragment, contract, selectors, {}, require_through=True)
|
|
rebuilt = runtime.rebuild(cdsl, str(root / "through"), "cad_123456abcdef", "candidate_cut")
|
|
verdict = default_registry().evaluate(
|
|
"through_cylindrical_bore", {"diameter_mm": 40, "count": 1, "tolerance_mm": 0.01},
|
|
{"topology": rebuilt["topology"]},
|
|
)
|
|
self.assertEqual(verdict["status"], "pass")
|
|
|
|
shallow = {**fragment, "feature": {"atomic_id": "extrude_cut_blind", "params": {"distance_mm": 12, "reverse": True}}}
|
|
with self.assertRaisesRegex(RuntimeAdapterError, "must exceed measured host-body thickness 12"):
|
|
runtime.materialize_fragment(base, shallow, contract, selectors, {}, require_through=True)
|
|
|
|
def test_selector_bound_fragment_receives_contract_without_ritual_reads(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
contract = actions.runtime.operation_contract("hole_blind")
|
|
pending = PendingAction("act_005", state.working_head, "Cut bore", ("req_001",), "hole_blind", "Add through bore", contract["contract_hash"], "hole-key")
|
|
selector_state = replace(state, pending_action=pending)
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, actions.runtime, None, None, RequirementsCommandHandler(repository, artifacts, default_registry()), actions)
|
|
|
|
initial = {item["function"]["name"] for item in workflow._action_tools(task_id, selector_state, set())}
|
|
self.assertEqual(initial, {"submit_cdsl_fragment"})
|
|
after_contract = {item["function"]["name"] for item in workflow._action_tools(task_id, selector_state, {"contract"})}
|
|
self.assertEqual(after_contract, {"submit_cdsl_fragment"})
|
|
after_both = {item["function"]["name"] for item in workflow._action_tools(task_id, selector_state, {"topology", "contract"})}
|
|
self.assertEqual(after_both, {"submit_cdsl_fragment"})
|
|
|
|
def test_fragment_contract_is_available_immediately_after_action_proposal(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, actions.runtime, None, None, RequirementsCommandHandler(repository, artifacts, default_registry()), actions)
|
|
before = {item["function"]["name"] for item in workflow._action_tools(task_id, state, set())}
|
|
self.assertEqual(before, {"submit_cdsl_fragment"})
|
|
after = {item["function"]["name"] for item in workflow._action_tools(task_id, state, {"contract"})}
|
|
self.assertEqual(after, {"submit_cdsl_fragment"})
|
|
|
|
def test_final_review_schema_binds_current_head_and_claim_ids(self) -> None:
|
|
schema = final_review_schema("cad_123456abcdef:rev_001:v8", ["claim_001"])
|
|
rejected = canonical_validate_schema(json.dumps({
|
|
"working_head": "cad_123456abcdef:rev_001:v7", "verdict": "pass",
|
|
"claim_coverage": [{"claim_id": "claim_001", "status": "pass"}],
|
|
}), schema)
|
|
self.assertEqual(rejected.code if rejected else None, ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
|
|
def test_observation_schemas_reject_stale_state_before_side_effects(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
self.assertIsNotNone(state)
|
|
before_version = state.version
|
|
before_head = state.working_head
|
|
|
|
stale_topology = canonical_validate_schema(json.dumps({
|
|
"working_head": "cad_123456abcdef:root:v0", "kind": "face", "limit": 16,
|
|
}), topology_request_schema(state.working_head))
|
|
wrong_operation = canonical_validate_schema(json.dumps({
|
|
"working_head": state.working_head, "atomic_id": "hole_blind",
|
|
}), operation_contract_request_schema(state.working_head, action.atomic_id))
|
|
|
|
self.assertEqual(stale_topology.code if stale_topology else None, ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
self.assertEqual(wrong_operation.code if wrong_operation else None, ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
after = repository.get_state(task_id)
|
|
self.assertEqual(after.version, before_version)
|
|
self.assertEqual(after.working_head, before_head)
|
|
self.assertEqual(list((artifacts.task_dir(task_id) / ".staging").iterdir()), [])
|
|
|
|
def test_geometry_conclusion_is_head_and_evidence_bound_before_state_change(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, _artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
self.assertIsNotNone(state)
|
|
schema = geometry_conclusion_schema(state.working_head, list(actions.diagnostic_evidence_refs(task_id, state)))
|
|
invalid = canonical_validate_schema(json.dumps({
|
|
"working_head": state.working_head,
|
|
"evidence_refs": ["evidence_unknown"],
|
|
"root_cause": "The candidate contradicts the action.",
|
|
"decision": "return_to_action_selection",
|
|
"corrective_intent": "Select a corrected action.",
|
|
}), schema)
|
|
self.assertEqual(invalid.code if invalid else None, ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
self.assertEqual(repository.get_state(task_id).working_head, state.working_head)
|
|
|
|
conclusion = GeometryConclusion(
|
|
working_head=state.working_head,
|
|
evidence_refs=["evidence_current_state"],
|
|
root_cause="The candidate contradicts the action.",
|
|
decision="return_to_action_selection",
|
|
corrective_intent="Select a corrected action.",
|
|
)
|
|
result = actions.record_geometry_conclusion(task_id, conclusion, invocation_id="diagnose-return")
|
|
self.assertEqual(getattr(result, "payload", {}).get("phase"), TaskPhase.AWAITING_ACTION.value)
|
|
after = repository.get_state(task_id)
|
|
self.assertIsNotNone(after)
|
|
self.assertTrue(after.repair_required)
|
|
self.assertIsNone(after.pending_action)
|
|
event = repository.ledger_events(task_id)[-1]
|
|
self.assertEqual(event["event"], "geometry_conclusion")
|
|
self.assertEqual(event["decision"], "return_to_action_selection")
|
|
|
|
def test_rollback_requires_current_diagnosis_and_an_ancestor_checkpoint(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, _artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
building = transition(state, "candidate_started", candidate_id="candidate_123456789012", candidate_stage_id="stage_1234567890abcdef12")
|
|
reviewing = transition(building, "candidate_built")
|
|
awaiting = transition(reviewing, "candidate_accepted", active_revision="rev_001")
|
|
self.assertTrue(repository.compare_and_swap(building))
|
|
self.assertTrue(repository.compare_and_swap(reviewing))
|
|
self.assertTrue(repository.compare_and_swap(awaiting, events=[{"event": "accepted", "revision_id": "rev_001"}]))
|
|
|
|
before_diagnosis = repository.get_state(task_id)
|
|
denied = actions.rollback_checkpoint(
|
|
task_id,
|
|
RollbackCheckpoint(working_head=before_diagnosis.working_head, checkpoint_token="checkpoint_root", reason="Discard the checkpoint."),
|
|
invocation_id="rollback-without-diagnosis",
|
|
)
|
|
self.assertEqual(getattr(denied, "error", None).code, ErrorCode.AUTHOR_DECISION_REJECTED)
|
|
|
|
conclusion = GeometryConclusion(
|
|
working_head=before_diagnosis.working_head,
|
|
evidence_refs=["evidence_current_model"],
|
|
root_cause="The accepted checkpoint has the wrong feature.",
|
|
decision="rollback",
|
|
corrective_intent="Return to the base checkpoint.",
|
|
)
|
|
self.assertIsNotNone(actions.record_geometry_conclusion(task_id, conclusion, invocation_id="diagnose-rollback"))
|
|
state = repository.get_state(task_id)
|
|
self.assertTrue(actions.rollback_available(task_id, state))
|
|
schema = rollback_checkpoint_schema(state.working_head, list(actions.checkpoint_tokens(task_id, state)))
|
|
invalid = canonical_validate_schema(json.dumps({
|
|
"working_head": state.working_head,
|
|
"checkpoint_token": "checkpoint_rev_999",
|
|
"reason": "Not in lineage.",
|
|
}), schema)
|
|
self.assertEqual(invalid.code if invalid else None, ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
|
|
result = actions.rollback_checkpoint(
|
|
task_id,
|
|
RollbackCheckpoint(working_head=state.working_head, checkpoint_token="checkpoint_root", reason="Discard the wrong checkpoint."),
|
|
invocation_id="rollback-root",
|
|
)
|
|
self.assertEqual(getattr(result, "payload", {}).get("status"), "rolled_back")
|
|
after = repository.get_state(task_id)
|
|
self.assertEqual(after.active_revision, "")
|
|
self.assertTrue(after.repair_required)
|
|
self.assertEqual(repository.ledger_events(task_id)[-1]["event"], "rollback")
|
|
self.assertEqual(actions._next_revision(task_id), "rev_002")
|
|
|
|
def test_repair_cannot_select_or_submit_again_before_a_geometry_conclusion(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, _artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
building = transition(state, "candidate_started", candidate_id="candidate_123456789012", candidate_stage_id="stage_1234567890abcdef12")
|
|
reviewing = transition(building, "candidate_built")
|
|
repairing = transition(reviewing, "candidate_rejected", repair_required=True, error=ErrorCode.CANDIDATE_REVIEW_REJECTED)
|
|
for value in (building, reviewing, repairing):
|
|
self.assertTrue(repository.compare_and_swap(value))
|
|
state = repository.get_state(task_id)
|
|
self.assertFalse(actions.repair_action_ready(task_id, state))
|
|
proposal = NextAction(
|
|
working_head=state.working_head,
|
|
intent="Create a corrected base.",
|
|
requirement_ids=["req_001"],
|
|
atomic_id="extrude_add_blind",
|
|
expected_change="Replace the rejected geometry.",
|
|
)
|
|
rejected = actions.propose_next_action(task_id, proposal, invocation_id="repair-before-diagnosis")
|
|
self.assertEqual(getattr(rejected, "error", None).code, ErrorCode.AUTHOR_DECISION_REJECTED)
|
|
|
|
conclusion = GeometryConclusion(
|
|
working_head=state.working_head,
|
|
evidence_refs=["evidence_current_state"],
|
|
root_cause="The rejected feature needs replacement.",
|
|
decision="return_to_action_selection",
|
|
corrective_intent="Choose a corrected base action.",
|
|
)
|
|
self.assertIsNotNone(actions.record_geometry_conclusion(task_id, conclusion, invocation_id="repair-diagnosis"))
|
|
state = repository.get_state(task_id)
|
|
self.assertTrue(actions.repair_action_ready(task_id, state))
|
|
accepted = actions.propose_next_action(
|
|
task_id,
|
|
NextAction(
|
|
working_head=state.working_head,
|
|
intent="Create a corrected base.",
|
|
requirement_ids=["req_001"],
|
|
atomic_id="extrude_add_blind",
|
|
expected_change="Replace the rejected geometry.",
|
|
),
|
|
invocation_id="repair-after-diagnosis",
|
|
)
|
|
self.assertEqual(getattr(accepted, "payload", {}).get("atomic_id"), "extrude_add_blind")
|
|
pending_state = repository.get_state(task_id)
|
|
self.assertTrue(actions.repair_action_ready(task_id, pending_state))
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, _artifacts, actions.runtime, None, None, RequirementsCommandHandler(repository, _artifacts, default_registry()), actions)
|
|
self.assertEqual(workflow._recovery_tools(task_id, pending_state), [])
|
|
self.assertEqual(
|
|
{item["function"]["name"] for item in workflow._action_tools(task_id, pending_state, set())},
|
|
{"submit_cdsl_fragment"},
|
|
)
|
|
|
|
def test_action_availability_hides_required_selector_operations_without_a_snapshot(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, _artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
available = set(actions.available_atomic_ids(task_id, state))
|
|
selector_required = {
|
|
atomic_id
|
|
for atomic_id in actions.runtime.supported_atomic_ids()
|
|
if str((actions.runtime.operation_contract(atomic_id).get("fragment_shape") or {}).get("selector_tokens") or "forbidden") == "required"
|
|
}
|
|
|
|
self.assertIn("extrude_add_blind", available)
|
|
self.assertTrue(selector_required)
|
|
self.assertTrue(selector_required.isdisjoint(available))
|
|
|
|
rejected = actions.propose_next_action(
|
|
task_id,
|
|
NextAction(
|
|
working_head=state.working_head,
|
|
intent="Cut a hole before any base exists.", requirement_ids=["req_001"],
|
|
atomic_id=next(iter(selector_required)), expected_change="This needs a face selector.",
|
|
),
|
|
invocation_id="unavailable-selector-action",
|
|
)
|
|
self.assertEqual(getattr(rejected, "error", None).code, ErrorCode.AUTHOR_DECISION_REJECTED)
|
|
|
|
def test_action_availability_hides_snapshot_bound_pattern_operations_without_source_features(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, _artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
available = set(actions.available_atomic_ids(task_id, state))
|
|
reference_required = {
|
|
atomic_id
|
|
for atomic_id in actions.runtime.supported_atomic_ids()
|
|
if (actions.runtime.operation_contract(atomic_id).get("reference_policy") or {}).get("mode") == "snapshot_bound"
|
|
}
|
|
|
|
self.assertIn("pattern_linear", reference_required)
|
|
self.assertTrue(reference_required.isdisjoint(available))
|
|
|
|
rejected = actions.propose_next_action(
|
|
task_id,
|
|
NextAction(
|
|
working_head=state.working_head,
|
|
intent="Pattern a source feature before any feature exists.", requirement_ids=["req_001"],
|
|
atomic_id="pattern_linear", expected_change="This needs a source feature reference.",
|
|
),
|
|
invocation_id="unavailable-reference-action",
|
|
)
|
|
self.assertEqual(getattr(rejected, "error", None).code, ErrorCode.AUTHOR_DECISION_REJECTED)
|
|
|
|
def test_action_availability_hides_operations_requiring_an_active_solid_at_root(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, _artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
available = set(actions.available_atomic_ids(task_id, state))
|
|
active_solid_required = {
|
|
atomic_id
|
|
for atomic_id in actions.runtime.supported_atomic_ids()
|
|
if "requires_active_solid" in (actions.runtime.operation_contract(atomic_id).get("semantic_preflight") or ())
|
|
}
|
|
|
|
self.assertEqual(
|
|
active_solid_required,
|
|
{"extrude_cut_blind", "revolve_cut", "reference_axis", "reference_plane"},
|
|
)
|
|
self.assertTrue(active_solid_required.isdisjoint(available))
|
|
|
|
rejected = actions.propose_next_action(
|
|
task_id,
|
|
NextAction(
|
|
working_head=state.working_head,
|
|
intent="Create a reference axis before any solid exists.", requirement_ids=["req_001"],
|
|
atomic_id="reference_axis", expected_change="This needs an active solid checkpoint.",
|
|
),
|
|
invocation_id="unavailable-root-reference-action",
|
|
)
|
|
self.assertEqual(getattr(rejected, "error", None).code, ErrorCode.AUTHOR_DECISION_REJECTED)
|
|
|
|
def test_new_candidate_rejection_invalidates_a_prior_repair_conclusion(self) -> None:
|
|
"""A conclusion may unlock one repair action, never its failed retries."""
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, first_action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
first_build = transition(state, "candidate_started", candidate_id="candidate_111111111111", candidate_stage_id="stage_111111111111111111")
|
|
first_rejected = transition(first_build, "candidate_rejected", repair_required=True, error=ErrorCode.CLAIM_VERIFICATION_FAILED)
|
|
self.assertTrue(repository.compare_and_swap(first_build))
|
|
self.assertTrue(repository.compare_and_swap(first_rejected, events=[{
|
|
"event": "candidate_rejected", "candidate_id": "candidate_111111111111",
|
|
"stage_id": "stage_111111111111111111", "action_id": first_action.action_id,
|
|
"working_head": first_action.working_head,
|
|
}]))
|
|
diagnosis_state = repository.get_state(task_id)
|
|
conclusion = GeometryConclusion(
|
|
working_head=diagnosis_state.working_head,
|
|
evidence_refs=["evidence_current_state"],
|
|
root_cause="The rejected base needs a different repair action.",
|
|
decision="return_to_action_selection",
|
|
corrective_intent="Select a corrected base feature.",
|
|
)
|
|
self.assertIsInstance(actions.record_geometry_conclusion(task_id, conclusion, invocation_id="first-diagnosis"), Accepted)
|
|
ready = repository.get_state(task_id)
|
|
self.assertTrue(actions.repair_action_ready(task_id, ready))
|
|
accepted = actions.propose_next_action(
|
|
task_id,
|
|
NextAction(
|
|
working_head=ready.working_head,
|
|
intent="Create a corrected base.", requirement_ids=["req_001"],
|
|
atomic_id="extrude_add_blind", expected_change="Replace the rejected geometry.",
|
|
),
|
|
invocation_id="second-action",
|
|
)
|
|
self.assertIsInstance(accepted, Accepted)
|
|
second_pending = repository.get_state(task_id)
|
|
second_action = second_pending.pending_action
|
|
self.assertIsNotNone(second_action)
|
|
second_build = transition(second_pending, "candidate_started", candidate_id="candidate_222222222222", candidate_stage_id="stage_222222222222222222")
|
|
second_rejected = transition(second_build, "candidate_rejected", repair_required=True, error=ErrorCode.CLAIM_VERIFICATION_FAILED)
|
|
self.assertTrue(repository.compare_and_swap(second_build))
|
|
self.assertTrue(repository.compare_and_swap(second_rejected, events=[{
|
|
"event": "candidate_rejected", "candidate_id": "candidate_222222222222",
|
|
"stage_id": "stage_222222222222222222", "action_id": second_action.action_id,
|
|
"working_head": second_action.working_head,
|
|
}]))
|
|
|
|
retrying = repository.get_state(task_id)
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, actions.runtime, None, None, RequirementsCommandHandler(repository, artifacts, default_registry()), actions)
|
|
|
|
self.assertFalse(actions.repair_action_ready(task_id, retrying))
|
|
self.assertEqual(
|
|
{item["function"]["name"] for item in workflow._recovery_tools(task_id, retrying)},
|
|
{"record_geometry_conclusion"},
|
|
)
|
|
|
|
def test_final_review_rejection_is_bounded_and_receives_diagnostic_feedback(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
artifacts.write_json_once(task_id, "revisions/rev_001/rebuild-report.json", {"health": {"solid_count": 1}})
|
|
artifacts.write_json_once(task_id, "revisions/rev_001/model.topology.json", {"records": []})
|
|
state = repository.get_state(task_id)
|
|
building = transition(state, "candidate_started", candidate_id="candidate_123456789012", candidate_stage_id="stage_1234567890abcdef12")
|
|
reviewing = transition(building, "candidate_built")
|
|
awaiting = transition(reviewing, "candidate_accepted", active_revision="rev_001")
|
|
for value in (building, reviewing, awaiting):
|
|
self.assertTrue(repository.compare_and_swap(value))
|
|
self.assertIsInstance(actions.complete_task(task_id, invocation_id="complete"), object)
|
|
|
|
payloads: list[dict[str, object]] = []
|
|
|
|
class InvalidCoverageGateway:
|
|
async def review(self, *, payload: dict[str, object], tool: dict[str, object], **_kwargs: object) -> dict[str, object]:
|
|
payloads.append(payload)
|
|
parameters = tool["function"]["parameters"] # type: ignore[index]
|
|
head = parameters["properties"]["working_head"]["const"] # type: ignore[index]
|
|
raw = json.dumps({
|
|
"working_head": head, "verdict": "pass",
|
|
"claim_coverage": [
|
|
{"claim_id": "claim_001", "status": "pass"},
|
|
{"claim_id": "claim_001", "status": "pass"},
|
|
],
|
|
})
|
|
return {"tool_calls": [{"function": {"name": "review_final", "arguments": raw}}], "usage": {"prompt_tokens": 1, "completion_tokens": 1}}
|
|
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=4, format_error_limit=2), repository, artifacts, actions.runtime,
|
|
None, InvalidCoverageGateway(), RequirementsCommandHandler(repository, artifacts, default_registry()), actions,
|
|
)
|
|
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
reviewer = type("Model", (), {"provider_id": "test", "model_id": "test-model"})()
|
|
return [item async for item in workflow.run(task_id=task_id, author=None, reviewer=reviewer)]
|
|
|
|
events = asyncio.run(run())
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.FAILED)
|
|
self.assertEqual(len(payloads), 2)
|
|
self.assertNotIn("previous_schema_or_state_error", payloads[0])
|
|
self.assertIn("previous_schema_or_state_error", payloads[1])
|
|
self.assertEqual(events[-1][1]["code"], ErrorCode.FAILED_AUTHOR_FORMAT.value)
|
|
|
|
def test_preflight_rejected_fragment_is_bounded_before_candidate_creation(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
calls = 0
|
|
|
|
class InvalidFragmentGateway:
|
|
async def call_tool(self, **_kwargs: object) -> dict[str, object]:
|
|
nonlocal calls
|
|
calls += 1
|
|
raw = json.dumps({"feature": {"atomic_id": "extrude_add_blind", "params": {"distance_mm": -1}}})
|
|
return {"tool_calls": [{"function": {"name": "submit_cdsl_fragment", "arguments": raw}}], "usage": {"prompt_tokens": 1, "completion_tokens": 1}}
|
|
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=4, format_error_limit=2), repository, artifacts, actions.runtime,
|
|
InvalidFragmentGateway(), None, RequirementsCommandHandler(repository, artifacts, default_registry()), actions,
|
|
)
|
|
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
author = type("Model", (), {"provider_id": "test", "model_id": "test-model"})()
|
|
return [item async for item in workflow.run(task_id=task_id, author=author, reviewer=None)]
|
|
|
|
events = asyncio.run(run())
|
|
self.assertEqual(calls, 2)
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.FAILED)
|
|
self.assertEqual(list((artifacts.task_dir(task_id) / ".staging").iterdir()), [])
|
|
self.assertEqual(events[-1][1]["code"], ErrorCode.FAILED_AUTHOR_FORMAT.value)
|
|
|
|
def test_candidate_artifact_io_failure_enters_waiting_retry_without_reclassifying_build(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, action = self._ready_action(Path(temporary))
|
|
fragment = {
|
|
"sketch": {
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 1},
|
|
},
|
|
"feature": {"atomic_id": action.atomic_id, "params": {"distance_mm": 2}},
|
|
}
|
|
with patch.object(actions.runtime, "rebuild", side_effect=OSError("artifact volume unavailable")):
|
|
result = actions.submit_cdsl_fragment(task_id, fragment, invocation_id="fragment_io")
|
|
|
|
self.assertIsInstance(result, Rejected)
|
|
self.assertEqual(result.error.code, ErrorCode.STORAGE_FAILURE)
|
|
after = repository.get_state(task_id)
|
|
self.assertEqual(after.phase, TaskPhase.WAITING_RETRY)
|
|
self.assertEqual(after.last_error, ErrorCode.STORAGE_FAILURE)
|
|
|
|
def test_candidate_render_failure_enters_render_retry_without_reclassifying_build(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, action = self._ready_action(Path(temporary))
|
|
fragment = {
|
|
"sketch": {
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 1},
|
|
},
|
|
"feature": {"atomic_id": action.atomic_id, "params": {"distance_mm": 2}},
|
|
}
|
|
with patch.object(actions.runtime, "rebuild", side_effect=RuntimeAdapterError("RENDER_SERVICE_UNAVAILABLE: renderer offline")):
|
|
result = actions.submit_cdsl_fragment(task_id, fragment, invocation_id="fragment_render")
|
|
|
|
self.assertIsInstance(result, Rejected)
|
|
self.assertEqual(result.error.code, ErrorCode.RENDER_SERVICE_UNAVAILABLE)
|
|
state = repository.get_state(task_id)
|
|
self.assertEqual(state.phase, TaskPhase.WAITING_RETRY)
|
|
self.assertEqual(state.last_error, ErrorCode.RENDER_SERVICE_UNAVAILABLE)
|
|
|
|
def test_candidate_stage_creation_failure_enters_waiting_retry_before_build(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, action = self._ready_action(Path(temporary))
|
|
fragment = {
|
|
"sketch": {
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 1},
|
|
},
|
|
"feature": {"atomic_id": action.atomic_id, "params": {"distance_mm": 2}},
|
|
}
|
|
with patch.object(artifacts, "start_candidate_stage", side_effect=OSError("disk full")):
|
|
result = actions.submit_cdsl_fragment(task_id, fragment, invocation_id="fragment_stage_io")
|
|
|
|
self.assertIsInstance(result, Rejected)
|
|
self.assertEqual(result.error.code, ErrorCode.STORAGE_FAILURE)
|
|
state = repository.get_state(task_id)
|
|
self.assertEqual(state.phase, TaskPhase.WAITING_RETRY)
|
|
self.assertEqual(state.retry_from_phase, TaskPhase.ACTION_PENDING)
|
|
|
|
def test_requirements_artifact_write_failure_enters_waiting_retry(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())
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{
|
|
"source_ids": ["src_001"], "statement": "One solid", "assumptions": [],
|
|
"acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}],
|
|
}]})
|
|
with patch.object(artifacts, "write_requirements_draft", side_effect=OSError("disk full")):
|
|
result = handler.submit_draft(task_id, batch, invocation_id="draft_io")
|
|
|
|
self.assertIsInstance(result, Rejected)
|
|
self.assertEqual(result.error.code, ErrorCode.STORAGE_FAILURE)
|
|
state = repository.get_state(task_id)
|
|
self.assertEqual(state.phase, TaskPhase.WAITING_RETRY)
|
|
self.assertEqual(state.retry_from_phase, TaskPhase.DRAFTING_REQUIREMENTS)
|
|
|
|
def test_requirements_freeze_is_json_first_and_markdown_is_derived(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
registry = default_registry()
|
|
handler = RequirementsCommandHandler(repository, artifacts, registry)
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a rectangular base.")
|
|
artifacts.initialize_task(task_id, "Create a rectangular base.")
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{"source_ids": ["src_001"], "statement": "One connected rectangular base", "assumptions": [], "acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}]}]})
|
|
self.assertEqual(handler.submit_draft(task_id, batch, invocation_id="draft_1").payload["draft_revision"], 1)
|
|
self.assertEqual(handler.finalize_draft(task_id, invocation_id="finalize_1").payload["phase"], TaskPhase.REVIEWING_REQUIREMENTS.value)
|
|
review = RequirementsReview.model_validate({"findings": []})
|
|
accepted = handler.record_review(task_id, review, invocation_id="review_1")
|
|
self.assertEqual(accepted.payload["phase"], TaskPhase.AWAITING_ACTION.value)
|
|
state = repository.get_state(task_id)
|
|
self.assertTrue(state.requirements_review_path.startswith("documents/requirements-review-"))
|
|
self.assertTrue(state.requirements_contract_path.startswith("documents/requirements-contract-"))
|
|
contract = artifacts.read_requirements_contract(task_id, state.requirements_contract_path)
|
|
self.assertEqual(contract["schema_version"], "cad.requirements-contract.v2")
|
|
self.assertEqual(contract["requirements"][0]["requirement_id"], "req_001")
|
|
self.assertEqual(contract["requirements"][0]["acceptance_claims"][0]["verification_mode"], "deterministic")
|
|
self.assertFalse((artifacts.task_dir(task_id) / "requirements.md").exists())
|
|
handler.ensure_rendered_contract_views(task_id, state)
|
|
self.assertTrue((artifacts.task_dir(task_id) / "requirements.md").is_file())
|
|
self.assertTrue((artifacts.task_dir(task_id) / "completion.md").is_file())
|
|
|
|
def test_live_evaluation_oracle_revises_reviewer_false_positive_before_freeze(self) -> None:
|
|
"""Fixture-only claims must block a reviewer pass before CAD begins."""
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
registry = default_registry()
|
|
handler = RequirementsCommandHandler(repository, artifacts, registry)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(registry))
|
|
task_id = "cad_123456abcdef"
|
|
request = "Create a single connected plate, thickness 10 mm."
|
|
repository.create_task(task_id, request)
|
|
artifacts.initialize_task(task_id, request)
|
|
handler.register_evaluation_contract_oracle(task_id, [
|
|
{"claim_kind": "solid_count_equals", "expected": {"value": 1}},
|
|
{"claim_kind": "bbox_dimension_mm", "expected": {"axis": "z", "value": 10}},
|
|
])
|
|
incomplete = RequirementsDraftBatch.model_validate({"items": [{
|
|
"source_ids": ["src_001"], "statement": "One solid plate", "assumptions": [],
|
|
"acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}],
|
|
}]})
|
|
reviewer_pass = RequirementsReview.model_validate({"findings": []})
|
|
self.assertIsInstance(handler.submit_draft(task_id, incomplete, invocation_id="draft_1"), Accepted)
|
|
self.assertIsInstance(handler.finalize_draft(task_id, invocation_id="finalize_1"), Accepted)
|
|
|
|
blocked = handler.record_review(task_id, reviewer_pass, invocation_id="review_1")
|
|
|
|
self.assertIsInstance(blocked, Accepted)
|
|
self.assertEqual(blocked.payload["phase"], TaskPhase.DRAFTING_REQUIREMENTS.value)
|
|
state = repository.get_state(task_id)
|
|
self.assertEqual(state.phase, TaskPhase.DRAFTING_REQUIREMENTS)
|
|
self.assertEqual(state.requirements_contract_path, "")
|
|
oracle = artifacts.read_requirements_review(task_id, state.requirements_review_path)
|
|
self.assertEqual(oracle["schema_version"], "cad.requirements-evaluation-oracle.v1")
|
|
self.assertEqual(oracle["reviewer_decision"], "freeze")
|
|
self.assertEqual(oracle["missing_claims"], [{
|
|
"claim_kind": "bbox_dimension_mm",
|
|
"expected": {"axis": "z", "value": 10},
|
|
}])
|
|
reviewer_artifacts = list((artifacts.task_dir(task_id) / "documents").glob("requirements-review-*.json"))
|
|
self.assertEqual(len(reviewer_artifacts), 1)
|
|
self.assertEqual(json.loads(reviewer_artifacts[0].read_text(encoding="utf-8"))["decision"], "freeze")
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, runtime, None, None, handler, actions)
|
|
author_context = json.loads(workflow._author_context(task_id, [])[1]["content"])
|
|
self.assertEqual(author_context["review"]["missing_claims"], oracle["missing_claims"])
|
|
|
|
patched = RequirementsPatchBatch.model_validate({"patches": [{
|
|
"target_draft_id": "draft_001", "op": "replace", "item": {
|
|
"source_ids": ["src_001"], "statement": "One solid plate, thickness 10 mm", "assumptions": [],
|
|
"acceptance_claims": [
|
|
{"claim_kind": "solid_count_equals", "expected": {"value": 1}},
|
|
{"claim_kind": "bbox_dimension_mm", "expected": {"axis": "z", "value": 10, "tolerance_mm": 0.01}},
|
|
],
|
|
},
|
|
}]})
|
|
self.assertIsInstance(handler.patch_draft(task_id, patched, invocation_id="patch_1"), Accepted)
|
|
self.assertIsInstance(handler.finalize_draft(task_id, invocation_id="finalize_2"), Accepted)
|
|
approved = handler.record_review(task_id, reviewer_pass, invocation_id="review_2")
|
|
|
|
self.assertIsInstance(approved, Accepted)
|
|
self.assertEqual(approved.payload["phase"], TaskPhase.AWAITING_ACTION.value)
|
|
contract = artifacts.read_requirements_contract(task_id, repository.get_state(task_id).requirements_contract_path)
|
|
self.assertEqual(
|
|
contract["requirements"][0]["acceptance_claims"][1]["expected"],
|
|
{"axis": "z", "value": 10, "tolerance_mm": 0.01},
|
|
)
|
|
|
|
def test_production_requirements_review_without_evaluation_oracle_still_freezes(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())
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{
|
|
"source_ids": ["src_001"], "statement": "One solid", "assumptions": [],
|
|
"acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}],
|
|
}]})
|
|
review = RequirementsReview.model_validate({"findings": []})
|
|
handler.submit_draft(task_id, batch, invocation_id="draft_1")
|
|
handler.finalize_draft(task_id, invocation_id="finalize_1")
|
|
result = handler.record_review(task_id, review, invocation_id="review_1")
|
|
|
|
self.assertIsInstance(result, Accepted)
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.AWAITING_ACTION)
|
|
|
|
def test_full_circle_equal_spacing_is_normalized_without_user_confirmation(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())
|
|
task_id = "cad_123456abcdef"
|
|
request = "Create eight equally spaced holes around a full circle at 60 degree spacing."
|
|
repository.create_task(task_id, request)
|
|
artifacts.initialize_task(task_id, request)
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{
|
|
"source_ids": ["src_001"], "statement": request, "assumptions": [],
|
|
"acceptance_claims": [{"claim_kind": "visual", "expected": {"description": "Eight holes are evenly distributed around the full circle."}}],
|
|
}]})
|
|
handler.submit_draft(task_id, batch, invocation_id="draft_1")
|
|
handler.finalize_draft(task_id, invocation_id="finalize_1")
|
|
review = RequirementsReview.model_validate({"findings": [{
|
|
"draft_id": "draft_001", "source_ids": ["src_001"],
|
|
"finding_type": "derivable_conflict", "description": "A full circle with eight equal instances has 45 degree spacing.",
|
|
"normalization": {"rule_id": "full_circle_equal_spacing", "count": 8, "declared_spacing_degrees": 60, "full_circle": True},
|
|
}]})
|
|
|
|
result = handler.record_review(task_id, review, invocation_id="review_1")
|
|
|
|
self.assertIsInstance(result, Accepted)
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.AWAITING_ACTION)
|
|
contract = artifacts.read_requirements_contract(task_id, repository.get_state(task_id).requirements_contract_path)
|
|
self.assertEqual(contract["applied_normalizations"][0]["adopted"]["spacing_degrees"], 45.0)
|
|
self.assertTrue(any(event.get("event") == "requirements_normalized" for event in repository.ledger_events(task_id)))
|
|
|
|
def test_verification_gap_with_visual_claim_freezes_with_risk(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())
|
|
task_id = "cad_123456abcdef"
|
|
request = "Place the counterbores on the front side."
|
|
repository.create_task(task_id, request)
|
|
artifacts.initialize_task(task_id, request)
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{
|
|
"source_ids": ["src_001"], "statement": request, "assumptions": [],
|
|
"acceptance_claims": [{"claim_kind": "visual", "expected": {"description": "Counterbores are visibly on the front side."}}],
|
|
}]})
|
|
handler.submit_draft(task_id, batch, invocation_id="draft_1")
|
|
handler.finalize_draft(task_id, invocation_id="finalize_1")
|
|
review = RequirementsReview.model_validate({"findings": [{
|
|
"draft_id": "draft_001", "source_ids": ["src_001"],
|
|
"finding_type": "verification_gap", "description": "Feature-local front-side placement requires independent visual review.",
|
|
}]})
|
|
|
|
result = handler.record_review(task_id, review, invocation_id="review_1")
|
|
|
|
self.assertIsInstance(result, Accepted)
|
|
contract = artifacts.read_requirements_contract(task_id, repository.get_state(task_id).requirements_contract_path)
|
|
self.assertEqual(contract["requirements"][0]["acceptance_claims"][0]["verification_mode"], "visual")
|
|
self.assertEqual(len(contract["verification_warnings"]), 1)
|
|
projection = repository.get_task_projection(task_id)
|
|
self.assertEqual(projection["verification_status"], "completed_with_risks")
|
|
|
|
def test_ambiguous_conflict_waits_with_a_precise_question(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())
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Use either four or six holes.")
|
|
artifacts.initialize_task(task_id, "Use either four or six holes.")
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{
|
|
"source_ids": ["src_001"], "statement": "Use either four or six holes.", "assumptions": [],
|
|
"acceptance_claims": [{"claim_kind": "visual", "expected": {"description": "The chosen hole pattern is visible."}}],
|
|
}]})
|
|
handler.submit_draft(task_id, batch, invocation_id="draft_1")
|
|
handler.finalize_draft(task_id, invocation_id="finalize_1")
|
|
question = "Should the part contain four holes or six holes?"
|
|
review = RequirementsReview.model_validate({"findings": [{
|
|
"draft_id": "draft_001", "source_ids": ["src_001"], "finding_type": "ambiguous_conflict",
|
|
"description": "Two different hole counts are allowed.", "question": question,
|
|
}]})
|
|
|
|
result = handler.record_review(task_id, review, invocation_id="review_1")
|
|
|
|
self.assertIsInstance(result, Waiting)
|
|
self.assertEqual(result.error.details["questions"], [question])
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.WAITING_FOR_USER)
|
|
projection = repository.get_task_projection(task_id)
|
|
self.assertEqual(projection["message"], "Requirements contain an ambiguity that cannot be resolved deterministically.")
|
|
self.assertEqual(projection["questions"], [question])
|
|
self.assertEqual(projection["issues"], ["Two different hole counts are allowed."])
|
|
self.assertTrue(projection["user_action_required"])
|
|
|
|
def test_missing_derived_contract_views_park_for_storage_retry_not_internal_failure(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(root)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
workflow = WorkflowCoordinator(WorkflowConfig(1, 1), repository, artifacts, runtime, None, None, requirements, actions)
|
|
|
|
with patch.object(requirements, "ensure_rendered_contract_views", side_effect=OSError("disk full")):
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
return [item async for item in workflow.run(task_id=task_id, author=None, reviewer=None)] # type: ignore[arg-type]
|
|
|
|
events = asyncio.run(run())
|
|
|
|
state = repository.get_state(task_id)
|
|
self.assertEqual(state.phase, TaskPhase.WAITING_RETRY)
|
|
self.assertEqual(state.last_error, ErrorCode.STORAGE_FAILURE)
|
|
self.assertEqual(events[-1][1]["code"], ErrorCode.STORAGE_FAILURE.value)
|
|
|
|
def test_action_ledger_mirror_failure_parks_for_storage_retry(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(root)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
workflow = WorkflowCoordinator(WorkflowConfig(1, 1), repository, artifacts, runtime, None, None, requirements, actions)
|
|
|
|
with patch.object(artifacts, "sync_action_ledger", side_effect=OSError("ledger disk unavailable")):
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
return [item async for item in workflow.run(task_id=task_id, author=None, reviewer=None)] # type: ignore[arg-type]
|
|
|
|
events = asyncio.run(run())
|
|
|
|
state = repository.get_state(task_id)
|
|
self.assertEqual(state.phase, TaskPhase.WAITING_RETRY)
|
|
self.assertEqual(state.last_error, ErrorCode.STORAGE_FAILURE)
|
|
self.assertEqual(events[-1][1]["lifecycle"], "waiting_retry")
|
|
|
|
def test_requirements_review_coverage_is_derived_from_draft_sources(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())
|
|
task_id = "cad_123456abcdef"
|
|
request = "Create a single connected base.\n\nAdd one cylindrical bore."
|
|
repository.create_task(task_id, request)
|
|
artifacts.initialize_task(task_id, request)
|
|
batch = RequirementsDraftBatch.model_validate({"items": [
|
|
{"source_ids": ["src_001"], "statement": "One connected base", "assumptions": [], "acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}]},
|
|
{"source_ids": ["src_002"], "statement": "One bore", "assumptions": [], "acceptance_claims": [{"claim_kind": "cylindrical_bore", "expected": {"diameter_mm": 5}}]},
|
|
]})
|
|
handler.submit_draft(task_id, batch, invocation_id="draft_1")
|
|
handler.finalize_draft(task_id, invocation_id="finalize_1")
|
|
review = RequirementsReview.model_validate({"findings": []})
|
|
result = handler.record_review(task_id, review, invocation_id="review_1")
|
|
|
|
self.assertIsInstance(result, Accepted)
|
|
persisted = artifacts.read_requirements_review(task_id, repository.get_state(task_id).requirements_review_path)
|
|
self.assertEqual(persisted["coverage"], [
|
|
{"source_id": "src_001", "requirement_ids": ["req_001"]},
|
|
{"source_id": "src_002", "requirement_ids": ["req_002"]},
|
|
])
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.AWAITING_ACTION)
|
|
|
|
def test_requirements_review_rejects_service_owned_decision_fields(self) -> None:
|
|
with self.assertRaisesRegex(ValueError, "Extra inputs are not permitted"):
|
|
RequirementsReview.model_validate({
|
|
"verdict": "pass",
|
|
"coverage": [{"source_id": "src_001", "requirement_ids": ["req_001"]}],
|
|
"item_verdicts": [{"draft_id": "draft_001", "verdict": "pass"}],
|
|
"issues": ["Conflicting bore diameter."],
|
|
})
|
|
|
|
def test_requirements_invocation_replays_after_the_state_has_advanced(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())
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a rectangular base.")
|
|
artifacts.initialize_task(task_id, "Create a rectangular base.")
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{"source_ids": ["src_001"], "statement": "One connected base", "assumptions": [], "acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}]}]})
|
|
|
|
first = handler.submit_draft(task_id, batch, invocation_id="draft_replay")
|
|
replay = handler.submit_draft(task_id, batch, invocation_id="draft_replay")
|
|
|
|
self.assertEqual(first.payload, replay.payload)
|
|
self.assertEqual(repository.get_state(task_id).version, 1)
|
|
self.assertEqual(len(artifacts.read_requirements_draft(task_id)["items"]), 1)
|
|
records = repository.invocation_records(task_id)
|
|
self.assertEqual(len(records), 1)
|
|
self.assertEqual(records[0]["status"], "finished")
|
|
|
|
def test_requirements_patch_tool_is_hidden_until_reviewer_requests_revision(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
registry = default_registry()
|
|
requirements = RequirementsCommandHandler(repository, artifacts, registry)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(registry))
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, runtime, None, None, requirements, actions)
|
|
draft_schema = requirements.draft_schema(task_id)
|
|
patch_schema = requirements.patch_schema(task_id)
|
|
initial = {item["function"]["name"] for item in workflow._requirements_author_tools(task_id, draft_schema, patch_schema)}
|
|
self.assertEqual(initial, {"submit_requirements_draft_batch"})
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{"source_ids": ["src_001"], "statement": "One solid", "assumptions": [], "acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}]}]})
|
|
requirements.submit_draft(task_id, batch, invocation_id="draft_1")
|
|
finalizable = {item["function"]["name"] for item in workflow._requirements_author_tools(task_id, draft_schema, patch_schema)}
|
|
self.assertEqual(finalizable, {"finalize_requirements_draft"})
|
|
requirements.finalize_draft(task_id, invocation_id="finalize_1")
|
|
review = RequirementsReview.model_validate({"findings": [{"draft_id": "draft_001", "source_ids": ["src_001"], "finding_type": "missing_source_semantics", "description": "Clarify the requirement."}]})
|
|
requirements.record_review(task_id, review, invocation_id="review_1")
|
|
revised = {item["function"]["name"] for item in workflow._requirements_author_tools(task_id, draft_schema, requirements.patch_schema(task_id))}
|
|
self.assertEqual(revised, {"patch_requirements_draft"})
|
|
|
|
def test_requirements_patch_context_distinguishes_server_owned_draft_id(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
registry = default_registry()
|
|
requirements = RequirementsCommandHandler(repository, artifacts, registry)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(registry))
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{"source_ids": ["src_001"], "statement": "One solid", "assumptions": [], "acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}]}]})
|
|
requirements.submit_draft(task_id, batch, invocation_id="draft_1")
|
|
requirements.finalize_draft(task_id, invocation_id="finalize_1")
|
|
requirements.record_review(task_id, RequirementsReview.model_validate({"findings": [{"draft_id": "draft_001", "source_ids": ["src_001"], "finding_type": "claim_mismatch", "description": "Revise dimensions."}]}), invocation_id="review_1")
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, runtime, None, None, requirements, actions)
|
|
|
|
context = json.loads(workflow._author_context(task_id, [])[1]["content"])
|
|
instruction = str(context["instruction"])
|
|
schema = requirements.patch_schema(task_id)
|
|
patch = schema["$defs"]["RequirementsPatch"]
|
|
self.assertIn("target_draft_id", instruction)
|
|
self.assertIn('"patches"', instruction)
|
|
self.assertIn('"op":"replace"', instruction)
|
|
self.assertIn("MUST NOT include draft_id", instruction)
|
|
self.assertIn("inside one patches[] entry", patch["properties"]["target_draft_id"]["description"])
|
|
|
|
def test_requirement_patch_recovers_from_the_cad_02df83c6d283_top_level_shape(self) -> None:
|
|
"""A reviewer-requested patch must be repairable without touching CAD state.
|
|
|
|
This is the exact malformed outer shape returned by cad_02df83c6d283:
|
|
target_draft_id/op/item were emitted at the tool-call root instead of
|
|
inside RequirementsPatchBatch.patches. The next author turn receives
|
|
precise feedback and can submit the correctly nested payload.
|
|
"""
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry()))
|
|
task_id = "cad_02df83c6d283"
|
|
request = "生成一个三孔机械连杆,三个孔的中心位于同一直线上,相邻孔中心距分别为60毫米和80毫米,三个孔直径均为12毫米,连杆厚度10毫米,每个孔周围外圆直径32毫米,各段外轮廓平滑连接。"
|
|
artifacts.initialize_task(task_id, request)
|
|
repository.create_task(task_id, request)
|
|
|
|
incomplete_item = {
|
|
"source_ids": ["src_001"],
|
|
"statement": "生成一个连通的三孔机械连杆。",
|
|
"assumptions": [],
|
|
"acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}],
|
|
}
|
|
revised_item = {
|
|
"source_ids": ["src_001"],
|
|
"statement": "生成一个三孔机械连杆:孔中心共线,相邻中心距为60 mm和80 mm,孔径12 mm,厚度10 mm,外圆直径32 mm且轮廓平滑连接。",
|
|
"assumptions": [],
|
|
"acceptance_claims": [
|
|
{"claim_kind": "solid_count_equals", "expected": {"value": 1}},
|
|
{"claim_kind": "bbox_dimension_mm", "expected": {"axis": "z", "value": 10, "tolerance_mm": 0.01}},
|
|
{"claim_kind": "cylindrical_bore", "expected": {"diameter_mm": 12, "count": 3, "tolerance_mm": 0.01}},
|
|
{"claim_kind": "through_cylindrical_bore", "expected": {"diameter_mm": 12, "count": 3, "tolerance_mm": 0.01}},
|
|
{"claim_kind": "collinear_through_bore_chain", "expected": {"diameter_mm": 12, "adjacent_distances_mm": [60, 80], "tolerance_mm": 0.01}},
|
|
],
|
|
}
|
|
|
|
class RepairingAuthor:
|
|
def __init__(self) -> None:
|
|
self.patch_messages: list[list[dict[str, object]]] = []
|
|
self.patch_draft_revisions: list[int] = []
|
|
self.calls: list[str] = []
|
|
|
|
async def call_tool(self, *, messages: list[dict[str, object]], tool: dict[str, object], required_tool_name: str, **_kwargs: object) -> dict[str, object]:
|
|
self.calls.append(required_tool_name)
|
|
if required_tool_name == "submit_requirements_draft_batch":
|
|
arguments: dict[str, object] = {"items": [incomplete_item]}
|
|
elif required_tool_name == "finalize_requirements_draft":
|
|
arguments = {}
|
|
elif required_tool_name == "patch_requirements_draft":
|
|
self.patch_messages.append(messages)
|
|
self.patch_draft_revisions.append(int(artifacts.read_requirements_draft(task_id)["revision"]))
|
|
arguments = (
|
|
{
|
|
"target_draft_id": "draft_001",
|
|
"op": "replace",
|
|
"reason": "补充可量测的尺寸验收声明。",
|
|
"item": revised_item,
|
|
}
|
|
if len(self.patch_messages) == 1
|
|
else {"patches": [{"target_draft_id": "draft_001", "op": "replace", "item": revised_item}]}
|
|
)
|
|
elif required_tool_name == "propose_next_action":
|
|
raise StructuredModelError("author transport paused after requirements approval")
|
|
else:
|
|
raise AssertionError(f"unexpected author tool: {required_tool_name}")
|
|
return {
|
|
"tool_calls": [{"function": {"name": required_tool_name, "arguments": json.dumps(arguments)}}],
|
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
|
}
|
|
|
|
class RevisingReviewer:
|
|
def __init__(self) -> None:
|
|
self.calls = 0
|
|
|
|
async def review(self, *, tool: dict[str, object], **_kwargs: object) -> dict[str, object]:
|
|
self.calls += 1
|
|
arguments = {"findings": [{
|
|
"draft_id": "draft_001",
|
|
"source_ids": ["src_001"],
|
|
"finding_type": "missing_source_semantics",
|
|
"description": "补充孔距、共线、厚度和外圆尺寸的可量测验收声明。",
|
|
}]} if self.calls == 1 else {"findings": []}
|
|
return {
|
|
"tool_calls": [{"function": {"name": "review_requirements", "arguments": json.dumps(arguments)}}],
|
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
|
}
|
|
|
|
author = RepairingAuthor()
|
|
reviewer = RevisingReviewer()
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=8, format_error_limit=2), repository, artifacts, runtime,
|
|
author, reviewer, requirements, actions,
|
|
)
|
|
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
identity = type("Model", (), {"provider_id": "test", "model_id": "test-model"})()
|
|
return [item async for item in workflow.run(task_id=task_id, author=identity, reviewer=identity)]
|
|
|
|
events = asyncio.run(run())
|
|
state = repository.get_state(task_id)
|
|
self.assertEqual(author.calls, [
|
|
"submit_requirements_draft_batch", "finalize_requirements_draft",
|
|
"patch_requirements_draft", "patch_requirements_draft",
|
|
"finalize_requirements_draft", "propose_next_action",
|
|
])
|
|
self.assertEqual(reviewer.calls, 2)
|
|
# The malformed root object is rejected before the handler sees a
|
|
# patch: the retry still observes the original frozen draft.
|
|
self.assertEqual(author.patch_draft_revisions, [1, 1])
|
|
self.assertEqual(state.phase, TaskPhase.WAITING_RETRY)
|
|
self.assertEqual(state.retry_from_phase, TaskPhase.AWAITING_ACTION)
|
|
self.assertTrue(state.requirements_contract_path)
|
|
self.assertEqual(artifacts.read_requirements_draft(task_id)["revision"], 2)
|
|
feedback = next(message["content"] for message in author.patch_messages[1] if message["role"] == "user" and "schema_or_state_error" in str(message["content"]))
|
|
self.assertIn('"path": "/patches"', str(feedback))
|
|
self.assertFalse(any(name == "candidate_result" for name, _payload in events))
|
|
self.assertEqual(list((artifacts.task_dir(task_id) / ".staging").iterdir()), [])
|
|
|
|
audits = [item for item in repository.tool_audits(task_id) if item["tool"] == "patch_requirements_draft"]
|
|
self.assertEqual(len(audits), 2)
|
|
self.assertFalse(audits[0]["canonical_schema_valid"])
|
|
self.assertTrue(audits[1]["canonical_schema_valid"])
|
|
malformed_event = next(
|
|
payload for name, payload in events
|
|
if name == "tool_call" and payload.get("tool") == "patch_requirements_draft" and payload.get("status") == "error"
|
|
)
|
|
self.assertTrue(any(error["path"] == "/patches" for error in malformed_event["result"]["field_errors"]))
|
|
self.assertTrue(any(error["path"] == "/reason" for error in malformed_event["result"]["field_errors"]))
|
|
|
|
def test_repeated_top_level_requirement_patch_shape_fails_with_field_audit(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry()))
|
|
task_id = "cad_123456abcdef"
|
|
artifacts.initialize_task(task_id, "Create a plate with a measurable thickness.")
|
|
repository.create_task(task_id, "Create a plate with a measurable thickness.")
|
|
item = {
|
|
"source_ids": ["src_001"], "statement": "One connected plate", "assumptions": [],
|
|
"acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}],
|
|
}
|
|
|
|
class RepeatingAuthor:
|
|
async def call_tool(self, *, required_tool_name: str, **_kwargs: object) -> dict[str, object]:
|
|
if required_tool_name == "submit_requirements_draft_batch":
|
|
arguments: dict[str, object] = {"items": [item]}
|
|
elif required_tool_name == "finalize_requirements_draft":
|
|
arguments = {}
|
|
elif required_tool_name == "patch_requirements_draft":
|
|
arguments = {
|
|
"target_draft_id": "draft_001",
|
|
"op": "replace",
|
|
"reason": "Add the missing dimension claim.",
|
|
"item": item,
|
|
}
|
|
else:
|
|
raise AssertionError(f"unexpected author tool: {required_tool_name}")
|
|
return {"tool_calls": [{"function": {"name": required_tool_name, "arguments": json.dumps(arguments)}}], "usage": {"prompt_tokens": 1, "completion_tokens": 1}}
|
|
|
|
class RevisingReviewer:
|
|
async def review(self, **_kwargs: object) -> dict[str, object]:
|
|
arguments = {"findings": [{
|
|
"draft_id": "draft_001", "source_ids": ["src_001"],
|
|
"finding_type": "missing_source_semantics", "description": "Add the thickness claim.",
|
|
}]}
|
|
return {"tool_calls": [{"function": {"name": "review_requirements", "arguments": json.dumps(arguments)}}], "usage": {"prompt_tokens": 1, "completion_tokens": 1}}
|
|
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=5, format_error_limit=2), repository, artifacts, runtime,
|
|
RepeatingAuthor(), RevisingReviewer(), requirements, actions,
|
|
)
|
|
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
identity = type("Model", (), {"provider_id": "test", "model_id": "test-model"})()
|
|
return [item async for item in workflow.run(task_id=task_id, author=identity, reviewer=identity)]
|
|
|
|
events = asyncio.run(run())
|
|
terminal = events[-1][1]
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.FAILED)
|
|
self.assertEqual(terminal["code"], ErrorCode.FAILED_AUTHOR_FORMAT.value)
|
|
self.assertEqual(terminal["tool"], "patch_requirements_draft")
|
|
self.assertTrue(any(error["path"] == "/patches" for error in terminal["field_errors"]))
|
|
self.assertTrue(any(error["path"] == "/reason" for error in terminal["field_errors"]))
|
|
patch_audits = [item for item in repository.tool_audits(task_id) if item["tool"] == "patch_requirements_draft"]
|
|
self.assertEqual(len(patch_audits), 2)
|
|
self.assertTrue(all(not item["canonical_schema_valid"] for item in patch_audits))
|
|
self.assertEqual(list((artifacts.task_dir(task_id) / ".staging").iterdir()), [])
|
|
|
|
def test_candidate_review_payload_exposes_render_evidence_at_the_top_level(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
stage = artifacts.start_candidate_stage(task_id, "candidate-key", {"idempotency_key": "candidate-key", "candidate_id": "candidate_123456789012", "action_id": action.action_id})
|
|
candidate = {"candidate_id": "candidate_123456789012", "stage_id": stage.stage_id, "action_id": action.action_id, "render_manifest": {"contact_sheet_path": "/tmp/contact-sheet.jpg"}, "claim_results": [{"claim_id": "claim_001", "claim_kind": "solid_count_equals", "deterministic": True, "status": "pass"}]}
|
|
artifacts.write_stage_json(task_id, stage.stage_id, "candidate.json", candidate)
|
|
building = transition(state, "candidate_started", candidate_id="candidate_123456789012", candidate_stage_id=stage.stage_id)
|
|
review = transition(building, "candidate_built", candidate_id="candidate_123456789012", candidate_stage_id=stage.stage_id)
|
|
self.assertTrue(repository.compare_and_swap(building))
|
|
self.assertTrue(repository.compare_and_swap(review))
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, actions.runtime, None, None, RequirementsCommandHandler(repository, artifacts, default_registry()), actions)
|
|
captured: dict[str, object] = {}
|
|
|
|
class CapturingGateway:
|
|
async def review(self, *, payload: dict[str, object], **_kwargs: object) -> dict[str, object]:
|
|
captured.update(payload)
|
|
return {"tool_calls": []}
|
|
|
|
workflow.review_gateway = CapturingGateway()
|
|
result = asyncio.run(workflow._review_candidate(task_id, type("Model", (), {"provider_id": "test", "model_id": "test-model"})(), review))
|
|
self.assertIsInstance(result, WorkflowError)
|
|
self.assertEqual(captured["render_manifest"], candidate["render_manifest"])
|
|
self.assertNotIn("source_requirements", captured)
|
|
self.assertIn("pending", str(captured["instruction"]))
|
|
self.assertIn("MUST NOT cause rejection", str(captured["instruction"]))
|
|
|
|
def test_candidate_review_audit_uses_checkpoint_head_bound_by_schema(self) -> None:
|
|
"""A built candidate is reviewed against its action checkpoint, not v+2 state."""
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
stage = artifacts.start_candidate_stage(task_id, "candidate-key", {
|
|
"idempotency_key": "candidate-key", "candidate_id": "candidate_123456789012", "action_id": action.action_id,
|
|
})
|
|
candidate = {
|
|
"candidate_id": "candidate_123456789012", "stage_id": stage.stage_id,
|
|
"action_id": action.action_id,
|
|
"claim_results": [{"claim_id": "claim_001", "claim_kind": "solid_count_equals", "deterministic": True, "status": "pass"}],
|
|
}
|
|
artifacts.write_stage_json(task_id, stage.stage_id, "candidate.json", candidate)
|
|
building = transition(state, "candidate_started", candidate_id="candidate_123456789012", candidate_stage_id=stage.stage_id)
|
|
reviewing = transition(building, "candidate_built", candidate_id="candidate_123456789012", candidate_stage_id=stage.stage_id)
|
|
self.assertTrue(repository.compare_and_swap(building))
|
|
self.assertTrue(repository.compare_and_swap(reviewing))
|
|
self.assertNotEqual(repository.get_state(task_id).working_head, action.working_head)
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, actions.runtime, None, None, RequirementsCommandHandler(repository, artifacts, default_registry()), actions)
|
|
|
|
class ValidGateway:
|
|
async def review(self, **_kwargs: object) -> dict[str, object]:
|
|
arguments = {
|
|
"candidate_id": "candidate_123456789012", "working_head": action.working_head,
|
|
"verdict": "accept", "claim_coverage": [{"claim_id": "claim_001", "status": "pass"}],
|
|
}
|
|
return {"tool_calls": [{"function": {"name": "review_candidate", "arguments": json.dumps(arguments)}}], "usage": {"prompt_tokens": 1, "completion_tokens": 1}}
|
|
|
|
workflow.review_gateway = ValidGateway()
|
|
result = asyncio.run(workflow._review_candidate(task_id, type("Model", (), {"provider_id": "test", "model_id": "test-model"})(), reviewing))
|
|
|
|
self.assertIsInstance(result, CandidateReview)
|
|
audit = repository.tool_audits(task_id)[-1]
|
|
self.assertTrue(audit["canonical_schema_valid"])
|
|
self.assertTrue(audit["state_binding"]["binding_valid"])
|
|
self.assertEqual(audit["state_binding"]["expected_working_head"], action.working_head)
|
|
self.assertEqual(audit["state_binding"]["working_head_binding_source"], "schema_const")
|
|
|
|
def test_requirements_review_instruction_requires_executable_claim_completeness(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry()))
|
|
task_id = "cad_123456abcdef"
|
|
request = "Create a single connected flange, diameter 120 mm and thickness 12 mm, with six 8 mm holes on a 90 mm PCD."
|
|
artifacts.initialize_task(task_id, request)
|
|
repository.create_task(task_id, request)
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{
|
|
"source_ids": ["src_001"], "statement": "Single flange", "assumptions": [],
|
|
"acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}],
|
|
}]})
|
|
requirements.submit_draft(task_id, batch, invocation_id="draft_1")
|
|
requirements.finalize_draft(task_id, invocation_id="finalize_1")
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, runtime, None, None, requirements, actions)
|
|
captured: dict[str, object] = {}
|
|
|
|
class CapturingGateway:
|
|
async def review(self, *, payload: dict[str, object], **_kwargs: object) -> dict[str, object]:
|
|
captured.update(payload)
|
|
arguments = {"findings": [{
|
|
"draft_id": "draft_001", "source_ids": ["src_001"],
|
|
"finding_type": "missing_source_semantics", "description": "Missing flange dimensions and hole-pattern claims.",
|
|
}]}
|
|
return {"tool_calls": [{"function": {"name": "review_requirements", "arguments": json.dumps(arguments)}}], "usage": {"prompt_tokens": 1, "completion_tokens": 1}}
|
|
|
|
workflow.review_gateway = CapturingGateway()
|
|
result = asyncio.run(workflow._review_requirements(task_id, type("Model", (), {"provider_id": "test", "model_id": "test-model"})()))
|
|
|
|
self.assertIsInstance(result, RequirementsReview)
|
|
instruction = str(captured["instruction"])
|
|
self.assertIn("service derives coverage", instruction)
|
|
self.assertIn("number, count, dimension, relationship", instruction)
|
|
self.assertIn("verification_gap", instruction)
|
|
self.assertIn("full-circle equally spaced pattern", instruction)
|
|
|
|
def test_live_evaluation_review_context_marks_only_declared_verifier_gaps(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
requirements = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry()))
|
|
task_id = "cad_123456abcdef"
|
|
request = "Create a plate with a visually smooth outside contour."
|
|
repository.create_task(task_id, request)
|
|
artifacts.initialize_task(task_id, request)
|
|
requirements.register_evaluation_contract_oracle(
|
|
task_id,
|
|
[{"claim_kind": "solid_count_equals", "expected": {"value": 1}}],
|
|
validation_capability_gaps=[{"id": "smooth_outline", "description": "Smooth exterior tangency"}],
|
|
)
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{
|
|
"source_ids": ["src_001"], "statement": "One connected smooth plate", "assumptions": [],
|
|
"acceptance_claims": [
|
|
{"claim_kind": "solid_count_equals", "expected": {"value": 1}},
|
|
{"claim_kind": "visual", "expected": {"description": "The outside contour is visibly smooth."}},
|
|
],
|
|
}]})
|
|
requirements.submit_draft(task_id, batch, invocation_id="draft_1")
|
|
requirements.finalize_draft(task_id, invocation_id="finalize_1")
|
|
workflow = WorkflowCoordinator(WorkflowConfig(8, 2), repository, artifacts, runtime, None, None, requirements, actions)
|
|
captured: dict[str, object] = {}
|
|
|
|
class CapturingGateway:
|
|
async def review(self, *, payload: dict[str, object], **_kwargs: object) -> dict[str, object]:
|
|
captured.update(payload)
|
|
arguments = {"findings": [{
|
|
"draft_id": "draft_001", "source_ids": ["src_001"],
|
|
"finding_type": "verification_gap", "description": "Smooth exterior tangency requires visual review.",
|
|
}]}
|
|
return {"tool_calls": [{"function": {"name": "review_requirements", "arguments": json.dumps(arguments)}}], "usage": {"prompt_tokens": 1, "completion_tokens": 1}}
|
|
|
|
workflow.review_gateway = CapturingGateway()
|
|
result = asyncio.run(workflow._review_requirements(task_id, type("Model", (), {"provider_id": "test", "model_id": "test-model"})()))
|
|
|
|
self.assertIsInstance(result, RequirementsReview)
|
|
self.assertEqual(captured["evaluation_context"], {
|
|
"evaluation_only": True,
|
|
"known_validation_capability_gaps": [{"id": "smooth_outline", "description": "Smooth exterior tangency"}],
|
|
})
|
|
self.assertIn("Do not request an invented executable claim", str(captured["instruction"]))
|
|
self.assertIn("Do not request an invented executable claim", str(captured["instruction"]))
|
|
|
|
ordinary = RequirementsCommandHandler(repository, artifacts, default_registry())
|
|
self.assertIsNone(ordinary.evaluation_review_context(task_id))
|
|
|
|
def test_author_and_reviewer_service_failures_record_usage_metadata(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
|
|
class UnavailableAuthor:
|
|
async def call_tool(self, **_kwargs: object) -> dict[str, object]:
|
|
raise StructuredModelError("author unavailable")
|
|
|
|
workflow = WorkflowCoordinator(
|
|
WorkflowConfig(max_turns=1, format_error_limit=1), repository, artifacts, runtime,
|
|
UnavailableAuthor(), None, RequirementsCommandHandler(repository, artifacts, default_registry()),
|
|
ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry())),
|
|
)
|
|
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
author = type("Model", (), {"provider_id": "test", "model_id": "test-model"})()
|
|
return [item async for item in workflow.run(task_id=task_id, author=author, reviewer=None)]
|
|
|
|
asyncio.run(run())
|
|
records = repository.usage_summary(task_id)["records"]
|
|
self.assertEqual(len(records), 1)
|
|
self.assertEqual(records[0]["retry_reason"], "provider_unavailable")
|
|
self.assertFalse(records[0]["usage_available"])
|
|
self.assertFalse(records[0]["cache_hit"])
|
|
self.assertGreater(records[0]["context_chars"], 0)
|
|
|
|
def test_dynamic_requirement_claim_schema_rejects_unknown_expected_fields_before_write(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())
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{"source_ids": ["src_001"], "statement": "One solid", "assumptions": [], "acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1, "ignored": True}}]}]})
|
|
result = handler.submit_draft(task_id, batch, invocation_id="draft_invalid")
|
|
self.assertEqual(result.error.code, ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
self.assertEqual(artifacts.read_requirements_draft(task_id)["items"], [])
|
|
self.assertEqual(repository.get_state(task_id).version, 0)
|
|
|
|
def test_pattern_contract_uses_opaque_current_head_reference_enum(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
runtime = ProfileCadRuntime(settings(Path(temporary)))
|
|
contract = runtime.operation_contract("pattern_linear")
|
|
token = next(iter(runtime.reference_tokens({"features": [{"id": "feature_001"}]})))
|
|
schema = fragment_schema(contract, selector_tokens=[], reference_tokens=[token])
|
|
items = schema["properties"]["feature"]["properties"]["params"]["properties"]["source_feature_ids"]["items"]
|
|
self.assertEqual(items, {"enum": [token]})
|
|
self.assertTrue(validate_fragment(contract, {"feature": {"atomic_id": "pattern_linear", "params": {"source_feature_ids": ["feature_001"], "direction_1": [1, 0, 0], "spacing_1_mm": 4, "pattern_count_1": 2}}}, selector_tokens=[], reference_tokens=[token]))
|
|
|
|
def test_candidate_stage_recovery_advances_without_rebuild(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
stage = artifacts.start_candidate_stage(task_id, "candidate-key", {"idempotency_key": "candidate-key", "candidate_id": "candidate_123456789012", "action_id": action.action_id})
|
|
artifacts.write_stage_json(task_id, stage.stage_id, "candidate.json", {"candidate_id": "candidate_123456789012", "stage_id": stage.stage_id, "action_id": action.action_id, "claim_results": [{"claim_id": "claim_001", "claim_kind": "solid_count_equals", "deterministic": True, "status": "pass"}]})
|
|
building = transition(state, "candidate_started", candidate_id="candidate_123456789012", candidate_stage_id=stage.stage_id)
|
|
self.assertTrue(repository.compare_and_swap(building))
|
|
result = actions.recover_candidate_build(task_id)
|
|
self.assertEqual(result.payload["status"], "awaiting_review")
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.CANDIDATE_REVIEW)
|
|
|
|
def test_rejected_candidate_stage_is_retained_for_restart_diagnostics(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
stage = artifacts.start_candidate_stage(task_id, "rejected-stage-key", {"candidate_id": "candidate_123456789012"})
|
|
building = transition(state, "candidate_started", candidate_id="candidate_123456789012", candidate_stage_id=stage.stage_id)
|
|
reviewing = transition(building, "candidate_built")
|
|
rejected = transition(reviewing, "candidate_rejected", repair_required=True, error=ErrorCode.CANDIDATE_REVIEW_REJECTED)
|
|
self.assertTrue(repository.compare_and_swap(building))
|
|
self.assertTrue(repository.compare_and_swap(reviewing))
|
|
self.assertTrue(repository.compare_and_swap(rejected, events=[{"event": "candidate_rejected", "candidate_id": "candidate_123456789012", "stage_id": stage.stage_id}]))
|
|
workflow = WorkflowCoordinator(WorkflowConfig(0, 1), repository, artifacts, actions.runtime, None, None, RequirementsCommandHandler(repository, artifacts, default_registry()), actions)
|
|
|
|
async def resume_once() -> list[tuple[str, dict[str, object]]]:
|
|
return [item async for item in workflow.run(task_id=task_id, author=None, reviewer=None)]
|
|
|
|
asyncio.run(resume_once())
|
|
self.assertTrue(Path(stage.output_dir).is_dir())
|
|
|
|
def test_published_candidate_review_recovery_commits_once(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
stage = artifacts.start_candidate_stage(task_id, "candidate-key", {"idempotency_key": "candidate-key", "candidate_id": "candidate_123456789012", "action_id": action.action_id})
|
|
candidate = {"candidate_id": "candidate_123456789012", "stage_id": stage.stage_id, "action_id": action.action_id, "claim_results": [{"claim_id": "claim_001", "claim_kind": "solid_count_equals", "deterministic": True, "status": "pass"}]}
|
|
artifacts.write_stage_json(task_id, stage.stage_id, "candidate.json", candidate)
|
|
review = CandidateReview.model_validate({"candidate_id": "candidate_123456789012", "working_head": action.working_head, "verdict": "accept", "claim_coverage": [{"claim_id": "claim_001", "status": "pass"}]})
|
|
artifacts.write_stage_json(task_id, stage.stage_id, "candidate-review.json", review.model_dump(mode="json"))
|
|
building = transition(state, "candidate_started", candidate_id="candidate_123456789012", candidate_stage_id=stage.stage_id)
|
|
reviewing = transition(building, "candidate_built", candidate_id="candidate_123456789012", candidate_stage_id=stage.stage_id)
|
|
self.assertTrue(repository.compare_and_swap(building))
|
|
self.assertTrue(repository.compare_and_swap(reviewing))
|
|
artifacts.publish_candidate(task_id, stage.stage_id, "rev_001")
|
|
result = actions.recover_candidate_review(task_id)
|
|
self.assertEqual(result.payload["revision_id"], "rev_001")
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.AWAITING_ACTION)
|
|
self.assertEqual(len([item for item in repository.get_task_projection(task_id)["action_ledger_summary"] if item.get("event") == "accepted"]), 1)
|
|
|
|
def test_candidate_publication_rebases_render_evidence_to_the_revision(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
task_id = "cad_123456abcdef"
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
stage = artifacts.start_candidate_stage(task_id, "publication-key", {"candidate_id": "candidate_123456789012"})
|
|
stage_root = Path(stage.output_dir)
|
|
render = stage_root / "renders" / "contact-sheet.jpg"
|
|
render.parent.mkdir(parents=True)
|
|
render.write_bytes(b"jpeg")
|
|
artifacts.write_stage_json(task_id, stage.stage_id, "candidate.json", {"render_manifest": {"contact_sheet_path": str(render)}})
|
|
artifacts.publish_candidate(task_id, stage.stage_id, "rev_001")
|
|
candidate = artifacts.read_json(task_id, "revisions/rev_001/candidate.json")
|
|
path = Path(str(candidate["render_manifest"]["contact_sheet_path"]))
|
|
self.assertEqual(path, artifacts.task_dir(task_id) / "revisions" / "rev_001" / "renders" / "contact-sheet.jpg")
|
|
self.assertTrue(path.is_file())
|
|
|
|
def test_complete_task_is_revision_scoped_idempotent(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
# A real candidate would have produced these immutable facts before
|
|
# action selection becomes available again.
|
|
artifacts.write_json_once(task_id, "revisions/rev_001/rebuild-report.json", {"health": {"solid_count": 1, "bbox_mm": {"dimensions": [10, 10, 1]}}})
|
|
artifacts.write_json_once(task_id, "revisions/rev_001/model.topology.json", {"records": []})
|
|
building = transition(state, "candidate_started", candidate_id="candidate_123456789012", candidate_stage_id="stage_1234567890abcdef12")
|
|
self.assertTrue(repository.compare_and_swap(building))
|
|
awaiting = transition(building, "candidate_built")
|
|
self.assertTrue(repository.compare_and_swap(awaiting))
|
|
awaiting = transition(awaiting, "candidate_accepted", active_revision="rev_001", repair_required=False)
|
|
self.assertTrue(repository.compare_and_swap(awaiting))
|
|
first = actions.complete_task(task_id, invocation_id="complete_first")
|
|
second = actions.complete_task(task_id, invocation_id="complete_duplicate")
|
|
self.assertEqual(first.payload, second.payload)
|
|
self.assertEqual(repository.get_state(task_id).phase, TaskPhase.FINAL_VALIDATION)
|
|
|
|
def test_unexpected_final_review_failure_preserves_checkpoint_once(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
building = transition(state, "candidate_started", candidate_id="candidate_123456789012", candidate_stage_id="stage_1234567890abcdef12")
|
|
reviewing = transition(building, "candidate_built")
|
|
awaiting = transition(reviewing, "candidate_accepted", active_revision="rev_001")
|
|
final = transition(awaiting, "final_requested")
|
|
for value in (building, reviewing, awaiting, final):
|
|
self.assertTrue(repository.compare_and_swap(value))
|
|
|
|
class BrokenActions(ActionCommandHandler):
|
|
def _facts(self, task_id: str, revision_id: str) -> dict[str, object]:
|
|
raise NameError("global_violations")
|
|
|
|
broken = BrokenActions(actions.repository, actions.artifacts, actions.runtime, actions.verifiers)
|
|
workflow = WorkflowCoordinator(WorkflowConfig(2, 1), repository, artifacts, actions.runtime, None, None, RequirementsCommandHandler(repository, artifacts, default_registry()), broken)
|
|
|
|
async def run() -> list[tuple[str, dict[str, object]]]:
|
|
return [item async for item in workflow.run(task_id=task_id, author=None, reviewer=None)]
|
|
|
|
events = asyncio.run(run())
|
|
after = repository.get_state(task_id)
|
|
self.assertEqual(after.phase, TaskPhase.FAILED)
|
|
self.assertEqual(after.active_revision, "rev_001")
|
|
self.assertEqual(after.last_error, ErrorCode.FAILED_INTERNAL)
|
|
self.assertEqual(events[-1][1]["code"], ErrorCode.FAILED_INTERNAL.value)
|
|
|
|
def test_final_review_requires_each_visual_claim_to_pass_independently(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(default_registry()))
|
|
task_id = "cad_123456abcdef"
|
|
state = repository.create_task(task_id, "Create a visually inspectable single solid.")
|
|
artifacts.initialize_task(task_id, "Create a visually inspectable single solid.")
|
|
contract = {
|
|
"schema_version": "cad.requirements-contract.v1",
|
|
"task_id": task_id,
|
|
"contract_hash": "visual-contract",
|
|
"requirements": [{
|
|
"requirement_id": "req_001", "draft_id": "draft_001", "source_ids": ["src_001"],
|
|
"statement": "One visible solid", "assumptions": [],
|
|
"acceptance_claims": [
|
|
{"claim_id": "claim_001", "claim_kind": "solid_count_equals", "expected": {"value": 1}},
|
|
{"claim_id": "claim_002", "claim_kind": "visual", "expected": {"description": "The rendered part is visibly coherent."}},
|
|
],
|
|
}],
|
|
}
|
|
contract_path = artifacts.write_requirements_contract(task_id, contract, invocation_id="visual_contract")
|
|
for event in ("draft_updated", "requirements_finalized", "requirements_approved"):
|
|
state = transition(state, event, requirements_contract_path=contract_path if event == "requirements_approved" else None)
|
|
self.assertTrue(repository.compare_and_swap(state))
|
|
state = transition(repository.get_state(task_id), "final_requested", active_revision="rev_001")
|
|
self.assertTrue(repository.compare_and_swap(state))
|
|
artifacts.write_json_once(task_id, "revisions/rev_001/rebuild-report.json", {"health": {"solid_count": 1}})
|
|
artifacts.write_json_once(task_id, "revisions/rev_001/model.topology.json", {"records": []})
|
|
|
|
result = actions.record_final_review(
|
|
task_id,
|
|
FinalReview.model_validate({
|
|
"working_head": state.working_head,
|
|
"verdict": "pass",
|
|
"claim_coverage": [
|
|
{"claim_id": "claim_001", "status": "pass"},
|
|
{"claim_id": "claim_002", "status": "pending"},
|
|
],
|
|
}),
|
|
invocation_id="visual-review-pending",
|
|
)
|
|
self.assertEqual(getattr(result, "payload", {}).get("status"), "repair")
|
|
after = repository.get_state(task_id)
|
|
self.assertEqual(after.phase, TaskPhase.AWAITING_ACTION)
|
|
self.assertTrue(after.repair_required)
|
|
event = repository.ledger_events(task_id)[-1]
|
|
self.assertEqual(event["event"], "final_review_repair")
|
|
self.assertEqual(event["visual_not_passed"], ["claim_002"])
|
|
self.assertTrue(artifacts.read_json(task_id, "reviews/final/rev_001/final-review.json"))
|
|
self.assertIsNone(artifacts.read_json(task_id, "revisions/rev_001/final-review.json"))
|
|
|
|
def test_action_rejections_do_not_create_stage_or_change_head(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
runtime = ProfileCadRuntime(settings(root))
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
registry = default_registry()
|
|
requirements = RequirementsCommandHandler(repository, artifacts, registry)
|
|
actions = ActionCommandHandler(repository, artifacts, runtime, RegistryVerifierExecutor(registry))
|
|
task_id = "cad_123456abcdef"
|
|
repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
batch = RequirementsDraftBatch.model_validate({"items": [{"source_ids": ["src_001"], "statement": "One plate", "assumptions": [], "acceptance_claims": [{"claim_kind": "solid_count_equals", "expected": {"value": 1}}]}]})
|
|
requirements.submit_draft(task_id, batch, invocation_id="draft_1")
|
|
requirements.finalize_draft(task_id, invocation_id="finalize_1")
|
|
requirements.record_review(task_id, RequirementsReview.model_validate({"findings": []}), invocation_id="review_1")
|
|
state = repository.get_state(task_id)
|
|
self.assertIsNotNone(state)
|
|
proposal = NextAction.model_validate({"working_head": state.working_head, "intent": "Create base", "requirement_ids": ["req_001"], "atomic_id": "extrude_add_blind", "expected_change": "Add base"})
|
|
self.assertTrue(hasattr(actions.propose_next_action(task_id, proposal, invocation_id="inv_1"), "payload"))
|
|
before = repository.get_state(task_id)
|
|
rejected = actions.submit_cdsl_fragment(task_id, {"feature": {"atomic_id": "extrude_add_blind", "params": {"distance_mm": -1}}}, invocation_id="inv_2")
|
|
after = repository.get_state(task_id)
|
|
self.assertEqual(rejected.error.code, ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
self.assertEqual(before.working_head, after.working_head)
|
|
self.assertEqual(list((artifacts.task_dir(task_id) / ".staging").iterdir()), [])
|
|
|
|
def test_wrong_selector_kind_fails_the_handler_dynamic_schema_before_materialization(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository, artifacts, actions, task_id, _action = self._ready_action(Path(temporary))
|
|
state = repository.get_state(task_id)
|
|
contract = actions.runtime.operation_contract("hole_blind")
|
|
pending = PendingAction("act_selector", state.working_head, "Cut bore", ("req_001",), "hole_blind", "Cut a bore", contract["contract_hash"], "selector-key")
|
|
selector_state = transition(state, "diagnosis_return_to_action_selection")
|
|
self.assertTrue(repository.compare_and_swap(selector_state))
|
|
selector_state = transition(selector_state, "action_proposed", pending_action=pending)
|
|
self.assertTrue(repository.compare_and_swap(selector_state))
|
|
artifacts.write_json_once(task_id, "revisions/rev_001/model.topology.json", {
|
|
"snapshot_id": "snapshot_001",
|
|
"records": [{
|
|
"record_id": "edge_001", "kind": "edge", "executable": True,
|
|
"geometry": {"surface_type": "line"}, "owner_feature_ids": ["feature_001"],
|
|
}],
|
|
})
|
|
# Point state at the topology-bearing checkpoint without changing
|
|
# the pending action head: this fixture only tests schema token
|
|
# narrowing, not a concurrent command transition.
|
|
current = repository.get_state(task_id)
|
|
repository.compare_and_swap(replace(current, version=current.version + 1, active_revision="rev_001"))
|
|
token = next(iter(actions.runtime.selector_tokens(artifacts.read_topology(task_id, "rev_001"))))
|
|
before = repository.get_state(task_id)
|
|
result = actions.submit_cdsl_fragment(
|
|
task_id,
|
|
{"feature": {"atomic_id": "hole_blind", "selector_tokens": [token], "params": {"diameter_mm": 2, "depth_mm": 4, "positions": [{"mm": [0, 0, 0]}]}}},
|
|
invocation_id="wrong-selector-kind",
|
|
)
|
|
after = repository.get_state(task_id)
|
|
self.assertEqual(result.error.code, ErrorCode.AUTHOR_FORMAT_INVALID)
|
|
self.assertEqual(before.version, after.version)
|
|
self.assertEqual(list((artifacts.task_dir(task_id) / ".staging").iterdir()), [])
|
|
|
|
def test_sqlite_cas_idempotency_and_outbox_are_atomic(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository = SqliteTaskRepository(Path(temporary) / "state.sqlite3")
|
|
task_id = "cad_123456abcdef"
|
|
initial = repository.create_task(task_id, "x")
|
|
changed = transition(initial, "draft_updated")
|
|
self.assertTrue(repository.compare_and_swap(changed, events=[{"event": "draft"}]))
|
|
self.assertFalse(repository.compare_and_swap(changed, events=[{"event": "duplicate"}]))
|
|
invocation = repository.begin_invocation(task_id, "inv_a", "same-key")
|
|
repository.finish_invocation(invocation.invocation_id, {"ok": True})
|
|
repeat = repository.begin_invocation(task_id, "inv_b", "same-key")
|
|
self.assertEqual(repeat.invocation_id, "inv_a")
|
|
self.assertEqual(repeat.result, {"ok": True})
|
|
outbox = repository.pending_outbox()
|
|
self.assertEqual(outbox[0]["event"], "draft")
|
|
repository.mark_outbox_published(outbox[0]["event_id"])
|
|
self.assertEqual(repository.pending_outbox(), [])
|
|
|
|
def test_invocation_completion_rolls_back_with_a_failed_state_cas(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository = SqliteTaskRepository(Path(temporary) / "state.sqlite3")
|
|
task_id = "cad_123456abcdef"
|
|
initial = repository.create_task(task_id, "x")
|
|
invocation = repository.begin_invocation(task_id, "inv_atomic", "atomic-key")
|
|
changed = transition(initial, "draft_updated")
|
|
|
|
with self.assertRaisesRegex(ValueError, "Invocation is not an active"):
|
|
repository.compare_and_swap(
|
|
changed,
|
|
events=[{"event": "must_not_commit"}],
|
|
invocation_id="missing_invocation",
|
|
invocation_result={"status": "accepted"},
|
|
)
|
|
|
|
self.assertEqual(repository.get_state(task_id).version, initial.version)
|
|
self.assertEqual(repository.ledger_events(task_id), [])
|
|
self.assertEqual(repository.get_invocation(task_id, invocation.invocation_id).status, "processing")
|
|
|
|
self.assertTrue(repository.compare_and_swap(
|
|
changed,
|
|
events=[{"event": "committed"}],
|
|
invocation_id=invocation.invocation_id,
|
|
invocation_result={"status": "accepted"},
|
|
))
|
|
completed = repository.get_invocation(task_id, invocation.invocation_id)
|
|
self.assertEqual(completed.status, "finished")
|
|
self.assertEqual(completed.result, {"status": "accepted"})
|
|
# A concurrent loser or legacy cleanup must never replace the
|
|
# result that was committed with the state transition.
|
|
repository.finish_invocation(invocation.invocation_id, {"status": "stale"})
|
|
self.assertEqual(repository.get_invocation(task_id, invocation.invocation_id).result, {"status": "accepted"})
|
|
|
|
def test_outbox_retries_delivery_and_uses_a_stable_idempotency_key(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository = SqliteTaskRepository(Path(temporary) / "state.sqlite3")
|
|
task_id = "cad_123456abcdef"
|
|
initial = repository.create_task(task_id, "x")
|
|
self.assertTrue(repository.compare_and_swap(transition(initial, "draft_updated"), events=[{"event": "draft"}]))
|
|
|
|
class FlakyPublisher:
|
|
def __init__(self) -> None:
|
|
self.calls = 0
|
|
self.events: list[dict[str, object]] = []
|
|
|
|
async def publish(self, event: dict[str, object]) -> None:
|
|
self.calls += 1
|
|
if self.calls == 1:
|
|
raise RuntimeError("transport disconnected")
|
|
self.events.append(event)
|
|
|
|
publisher = FlakyPublisher()
|
|
dispatcher = OutboxDispatcher(repository, publisher)
|
|
with self.assertRaisesRegex(RuntimeError, "transport disconnected"):
|
|
asyncio.run(dispatcher.dispatch_pending())
|
|
self.assertEqual(len(repository.pending_outbox()), 1)
|
|
delivered = asyncio.run(dispatcher.dispatch_pending())
|
|
self.assertEqual(len(delivered), 1)
|
|
self.assertEqual(delivered[0]["event_id"], publisher.events[0]["event_id"])
|
|
self.assertEqual(repository.pending_outbox(), [])
|
|
|
|
idempotent = IdempotentInProcessPublisher()
|
|
asyncio.run(idempotent.publish(delivered[0]))
|
|
asyncio.run(idempotent.publish(delivered[0]))
|
|
self.assertEqual(len(idempotent.delivered), 1)
|
|
|
|
def test_outbox_dispatch_scope_does_not_acknowledge_another_task(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
repository = SqliteTaskRepository(Path(temporary) / "state.sqlite3")
|
|
first = repository.create_task("cad_123456abcdef", "x")
|
|
second = repository.create_task("cad_abcdef123456", "y")
|
|
self.assertTrue(repository.compare_and_swap(transition(first, "draft_updated"), events=[{"event": "first"}]))
|
|
self.assertTrue(repository.compare_and_swap(transition(second, "draft_updated"), events=[{"event": "second"}]))
|
|
publisher = IdempotentInProcessPublisher()
|
|
first_events = asyncio.run(OutboxDispatcher(repository, publisher).dispatch_pending(task_id=first.task_id))
|
|
self.assertEqual([item["task_id"] for item in first_events], [first.task_id])
|
|
self.assertEqual([item["task_id"] for item in repository.pending_outbox()], [second.task_id])
|
|
|
|
def test_action_ledger_jsonl_replays_committed_sqlite_events_idempotently(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
repository = SqliteTaskRepository(root / "state.sqlite3")
|
|
artifacts = FileArtifactStore(root / "tasks")
|
|
task_id = "cad_123456abcdef"
|
|
state = repository.create_task(task_id, "Create a plate.")
|
|
artifacts.initialize_task(task_id, "Create a plate.")
|
|
updated = transition(state, "draft_updated")
|
|
self.assertTrue(repository.compare_and_swap(updated, events=[{"event": "requirements_draft_updated", "draft_revision": 1}]))
|
|
|
|
events = repository.ledger_events(task_id)
|
|
self.assertEqual(artifacts.sync_action_ledger(task_id, events), "actions/action-ledger.jsonl")
|
|
self.assertEqual(artifacts.sync_action_ledger(task_id, events), "actions/action-ledger.jsonl")
|
|
ledger = artifacts.task_dir(task_id) / "actions" / "action-ledger.jsonl"
|
|
lines = [json.loads(line) for line in ledger.read_text(encoding="utf-8").splitlines()]
|
|
self.assertEqual(len(lines), 1)
|
|
self.assertEqual(lines[0]["schema_version"], "cad.action-ledger.v1")
|
|
self.assertEqual(lines[0]["sequence"], events[0]["sequence"])
|
|
self.assertEqual(lines[0]["event"], "requirements_draft_updated")
|
|
|
|
def test_domain_and_application_do_not_import_delivery_or_adapter_implementations(self) -> None:
|
|
forbidden_domain = ("fastapi", "sqlite", "pathlib", "app.services", "app.settings", "langchain", "engine")
|
|
forbidden_application = ("app.cad_agent.adapters", "app.services", "app.settings")
|
|
for path in (ROOT / "backend" / "app" / "cad_agent" / "domain").glob("*.py"):
|
|
source = path.read_text(encoding="utf-8")
|
|
self.assertFalse(any(token in source for token in forbidden_domain), path.name)
|
|
for path in (ROOT / "backend" / "app" / "cad_agent" / "application").glob("*.py"):
|
|
source = path.read_text(encoding="utf-8")
|
|
self.assertFalse(any(token in source for token in forbidden_application), path.name)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|