AICEPAICEP AICEP 0.1 — Public Draft (RFC)
AICEP is an experimental public draft, provided “as is” and without warranty. Use it at your own risk. We test it extensively, but testing is not a guarantee. Full disclaimer

← AICEP home · Runbook index

reference/tests/test_review_r2b.py

Download test_review_r2b.py

"""v2.4 tests for design review V1-V8 (beyond the original repro tests in test_review_r1.py)."""
import json, os, shutil, sys
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import aicep_sub
from aicep_common import Ledger, Reject, canonical, b64
from aicep_pub import publish
from aicep_sub import Held, ingest
from test_delivery import w, send, OBJ  # noqa: F401
from test_tamper import env, pub, sub, resign, NOW  # noqa: F401


def skid_seq(d):
    m = json.load(open(os.path.join(d, "manifest.json"))); return m["sender_key_id"], m["seq"]


# V1: a forged manifest sitting in holding/ is dead-lettered, never alerted on
def test_forged_batch_in_holding_goes_to_dlq(w):
    res = send(w); d = res["batch_dir"]
    files = {n: open(os.path.join(d, n), "rb").read() for n in os.listdir(d)}
    m = json.loads(files["manifest.json"]); m["seq"] = 50; files["manifest.json"] = canonical(m)
    hp = os.path.join(w["bus"].root, "c1", "holding", "forged-h"); os.makedirs(hp)
    for n, b in files.items():
        open(os.path.join(hp, n), "wb").write(b)
    r = w["recv"](); w["clk"].t += 4000
    out = r.run_once()
    assert out["forged-h"] == "dlq"
    assert not any(n["event"] == "gap_timeout" for n in r.notices)


# V2b: commit merges, never rolls back last_seq written by another receiver
def test_commit_state_never_rolls_back_last_seq(w):
    r = w["recv"]()
    aicep_sub.atomic_write(r.sub_state, json.dumps({"last_seq": {"k": 9, "o": 1}}).encode())
    tmp = os.path.join(r.dir, "sub_state.x.tmp")
    aicep_sub.atomic_write(tmp, json.dumps({"last_seq": {"k": 3, "o": 2}}).encode())
    r._commit_state(tmp)
    assert json.load(open(r.sub_state))["last_seq"] == {"k": 9, "o": 2}


# V2c: losing the pub->processing rename race skips the batch instead of aborting fetch
def test_fetch_rename_race_skips(w, monkeypatch):
    send(w); send(w)
    real = os.rename; calls = []
    def flaky(a, b):
        calls.append(a)
        if len(calls) == 1:
            raise FileNotFoundError(a)
        return real(a, b)
    monkeypatch.setattr(os, "rename", flaky)
    got = w["bus"].fetch("c1", 10, 300, "r1", folders={"agent-a"})
    assert len(got) == 1


# V3: ingest-level downgrade guard (CLI path too)
def test_ingest_rejects_v1_after_chain(env):
    d1 = pub(env, prev_batch_id="genesis"); sub(env, d1)
    d2 = pub(env)  # no prev_batch_id
    with pytest.raises(Reject, match="chain downgrade"):
        sub(env, d2)


def test_ingest_rejects_wrong_prev(env):
    d1 = pub(env, prev_batch_id="genesis"); sub(env, d1)
    d2 = pub(env, prev_batch_id="not-the-last")
    with pytest.raises(Reject, match="chain break"):
        sub(env, d2)


# V4: missing size_padded is a schema reject
def test_missing_size_padded_rejected(env):
    d = pub(env); resign(env, d, lambda m: [f.pop("size_padded") for f in m["files"]])
    with pytest.raises(Reject, match="schema"):
        sub(env, d)


# V5: receiver routes a signed-but-malformed manifest to the DLQ (no exception, no poison loop)
def test_receiver_non_int_size_padded_to_dlq(w):
    res = send(w); d = os.path.join(w["bus"].root, "c1", "pub", "agent-a", res["batch_id"])
    mp = os.path.join(d, "manifest.json"); m = json.load(open(mp))
    for f in m["files"]:
        f["size_padded"] = "big"
    mb = canonical(m); open(mp, "wb").write(mb); open(mp + ".sig", "w").write(b64(w["sk"].sign(mb)))
    assert w["recv"]().run_once()[res["batch_id"]] == "dlq"


def test_non_int_size_padded_reject(env):
    d = pub(env); resign(env, d, lambda m: [f.__setitem__("size_padded", "4096") for f in m["files"]])
    with pytest.raises(Reject):
        sub(env, d)


# V6: expected_head mismatch and malformed lines fail verify (no exception)
def test_ledger_expected_head_and_malformed(tmp_path):
    L = Ledger(str(tmp_path / "l.jsonl")); h1 = L.append("a", t=1); L.append("b", t=2)
    assert L.verify() and L.verify(expected_head=h1)
    assert not L.verify(expected_head="f" * 64)
    open(L.path, "a").write("{not json\n")
    assert L.verify() is False


# V7: created in the future (beyond skew) is rejected
def test_future_created_rejected(env):
    d = pub(env); resign(env, d, lambda m: m.__setitem__("created", NOW + 10_000))
    with pytest.raises(Reject, match="future"):
        sub(env, d)


