From 597423f21e2638ab22dc51dd7449e9b2a672e47d Mon Sep 17 00:00:00 2001 From: Aniketh Maddipati Date: Tue, 2 Jun 2026 17:16:17 -0400 Subject: [PATCH] harden receipt and cli test coverage --- tests/cli/test_e2e.py | 143 ++++++++++++++++--------------- tests/conftest.py | 67 +++++++++++++++ tests/strategies.py | 43 ++++++++++ tests/test_aerf_conformance.py | 106 ++++++++++++++++------- tests/test_crypto_kat.py | 87 +++++++++++++++++++ tests/test_integration.py | 151 +++++++++++++++++++++++++++++++++ tests/test_invariants.py | 84 ++++++++++++++++++ 7 files changed, 583 insertions(+), 98 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/strategies.py create mode 100644 tests/test_crypto_kat.py create mode 100644 tests/test_integration.py create mode 100644 tests/test_invariants.py diff --git a/tests/cli/test_e2e.py b/tests/cli/test_e2e.py index c623d5b..20f88c5 100644 --- a/tests/cli/test_e2e.py +++ b/tests/cli/test_e2e.py @@ -1,103 +1,112 @@ +from __future__ import annotations + +import os import subprocess -import textwrap +import sys +from pathlib import Path + +from agentmint import Notary -import pytest +ROOT = Path(__file__).resolve().parents[2] -def _write_agent(tmp_path): - (tmp_path / "agent.py").write_text( - textwrap.dedent( - """ - from agentmint import Notary, notarise - notary = Notary() +def run_cli(*args: str, cwd: Path) -> subprocess.CompletedProcess: + env = os.environ.copy() + env["PYTHONPATH"] = str(ROOT) + os.pathsep + env.get("PYTHONPATH", "") + return subprocess.run( + [sys.executable, "-c", "from agentmint.cli.app import app; app()", *args], + cwd=cwd, + env=env, + text=True, + capture_output=True, + check=False, + ) - @notarise(notary, action="test:hello") - def greet(name): - return {"greeting": f"hello {name}"} - if __name__ == "__main__": - greet("world") - """ - ) +def write_receipt(tmp_path: Path) -> Path: + notary = Notary(key=tmp_path / ".agentmint" / "keys") + plan = notary.create_plan(user="cli@test", action="cli", scope=["*"], ttl_seconds=60) + receipt = notary.notarise( + action="files:read", + agent="cli-agent", + plan=plan, + evidence={"path": "demo.txt"}, + enable_timestamp=False, ) + receipt_path = tmp_path / "receipts" / "receipt.json" + receipt_path.parent.mkdir(exist_ok=True) + receipt_path.write_text(receipt.to_json()) + return receipt_path + + +def test_help_renders(tmp_path): + result = run_cli("--help", cwd=ROOT) + + assert result.returncode == 0 + assert "AgentMint" in result.stdout + assert "init" in result.stdout + assert "doctor" in result.stdout + assert "verify" in result.stdout def test_init_creates_workspace(tmp_path): - result = subprocess.run( - ["agentmint", "init", "--yes"], cwd=tmp_path, capture_output=True, text=True - ) + (tmp_path / "agent.py").write_text("def submit_prior_auth(cpt_code, icd10_code, patient_id):\n return True\n") + + result = run_cli("init", "--yes", cwd=tmp_path) + assert result.returncode == 0 assert (tmp_path / ".agentmint" / "config.toml").exists() assert (tmp_path / ".agentmint" / "keys").is_dir() + assert (tmp_path / "receipts").is_dir() assert (tmp_path / ".gitignore").read_text().count(".agentmint/") == 1 -def test_three_minute_flow(tmp_path): - subprocess.run(["agentmint", "init", "--yes"], cwd=tmp_path, check=True) - _write_agent(tmp_path) - result = subprocess.run(["python3", "agent.py"], cwd=tmp_path, capture_output=True, text=True) - assert result.returncode == 0 - receipts = list((tmp_path / "receipts").rglob("*.json")) - assert len(receipts) == 1 - verify_result = subprocess.run( - ["agentmint", "verify", str(receipts[0])], cwd=tmp_path, capture_output=True, text=True - ) - assert verify_result.returncode == 0 - assert "valid" in verify_result.stdout.lower() +def test_doctor_green_on_fresh_init(tmp_path): + run_cli("init", "--yes", cwd=tmp_path) + result = run_cli("doctor", cwd=tmp_path) -def test_doctor_passes_on_fresh_init(tmp_path): - subprocess.run(["agentmint", "init", "--yes"], cwd=tmp_path, check=True) - result = subprocess.run(["agentmint", "doctor"], cwd=tmp_path, capture_output=True, text=True) assert result.returncode == 0 - assert "healthy" in result.stdout.lower() or "needs attention" in result.stdout.lower() + assert "Result:" in result.stdout + assert "needs attention" in result.stdout.lower() or "healthy" in result.stdout.lower() def test_privacy_zero_network_default(tmp_path): - subprocess.run(["agentmint", "init", "--yes"], cwd=tmp_path, check=True) - result = subprocess.run(["agentmint", "privacy"], cwd=tmp_path, capture_output=True, text=True) + run_cli("init", "--yes", cwd=tmp_path) + + result = run_cli("privacy", cwd=tmp_path) + assert result.returncode == 0 assert "None" in result.stdout + assert ".agentmint" in result.stdout -def test_init_detects_healthcare(tmp_path): - (tmp_path / "agent.py").write_text( - "def submit_prior_auth(cpt_code, icd10_code, patient_id):\n" - " # HIPAA-compliant submission to payer\n" - " pass\n" - ) - result = subprocess.run( - ["agentmint", "init"], cwd=tmp_path, input="n\n", capture_output=True, text=True - ) - assert "healthcare" in result.stdout.lower() +def test_verify_validates_generated_receipt(tmp_path): + run_cli("init", "--yes", cwd=tmp_path) + receipt_path = write_receipt(tmp_path) + + result = run_cli("verify", str(receipt_path), cwd=tmp_path) + + assert result.returncode == 0 + assert "valid" in result.stdout.lower() def test_show_renders_receipt(tmp_path): - subprocess.run(["agentmint", "init", "--yes"], cwd=tmp_path, check=True) - _write_agent(tmp_path) - subprocess.run(["python3", "agent.py"], cwd=tmp_path, check=True) - receipt_path = next((tmp_path / "receipts").rglob("*.json")) - result = subprocess.run( - ["agentmint", "show", str(receipt_path)], cwd=tmp_path, capture_output=True, text=True - ) + run_cli("init", "--yes", cwd=tmp_path) + receipt_path = write_receipt(tmp_path) + + result = run_cli("show", str(receipt_path), cwd=tmp_path) + assert result.returncode == 0 assert "Receipt" in result.stdout assert "Signature" in result.stdout -def test_no_color_flag_strips_ansi(tmp_path): - subprocess.run(["agentmint", "init", "--yes"], cwd=tmp_path, check=True) - result = subprocess.run( - ["agentmint", "--no-color", "doctor"], cwd=tmp_path, capture_output=True, text=True - ) - assert "\033[" not in result.stdout +def test_show_raw_renders_json(tmp_path): + run_cli("init", "--yes", cwd=tmp_path) + receipt_path = write_receipt(tmp_path) + result = run_cli("show", str(receipt_path), "--raw", cwd=tmp_path) -def test_init_interactive(tmp_path): - pexpect = pytest.importorskip("pexpect") - child = pexpect.spawn("agentmint init", cwd=str(tmp_path), timeout=10) - child.expect("Apply suggestions") - child.sendline("y") - child.expect_exact("Setup complete") - child.expect(pexpect.EOF) - assert (tmp_path / ".agentmint").exists() + assert result.returncode == 0 + assert '"type": "notarised_evidence"' in result.stdout diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..64197e2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, List + +import pytest + +from agentmint.notary import Notary + + +class RecordingSink: + """In-memory sink used by integration-style tests.""" + + def __init__(self) -> None: + self.receipts: List[Any] = [] + + def emit(self, receipt: Any) -> None: + self.receipts.append(receipt) + + def flush(self) -> None: + pass + + def close(self) -> None: + pass + + +@pytest.fixture +def tmp_key_dir(tmp_path: Path) -> Path: + return tmp_path / "keys" + + +@pytest.fixture +def recording_sink() -> RecordingSink: + return RecordingSink() + + +@pytest.fixture +def notary(tmp_key_dir: Path, recording_sink: RecordingSink) -> Notary: + return Notary(key=tmp_key_dir, sink=recording_sink) + + +@pytest.fixture +def plan(notary: Notary): + return notary.create_plan( + user="tests@agentmint.dev", + action="tests", + scope=["tool:*", "read:*", "write:*"], + delegates_to=["test-agent"], + ) + + +@pytest.fixture +def aerf_schema() -> dict[str, Any]: + schema_path = Path(__file__).parent.parent / "schemas" / "aerf-v0.1.json" + return json.loads(schema_path.read_text()) + + +@pytest.fixture +def receipt(notary: Notary, plan): + return notary.notarise( + action="tool:test", + agent="test-agent", + plan=plan, + evidence={"key": "value"}, + enable_timestamp=False, + ) diff --git a/tests/strategies.py b/tests/strategies.py new file mode 100644 index 0000000..f44e1fe --- /dev/null +++ b/tests/strategies.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import random +import string +from typing import Dict, Iterable, Iterator, Tuple + + +_ACTION_ALPHABET = string.ascii_lowercase + string.digits + "_-" +_EVIDENCE_KEY_ALPHABET = string.ascii_letters + string.digits + "_" + + +def iter_actions(seed: int = 0, count: int = 25) -> Iterator[str]: + rng = random.Random(seed) + for _ in range(count): + part_count = rng.randint(1, 4) + parts = [] + for _ in range(part_count): + size = rng.randint(1, 12) + parts.append("".join(rng.choice(_ACTION_ALPHABET) for _ in range(size))) + yield ":".join(parts) + + +def iter_evidence(seed: int = 0, count: int = 25) -> Iterator[Dict[str, object]]: + rng = random.Random(seed + 1) + for _ in range(count): + item_count = rng.randint(1, 6) + evidence: Dict[str, object] = {} + for idx in range(item_count): + key_len = rng.randint(1, 12) + key = "".join(rng.choice(_EVIDENCE_KEY_ALPHABET) for _ in range(key_len)) + choice = (idx + rng.randint(0, 2)) % 3 + if choice == 0: + evidence[key] = rng.randint(-1000, 1000) + elif choice == 1: + evidence[key] = bool(rng.randint(0, 1)) + else: + value_len = rng.randint(0, 32) + evidence[key] = "".join(rng.choice(_ACTION_ALPHABET) for _ in range(value_len)) + yield evidence + + +def iter_action_evidence_cases(seed: int = 0, count: int = 25) -> Iterable[Tuple[str, Dict[str, object]]]: + return zip(iter_actions(seed=seed, count=count), iter_evidence(seed=seed, count=count)) diff --git a/tests/test_aerf_conformance.py b/tests/test_aerf_conformance.py index d857030..7a8a63e 100644 --- a/tests/test_aerf_conformance.py +++ b/tests/test_aerf_conformance.py @@ -1,48 +1,92 @@ -"""AERF v0.1 conformance coverage for receipts produced by Notary.""" - from __future__ import annotations -import json -from pathlib import Path +from jsonschema import Draft202012Validator -import pytest -from jsonschema import Draft202012Validator, FormatChecker +from agentmint.notary import verify_chain -from agentmint.notary import Notary +def _errors(validator: Draft202012Validator, payload): + return sorted(validator.iter_errors(payload), key=lambda err: list(err.path)) -SCHEMA_PATH = Path(__file__).parent.parent / "schemas" / "aerf-v0.1.json" +def test_minimal_receipt_validates(notary, plan, aerf_schema): + validator = Draft202012Validator(aerf_schema) + receipt = notary.notarise( + action="tool:minimal", + agent="test-agent", + plan=plan, + evidence={"k": "v"}, + enable_timestamp=False, + ) -def _format_validation_errors(errors: list[object]) -> str: - lines = ["receipt failed AERF v0.1 schema validation:"] - for error in errors: - json_path = getattr(error, "json_path", "$") - message = getattr(error, "message", str(error)) - lines.append(f"- {json_path}: {message}") - return "\n".join(lines) + errors = _errors(validator, receipt.to_dict()) + assert not errors, "\n".join(f"{err.json_path}: {err.message}" for err in errors) -def test_notary_receipt_matches_aerf_v01_schema() -> None: - """Produce a receipt through Notary.notarise and validate it against AERF v0.1.""" +def test_chain_of_ten_all_validate(notary, plan, aerf_schema): + validator = Draft202012Validator(aerf_schema) + receipts = [] - schema = json.loads(SCHEMA_PATH.read_text()) - validator = Draft202012Validator(schema, format_checker=FormatChecker()) + for i in range(10): + receipt = notary.notarise( + action=f"tool:step:{i}", + agent="test-agent", + plan=plan, + evidence={"i": i}, + output={"i": i}, + reasoning=f"step {i}", + enable_timestamp=False, + ) + receipts.append(receipt) + errors = _errors(validator, receipt.to_dict()) + assert not errors, f"receipt {i} failed validation: {errors[0].message}" - notary = Notary() - plan = notary.create_plan( - user="auditor@example.com", - action="files:read", - scope=["files:*"], - ttl_seconds=60, + chain = verify_chain(receipts) + assert chain.valid is True + assert chain.length == 10 + + +def test_out_of_policy_receipt_still_validates(notary, aerf_schema): + validator = Draft202012Validator(aerf_schema) + restricted_plan = notary.create_plan( + user="tests@agentmint.dev", + action="tests", + scope=["read:*"], + delegates_to=["test-agent"], ) + receipt = notary.notarise( - action="files:read", - agent="conformance-agent", + action="write:blocked", + agent="test-agent", + plan=restricted_plan, + evidence={"attempt": True}, + enable_timestamp=False, + ) + + assert receipt.in_policy is False + errors = _errors(validator, receipt.to_dict()) + assert not errors, "\n".join(f"{err.json_path}: {err.message}" for err in errors) + + +def test_session_context_validates(notary, plan, aerf_schema): + validator = Draft202012Validator(aerf_schema) + + first = notary.notarise( + action="read:parent", + agent="test-agent", plan=plan, - evidence={"path": "/tmp/demo.txt", "operation": "read"}, + evidence={"step": 1}, enable_timestamp=False, - ).to_dict() + ) + second = notary.notarise( + action="read:child", + agent="test-agent", + plan=plan, + evidence={"step": 2}, + enable_timestamp=False, + ) - errors = sorted(validator.iter_errors(receipt), key=lambda error: list(error.path)) - assert not errors, _format_validation_errors(errors) + assert first.session_id == second.session_id + for receipt in (first, second): + errors = _errors(validator, receipt.to_dict()) + assert not errors, "\n".join(f"{err.json_path}: {err.message}" for err in errors) diff --git a/tests/test_crypto_kat.py b/tests/test_crypto_kat.py new file mode 100644 index 0000000..2b88cb6 --- /dev/null +++ b/tests/test_crypto_kat.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import json + +import pytest +from nacl.exceptions import BadSignatureError +from nacl.signing import SigningKey + +from agentmint.merkle import MerkleProof, build_tree, verify_proof +from agentmint.notary import _canonical_json + + +ED25519_PRIVATE_KEY_HEX = ( + "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae3d55" +) +ED25519_PUBLIC_KEY_HEX = ( + "700e2ce7c4b674427eab27ba820bcf6f0faebe68e09fe8564292114e41dc6a41" +) +ED25519_MESSAGE = b"test message for agentmint kat" +ED25519_EXPECTED_SIG_HEX = ( + "405709af3130840003e857476296bac716082e60d4acee609383dcabeaa5af4b" + "880276fe4dadcccd89bfe64c5f77c1ea8c92b431fc20d88c8f5614b1a94afe06" +) + +CANONICAL_VECTORS = [ + ({}, b"{}"), + ({"b": 2, "a": 1}, b'{"a":1,"b":2}'), + ({"key": "value with \u00e9"}, b'{"key":"value with \\u00e9"}'), + ({"nested": {"b": True, "a": False}}, b'{"nested":{"a":false,"b":true}}'), +] + +MERKLE_LEAVES = [b"leaf0", b"leaf1", b"leaf2", b"leaf3"] +MERKLE_EXPECTED_ROOT = "86f9ec25a8a2b32a4bd733e04c213de63c8b0655bcb887b75cfd8b02691be0e5" + + +def test_ed25519_signature_matches_known_answer(): + key = SigningKey(bytes.fromhex(ED25519_PRIVATE_KEY_HEX)) + + assert key.verify_key.encode().hex() == ED25519_PUBLIC_KEY_HEX + assert key.sign(ED25519_MESSAGE).signature.hex() == ED25519_EXPECTED_SIG_HEX + + +def test_ed25519_wrong_message_fails(): + key = SigningKey(bytes.fromhex(ED25519_PRIVATE_KEY_HEX)) + sig = key.sign(ED25519_MESSAGE).signature + + with pytest.raises(BadSignatureError): + key.verify_key.verify(b"tampered message", sig) + + +def test_canonical_json_vectors(): + for payload, expected in CANONICAL_VECTORS: + assert _canonical_json(payload) == expected + + +def test_canonical_json_deterministic_across_roundtrips(): + payload = {"z": 1, "a": 2, "m": [3, 1, 2], "nested": {"b": True, "a": False}} + first = _canonical_json(payload) + for _ in range(25): + reparsed = json.loads(first.decode("utf-8")) + assert _canonical_json(reparsed) == first + + +def test_merkle_root_matches_known_answer(): + tree = build_tree(MERKLE_LEAVES) + assert tree.root == MERKLE_EXPECTED_ROOT + + +def test_merkle_inclusion_proof_verifies(): + tree = build_tree(MERKLE_LEAVES) + + for index in range(len(MERKLE_LEAVES)): + proof = tree.proof(index) + assert verify_proof(proof), f"proof failed for leaf {index}" + + +def test_merkle_tampered_leaf_fails(): + tree = build_tree(MERKLE_LEAVES) + proof = tree.proof(0) + tampered = MerkleProof( + leaf_index=proof.leaf_index, + leaf_hash="deadbeef" * 8, + siblings=proof.siblings, + root_hash=proof.root_hash, + ) + + assert not verify_proof(tampered) diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..35b100a --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +import asyncio +import json +import zipfile + +from agentmint.circuit_breaker import CircuitBreaker +from agentmint.notary import verify_chain + + +def test_sync_flow_emits_valid_receipt(notary, plan, recording_sink): + def greet(name: str) -> dict[str, str]: + return {"greeting": f"hello {name}"} + + result = greet("world") + receipt = notary.notarise( + action="tool:greet", + agent="test-agent", + plan=plan, + evidence={"name": "world"}, + output=result, + enable_timestamp=False, + ) + + assert result["greeting"] == "hello world" + assert receipt.output_hash + assert receipt.in_policy is True + assert recording_sink.receipts[-1].id == receipt.id + assert notary.verify_receipt(receipt) is True + + +def test_async_flow_emits_valid_receipt(notary, plan, recording_sink): + async def async_greet(name: str) -> dict[str, str]: + await asyncio.sleep(0) + return {"greeting": f"async hello {name}"} + + result = asyncio.run(async_greet("world")) + receipt = notary.notarise( + action="tool:async_greet", + agent="test-agent", + plan=plan, + evidence={"name": "world"}, + output=result, + reasoning="async greeting path", + enable_timestamp=False, + ) + + assert result["greeting"] == "async hello world" + assert receipt.reasoning_hash + assert recording_sink.receipts[-1].id == receipt.id + assert notary.verify_receipt(receipt) is True + + +def test_circuit_breaker_denial_emits_signed_receipt(tmp_path): + breaker = CircuitBreaker(max_calls=1, window_seconds=60) + from agentmint.notary import Notary + + notary = Notary(key=tmp_path / "keys", circuit_breaker=breaker) + plan = notary.create_plan( + user="tests@agentmint.dev", + action="breaker", + scope=["tool:*"], + delegates_to=["agent"], + ) + + first = notary.notarise( + action="tool:once", + agent="agent", + plan=plan, + evidence={"n": 1}, + enable_timestamp=False, + ) + second = notary.notarise( + action="tool:twice", + agent="agent", + plan=plan, + evidence={"n": 2}, + enable_timestamp=False, + ) + + assert first.in_policy is True + assert second.in_policy is False + assert second.policy_reason.startswith("circuit_breaker:") + assert notary.verify_receipt(second) is True + + +def test_delegated_plan_and_parent_plan_keep_separate_chains(notary): + parent = notary.create_plan( + user="tests@agentmint.dev", + action="delegation", + scope=["read:*", "write:summary:*"], + delegates_to=["parent-agent"], + ) + child = notary.delegate_to_agent( + parent, + "child-agent", + requested_scope=["write:summary:daily"], + ) + + parent_receipts = [ + notary.notarise( + action="read:source", + agent="parent-agent", + plan=parent, + evidence={"step": 1}, + enable_timestamp=False, + ), + notary.notarise( + action="write:summary:overview", + agent="parent-agent", + plan=parent, + evidence={"step": 2}, + enable_timestamp=False, + ), + ] + child_receipt = notary.notarise( + action="write:summary:daily", + agent="child-agent", + plan=child, + evidence={"step": 3}, + enable_timestamp=False, + ) + + assert verify_chain(parent_receipts).valid is True + assert child_receipt.previous_receipt_hash is None + assert notary.audit_tree(parent.id)["children"][0]["plan_id"] == child.id + + +def test_evidence_package_export_contains_verifiable_artifacts(notary, plan, tmp_path): + notary.notarise( + action="tool:export", + agent="test-agent", + plan=plan, + evidence={"exported": True}, + output={"ok": True}, + enable_timestamp=False, + ) + + zip_path = notary.export_evidence(tmp_path) + + with zipfile.ZipFile(zip_path) as zf: + names = set(zf.namelist()) + assert "plan.json" in names + assert "receipt_index.json" in names + assert "public_key.pem" in names + receipt_files = [name for name in names if name.startswith("receipts/") and name.endswith(".json")] + assert receipt_files + + index = json.loads(zf.read("receipt_index.json")) + assert index["chain"]["valid"] is True + assert index["total_receipts"] == 1 diff --git a/tests/test_invariants.py b/tests/test_invariants.py new file mode 100644 index 0000000..730f801 --- /dev/null +++ b/tests/test_invariants.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from dataclasses import replace + +from agentmint.notary import verify_chain + +from .strategies import iter_action_evidence_cases + + +def test_signed_receipt_always_verifies(tmp_path): + from agentmint.notary import Notary + + notary = Notary(key=tmp_path / "keys") + plan = notary.create_plan( + user="tests@agentmint.dev", + action="invariants", + scope=["*"], + delegates_to=["agent"], + ) + + for action, evidence in iter_action_evidence_cases(seed=11, count=30): + receipt = notary.notarise( + action=action, + agent="agent", + plan=plan, + evidence=evidence, + enable_timestamp=False, + ) + assert notary.verify_receipt(receipt), f"failed for action={action!r}" + + +def test_field_tampering_is_detected(tmp_path): + from agentmint.notary import Notary + + notary = Notary(key=tmp_path / "keys") + plan = notary.create_plan( + user="tests@agentmint.dev", + action="invariants", + scope=["*"], + delegates_to=["agent"], + ) + + for index, (action, evidence) in enumerate(iter_action_evidence_cases(seed=21, count=20)): + receipt = notary.notarise( + action=action, + agent="agent", + plan=plan, + evidence=evidence, + enable_timestamp=False, + ) + tampered = replace( + receipt, + policy_reason=f"tampered:{index}", + signature=receipt.signature, + ) + assert not notary.verify_receipt(tampered), f"tamper not detected for {action!r}" + + +def test_chain_integrity_holds_for_generated_cases(tmp_path): + from agentmint.notary import Notary + + notary = Notary(key=tmp_path / "keys") + plan = notary.create_plan( + user="tests@agentmint.dev", + action="invariants", + scope=["*"], + delegates_to=["agent"], + ) + + receipts = [] + for action, evidence in iter_action_evidence_cases(seed=31, count=12): + receipts.append( + notary.notarise( + action=action, + agent="agent", + plan=plan, + evidence=evidence, + enable_timestamp=False, + ) + ) + + chain = verify_chain(receipts) + assert chain.valid is True + assert chain.length == len(receipts)