test_architecture_boundaries.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. from __future__ import annotations
  2. from datetime import datetime, timedelta, timezone
  3. import pytest
  4. from agent.orchestration import (
  5. AgentRole,
  6. ArtifactRef,
  7. AttemptSubmission,
  8. CriterionResult,
  9. FrozenContextDocument,
  10. ResourceClaim,
  11. TaskStatus,
  12. ValidationVerdict,
  13. )
  14. from agent.orchestration.coordinator import TaskConflict
  15. from agent.orchestration.models import ValidationEvidenceGrant, ValidationEvidenceSnapshot
  16. from agent.orchestration.prompt_budget import PromptBudgetExceeded, RolePromptEnvelopePacker
  17. from agent.orchestration.validation_evidence import (
  18. ValidationEvidenceConflict,
  19. ValidationEvidenceUnauthorized,
  20. complete_read,
  21. extend_snapshot,
  22. require_authorized_refs,
  23. require_complete,
  24. reserve_read,
  25. start_read_session,
  26. )
  27. def _grant(handle: str, uri: str, digest: str) -> ValidationEvidenceGrant:
  28. return ValidationEvidenceGrant(
  29. ArtifactRef(uri, "evidence", "1", digest),
  30. handle,
  31. )
  32. def test_validation_snapshot_deduplicates_and_rejects_digest_conflict() -> None:
  33. original = _grant("handle-a", "evidence://a", "sha256:" + "1" * 64)
  34. snapshot = extend_snapshot(ValidationEvidenceSnapshot(), (original, original))
  35. assert snapshot.revision == 1
  36. assert snapshot.grants == (original,)
  37. with pytest.raises(ValidationEvidenceConflict):
  38. extend_snapshot(
  39. snapshot,
  40. (_grant("handle-b", "evidence://a", "sha256:" + "2" * 64),),
  41. )
  42. def test_validation_snapshot_rejects_version_only_identity_conflict() -> None:
  43. first = ValidationEvidenceGrant(
  44. ArtifactRef("evidence://versioned", "evidence", "1"),
  45. "handle-v1",
  46. )
  47. second = ValidationEvidenceGrant(
  48. ArtifactRef("evidence://versioned", "evidence", "2"),
  49. "handle-v2",
  50. )
  51. snapshot = extend_snapshot(ValidationEvidenceSnapshot(), (first,))
  52. with pytest.raises(ValidationEvidenceConflict):
  53. extend_snapshot(snapshot, (second,))
  54. def test_validation_submission_accepts_only_snapshot_refs() -> None:
  55. grant = _grant("handle-a", "evidence://a", "sha256:" + "1" * 64)
  56. snapshot = extend_snapshot(ValidationEvidenceSnapshot(), (grant,))
  57. require_authorized_refs(snapshot, (grant.artifact_ref,))
  58. with pytest.raises(ValidationEvidenceUnauthorized):
  59. require_authorized_refs(
  60. snapshot,
  61. (
  62. ArtifactRef(
  63. "evidence://forged",
  64. "evidence",
  65. "1",
  66. "sha256:" + "2" * 64,
  67. ),
  68. ),
  69. )
  70. def test_validation_read_replay_is_core_session_semantics() -> None:
  71. snapshot = extend_snapshot(
  72. ValidationEvidenceSnapshot(),
  73. (_grant("handle-a", "evidence://a", "sha256:" + "1" * 64),),
  74. )
  75. session = start_read_session(snapshot, ("handle-a",))
  76. session, replay_result = reserve_read(
  77. session,
  78. evidence_revision=snapshot.revision,
  79. handle="handle-a",
  80. cursor=None,
  81. )
  82. assert replay_result is None
  83. session = complete_read(
  84. session,
  85. evidence_revision=snapshot.revision,
  86. handle="handle-a",
  87. cursor=None,
  88. next_cursor=None,
  89. exhausted=True,
  90. result={"content_fragment": "frozen", "exhausted": True},
  91. )
  92. require_complete(session)
  93. same, replay_result = reserve_read(
  94. session,
  95. evidence_revision=snapshot.revision,
  96. handle="handle-a",
  97. cursor=None,
  98. )
  99. assert replay_result == {"content_fragment": "frozen", "exhausted": True}
  100. assert same is session
  101. def test_prompt_packer_counts_system_tools_history_and_payload() -> None:
  102. packer = RolePromptEnvelopePacker(token_limit=300, output_reserve=100)
  103. with pytest.raises(PromptBudgetExceeded):
  104. packer.pack(
  105. {"role_context": {"artifact": {"items": []}}},
  106. system_prompt="s" * 500,
  107. tool_schemas=({"name": "tool", "description": "x" * 500},),
  108. history=({"role": "user", "content": "h" * 500},),
  109. )
  110. def test_prompt_packer_uses_the_same_final_envelope_for_planner_messages() -> None:
  111. packer = RolePromptEnvelopePacker(token_limit=2_000, output_reserve=100)
  112. packed = packer.pack_messages(
  113. (
  114. {"role": "system", "content": "planner policy"},
  115. {
  116. "role": "user",
  117. "content": (
  118. '{"role_context":{"artifact":{"items":["body"]},'
  119. '"context_bundle":{"cards":[{"id":"duplicate"}]}}}'
  120. ),
  121. },
  122. ),
  123. tool_schemas=({"name": "plan_script_tasks"},),
  124. )
  125. assert packed.packing_mode == "full"
  126. assert packed.deduplicated_segments == 1
  127. assert '"cards": []' in packed.messages[-1]["content"]
  128. def test_resource_claim_requires_a_nonblank_canonical_identity() -> None:
  129. assert ResourceClaim("script-build://resources/workspace/a").exclusive
  130. with pytest.raises(ValueError):
  131. ResourceClaim(" ")
  132. @pytest.mark.asyncio
  133. async def test_agent_validation_cannot_submit_before_evidence_initialization(
  134. tmp_path,
  135. ) -> None:
  136. from test_coordinator_integration import (
  137. FakeExecutor,
  138. create_task,
  139. make_coordinator,
  140. )
  141. coordinator, store, _ = await make_coordinator(tmp_path, FakeExecutor([]))
  142. task_id = await create_task(coordinator, "evidence must be initialized")
  143. created = await coordinator._create_attempt("root", task_id, "worker", None)
  144. ledger = await store.load("root")
  145. attempt = ledger.attempts[created["attempt_id"]]
  146. await coordinator.submit_attempt(
  147. {
  148. "role": AgentRole.WORKER.value,
  149. "root_trace_id": "root",
  150. "task_id": task_id,
  151. "attempt_id": attempt.attempt_id,
  152. "spec_version": attempt.spec_version,
  153. "trace_id": attempt.worker_trace_id,
  154. "tool_call_id": "submit-attempt",
  155. },
  156. AttemptSubmission("done", [ArtifactRef("memory://artifact", version="1")]),
  157. )
  158. ledger = await store.load("root")
  159. attempt = ledger.attempts[attempt.attempt_id]
  160. validation_id = await coordinator._start_validation(
  161. "root", task_id, attempt.attempt_id
  162. )
  163. ledger = await store.load("root")
  164. report = ledger.validations[validation_id]
  165. with pytest.raises(TaskConflict, match="evidence was not initialized"):
  166. await coordinator.submit_validation(
  167. {
  168. "role": AgentRole.VALIDATOR.value,
  169. "root_trace_id": "root",
  170. "task_id": task_id,
  171. "attempt_id": attempt.attempt_id,
  172. "validation_id": validation_id,
  173. "snapshot_id": report.snapshot_id,
  174. "trace_id": report.validator_trace_id,
  175. "tool_call_id": "premature-validation",
  176. },
  177. ValidationVerdict.PASSED,
  178. [CriterionResult("c1", ValidationVerdict.PASSED, "checked")],
  179. "passed",
  180. [],
  181. [],
  182. [],
  183. "accept",
  184. )
  185. @pytest.mark.asyncio
  186. async def test_durable_lease_renewal_preserves_epoch_and_rejects_stale_owner(
  187. tmp_path,
  188. ) -> None:
  189. from test_coordinator_integration import FakeExecutor, make_coordinator
  190. coordinator, store, _ = await make_coordinator(tmp_path, FakeExecutor([]))
  191. reserved = await coordinator.reserve_durable_lease(
  192. "root", "retrieval:test", owner="worker-a", lease_seconds=30
  193. )
  194. await coordinator.renew_durable_lease(
  195. "root",
  196. "retrieval:test",
  197. owner="worker-a",
  198. epoch=reserved["epoch"],
  199. lease_seconds=60,
  200. )
  201. ledger = await store.load("root")
  202. renewed = ledger.durable_leases["retrieval:test"]
  203. assert renewed.epoch == reserved["epoch"]
  204. assert datetime.fromisoformat(renewed.expires_at) > datetime.now(
  205. timezone.utc
  206. ) + timedelta(seconds=50)
  207. with pytest.raises(TaskConflict, match="ownership is stale"):
  208. await coordinator.renew_durable_lease(
  209. "root",
  210. "retrieval:test",
  211. owner="worker-b",
  212. epoch=reserved["epoch"],
  213. lease_seconds=60,
  214. )
  215. @pytest.mark.asyncio
  216. async def test_host_contract_revision_and_document_freeze_are_one_mutation(
  217. tmp_path,
  218. ) -> None:
  219. from test_coordinator_integration import FakeExecutor, make_coordinator
  220. coordinator, store, _ = await make_coordinator(tmp_path, FakeExecutor([]))
  221. root_task_id = (await store.load("root")).root_task_id
  222. first = await coordinator.create_task_graph(
  223. "root",
  224. (
  225. {
  226. "_task_id": "container",
  227. "_parent_task_id": root_task_id,
  228. "objective": "open",
  229. "acceptance_criteria": (
  230. {"criterion_id": "closed", "description": "closed", "hard": True},
  231. ),
  232. "context_refs": ("contract://v1",),
  233. },
  234. ),
  235. idempotency_key="open",
  236. context_documents=(
  237. FrozenContextDocument(
  238. "contract://v1",
  239. "sha256:" + "1" * 64,
  240. {"schema_version": "test/v1"},
  241. ),
  242. ),
  243. )
  244. assert first["tasks"][0]["task_id"] == "container"
  245. ledger = await store.load("root")
  246. ledger.tasks["container"].status = TaskStatus.NEEDS_REPLAN
  247. await store.commit(ledger, ledger.revision)
  248. await coordinator.create_task_graph(
  249. "root",
  250. (
  251. {
  252. "_task_id": "container",
  253. "_revision_task_id": "container",
  254. "_parent_task_id": root_task_id,
  255. "objective": "closed",
  256. "acceptance_criteria": (
  257. {"criterion_id": "closed", "description": "closed", "hard": True},
  258. ),
  259. "context_refs": ("contract://v2",),
  260. },
  261. ),
  262. idempotency_key="close",
  263. context_documents=(
  264. FrozenContextDocument(
  265. "contract://v2",
  266. "sha256:" + "2" * 64,
  267. {"schema_version": "test/v2"},
  268. ),
  269. ),
  270. )
  271. revised = await store.load("root")
  272. task = revised.tasks["container"]
  273. assert task.status is TaskStatus.PENDING
  274. assert task.current_spec_version == 2
  275. assert task.current_spec.objective == "closed"
  276. assert "contract://v2" in revised.context_documents