# V8: exact-match human gap ack, logged to the ledger
def test_ack_gap_to_must_match_and_is_logged(env):
    pub(env); d2 = pub(env); skid, seq = skid_seq(d2)
    kw = dict(now=NOW + 5)
    with pytest.raises(Held):
        ingest(d2, env["ident"], env["registry"], env["sstate"], "c1", ack_gap_to=f"{skid}:{seq + 1}", operator="h", **kw)
    with pytest.raises(Held):
        ingest(d2, env["ident"], env["registry"], env["sstate"], "c1", ack_gap_to=f"ed25519:other:{seq}", operator="h", **kw)
    with pytest.raises(ValueError):
        ingest(d2, env["ident"], env["registry"], env["sstate"], "c1", ack_gap_to=f"{skid}:{seq}", **kw)
    out = ingest(d2, env["ident"], env["registry"], env["sstate"], "c1", ack_gap_to=f"{skid}:{seq}", operator="h", **kw)
    assert out["gap_alarm"] is True
    L = Ledger(os.path.join(os.path.dirname(env["sstate"]), "ledger.jsonl"))
    e = [x for x in L.entries() if x["event"] == "gap_acked"]
    assert e and e[0]["operator"] == "h" and e[0]["skid"] == skid and (e[0]["from"], e[0]["to"]) == (0, seq)
    assert L.verify()


def test_cli_has_no_blanket_ack_gap():
    src = open(aicep_sub.__file__).read()
    assert '"--ack-gap"' not in src and '"--ack-gap-to"' in src


# ---- v2.4 fixes (design review re-review W1-W4)
import subprocess
from aicep_common import LedgerError, MAX_TTL


def test_w1_expires_not_after_created_rejected(env):
    d = pub(env); resign(env, d, lambda m: m.__setitem__("expires", m["created"]))
    with pytest.raises(Reject, match="expires <= created"):
        sub(env, d)


def test_w1_ttl_at_cap_ok_over_cap_rejected(env):
    sub(env, pub(env, ttl=MAX_TTL))
    with pytest.raises(Reject, match="MAX_TTL"):
        sub(env, pub(env, ttl=MAX_TTL + 1))


def test_w3_reanchor_is_human_only_and_logged(tmp_path):
    p = str(tmp_path / "l.jsonl"); L = Ledger(p); L.append("e", t=1); L.append("e", t=2)
    os.remove(L.anchor_path)
    with pytest.raises(LedgerError):
        L.append("e", t=3)
    with pytest.raises(PermissionError):
        L.reanchor("")
    L.reanchor("human-x", t=4)
    assert L.verify() and L.entries()[-1]["event"] == "reanchored" and L.entries()[-1]["operator"] == "human-x"
    L.append("e", t=5); assert L.verify()


def test_w3_reanchor_refuses_broken_links(tmp_path):
    p = str(tmp_path / "l.jsonl"); L = Ledger(p); L.append("e", t=1); L.append("e", t=2); L.append("e", t=3)
    lines = open(p).read().splitlines(True); open(p, "w").writelines([lines[0], lines[2]])
    with pytest.raises(LedgerError):
        L.reanchor("human-x")


def test_w3_cli_reanchor(tmp_path):
    p = str(tmp_path / "l.jsonl"); L = Ledger(p); L.append("e", t=1); os.remove(L.anchor_path)
    sub_py = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "aicep_sub.py")
    assert subprocess.run([sys.executable, sub_py, "--reanchor-ledger", p], capture_output=True).returncode != 0
    r = subprocess.run([sys.executable, sub_py, "--reanchor-ledger", p, "--operator", "human-x"], capture_output=True)
    assert r.returncode == 0 and Ledger(p).verify()


def test_w3_receiver_start_fails_closed_on_truncated_ledger(w):
    send(w); send(w); w["recv"]().run_once()
    p = w["t"] / "r" / "ledger.jsonl"
    lines = open(p).read().splitlines(True); open(p, "w").writelines(lines[:-3])
    with pytest.raises(LedgerError):
        w["recv"]()
    Ledger(str(p)).reanchor("human-x")      # human re-anchor still can't hide receipt heads that are gone
    with pytest.raises(LedgerError):
        w["recv"]()


def test_w4_existing_v1_sender_then_genesis_chain(w):
    def put(seq, prev):
        b = publish(OBJ, [w["recip"]], w["sk"], "A", "c1", str(w["t"] / "f"), str(w["t"] / "fps.json"),
                    now=w["clk"].t, seq=seq, ttl=3600, prev_batch_id=prev)
        w["bus"].publish(b["batch_dir"], {"compartment": "c1", "batch_id": b["batch_id"], "folder": "agent-a"}); return b["batch_id"]
    r = w["recv"]()
    b1 = put(1, None); assert r.run_once()[b1][0] == "committed"          # legacy v1 history
    b2 = put(2, "genesis"); assert r.run_once()[b2][0] == "committed"     # outbox with no last_published
    b3 = put(3, b2); assert r.run_once()[b3][0] == "committed"
    b4 = put(4, "wrong"); assert r.run_once()[b4] == "dlq"


def test_w3_reanchor_blank_operator_refused(tmp_path):
    L = Ledger(str(tmp_path / "l.jsonl")); L.append("e", t=1); os.remove(L.anchor_path)
    for op in ("", "   ", "\t\n", None):
        with pytest.raises(PermissionError):
            L.reanchor(op)
    L.reanchor("  human-x  ", t=2)
    assert L.entries()[-1]["operator"] == "human-x" and L.verify()