158 lines
5.1 KiB
Python
158 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
|
|
|
from aios_convergence_contracts import (
|
|
AcceptanceCriterion,
|
|
AcceptanceDecision,
|
|
AcceptanceInvariant,
|
|
AcceptanceVector,
|
|
ContinuationManifest,
|
|
ContinuationStopReason,
|
|
ConvergenceContractError,
|
|
DesiredState,
|
|
EvidenceState,
|
|
ObservedHealth,
|
|
ObservedState,
|
|
ReconciliationState,
|
|
StateOwnership,
|
|
evaluate_acceptance,
|
|
ownership_index,
|
|
plan_reconciliation,
|
|
validate_state_ownership,
|
|
)
|
|
|
|
|
|
def test_acceptance_never_promotes_required_unknown():
|
|
vector = AcceptanceVector(
|
|
subject_type="goal",
|
|
subject_id="goal-1",
|
|
subject_revision=1,
|
|
criteria=(
|
|
AcceptanceCriterion(
|
|
criterion_id="runtime",
|
|
required=True,
|
|
state=EvidenceState.UNKNOWN,
|
|
verifier_id="runtime-proof",
|
|
),
|
|
),
|
|
)
|
|
result = evaluate_acceptance(vector)
|
|
assert result.decision == AcceptanceDecision.UNKNOWN
|
|
assert result.reasons == ("criterion_unknown:runtime",)
|
|
|
|
|
|
def test_acceptance_rejects_failed_invariant():
|
|
vector = AcceptanceVector(
|
|
subject_type="release",
|
|
subject_id="V2R2C2",
|
|
subject_revision=1,
|
|
criteria=(
|
|
AcceptanceCriterion("tests", True, EvidenceState.PASS, "pytest"),
|
|
),
|
|
invariants=(
|
|
AcceptanceInvariant("protected-ref", EvidenceState.FAIL),
|
|
),
|
|
)
|
|
assert evaluate_acceptance(vector).decision == AcceptanceDecision.REJECT
|
|
|
|
|
|
def test_acceptance_blocks_on_unresolved_blocker():
|
|
vector = AcceptanceVector(
|
|
subject_type="goal",
|
|
subject_id="goal-1",
|
|
subject_revision=1,
|
|
criteria=(AcceptanceCriterion("code", True, EvidenceState.PASS, "ci"),),
|
|
unresolved_blockers=("blocker-1",),
|
|
)
|
|
assert evaluate_acceptance(vector).decision == AcceptanceDecision.BLOCKED
|
|
|
|
|
|
def test_state_ownership_is_unique_and_final_writer_is_enforced():
|
|
custom = (
|
|
StateOwnership(
|
|
object_type="goal",
|
|
authoritative_service="goal-kernel",
|
|
authoritative_store="goal-store",
|
|
final_writer="goal-kernel",
|
|
),
|
|
)
|
|
validate_state_ownership("goal", "goal-kernel", custom, final_state=True)
|
|
with pytest.raises(ConvergenceContractError, match="STATE_OWNERSHIP_VIOLATION"):
|
|
validate_state_ownership("goal", "worker", custom, final_state=True)
|
|
with pytest.raises(ConvergenceContractError, match="duplicate_state_owner"):
|
|
ownership_index(custom + custom)
|
|
|
|
|
|
def test_missing_or_unknown_observation_never_converges():
|
|
desired = DesiredState("contract:gpt", "external_projection", 2, {"hash": "abc"})
|
|
missing = plan_reconciliation(desired, None)
|
|
assert missing.state == ReconciliationState.OBSERVING
|
|
assert missing.observation_required is True
|
|
|
|
unknown = ObservedState(
|
|
resource_id="contract:gpt",
|
|
observed_generation=None,
|
|
observed_spec={},
|
|
health=ObservedHealth.UNKNOWN,
|
|
source_identity="gpt-action",
|
|
observed_at="2026-08-25T00:00:00Z",
|
|
)
|
|
result = plan_reconciliation(desired, unknown)
|
|
assert result.state == ReconciliationState.OBSERVING
|
|
|
|
|
|
def test_equal_healthy_observation_converges_and_key_is_stable():
|
|
desired = DesiredState("worker:a", "worker_binding", 3, {"build": "x"})
|
|
observed = ObservedState(
|
|
resource_id="worker:a",
|
|
observed_generation=3,
|
|
observed_spec={"build": "x"},
|
|
health=ObservedHealth.HEALTHY,
|
|
source_identity="worker-registry",
|
|
observed_at="2026-08-25T00:00:00Z",
|
|
)
|
|
one = plan_reconciliation(desired, observed)
|
|
two = plan_reconciliation(desired, observed)
|
|
assert one.state == ReconciliationState.CONVERGED
|
|
assert one.idempotency_key == two.idempotency_key
|
|
|
|
|
|
def test_drift_is_diff_found_not_auto_applied():
|
|
desired = DesiredState("projection:openapi", "external_projection", 4, {"hash": "new"})
|
|
observed = ObservedState(
|
|
resource_id="projection:openapi",
|
|
observed_generation=3,
|
|
observed_spec={"hash": "old"},
|
|
health=ObservedHealth.HEALTHY,
|
|
source_identity="published-openapi",
|
|
observed_at="2026-08-25T00:00:00Z",
|
|
)
|
|
result = plan_reconciliation(desired, observed)
|
|
assert result.state == ReconciliationState.DIFF_FOUND
|
|
assert result.observation_required is False
|
|
|
|
|
|
def test_continuation_manifest_is_durable_and_hashable():
|
|
manifest = ContinuationManifest(
|
|
trace_id="trace-1",
|
|
goal_id="goal-1",
|
|
goal_revision=1,
|
|
task_id="task-1",
|
|
run_id="run-1",
|
|
current_node_id="node-4",
|
|
acceptance_vector_ref="acceptance:goal-1:1",
|
|
next_action="resume node-4",
|
|
stop_reason=ContinuationStopReason.CONTEXT_BOUNDARY,
|
|
checkpoint_ref="checkpoint-9",
|
|
unresolved_blocker_refs=("blocker-a",),
|
|
)
|
|
assert manifest.as_dict()["stop_reason"] == "context_boundary"
|
|
assert manifest.content_hash.startswith("sha256:")
|