abd4c6de49
CI / lint (pull_request) Successful in 14s
CI / typecheck (pull_request) Successful in 32s
CI / security (pull_request) Successful in 30s
CI / quality (pull_request) Successful in 18s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / unit_tests (pull_request) Successful in 1m59s
CI / integration_tests (pull_request) Successful in 3m47s
CI / docker (pull_request) Successful in 47s
CI / coverage (pull_request) Successful in 5m48s
CI / lint (push) Successful in 13s
CI / typecheck (push) Successful in 35s
CI / security (push) Successful in 34s
CI / quality (push) Successful in 19s
CI / build (push) Successful in 17s
CI / unit_tests (push) Successful in 2m27s
CI / integration_tests (push) Successful in 2m59s
CI / benchmark-regression (push) Has been skipped
CI / coverage (push) Successful in 4m9s
CI / benchmark-publish (push) Successful in 16m43s
CI / docker (push) Successful in 39s
CI / benchmark-regression (pull_request) Successful in 30m2s
Add InvariantReconciliationActor that runs at the start of the Strategize phase to reconcile invariants from four scopes (global, project, action, plan). The actor detects conflicts, resolves them using specificity-based precedence (plan > action > project > global), honours non_overridable global invariants, records invariant_enforced decisions, and produces a reconciled InvariantSet. Changes: - New: src/cleveragents/actor/reconciliation.py - InvariantReconciliationActor class with collect_invariants() and run() - reconcile_invariants() pure function - ScopeInvariants, ConflictRecord, ReconciliationResult dataclasses - Modified: src/cleveragents/domain/models/core/invariant.py - Added non_overridable: bool field to Invariant model - New: features/invariant_reconciliation_actor.feature (26 BDD scenarios) - New: features/steps/invariant_reconciliation_actor_steps.py - New: robot/invariant_reconciliation_actor.robot - New: robot/helper_invariant_reconciliation.py - New: benchmarks/invariant_reconciliation_bench.py Closes #549
220 lines
6.1 KiB
Python
220 lines
6.1 KiB
Python
"""Robot Framework helper for invariant reconciliation actor integration tests.
|
|
|
|
Exercises the full reconciliation pipeline: collect → reconcile → enforce,
|
|
then verifies the reconciled set is respected by subsequent operations.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Ensure source tree is importable
|
|
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
|
if _SRC not in sys.path:
|
|
sys.path.insert(0, _SRC)
|
|
|
|
from cleveragents.actor.reconciliation import ( # noqa: E402
|
|
InvariantReconciliationActor,
|
|
)
|
|
from cleveragents.application.services.decision_service import ( # noqa: E402
|
|
DecisionService,
|
|
)
|
|
from cleveragents.application.services.invariant_service import ( # noqa: E402
|
|
InvariantService,
|
|
)
|
|
from cleveragents.domain.models.core.decision import DecisionType # noqa: E402
|
|
from cleveragents.domain.models.core.invariant import InvariantScope # noqa: E402
|
|
|
|
|
|
def _run_reconciliation_pipeline() -> dict[str, object]:
|
|
"""Run the full reconciliation pipeline and return results as dict."""
|
|
inv_svc = InvariantService()
|
|
dec_svc = DecisionService()
|
|
|
|
# Add invariants across scopes
|
|
inv_svc.add_invariant(
|
|
"Never delete production data",
|
|
InvariantScope.GLOBAL,
|
|
"system",
|
|
)
|
|
inv_svc.add_invariant(
|
|
"Use ORM for all queries",
|
|
InvariantScope.PROJECT,
|
|
"local/api-service",
|
|
)
|
|
inv_svc.add_invariant(
|
|
"Separate commits per file",
|
|
InvariantScope.ACTION,
|
|
"local/code-coverage",
|
|
)
|
|
inv_svc.add_invariant(
|
|
"Mock all network calls",
|
|
InvariantScope.PLAN,
|
|
"01JQAAAAAAAAAAAAAAAAAAAA01",
|
|
)
|
|
|
|
actor = InvariantReconciliationActor(
|
|
invariant_service=inv_svc,
|
|
decision_service=dec_svc,
|
|
)
|
|
|
|
result = actor.run(
|
|
plan_id="01JQAAAAAAAAAAAAAAAAAAAA01",
|
|
project_name="local/api-service",
|
|
action_name="local/code-coverage",
|
|
)
|
|
|
|
return {
|
|
"invariant_count": len(result.reconciled_set.invariants),
|
|
"conflict_count": len(result.conflicts),
|
|
"decision_count": len(result.enforced_decision_ids),
|
|
"invariant_texts": [inv.text for inv in result.reconciled_set.invariants],
|
|
}
|
|
|
|
|
|
def _run_conflict_resolution() -> dict[str, object]:
|
|
"""Run reconciliation with conflicts and verify resolution."""
|
|
inv_svc = InvariantService()
|
|
dec_svc = DecisionService()
|
|
|
|
inv_svc.add_invariant(
|
|
"All tests must pass",
|
|
InvariantScope.GLOBAL,
|
|
"system",
|
|
)
|
|
inv_svc.add_invariant(
|
|
"All tests must pass",
|
|
InvariantScope.PLAN,
|
|
"01JQAAAAAAAAAAAAAAAAAAAA01",
|
|
)
|
|
|
|
actor = InvariantReconciliationActor(
|
|
invariant_service=inv_svc,
|
|
decision_service=dec_svc,
|
|
)
|
|
|
|
result = actor.run(plan_id="01JQAAAAAAAAAAAAAAAAAAAA01")
|
|
|
|
winner_scope = ""
|
|
for inv in result.reconciled_set.invariants:
|
|
if inv.text.lower() == "all tests must pass":
|
|
winner_scope = inv.scope.value
|
|
break
|
|
|
|
return {
|
|
"invariant_count": len(result.reconciled_set.invariants),
|
|
"conflict_count": len(result.conflicts),
|
|
"winner_scope": winner_scope,
|
|
}
|
|
|
|
|
|
def _run_non_overridable_test() -> dict[str, object]:
|
|
"""Verify non_overridable global invariants block overrides."""
|
|
inv_svc = InvariantService()
|
|
dec_svc = DecisionService()
|
|
|
|
inv = inv_svc.add_invariant(
|
|
"Payment processing must be idempotent",
|
|
InvariantScope.GLOBAL,
|
|
"system",
|
|
)
|
|
inv.non_overridable = True
|
|
|
|
inv_svc.add_invariant(
|
|
"Payment processing must be idempotent",
|
|
InvariantScope.PLAN,
|
|
"01JQAAAAAAAAAAAAAAAAAAAA01",
|
|
)
|
|
|
|
actor = InvariantReconciliationActor(
|
|
invariant_service=inv_svc,
|
|
decision_service=dec_svc,
|
|
)
|
|
|
|
result = actor.run(plan_id="01JQAAAAAAAAAAAAAAAAAAAA01")
|
|
|
|
winner_scope = ""
|
|
for r_inv in result.reconciled_set.invariants:
|
|
if "idempotent" in r_inv.text.lower():
|
|
winner_scope = r_inv.scope.value
|
|
break
|
|
|
|
return {
|
|
"invariant_count": len(result.reconciled_set.invariants),
|
|
"conflict_count": len(result.conflicts),
|
|
"winner_scope": winner_scope,
|
|
"has_non_overridable_reason": any(
|
|
"non_overridable" in c.reason for c in result.conflicts
|
|
),
|
|
}
|
|
|
|
|
|
def _run_decision_verification() -> dict[str, object]:
|
|
"""Verify invariant_enforced decisions are properly recorded."""
|
|
inv_svc = InvariantService()
|
|
dec_svc = DecisionService()
|
|
|
|
inv_svc.add_invariant(
|
|
"Never delete production data",
|
|
InvariantScope.GLOBAL,
|
|
"system",
|
|
)
|
|
inv_svc.add_invariant(
|
|
"Mock all network calls",
|
|
InvariantScope.PLAN,
|
|
"01JQAAAAAAAAAAAAAAAAAAAA01",
|
|
)
|
|
|
|
actor = InvariantReconciliationActor(
|
|
invariant_service=inv_svc,
|
|
decision_service=dec_svc,
|
|
)
|
|
|
|
result = actor.run(plan_id="01JQAAAAAAAAAAAAAAAAAAAA01")
|
|
|
|
# Verify decisions
|
|
all_correct_type = True
|
|
all_correct_plan = True
|
|
for did in result.enforced_decision_ids:
|
|
decision = dec_svc.get_decision(did)
|
|
if decision.decision_type != DecisionType.INVARIANT_ENFORCED:
|
|
all_correct_type = False
|
|
if decision.plan_id != "01JQAAAAAAAAAAAAAAAAAAAA01":
|
|
all_correct_plan = False
|
|
|
|
return {
|
|
"decision_count": len(result.enforced_decision_ids),
|
|
"all_correct_type": all_correct_type,
|
|
"all_correct_plan": all_correct_plan,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
"""Entry point for Robot Framework helper."""
|
|
if len(sys.argv) < 2:
|
|
print("Usage: helper_invariant_reconciliation.py <command>")
|
|
sys.exit(1)
|
|
|
|
command = sys.argv[1]
|
|
handlers = {
|
|
"reconcile": _run_reconciliation_pipeline,
|
|
"conflict": _run_conflict_resolution,
|
|
"non-overridable": _run_non_overridable_test,
|
|
"decisions": _run_decision_verification,
|
|
}
|
|
|
|
handler = handlers.get(command)
|
|
if handler is None:
|
|
print(f"Unknown command: {command}")
|
|
sys.exit(1)
|
|
|
|
result = handler()
|
|
print(json.dumps(result))
|
|
print("reconciliation-ok")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|