0e36755db9
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 14s
CI / quality (pull_request) Successful in 16s
CI / security (pull_request) Successful in 31s
CI / typecheck (pull_request) Successful in 43s
CI / unit_tests (pull_request) Successful in 2m18s
CI / integration_tests (pull_request) Successful in 2m53s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 3m58s
CI / benchmark-regression (pull_request) Successful in 24m44s
- Rehydrate sequence counter from DB on restart (BUG-1) - Add uniqueness guard raising SequenceConflictError (BUG-2) - Fix delete_decision consistency between persisted/in-memory (BUG-3) - Validate new_decision_id exists in mark_superseded (BUG-4) - Include orphaned subtrees in get_tree output (BUG-5) - Remove dead _decision_seq/_next_seq from plan_lifecycle (BUG-7) - Export SequenceConflictError from services __init__ (SPEC-2) - Replace list[Any] with list[ArtifactRef] typing (SPEC-4) - Add actor_reasoning max_length validation (SEC-1) - Use SELECT COUNT(*) in count_decisions (PERF-1) - Replace MagicMock with create_autospec(Settings) (TEST-3) - Eliminate UnitOfWork.__new__() anti-pattern (TEST-4) - Use exact assertion counts in DI tests (TEST-5) - Add restart rehydration, BFS order, invalid type, and confidence boundary test scenarios (TEST-1/2/6/7) - Fix decision_service_coverage.feature to match actual API ISSUES CLOSED: #172
246 lines
6.9 KiB
Python
246 lines
6.9 KiB
Python
"""Helper script for Robot Framework decision recording smoke tests.
|
|
|
|
Usage:
|
|
python robot/helper_decision_recording.py <subcommand>
|
|
|
|
Subcommands:
|
|
record-retrieve Record + retrieve a decision via DecisionService
|
|
sequencing Verify monotonic sequencing
|
|
snapshot-capture Verify auto-captured context snapshot
|
|
tree-bfs Build a tree and verify BFS via get_tree
|
|
superseded Mark a decision as superseded via service
|
|
delete Delete a decision and verify snapshot removal
|
|
snapshot-dedup Verify hash-based deduplication in SnapshotStore
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Ensure src is importable when run from workspace root
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
|
from cleveragents.application.services.decision_service import (
|
|
DecisionService,
|
|
SnapshotStore,
|
|
)
|
|
from cleveragents.domain.models.core.decision import (
|
|
ContextSnapshot,
|
|
DecisionType,
|
|
)
|
|
|
|
_PLAN_ID = "01HV000000000000000000RS01"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Subcommands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _record_retrieve():
|
|
svc = DecisionService()
|
|
d = svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.PROMPT_DEFINITION,
|
|
question="What approach?",
|
|
chosen_option="Build API",
|
|
)
|
|
got = svc.get_decision(d.decision_id)
|
|
assert got.decision_id == d.decision_id
|
|
assert got.is_root
|
|
assert str(got.decision_type) == "prompt_definition"
|
|
print("record-retrieve-ok")
|
|
|
|
|
|
def _sequencing():
|
|
svc = DecisionService()
|
|
d0 = svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.PROMPT_DEFINITION,
|
|
question="First",
|
|
chosen_option="Chosen first",
|
|
)
|
|
d1 = svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Second",
|
|
chosen_option="Chosen second",
|
|
parent_decision_id=d0.decision_id,
|
|
)
|
|
d2 = svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.IMPLEMENTATION_CHOICE,
|
|
question="Third",
|
|
chosen_option="Chosen third",
|
|
parent_decision_id=d0.decision_id,
|
|
)
|
|
assert d0.sequence_number == 0
|
|
assert d1.sequence_number == 1
|
|
assert d2.sequence_number == 2
|
|
assert svc.count_decisions(_PLAN_ID) == 3
|
|
print("sequencing-ok")
|
|
|
|
|
|
def _snapshot_capture():
|
|
svc = DecisionService()
|
|
d = svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Snapshot test",
|
|
chosen_option="Chosen",
|
|
)
|
|
snap = svc.get_snapshot(d.decision_id)
|
|
assert snap is not None
|
|
assert snap.hot_context_hash.startswith("sha256:")
|
|
print("snapshot-capture-ok")
|
|
|
|
|
|
def _tree_bfs():
|
|
svc = DecisionService()
|
|
root = svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.PROMPT_DEFINITION,
|
|
question="Root",
|
|
chosen_option="Root choice",
|
|
)
|
|
child1 = svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Child 1",
|
|
chosen_option="C1",
|
|
parent_decision_id=root.decision_id,
|
|
)
|
|
svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Child 2",
|
|
chosen_option="C2",
|
|
parent_decision_id=root.decision_id,
|
|
)
|
|
svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.IMPLEMENTATION_CHOICE,
|
|
question="Grandchild",
|
|
chosen_option="GC",
|
|
parent_decision_id=child1.decision_id,
|
|
)
|
|
tree = svc.get_tree(_PLAN_ID)
|
|
assert len(tree) == 4, f"expected 4, got {len(tree)}"
|
|
assert tree[0].is_root
|
|
|
|
# Verify BFS level order: compute depths and check non-decreasing.
|
|
by_id = {d.decision_id: d for d in tree}
|
|
|
|
def _depth(node):
|
|
depth = 0
|
|
current = node
|
|
while current.parent_decision_id is not None:
|
|
parent = by_id.get(current.parent_decision_id)
|
|
if parent is None:
|
|
break
|
|
depth += 1
|
|
current = parent
|
|
return depth
|
|
|
|
depths = [_depth(d) for d in tree]
|
|
for i in range(1, len(depths)):
|
|
assert depths[i] >= depths[i - 1], (
|
|
f"BFS order violated at index {i}: "
|
|
f"depth {depths[i]} follows depth {depths[i - 1]}. "
|
|
f"Depths: {depths}"
|
|
)
|
|
print("tree-bfs-ok")
|
|
|
|
|
|
def _superseded():
|
|
svc = DecisionService()
|
|
d1 = svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.PROMPT_DEFINITION,
|
|
question="Original",
|
|
chosen_option="Orig",
|
|
)
|
|
d2 = svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Replacement",
|
|
chosen_option="New",
|
|
parent_decision_id=d1.decision_id,
|
|
)
|
|
updated = svc.mark_superseded(d1.decision_id, d2.decision_id)
|
|
assert updated.is_superseded
|
|
assert updated.superseded_by == d2.decision_id
|
|
superseded = svc.get_superseded(_PLAN_ID)
|
|
assert len(superseded) == 1
|
|
print("superseded-ok")
|
|
|
|
|
|
def _delete():
|
|
svc = DecisionService()
|
|
d = svc.record_decision(
|
|
plan_id=_PLAN_ID,
|
|
decision_type=DecisionType.PROMPT_DEFINITION,
|
|
question="Ephemeral",
|
|
chosen_option="Gone",
|
|
)
|
|
did = d.decision_id
|
|
assert svc.get_snapshot(did) is not None
|
|
result = svc.delete_decision(did)
|
|
assert result is True
|
|
assert svc.get_snapshot(did) is None
|
|
assert svc.count_decisions(_PLAN_ID) == 0
|
|
print("delete-ok")
|
|
|
|
|
|
def _snapshot_dedup():
|
|
store = SnapshotStore()
|
|
snap = ContextSnapshot(
|
|
hot_context_hash="sha256:samehash",
|
|
hot_context_ref="ref1",
|
|
)
|
|
store.store("DEC_A", snap)
|
|
store.store("DEC_B", snap)
|
|
|
|
ids = store.get_by_hash("sha256:samehash")
|
|
assert len(ids) == 2, f"expected 2, got {len(ids)}"
|
|
assert "DEC_A" in ids
|
|
assert "DEC_B" in ids
|
|
|
|
# Remove one
|
|
store.remove("DEC_A")
|
|
ids2 = store.get_by_hash("sha256:samehash")
|
|
assert len(ids2) == 1
|
|
assert "DEC_B" in ids2
|
|
|
|
print("snapshot-dedup-ok")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatcher
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_COMMANDS = {
|
|
"record-retrieve": _record_retrieve,
|
|
"sequencing": _sequencing,
|
|
"snapshot-capture": _snapshot_capture,
|
|
"tree-bfs": _tree_bfs,
|
|
"superseded": _superseded,
|
|
"delete": _delete,
|
|
"snapshot-dedup": _snapshot_dedup,
|
|
}
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
raise SystemExit(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>")
|
|
command = sys.argv[1]
|
|
handler = _COMMANDS.get(command)
|
|
if handler is None:
|
|
raise SystemExit(f"Unknown command: {command}")
|
|
handler()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|