crisis/tests/test_demo_fact_check.py
saymrwulf b8684297fa Add crisis_agents — Crisis as a coordination layer for AI agent teams
A new sibling Python package, `crisis_agents`, that lifts the Crisis
protocol from "consensus between machines" to "consensus between AI
agents". Threat model: a team of sub-agents normally talks freely
with its orchestrator (the "mothership"); when the team's boundary
opens and an external agent of unknown trust joins, the mothership
activates the Crisis layer so byzantine equivocation is detectable.

Two-phase orchestration model:

  Phase 1 — closed team, no Crisis: agents emit claims directly, the
  mothership collects them flat.

  Phase 2 — boundary opens: every subsequent claim is wrapped into a
  Crisis Message with the agent's stable process_id and a PoW nonce,
  delivered into per-agent LamportGraphs, and after each turn the
  mothership scans for mutations via LamportGraph.find_mutations.

  Phase 3 — proof: when an alarm fires, the mothership emits a
  replayable JSON proof-of-malfeasance document with the contradictory
  witnesses, their delivery sets, and DAG cross-references showing
  which honest agents saw what.

Modules:
  - claim.py      Claim dataclass + JSON round-trip
  - boundary.py   membership tracker + open() trigger
  - agent.py      CrisisAgent abstract + MockAgent + MockByzantineAgent
                  (the latter equivocates by emitting two variants to
                  disjoint peer subsets at the same logical turn)
  - mothership.py orchestrator driving both phases, building Crisis
                  Messages from Claims, per-agent LamportGraphs, log
  - alarm.py      scan_for_mutations: same-agent same-turn distinct
                  digests with non-identical delivery sets, verified
                  spacelike via LamportGraph.are_spacelike on the
                  honest-agent graphs
  - proof.py      build_proof + ProofDocument + JSON serializer +
                  verify_proof_self_consistent
  - cli.py        `crisis-agents demo` + `crisis-agents verify`
  - scenarios/    fact_check: reference doc + 6 statements + scripted
                  honest/byzantine agents producing a deterministic
                  equivocation on statement s03

Tests: 50 new tests across test_claim, test_boundary, test_mothership,
test_alarm, test_proof, test_demo_fact_check. End-to-end test runs the
fact_check scenario, asserts exactly one alarm raised, proof is built,
re-serialized JSON passes self-consistency. Full suite (existing
crisis + new crisis_agents) green in 0.77s — 145 tests.

Out of scope (deliberately): visualization (separate CrisisViz upgrade
later), real TCP gossip (agents talk via in-process function calls in
the mothership), false-claim detection without equivocation (an
agent that consistently lies but never equivocates is out-voted, not
"caught"; catching it would require a ground-truth oracle).

Reuse from existing crisis package: Message, Vertex, LamportGraph,
LamportGraph.find_mutations, ProofOfWorkWeight, digest. The new code
is a thin adapter layer; the protocol substrate did the heavy lifting.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 16:38:11 +02:00

101 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""End-to-end test: fact_check scenario runs cleanly and emits a valid proof."""
import json
from pathlib import Path
from crisis_agents.alarm import scan_for_mutations
from crisis_agents.cli import main as cli_main
from crisis_agents.mothership import Mothership
from crisis_agents.proof import (
build_proof,
verify_proof_self_consistent,
)
from crisis_agents.scenarios import build_fact_check_scenario
class TestFactCheckEndToEnd:
def test_scenario_loads(self):
s = build_fact_check_scenario()
assert s.name == "fact_check"
assert len(s.honest_agents) == 3
assert s.byzantine_joiner.name == "agent_delta"
assert "Pluto" in s.reference_doc
def test_runs_through_both_phases_and_raises_one_alarm(self):
s = build_fact_check_scenario()
m = Mothership()
for agent in s.honest_agents:
m.add_agent(agent)
m.run_closed_phase(num_turns=s.closed_phase_turns)
assert len(m.run_result.closed_log) == 3 * 6 # 3 agents × 6 statements
assert m.all_graphs() == {} # no DAG in closed phase
m.open_boundary(s.byzantine_joiner)
m.run_crisis_phase(num_turns=s.crisis_phase_turns)
# The byzantine emitted two contradictory variants of s03;
# honest agents emitted nothing in the Crisis phase (their script
# was exhausted in the closed phase).
assert len(m.run_result.crisis_log) == 2
alarms = scan_for_mutations(m)
assert len(alarms) == 1
a = alarms[0]
assert a.accused_agent == "agent_delta"
assert a.statement_id == "s03"
assert a.spacelike_verified is True
def test_proof_is_self_consistent_and_round_trips(self, tmp_path):
s = build_fact_check_scenario()
m = Mothership()
for agent in s.honest_agents:
m.add_agent(agent)
m.run_closed_phase(num_turns=s.closed_phase_turns)
m.open_boundary(s.byzantine_joiner)
m.run_crisis_phase(num_turns=s.crisis_phase_turns)
alarm = scan_for_mutations(m)[0]
proof = build_proof(m, alarm)
out = tmp_path / "proof.json"
out.write_text(proof.to_json())
# Reload, re-verify
from crisis_agents.proof import ProofDocument
reloaded = ProofDocument.from_json(out.read_text())
assert verify_proof_self_consistent(reloaded).ok
class TestCli:
def test_cli_demo_runs(self, tmp_path, capsys):
exit_code = cli_main(["demo", "--scenario", "fact_check",
"--out-dir", str(tmp_path)])
assert exit_code == 0
captured = capsys.readouterr()
assert "crisis-agents demo" in captured.out
assert "Phase 1" in captured.out
assert "Phase 2" in captured.out
assert "alarm" in captured.out.lower()
# A proof file landed
proofs = list(tmp_path.glob("proof_*.json"))
assert len(proofs) == 1
# The proof file is valid JSON
obj = json.loads(proofs[0].read_text())
assert obj["accused_agent"] == "agent_delta"
def test_cli_verify_passes_on_valid_proof(self, tmp_path, capsys):
# First produce a proof via the demo
cli_main(["demo", "--scenario", "fact_check", "--out-dir", str(tmp_path)])
proof_path = next(tmp_path.glob("proof_*.json"))
capsys.readouterr() # drain demo output
exit_code = cli_main(["verify", str(proof_path)])
assert exit_code == 0
out = capsys.readouterr().out
assert "self-consistent: True" in out
def test_cli_unknown_scenario(self, capsys):
exit_code = cli_main(["demo", "--scenario", "nonexistent"])
assert exit_code == 2