"""Helper script for subplan spawn Robot Framework smoke tests. Exercises SubplanService spawn workflow, validation, and metadata persistence without requiring the full service layer. """ from __future__ import annotations import sys from unittest.mock import MagicMock from cleveragents.application.services.subplan_service import ( SpawnEntry, SubplanService, ) from cleveragents.domain.models.core.decision import ( ContextSnapshot, Decision, DecisionType, ResourceRef, ) from cleveragents.domain.models.core.plan import ( ExecutionMode, NamespacedName, Plan, PlanIdentity, SubplanConfig, ) _PLAN_ID = "01HGZ6FE0AQDYTR4BXVQZ6PN00" _ROOT_ID = "01HGZ6FE0AQDYTR4BXVQZ6RF00" _DEC_ID1 = "01HGZ6FE0AQDYTR4BXVQZ6DA00" _DEC_ID2 = "01HGZ6FE0AQDYTR4BXVQZ6DB00" _DEC_ID3 = "01HGZ6FE0AQDYTR4BXVQZ6DC00" def _mock_decision_service() -> MagicMock: svc: MagicMock = MagicMock() svc.list_by_type = MagicMock(return_value=[]) return svc def _make_decision( decision_id: str, plan_id: str = _PLAN_ID, decision_type: DecisionType = DecisionType.SUBPLAN_SPAWN, sequence: int = 0, chosen_option: str = "local/sub-action", resource_ids: list[str] | None = None, ) -> Decision: resources: list[ResourceRef] = [] if resource_ids: resources = [ResourceRef(resource_id=rid) for rid in resource_ids] return Decision( decision_id=decision_id, plan_id=plan_id, decision_type=decision_type, sequence_number=sequence, question="Spawn child plan?", chosen_option=chosen_option, context_snapshot=ContextSnapshot(relevant_resources=resources), ) def _make_plan( plan_id: str = _PLAN_ID, parent_plan_id: str | None = None, root_plan_id: str | None = None, ) -> Plan: return Plan( identity=PlanIdentity( plan_id=plan_id, parent_plan_id=parent_plan_id, root_plan_id=root_plan_id, ), namespaced_name=NamespacedName(namespace="local", name="test-plan"), description="Robot test plan", action_name="local/test-action", ) def _spawn_single() -> None: """Spawn a single child plan from a decision entry.""" svc = SubplanService(decision_service=_mock_decision_service()) plan = _make_plan() config = SubplanConfig(execution_mode=ExecutionMode.SEQUENTIAL) dec = _make_decision(_DEC_ID1) entries = [SpawnEntry(decision=dec, action_name="local/sub-action")] result = svc.spawn(parent_plan=plan, config=config, spawn_entries=entries) assert result.total_spawned == 1 assert len(result.spawned_statuses) == 1 meta = next(iter(result.metadata.values())) assert meta.parent_plan_id == _PLAN_ID print("spawn-single-ok") def _spawn_multiple() -> None: """Spawn multiple child plans from decision entries.""" svc = SubplanService(decision_service=_mock_decision_service()) plan = _make_plan() config = SubplanConfig( execution_mode=ExecutionMode.PARALLEL, max_parallel=5, ) entries = [] for i, did in enumerate([_DEC_ID1, _DEC_ID2, _DEC_ID3]): dec = _make_decision(did, sequence=i) entries.append(SpawnEntry(decision=dec, action_name=f"local/sub-{i}")) result = svc.spawn(parent_plan=plan, config=config, spawn_entries=entries) assert result.total_spawned == 3 ids = [s.subplan_id for s in result.spawned_statuses] assert len(ids) == len(set(ids)), "Duplicate IDs" print("spawn-multiple-ok") def _validate_resource_scope() -> None: """Validate spawn with missing resource scope.""" svc = SubplanService(decision_service=_mock_decision_service()) config = SubplanConfig(execution_mode=ExecutionMode.SEQUENTIAL) dec = _make_decision(_DEC_ID1, resource_ids=["res-missing"]) entries = [ SpawnEntry( decision=dec, action_name="local/sub-action", target_resources=["res-missing"], ) ] result = svc.validate_spawn( config=config, spawn_entries=entries, available_resources=frozenset({"res-a", "res-b"}), ) assert not result.valid assert any("resource" in e.lower() for e in result.errors) print("validate-resource-ok") def _validate_max_parallel() -> None: """Validate spawn exceeding max_parallel bound.""" svc = SubplanService(decision_service=_mock_decision_service()) config = SubplanConfig( execution_mode=ExecutionMode.PARALLEL, max_parallel=2, ) entries = [] for i in range(5): did = f"01HGZ6FE0AQDYTR4BXVQZ6D{i:02d}0" dec = _make_decision(did, sequence=i) entries.append(SpawnEntry(decision=dec, action_name=f"local/sub-{i}")) result = svc.validate_spawn(config=config, spawn_entries=entries) assert not result.valid assert any("max_parallel" in e.lower() for e in result.errors) print("validate-max-parallel-ok") def _metadata_persistence() -> None: """Verify spawn metadata contains correct plan IDs.""" svc = SubplanService(decision_service=_mock_decision_service()) plan = _make_plan( parent_plan_id="01HGZ6FE0AQDYTR4BXVQZ6PP00", root_plan_id=_ROOT_ID, ) config = SubplanConfig(execution_mode=ExecutionMode.SEQUENTIAL) dec = _make_decision(_DEC_ID1) entries = [SpawnEntry(decision=dec, action_name="local/sub-action")] result = svc.spawn(parent_plan=plan, config=config, spawn_entries=entries) meta = next(iter(result.metadata.values())) assert meta.root_plan_id == _ROOT_ID assert meta.execution_mode == "sequential" print("metadata-persistence-ok") def _validation_guards() -> None: """Verify service rejects invalid construction and inputs.""" # None decision_service try: SubplanService(decision_service=None) # type: ignore[arg-type] raise AssertionError("Should have raised ValueError") except ValueError: pass # None parent_plan svc = SubplanService(decision_service=_mock_decision_service()) try: svc.spawn( parent_plan=None, # type: ignore[arg-type] config=SubplanConfig(), spawn_entries=[ SpawnEntry( decision=_make_decision(_DEC_ID1), action_name="local/test", ) ], ) raise AssertionError("Should have raised ValueError") except ValueError: pass # Empty spawn_entries try: svc.spawn( parent_plan=_make_plan(), config=SubplanConfig(), spawn_entries=[], ) raise AssertionError("Should have raised ValueError") except ValueError: pass print("validation-guards-ok") def _build_entries() -> None: """Build spawn entries from decisions.""" svc = SubplanService(decision_service=_mock_decision_service()) decisions = [ _make_decision( _DEC_ID1, resource_ids=["01HGZ6FE0AQDYTR4BXVQZ6RA00"], chosen_option="local/test-sub", ), _make_decision( _DEC_ID2, sequence=1, chosen_option="local/test-sub-2", ), ] entries = svc.build_spawn_entries(decisions) assert len(entries) == 2 assert entries[0].action_name == "local/test-sub" assert len(entries[0].target_resources) == 1 print("build-entries-ok") _COMMANDS: dict[str, object] = { "spawn-single": _spawn_single, "spawn-multiple": _spawn_multiple, "validate-resource": _validate_resource_scope, "validate-max-parallel": _validate_max_parallel, "metadata-persistence": _metadata_persistence, "validation-guards": _validation_guards, "build-entries": _build_entries, } if __name__ == "__main__": if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS: print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr) sys.exit(1) fn = _COMMANDS[sys.argv[1]] assert callable(fn) fn()