from pathlib import Path import pytest from pydantic import ValidationError from gewu.application.acceptance import ActorContext from gewu.application.runs import LocalRunService, RunError from gewu.domain.contracts import ( RunCheckpointRequest, RunCreateRequest, RunModulePin, RunState, Scope, ) def _create(run_id: str = "run-pilot") -> RunCreateRequest: return RunCreateRequest( run_id=run_id, objective="Compare audience response", scope=Scope(projects=("dome-performance",), include_shared=False), lens="Audience behavior", pack_id="gewu", pack_version="0.3.0", pack_snapshot_digest="a" * 64, module_pins=( RunModulePin( module_id="profile", implementation_version="1.0.0", sha256="b" * 64, ), ), ) def _checkpoint(run_id: str = "run-pilot", **overrides: object) -> RunCheckpointRequest: values: dict[str, object] = { "run_id": run_id, "expected_revision": 0, "stage": "source-review", "state": RunState.ACTIVE, "budgets": {"source_count": 3}, } values.update(overrides) return RunCheckpointRequest.model_validate(values) def test_run_create_and_checkpoint_are_idempotent_and_revision_checked(tmp_path: Path) -> None: current = True actor = ActorContext("agent-pilot", "pilot-key") service = LocalRunService( workflow_root=tmp_path / "workflow", actor_is_current=lambda _: current, ) created = service.create(_create(), actor=actor) assert created.revision == 0 assert service.create(_create(), actor=actor) == created with pytest.raises(RunError, match="different input"): service.create(_create().model_copy(update={"objective": "Different"}), actor=actor) checkpoint = _checkpoint() updated = service.checkpoint(checkpoint, actor=actor) assert updated.revision == 1 assert service.checkpoint(checkpoint, actor=actor) == updated with pytest.raises(RunError, match="changed content"): service.checkpoint(_checkpoint(stage="different-stage"), actor=actor) service.checkpoint(_checkpoint(expected_revision=1, stage="next-stage"), actor=actor) with pytest.raises(RunError, match="stale"): service.checkpoint(_checkpoint(expected_revision=0, stage="different-stage"), actor=actor) assert service.status("run-pilot", actor=actor).revision == 2 def test_run_owner_revocation_and_terminal_state_are_enforced(tmp_path: Path) -> None: active = True owner = ActorContext("agent-pilot", "pilot-key") other = ActorContext("agent-other", "other-key") service = LocalRunService( workflow_root=tmp_path / "workflow", actor_is_current=lambda actor: active and actor.credential_id in {"pilot-key", "other-key"}, ) service.create(_create(), actor=owner) with pytest.raises(RunError, match="unavailable"): service.status("run-pilot", actor=other) completed = service.checkpoint(_checkpoint(state=RunState.COMPLETED), actor=owner) assert completed.state is RunState.COMPLETED with pytest.raises(RunError, match="cannot be updated"): service.checkpoint(_checkpoint(expected_revision=1), actor=owner) active = False with pytest.raises(RunError, match="no longer active"): service.checkpoint(_checkpoint(expected_revision=1), actor=owner) with pytest.raises(RunError, match="no longer active"): service.status("run-pilot", actor=owner) def test_run_contract_rejects_global_scope_and_negative_budgets() -> None: with pytest.raises(ValidationError): RunCreateRequest.model_validate( _create().model_dump() | {"scope": Scope(projects=("all",), all_projects=True)} ) with pytest.raises(ValidationError): RunCheckpointRequest.model_validate( {"run_id": "run-pilot", "expected_revision": 0, "stage": "x", "budgets": {"tokens": -1}} )