From 26c20f052bd998d5549f16e63d4f05dc425bb88e Mon Sep 17 00:00:00 2001 From: Josh Stevenson Date: Sat, 3 Oct 2026 21:51:09 -0700 Subject: [PATCH] fix(collaboration): allow identical closed projection rebuilds --- ares_runtime/collaboration.py | 17 +++- .../test_closure_projection_rebuild.py | 92 +++++++++++++++++++ 2 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 tests/ares_runtime/test_closure_projection_rebuild.py diff --git a/ares_runtime/collaboration.py b/ares_runtime/collaboration.py index 7887c09be4e59..3820e861927e8 100644 --- a/ares_runtime/collaboration.py +++ b/ares_runtime/collaboration.py @@ -3112,6 +3112,7 @@ def project( _check_ref(event_ref, "source_event_refs") if not source_event_exists(event_ref): raise ContractError("MISSING_SOURCE_EVENT", event_ref) + prior = None if previous_projection is not None: prior = ( previous_projection.to_dict() @@ -3123,10 +3124,6 @@ def project( or prior.get("closure_profile") != closure_profile ): raise ContractError("PROJECTION_LINEAGE_MISMATCH") - if prior.get("state") == "closed" and not set(normalized_events).difference( - prior.get("source_event_refs", []) - ): - raise ContractError("REOPEN_REQUIRES_NEW_EVIDENCE") unknown = set(flags) - self.ALLOWED_FLAGS if unknown: raise ContractError("UNKNOWN_DIVERGENCE_FLAG", sorted(unknown)[0]) @@ -3140,7 +3137,7 @@ def project( if not unsatisfied and not flags else ("quarantined" if "AMBIGUOUS_EFFECT" in flags else "evidence_pending") ) - return make_artifact( + projection = make_artifact( "closure", { "mission_ref": mission_ref, @@ -3154,6 +3151,16 @@ def project( "divergence_flags": sorted(set(flags)), }, ) + if ( + prior is not None + and prior.get("state") == "closed" + and not set(normalized_events).difference(prior.get("source_event_refs", [])) + and projection.to_dict() != prior + ): + # Replaying identical owner evidence is a rebuild, not a reopen. + # Changed gates, flags or provenance still require a new event. + raise ContractError("REOPEN_REQUIRES_NEW_EVIDENCE") + return projection BASELINE_DEFINITIONS: Mapping[str, Mapping[str, str]] = { diff --git a/tests/ares_runtime/test_closure_projection_rebuild.py b/tests/ares_runtime/test_closure_projection_rebuild.py new file mode 100644 index 0000000000000..41045a3623b37 --- /dev/null +++ b/tests/ares_runtime/test_closure_projection_rebuild.py @@ -0,0 +1,92 @@ +"""Rebuilding closed evidence is idempotent; reopening still needs evidence.""" + +import pytest + +from ares_runtime.collaboration import ClosureProjector, ContractError + + +def project(projector, *, gates=None, events=("event:close",), flags=(), prior=None): + return projector.project( + "mission:rebuild", + "engineering", + {"test": True} if gates is None else gates, + source_event_refs=events, + source_event_exists=lambda _ref: True, + flags=flags, + previous_projection=prior, + ) + + +@pytest.mark.parametrize("prior_form", ["artifact", "mapping"]) +def test_identical_closed_projection_rebuilds_same_artifact(prior_form): + projector = ClosureProjector() + closed = project(projector) + prior = closed if prior_form == "artifact" else closed.to_dict() + rebuilt = project(projector, prior=prior) + assert rebuilt.canonical_bytes() == closed.canonical_bytes() + assert rebuilt.artifact_digest == closed.artifact_digest + + +def test_rebuild_normalizes_duplicate_event_order_without_new_evidence(): + projector = ClosureProjector() + closed = project(projector, events=("event:a", "event:b")) + rebuilt = project(projector, events=("event:b", "event:a", "event:a"), prior=closed) + assert rebuilt.canonical_bytes() == closed.canonical_bytes() + + +@pytest.mark.parametrize( + "gates,flags", + [ + ({"test": False}, ()), + ({"test": True}, ("LEDGER_AHEAD_OF_UI",)), + ({"test": True}, ("AMBIGUOUS_EFFECT",)), + ], +) +def test_departure_from_closed_state_without_new_evidence_remains_rejected(gates, flags): + projector = ClosureProjector() + closed = project(projector) + with pytest.raises(ContractError, match="REOPEN_REQUIRES_NEW_EVIDENCE"): + project(projector, gates=gates, flags=flags, prior=closed) + + +@pytest.mark.parametrize("flags,state", [((), "evidence_pending"), (("AMBIGUOUS_EFFECT",), "quarantined")]) +def test_new_source_event_allows_derived_reopening(flags, state): + projector = ClosureProjector() + closed = project(projector) + reopened = project( + projector, + gates={"test": False}, + events=("event:close", "event:failure"), + flags=flags, + prior=closed, + ) + assert reopened.to_dict()["state"] == state + + +def test_same_evidence_closed_rebuild_still_requires_matching_lineage(): + projector = ClosureProjector() + closed = project(projector).to_dict() + closed["mission_ref"] = "mission:other" + with pytest.raises(ContractError, match="PROJECTION_LINEAGE_MISMATCH"): + project(projector, prior=closed) + + +@pytest.mark.parametrize("change", ["gate", "event"]) +def test_changed_closed_projection_without_new_evidence_is_still_rejected(change): + projector = ClosureProjector() + closed = project(projector, events=("event:a", "event:b")) + gates = {"test": True, "other_check": True} if change == "gate" else None + events = ("event:a",) if change == "event" else ("event:a", "event:b") + with pytest.raises(ContractError, match="REOPEN_REQUIRES_NEW_EVIDENCE"): + project(projector, gates=gates, events=events, prior=closed) + + +def test_identical_rebuild_rechecks_source_event_existence(): + projector = ClosureProjector() + closed = project(projector) + with pytest.raises(ContractError, match="MISSING_SOURCE_EVENT"): + projector.project( + "mission:rebuild", "engineering", {"test": True}, + source_event_refs=["event:close"], source_event_exists=lambda _ref: False, + previous_projection=closed, + )