Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions ares_runtime/collaboration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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])
Expand All @@ -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,
Expand All @@ -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]] = {
Expand Down
92 changes: 92 additions & 0 deletions tests/ares_runtime/test_closure_projection_rebuild.py
Original file line number Diff line number Diff line change
@@ -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,
)
Loading