Files
cleveragents-core/robot/helper_decision_persistence_serialization.py
brent.edwards 32b81793a2 refactor(test): rename decision persistence files to avoid conflict with master
Rename our serialization-focused decision persistence suites to
*_serialization to avoid add/add conflicts with the repository-based
decision persistence suites that landed on master independently:

- decision_persistence.feature -> decision_persistence_serialization.feature
- decision_persistence_steps.py -> decision_persistence_serialization_steps.py
- decision_persistence_bench.py -> decision_persistence_serialization_bench.py
- decision_persistence.robot -> decision_persistence_serialization.robot
- helper_decision_persistence.py -> helper_decision_persistence_serialization.py

Updated robot helper path, step docstring, and testing.md references.
2026-02-26 16:13:44 +00:00

283 lines
9.4 KiB
Python

"""Helper script for Robot Framework decision persistence integration tests.
Each test function exercises a serialization round-trip path for
the Decision domain model (model_dump / model_validate, JSON) and
prints a deterministic tag on success.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
# Ensure src is importable when run from workspace root
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from ulid import ULID
from cleveragents.domain.models.core.decision import (
ArtifactRef,
ContextSnapshot,
Decision,
DecisionType,
ResourceRef,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_ALL_DECISION_TYPES: list[DecisionType] = list(DecisionType)
def _make_decision(**overrides: Any) -> Decision:
"""Build a Decision with sensible defaults, merged with *overrides*."""
defaults: dict[str, Any] = {
"plan_id": str(ULID()),
"sequence_number": 0,
"decision_type": DecisionType.PROMPT_DEFINITION,
"question": "What approach should we take?",
"chosen_option": "Build a REST API",
}
dt = overrides.get("decision_type", defaults["decision_type"])
if dt != DecisionType.PROMPT_DEFINITION and "parent_decision_id" not in overrides:
defaults["parent_decision_id"] = str(ULID())
defaults.update(overrides)
return Decision(**defaults)
def _roundtrip_dump(d: Decision) -> Decision:
data = d.model_dump()
return Decision.model_validate(data)
def _roundtrip_json(d: Decision) -> Decision:
json_str = d.model_dump_json()
raw = json.loads(json_str)
return Decision.model_validate(raw)
# ---------------------------------------------------------------------------
# Test functions
# ---------------------------------------------------------------------------
def _test_roundtrip_root() -> None:
"""Root decision survives model_dump round-trip."""
original = _make_decision()
restored = _roundtrip_dump(original)
assert restored.decision_id == original.decision_id
assert restored.decision_type == DecisionType.PROMPT_DEFINITION
assert restored.is_root
assert restored.question == original.question
assert restored.chosen_option == original.chosen_option
print("decision-persist-roundtrip-root-ok")
def _test_roundtrip_child() -> None:
"""Child decision with all fields survives model_dump round-trip."""
parent_id = str(ULID())
original = _make_decision(
parent_decision_id=parent_id,
decision_type=DecisionType.STRATEGY_CHOICE,
question="Which framework?",
chosen_option="FastAPI",
alternatives_considered=["Flask", "Django", "Starlette"],
confidence_score=0.88,
rationale="Async support and auto docs",
)
restored = _roundtrip_dump(original)
assert restored.decision_id == original.decision_id
assert restored.parent_decision_id == parent_id
assert not restored.is_root
assert len(restored.alternatives_considered) == 3
assert restored.confidence_score == 0.88
assert restored.rationale == "Async support and auto docs"
print("decision-persist-roundtrip-child-ok")
def _test_snapshot() -> None:
"""Context snapshot with resources survives round-trip."""
snap = ContextSnapshot(
hot_context_hash="sha256:persist_snap",
hot_context_ref="store://snapshots/persist_snap",
relevant_resources=[
ResourceRef(resource_id=str(ULID()), path="src/main.py"),
ResourceRef(resource_id=str(ULID()), path="src/utils.py"),
],
actor_state_ref="checkpoint://actor/persist",
)
original = _make_decision(
decision_type=DecisionType.IMPLEMENTATION_CHOICE,
context_snapshot=snap,
)
restored = _roundtrip_dump(original)
assert (
restored.context_snapshot.hot_context_hash
== original.context_snapshot.hot_context_hash
)
assert len(restored.context_snapshot.relevant_resources) == 2
assert restored.context_snapshot.actor_state_ref == "checkpoint://actor/persist"
print("decision-persist-snapshot-ok")
def _test_json_roundtrip() -> None:
"""Decision survives JSON serialization round-trip."""
original = _make_decision(
decision_type=DecisionType.RESOURCE_SELECTION,
artifacts_produced=[
ArtifactRef(artifact_path="src/api.py", artifact_type="file"),
ArtifactRef(artifact_path="patches/fix.patch", artifact_type="patch"),
],
)
restored = _roundtrip_json(original)
assert restored.decision_id == original.decision_id
assert restored.decision_type == DecisionType.RESOURCE_SELECTION
assert len(restored.artifacts_produced) == 2
print("decision-persist-json-roundtrip-ok")
def _test_correction_chain() -> None:
"""Correction chain of 3 decisions persists and reconstructs."""
plan_id = str(ULID())
decisions: list[Decision] = []
original = _make_decision(
plan_id=plan_id,
sequence_number=0,
question="Initial approach?",
chosen_option="Monolith",
)
decisions.append(original)
prev_id = original.decision_id
for i in range(1, 3):
correction = _make_decision(
plan_id=plan_id,
sequence_number=i,
decision_type=DecisionType.STRATEGY_CHOICE,
question=f"Correction {i}?",
chosen_option=f"Approach v{i + 1}",
is_correction=True,
corrects_decision_id=prev_id,
correction_reason=f"Reason {i}",
)
decisions[-1] = decisions[-1].with_superseded_by(correction.decision_id)
decisions.append(correction)
prev_id = correction.decision_id
restored = [_roundtrip_dump(d) for d in decisions]
for orig, rest in zip(decisions, restored, strict=True):
assert rest.decision_id == orig.decision_id
assert rest.is_correction == orig.is_correction
assert rest.corrects_decision_id == orig.corrects_decision_id
assert rest.superseded_by == orig.superseded_by
for idx in range(1, len(restored)):
assert restored[idx].corrects_decision_id == restored[idx - 1].decision_id
assert restored[idx - 1].superseded_by == restored[idx].decision_id
print("decision-persist-correction-chain-ok")
def _test_tree_reconstruction() -> None:
"""3-level tree (7 nodes) reconstructs from serialized data."""
plan_id = str(ULID())
nodes: list[Decision] = []
seq = 0
root = _make_decision(
plan_id=plan_id,
sequence_number=seq,
question="Root?",
chosen_option="Start",
)
nodes.append(root)
seq += 1
for ci in range(2):
child = _make_decision(
plan_id=plan_id,
parent_decision_id=root.decision_id,
sequence_number=seq,
decision_type=DecisionType.STRATEGY_CHOICE,
question=f"Child {ci}?",
chosen_option=f"Option C{ci}",
)
nodes.append(child)
seq += 1
for gi in range(2):
grandchild = _make_decision(
plan_id=plan_id,
parent_decision_id=child.decision_id,
sequence_number=seq,
decision_type=DecisionType.IMPLEMENTATION_CHOICE,
question=f"Grandchild {ci}.{gi}?",
chosen_option=f"Impl {ci}.{gi}",
)
nodes.append(grandchild)
seq += 1
restored = [_roundtrip_dump(n) for n in nodes]
assert len(restored) == 7
id_set = {n.decision_id for n in restored}
root_count = 0
for n in restored:
if n.parent_decision_id is None:
root_count += 1
else:
assert n.parent_decision_id in id_set
assert root_count == 1
print("decision-persist-tree-reconstruction-ok")
def _test_all_types() -> None:
"""All decision types survive model_dump round-trip."""
plan_id = str(ULID())
for i, dt in enumerate(_ALL_DECISION_TYPES):
kwargs: dict[str, Any] = {
"plan_id": plan_id,
"sequence_number": i,
"decision_type": dt,
"question": f"Q for {dt.value}?",
"chosen_option": f"A for {dt.value}",
}
if dt != DecisionType.PROMPT_DEFINITION:
kwargs["parent_decision_id"] = str(ULID())
original = Decision(**kwargs)
restored = _roundtrip_dump(original)
assert restored.decision_id == original.decision_id
assert restored.decision_type == original.decision_type
print("decision-persist-all-types-ok")
# ---------------------------------------------------------------------------
# Dispatcher
# ---------------------------------------------------------------------------
_TESTS = {
"roundtrip_root": _test_roundtrip_root,
"roundtrip_child": _test_roundtrip_child,
"snapshot": _test_snapshot,
"json_roundtrip": _test_json_roundtrip,
"correction_chain": _test_correction_chain,
"tree_reconstruction": _test_tree_reconstruction,
"all_types": _test_all_types,
}
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <test_name>")
print(f"Available tests: {', '.join(sorted(_TESTS))}")
sys.exit(1)
test_name = sys.argv[1]
if test_name not in _TESTS:
print(f"Unknown test: {test_name}")
print(f"Available: {', '.join(sorted(_TESTS))}")
sys.exit(1)
_TESTS[test_name]()