241c2602b0
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 1m1s
CI / helm (pull_request) Successful in 36s
CI / typecheck (pull_request) Successful in 1m11s
CI / push-validation (pull_request) Successful in 34s
CI / build (pull_request) Successful in 53s
CI / security (pull_request) Successful in 1m17s
CI / quality (pull_request) Successful in 1m23s
CI / benchmark-regression (pull_request) Failing after 1m14s
CI / integration_tests (pull_request) Successful in 5m14s
CI / e2e_tests (pull_request) Successful in 5m22s
CI / unit_tests (pull_request) Successful in 9m0s
CI / docker (pull_request) Successful in 1m51s
CI / coverage (pull_request) Successful in 11m11s
CI / status-check (push) Blocked by required conditions
CI / benchmark-publish (push) Has started running
CI / benchmark-regression (push) Has been skipped
CI / status-check (pull_request) Successful in 4s
CI / lint (push) Successful in 1m17s
CI / push-validation (push) Successful in 35s
CI / build (push) Successful in 54s
CI / quality (push) Successful in 1m29s
CI / helm (push) Successful in 47s
CI / security (push) Successful in 1m33s
CI / typecheck (push) Successful in 1m38s
CI / integration_tests (push) Successful in 4m25s
CI / e2e_tests (push) Failing after 5m25s
CI / unit_tests (push) Successful in 7m18s
CI / coverage (push) Has started running
CI / docker (push) Has started running
Fix two lint blockers identified in PR review: 1. Collapsible list comprehension format (ruff RUF015): Collapse the "projects" list comprehension in _build_strategize_context_snapshot() from 3 lines to a single line within the 88-character limit. 2. Missing type annotations on 13 new BDD step functions: All existing step functions use explicit `context: Context` and `-> None` return type annotations per project style. The 13 new step functions added for issue #9056 were missing these declarations, causing lint warning RUF012 (missing type annotations on function arguments). Verified: - nox -s lint passes (ruff check + ruff format --check) - nox -s typecheck passes (pyright 0 errors) - Pre-existing unit_tests/CI timeouts are infrastructure-related ISSUES CLOSED: #9056
1251 lines
44 KiB
Python
1251 lines
44 KiB
Python
"""Step definitions for decision_recording.feature.
|
|
|
|
All ``Then`` step texts are prefixed with ``dsvc`` (decision-service) to
|
|
avoid collisions with the existing ``decision_model_steps.py`` which
|
|
defines similar assertion patterns for the domain model layer.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import re
|
|
import tempfile
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from ulid import ULID
|
|
|
|
from cleveragents.application.services.decision_service import (
|
|
DecisionNotFoundError,
|
|
DecisionService,
|
|
DuplicateDecisionError,
|
|
SequenceConflictError,
|
|
SnapshotStore,
|
|
)
|
|
from cleveragents.core.exceptions import ValidationError
|
|
from cleveragents.domain.models.core.decision import (
|
|
ArtifactRef,
|
|
ContextSnapshot,
|
|
Decision,
|
|
DecisionType,
|
|
)
|
|
|
|
|
|
def _resolve_plan_id(context: Context, symbolic_id: str) -> str:
|
|
"""Map a human-readable plan name to a stable ULID.
|
|
|
|
The Decision model requires plan_id to be a valid 26-char ULID.
|
|
Feature files use friendly names like ``"P1"``; this helper
|
|
lazily generates and caches a ULID for each symbolic name.
|
|
"""
|
|
registry: dict[str, str] = getattr(context, "_plan_id_registry", {})
|
|
if symbolic_id not in registry:
|
|
registry[symbolic_id] = str(ULID())
|
|
context._plan_id_registry = registry
|
|
return registry[symbolic_id]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a decision service")
|
|
def step_decision_service(context: Context) -> None:
|
|
context.decision_service = DecisionService()
|
|
context.recorded_decisions = []
|
|
context.decision_error = None
|
|
context.decision_result = None
|
|
context.decision_list = None
|
|
context.tree_result = None
|
|
context.path_result = None
|
|
context.superseded_result = None
|
|
context.snapshot_result = None
|
|
context.snapshots_dict = None
|
|
context.hash_query_result = None
|
|
context.delete_result = None
|
|
context.explicit_snapshot = None
|
|
context._plan_id_registry = {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('a context snapshot with hash "{ctx_hash}" and ref "{ctx_ref}"')
|
|
def step_explicit_snapshot(context: Context, ctx_hash: str, ctx_ref: str) -> None:
|
|
context.explicit_snapshot = ContextSnapshot(
|
|
hot_context_hash=ctx_hash,
|
|
hot_context_ref=ctx_ref,
|
|
)
|
|
|
|
|
|
@given('a DuplicateDecisionError for decision "{decision_id}"')
|
|
def step_duplicate_error(context: Context, decision_id: str) -> None:
|
|
context.decision_error = DuplicateDecisionError(decision_id)
|
|
|
|
|
|
@given('a SequenceConflictError for plan "{plan_id}" sequence {seq:d}')
|
|
def step_sequence_error(context: Context, plan_id: str, seq: int) -> None:
|
|
context.decision_error = SequenceConflictError(plan_id, seq)
|
|
|
|
|
|
@given("a standalone snapshot store")
|
|
def step_standalone_snapshot_store(context: Context) -> None:
|
|
context.standalone_store = SnapshotStore()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — Recording
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when(
|
|
'I record a prompt_definition decision for plan "{plan_id}" '
|
|
'with question "{question}"'
|
|
)
|
|
def step_record_root(context: Context, plan_id: str, question: str) -> None:
|
|
svc = context.decision_service
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.PROMPT_DEFINITION,
|
|
question=question,
|
|
chosen_option=question,
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when(
|
|
'I record a strategy_choice decision for plan "{plan_id}" '
|
|
'with question "{question}"'
|
|
)
|
|
def step_record_strategy(context: Context, plan_id: str, question: str) -> None:
|
|
svc = context.decision_service
|
|
parent_id = None
|
|
if context.recorded_decisions:
|
|
parent_id = context.recorded_decisions[0].decision_id
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question=question,
|
|
chosen_option=f"Chosen: {question}",
|
|
parent_decision_id=parent_id,
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when(
|
|
'I record an implementation_choice decision for plan "{plan_id}" '
|
|
'with question "{question}"'
|
|
)
|
|
def step_record_impl(context: Context, plan_id: str, question: str) -> None:
|
|
svc = context.decision_service
|
|
parent_id = None
|
|
if context.recorded_decisions:
|
|
parent_id = context.recorded_decisions[0].decision_id
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.IMPLEMENTATION_CHOICE,
|
|
question=question,
|
|
chosen_option=f"Chosen: {question}",
|
|
parent_decision_id=parent_id,
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when(
|
|
'I record a strategy_choice decision for plan "{plan_id}" '
|
|
"with the explicit snapshot"
|
|
)
|
|
def step_record_with_snapshot(context: Context, plan_id: str) -> None:
|
|
svc = context.decision_service
|
|
parent_id = None
|
|
if context.recorded_decisions:
|
|
parent_id = context.recorded_decisions[0].decision_id
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Snapshot question",
|
|
chosen_option="Snapshot choice",
|
|
parent_decision_id=parent_id,
|
|
context_snapshot=context.explicit_snapshot,
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when(
|
|
'I record a strategy_choice decision for plan "{plan_id}" '
|
|
"with the same explicit snapshot"
|
|
)
|
|
def step_record_with_same_snapshot(context: Context, plan_id: str) -> None:
|
|
svc = context.decision_service
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Another snapshot question",
|
|
chosen_option="Another choice",
|
|
context_snapshot=context.explicit_snapshot,
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when(
|
|
'I record a strategy_choice decision for plan "{plan_id}" with confidence {score:g}'
|
|
)
|
|
def step_record_with_confidence(context: Context, plan_id: str, score: float) -> None:
|
|
svc = context.decision_service
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Confidence test",
|
|
chosen_option="Chosen",
|
|
confidence_score=score,
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when(
|
|
'I record a correction decision for plan "{plan_id}" correcting the first decision'
|
|
)
|
|
def step_record_correction(context: Context, plan_id: str) -> None:
|
|
svc = context.decision_service
|
|
first = context.recorded_decisions[0]
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Correction question",
|
|
chosen_option="Corrected choice",
|
|
parent_decision_id=first.parent_decision_id,
|
|
is_correction=True,
|
|
corrects_decision_id=first.decision_id,
|
|
correction_reason="Fixed mistake",
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when(
|
|
'I record a strategy_choice decision for plan "{plan_id}" '
|
|
'with alternatives "{a1}" "{a2}" "{a3}"'
|
|
)
|
|
def step_record_with_alternatives(
|
|
context: Context, plan_id: str, a1: str, a2: str, a3: str
|
|
) -> None:
|
|
svc = context.decision_service
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Alternatives test",
|
|
chosen_option="Chosen",
|
|
alternatives_considered=[a1, a2, a3],
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when('I record an implementation_choice decision for plan "{plan_id}" with artifacts')
|
|
def step_record_with_artifacts(context: Context, plan_id: str) -> None:
|
|
svc = context.decision_service
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.IMPLEMENTATION_CHOICE,
|
|
question="Artifact test",
|
|
chosen_option="Chosen",
|
|
artifacts_produced=[
|
|
ArtifactRef(artifact_path="src/main.py", artifact_type="file"),
|
|
ArtifactRef(artifact_path="tests/test_main.py", artifact_type="file"),
|
|
],
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when('I record a decision for plan "{plan_id}" with string type "{dtype}"')
|
|
def step_record_string_type(context: Context, plan_id: str, dtype: str) -> None:
|
|
svc = context.decision_service
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=dtype,
|
|
question="String type test",
|
|
chosen_option="Chosen",
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
# --- Recording errors ---
|
|
|
|
|
|
@when("I try to record a decision with empty plan_id")
|
|
def step_try_empty_plan_id(context: Context) -> None:
|
|
try:
|
|
context.decision_service.record_decision(
|
|
plan_id="",
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Q",
|
|
chosen_option="A",
|
|
)
|
|
context.decision_error = None
|
|
except ValidationError as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
@when("I try to record a decision with empty question")
|
|
def step_try_empty_question(context: Context) -> None:
|
|
try:
|
|
context.decision_service.record_decision(
|
|
plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="",
|
|
chosen_option="A",
|
|
)
|
|
context.decision_error = None
|
|
except ValidationError as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
@when("I try to record a decision with empty chosen_option")
|
|
def step_try_empty_chosen(context: Context) -> None:
|
|
try:
|
|
context.decision_service.record_decision(
|
|
plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Q",
|
|
chosen_option="",
|
|
)
|
|
context.decision_error = None
|
|
except ValidationError as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
@when('I try to record a decision with invalid type "{dtype}"')
|
|
def step_try_invalid_type(context: Context, dtype: str) -> None:
|
|
try:
|
|
context.decision_service.record_decision(
|
|
plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
decision_type=dtype,
|
|
question="Q",
|
|
chosen_option="A",
|
|
)
|
|
context.decision_error = None
|
|
except (ValueError, ValidationError) as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
@when("I try to record a decision with confidence {score:g}")
|
|
def step_try_invalid_confidence(context: Context, score: float) -> None:
|
|
import pydantic
|
|
|
|
try:
|
|
context.decision_service.record_decision(
|
|
plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Q",
|
|
chosen_option="A",
|
|
confidence_score=score,
|
|
)
|
|
context.decision_error = None
|
|
except (ValidationError, pydantic.ValidationError) as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — Retrieval
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I get the decision by its ID")
|
|
def step_get_by_id(context: Context) -> None:
|
|
d = context.decision_result
|
|
context.decision_result = context.decision_service.get_decision(d.decision_id)
|
|
|
|
|
|
@when('I try to get decision "{decision_id}"')
|
|
def step_try_get(context: Context, decision_id: str) -> None:
|
|
try:
|
|
context.decision_service.get_decision(decision_id)
|
|
context.decision_error = None
|
|
except DecisionNotFoundError as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
@when('I list decisions for plan "{plan_id}"')
|
|
def step_list_decisions(context: Context, plan_id: str) -> None:
|
|
context.decision_list = context.decision_service.list_decisions(
|
|
_resolve_plan_id(context, plan_id)
|
|
)
|
|
|
|
|
|
@when('I filter decisions for plan "{plan_id}" by type "{dtype}"')
|
|
def step_list_by_type(context: Context, plan_id: str, dtype: str) -> None:
|
|
context.decision_list = context.decision_service.list_by_type(
|
|
_resolve_plan_id(context, plan_id), dtype
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — Tree operations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when(
|
|
'I record a strategy_choice decision for plan "{plan_id}" '
|
|
'with parent as child "{question}"'
|
|
)
|
|
def step_record_child(context: Context, plan_id: str, question: str) -> None:
|
|
svc = context.decision_service
|
|
parent = context.recorded_decisions[0]
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question=question,
|
|
chosen_option=f"Chosen: {question}",
|
|
parent_decision_id=parent.decision_id,
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when(
|
|
'I record an implementation_choice decision for plan "{plan_id}" '
|
|
'with second parent as child "{question}"'
|
|
)
|
|
def step_record_grandchild(context: Context, plan_id: str, question: str) -> None:
|
|
svc = context.decision_service
|
|
# Use the second recorded decision (index 1) as parent
|
|
parent = context.recorded_decisions[1]
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.IMPLEMENTATION_CHOICE,
|
|
question=question,
|
|
chosen_option=f"Chosen: {question}",
|
|
parent_decision_id=parent.decision_id,
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when('I get the tree for plan "{plan_id}"')
|
|
def step_get_tree(context: Context, plan_id: str) -> None:
|
|
context.tree_result = context.decision_service.get_tree(
|
|
_resolve_plan_id(context, plan_id)
|
|
)
|
|
|
|
|
|
@when("I get the path to root from the last decision")
|
|
def step_get_path(context: Context) -> None:
|
|
last = context.recorded_decisions[-1]
|
|
context.path_result = context.decision_service.get_path_to_root(last.decision_id)
|
|
|
|
|
|
@when('I try to get path to root for decision "{decision_id}"')
|
|
def step_try_path(context: Context, decision_id: str) -> None:
|
|
try:
|
|
context.decision_service.get_path_to_root(decision_id)
|
|
context.decision_error = None
|
|
except DecisionNotFoundError as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — Superseded
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I mark the first decision as superseded by the second")
|
|
def step_mark_superseded(context: Context) -> None:
|
|
first = context.recorded_decisions[0]
|
|
second = context.recorded_decisions[1]
|
|
context.decision_result = context.decision_service.mark_superseded(
|
|
first.decision_id, second.decision_id
|
|
)
|
|
# Update stored reference
|
|
context.recorded_decisions[0] = context.decision_result
|
|
|
|
|
|
@when('I get superseded decisions for plan "{plan_id}"')
|
|
def step_get_superseded(context: Context, plan_id: str) -> None:
|
|
context.superseded_result = context.decision_service.get_superseded(
|
|
_resolve_plan_id(context, plan_id)
|
|
)
|
|
|
|
|
|
@when('I try to mark decision "{decision_id}" as superseded')
|
|
def step_try_supersede(context: Context, decision_id: str) -> None:
|
|
try:
|
|
context.decision_service.mark_superseded(
|
|
decision_id, "01FAKE0000000000000000000"
|
|
)
|
|
context.decision_error = None
|
|
except DecisionNotFoundError as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — Delete
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I delete the recorded decision")
|
|
def step_delete(context: Context) -> None:
|
|
d = context.decision_result
|
|
context.delete_result = context.decision_service.delete_decision(d.decision_id)
|
|
context.deleted_decision_id = d.decision_id
|
|
|
|
|
|
@when('I try to delete decision "{decision_id}"')
|
|
def step_try_delete(context: Context, decision_id: str) -> None:
|
|
try:
|
|
context.decision_service.delete_decision(decision_id)
|
|
context.decision_error = None
|
|
except DecisionNotFoundError as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — Snapshots
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I get the snapshot for the recorded decision")
|
|
def step_get_snapshot(context: Context) -> None:
|
|
d = context.decision_result
|
|
context.snapshot_result = context.decision_service.get_snapshot(d.decision_id)
|
|
|
|
|
|
@when("I get the snapshot for the deleted decision")
|
|
def step_get_snapshot_deleted(context: Context) -> None:
|
|
context.snapshot_result = context.decision_service.get_snapshot(
|
|
context.deleted_decision_id
|
|
)
|
|
|
|
|
|
@when('I get snapshots for plan "{plan_id}"')
|
|
def step_get_plan_snapshots(context: Context, plan_id: str) -> None:
|
|
context.snapshots_dict = context.decision_service.get_snapshots_for_plan(
|
|
_resolve_plan_id(context, plan_id)
|
|
)
|
|
|
|
|
|
@when('I query the snapshot store by hash "{ctx_hash}"')
|
|
def step_query_hash(context: Context, ctx_hash: str) -> None:
|
|
context.hash_query_result = context.decision_service.snapshots.get_by_hash(ctx_hash)
|
|
|
|
|
|
# --- Standalone snapshot store ---
|
|
|
|
|
|
@when('I remove snapshot for decision "{decision_id}"')
|
|
def step_standalone_remove(context: Context, decision_id: str) -> None:
|
|
context.snapshot_remove_result = context.standalone_store.remove(decision_id)
|
|
|
|
|
|
@when('I get snapshot for decision "{decision_id}"')
|
|
def step_standalone_get(context: Context, decision_id: str) -> None:
|
|
context.standalone_snapshot = context.standalone_store.get(decision_id)
|
|
|
|
|
|
@when('I query the standalone store by hash "{ctx_hash}"')
|
|
def step_standalone_hash(context: Context, ctx_hash: str) -> None:
|
|
context.standalone_hash_result = context.standalone_store.get_by_hash(ctx_hash)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — Recording assertions (dsvc-prefixed)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the dsvc decision should be recorded successfully")
|
|
def step_recorded(context: Context) -> None:
|
|
assert context.decision_result is not None
|
|
|
|
|
|
@then("the dsvc decision sequence number should be {seq:d}")
|
|
def step_seq_number(context: Context, seq: int) -> None:
|
|
assert context.decision_result.sequence_number == seq
|
|
|
|
|
|
@then('the dsvc decision type should be "{dtype}"')
|
|
def step_decision_type(context: Context, dtype: str) -> None:
|
|
assert str(context.decision_result.decision_type) == dtype
|
|
|
|
|
|
@then("the dsvc decision should be a root decision")
|
|
def step_is_root(context: Context) -> None:
|
|
assert context.decision_result.is_root
|
|
|
|
|
|
@then("the dsvc decision should have a valid ULID as decision_id")
|
|
def step_valid_ulid(context: Context) -> None:
|
|
ulid_re = re.compile(r"^[0-9A-HJKMNP-TV-Z]{26}$")
|
|
assert ulid_re.match(context.decision_result.decision_id)
|
|
|
|
|
|
@then("the dsvc decision context snapshot hash should not be empty")
|
|
def step_snapshot_hash_not_empty(context: Context) -> None:
|
|
assert context.decision_result.context_snapshot.hot_context_hash != ""
|
|
|
|
|
|
@then('the dsvc plan "{plan_id}" should have {count:d} decisions')
|
|
def step_plan_count(context: Context, plan_id: str, count: int) -> None:
|
|
assert (
|
|
context.decision_service.count_decisions(_resolve_plan_id(context, plan_id))
|
|
== count
|
|
)
|
|
|
|
|
|
@then("the dsvc decisions should have sequence numbers {nums}")
|
|
def step_seq_numbers(context: Context, nums: str) -> None:
|
|
expected = [int(n) for n in nums.split()]
|
|
actual = [d.sequence_number for d in context.recorded_decisions]
|
|
assert actual == expected, f"Expected {expected}, got {actual}"
|
|
|
|
|
|
@then('the dsvc decision context snapshot hash should be "{expected}"')
|
|
def step_snapshot_hash_exact(context: Context, expected: str) -> None:
|
|
assert context.decision_result.context_snapshot.hot_context_hash == expected
|
|
|
|
|
|
@then('the dsvc decision context snapshot ref should be "{expected}"')
|
|
def step_snapshot_ref_exact(context: Context, expected: str) -> None:
|
|
assert context.decision_result.context_snapshot.hot_context_ref == expected
|
|
|
|
|
|
@then("the dsvc decision confidence score should be {score:g}")
|
|
def step_confidence(context: Context, score: float) -> None:
|
|
assert context.decision_result.confidence_score == score
|
|
|
|
|
|
@then("the dsvc correction decision is_correction should be true")
|
|
def step_is_correction(context: Context) -> None:
|
|
assert context.decision_result.is_correction
|
|
|
|
|
|
@then(
|
|
"the dsvc correction decision corrects_decision_id should match the first decision"
|
|
)
|
|
def step_corrects_first(context: Context) -> None:
|
|
first = context.recorded_decisions[0]
|
|
assert context.decision_result.corrects_decision_id == first.decision_id
|
|
|
|
|
|
@then("the dsvc decision should have {count:d} alternatives considered")
|
|
def step_alternatives_count(context: Context, count: int) -> None:
|
|
assert len(context.decision_result.alternatives_considered) == count
|
|
|
|
|
|
@then("the dsvc decision should have {count:d} artifacts produced")
|
|
def step_artifacts_count(context: Context, count: int) -> None:
|
|
assert len(context.decision_result.artifacts_produced) == count
|
|
|
|
|
|
@then('the dsvc decision context snapshot hash should start with "{prefix}"')
|
|
def step_snapshot_hash_prefix(context: Context, prefix: str) -> None:
|
|
assert context.decision_result.context_snapshot.hot_context_hash.startswith(prefix)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — Validation errors (dsvc-prefixed)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("a dsvc validation error should be raised")
|
|
def step_validation_error(context: Context) -> None:
|
|
import pydantic
|
|
|
|
assert context.decision_error is not None
|
|
assert isinstance(
|
|
context.decision_error, (ValidationError, ValueError, pydantic.ValidationError)
|
|
)
|
|
|
|
|
|
@then('the dsvc error should mention "{text}"')
|
|
def step_error_mention(context: Context, text: str) -> None:
|
|
assert text in str(context.decision_error)
|
|
|
|
|
|
@then("a dsvc value error should be raised")
|
|
def step_value_error(context: Context) -> None:
|
|
assert context.decision_error is not None
|
|
assert isinstance(context.decision_error, ValueError)
|
|
|
|
|
|
@then("a dsvc decision not found error should be raised")
|
|
def step_not_found_error(context: Context) -> None:
|
|
assert context.decision_error is not None
|
|
assert isinstance(context.decision_error, DecisionNotFoundError)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — Retrieval assertions (dsvc-prefixed)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the dsvc retrieved decision should match the recorded decision")
|
|
def step_match_recorded(context: Context) -> None:
|
|
recorded = context.recorded_decisions[-1]
|
|
assert context.decision_result.decision_id == recorded.decision_id
|
|
|
|
|
|
@then("the dsvc decision list should have {count:d} entries")
|
|
def step_list_count(context: Context, count: int) -> None:
|
|
assert len(context.decision_list) == count
|
|
|
|
|
|
@then("the dsvc decision list should be ordered by sequence number")
|
|
def step_list_ordered(context: Context) -> None:
|
|
seqs = [d.sequence_number for d in context.decision_list]
|
|
assert seqs == sorted(seqs)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — Tree assertions (dsvc-prefixed)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the dsvc tree should have {count:d} decisions")
|
|
def step_tree_count(context: Context, count: int) -> None:
|
|
assert len(context.tree_result) == count
|
|
|
|
|
|
@then("the dsvc first tree decision should be a root decision")
|
|
def step_tree_root(context: Context) -> None:
|
|
assert context.tree_result[0].is_root
|
|
|
|
|
|
@then("the dsvc tree should be in BFS level order")
|
|
def step_tree_bfs_order(context: Context) -> None:
|
|
"""Verify that all nodes at depth *d* appear before any node at depth *d+1*.
|
|
|
|
We compute each node's depth by walking ``parent_decision_id`` links,
|
|
then assert the depth sequence is non-decreasing.
|
|
"""
|
|
tree: list[Decision] = context.tree_result
|
|
if len(tree) <= 1:
|
|
return # trivially ordered
|
|
|
|
# Build a lookup: decision_id -> Decision
|
|
by_id = {d.decision_id: d for d in tree}
|
|
|
|
def _depth(node: Decision) -> int:
|
|
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}"
|
|
)
|
|
|
|
|
|
@then("the dsvc path should have {count:d} decisions")
|
|
def step_path_count(context: Context, count: int) -> None:
|
|
assert len(context.path_result) == count
|
|
|
|
|
|
@then("the dsvc first path decision should be the last recorded decision")
|
|
def step_path_first(context: Context) -> None:
|
|
last = context.recorded_decisions[-1]
|
|
assert context.path_result[0].decision_id == last.decision_id
|
|
|
|
|
|
@then("the dsvc last path decision should be the root")
|
|
def step_path_last_root(context: Context) -> None:
|
|
assert context.path_result[-1].is_root
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — Superseded assertions (dsvc-prefixed)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the dsvc first decision should be superseded")
|
|
def step_first_superseded(context: Context) -> None:
|
|
assert context.recorded_decisions[0].is_superseded
|
|
|
|
|
|
@then("the dsvc first decision superseded_by should match the second decision")
|
|
def step_superseded_by(context: Context) -> None:
|
|
first = context.recorded_decisions[0]
|
|
second = context.recorded_decisions[1]
|
|
assert first.superseded_by == second.decision_id
|
|
|
|
|
|
@then("the dsvc superseded list should have {count:d} entry")
|
|
def step_superseded_count(context: Context, count: int) -> None:
|
|
assert len(context.superseded_result) == count
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — Delete assertions (dsvc-prefixed)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the dsvc decision should be deleted")
|
|
def step_deleted(context: Context) -> None:
|
|
assert context.delete_result is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — Snapshot assertions (dsvc-prefixed)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the dsvc snapshot should not be None")
|
|
def step_snapshot_not_none(context: Context) -> None:
|
|
assert context.snapshot_result is not None
|
|
|
|
|
|
@then("the dsvc snapshot should be None")
|
|
def step_snapshot_none(context: Context) -> None:
|
|
assert context.snapshot_result is None
|
|
|
|
|
|
@then("the dsvc snapshots dict should have {count:d} entries")
|
|
def step_snapshots_dict_count(context: Context, count: int) -> None:
|
|
assert len(context.snapshots_dict) == count
|
|
|
|
|
|
@then("the dsvc hash query should return {count:d} decision IDs")
|
|
def step_hash_count(context: Context, count: int) -> None:
|
|
assert len(context.hash_query_result) == count
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — Statistics assertions (dsvc-prefixed)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then('the dsvc decision count for plan "{plan_id}" should be {count:d}')
|
|
def step_count(context: Context, plan_id: str, count: int) -> None:
|
|
assert (
|
|
context.decision_service.count_decisions(_resolve_plan_id(context, plan_id))
|
|
== count
|
|
)
|
|
|
|
|
|
@then('the dsvc next sequence for plan "{plan_id}" should be {seq:d}')
|
|
def step_next_seq(context: Context, plan_id: str, seq: int) -> None:
|
|
assert (
|
|
context.decision_service.get_next_sequence(_resolve_plan_id(context, plan_id))
|
|
== seq
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — Error object assertions (dsvc-prefixed)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then('the dsvc duplicate error decision_id should be "{decision_id}"')
|
|
def step_dup_id(context: Context, decision_id: str) -> None:
|
|
assert context.decision_error.decision_id == decision_id
|
|
|
|
|
|
@then('the dsvc duplicate error message should contain "{text}"')
|
|
def step_dup_msg(context: Context, text: str) -> None:
|
|
assert text in str(context.decision_error)
|
|
|
|
|
|
@then('the dsvc sequence error plan_id should be "{plan_id}"')
|
|
def step_seq_err_plan(context: Context, plan_id: str) -> None:
|
|
assert context.decision_error.plan_id == plan_id
|
|
|
|
|
|
@then("the dsvc sequence error sequence_number should be {seq:d}")
|
|
def step_seq_err_num(context: Context, seq: int) -> None:
|
|
assert context.decision_error.sequence_number == seq
|
|
|
|
|
|
@then('the dsvc sequence error message should contain "{text}"')
|
|
def step_seq_err_msg(context: Context, text: str) -> None:
|
|
assert text in str(context.decision_error)
|
|
|
|
|
|
# --- Standalone snapshot store (dsvc-prefixed) ---
|
|
|
|
|
|
@then("the dsvc snapshot remove result should be False")
|
|
def step_standalone_remove_false(context: Context) -> None:
|
|
assert context.snapshot_remove_result is False
|
|
|
|
|
|
@then("the dsvc standalone snapshot should be None")
|
|
def step_standalone_none(context: Context) -> None:
|
|
assert context.standalone_snapshot is None
|
|
|
|
|
|
@then("the dsvc standalone hash query should return {count:d} decision IDs")
|
|
def step_standalone_hash_count(context: Context, count: int) -> None:
|
|
assert len(context.standalone_hash_result) == count
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Edge-case coverage steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a context snapshot with no hash")
|
|
def step_snapshot_no_hash(context: Context) -> None:
|
|
context.explicit_snapshot = ContextSnapshot(
|
|
hot_context_hash="",
|
|
hot_context_ref="",
|
|
)
|
|
|
|
|
|
@when('I store the hashless snapshot for decision "{decision_id}"')
|
|
def step_store_hashless(context: Context, decision_id: str) -> None:
|
|
context.standalone_store.store(decision_id, context.explicit_snapshot)
|
|
|
|
|
|
@when('I get standalone snapshot for decision "{decision_id}"')
|
|
def step_get_standalone_snapshot(context: Context, decision_id: str) -> None:
|
|
context.standalone_retrieved = context.standalone_store.get(decision_id)
|
|
|
|
|
|
@when('I store the explicit snapshot for decision "{decision_id}"')
|
|
def step_store_explicit(context: Context, decision_id: str) -> None:
|
|
context.standalone_store.store(decision_id, context.explicit_snapshot)
|
|
|
|
|
|
@then("the dsvc standalone retrieved snapshot should not be None")
|
|
def step_standalone_retrieved_not_none(context: Context) -> None:
|
|
assert context.standalone_retrieved is not None
|
|
|
|
|
|
@then("the dsvc standalone retrieved snapshot hash should be empty")
|
|
def step_standalone_retrieved_hash_empty(context: Context) -> None:
|
|
assert context.standalone_retrieved.hot_context_hash == ""
|
|
|
|
|
|
@then("the dsvc snapshot remove result should be True")
|
|
def step_standalone_remove_true(context: Context) -> None:
|
|
assert context.snapshot_remove_result is True
|
|
|
|
|
|
@then(
|
|
'the dsvc standalone hash query for "{ctx_hash}" '
|
|
"should return {count:d} decision IDs"
|
|
)
|
|
def step_standalone_hash_query_count(
|
|
context: Context, ctx_hash: str, count: int
|
|
) -> None:
|
|
result = context.standalone_store.get_by_hash(ctx_hash)
|
|
assert len(result) == count, f"Expected {count}, got {len(result)}"
|
|
|
|
|
|
@when('I record a decision for plan "{plan_id}" with a forced non-root parent')
|
|
def step_record_forced_non_root(context: Context, plan_id: str) -> None:
|
|
"""Record a decision with a fake parent_decision_id so it's not a root."""
|
|
svc = context.decision_service
|
|
d = svc.record_decision(
|
|
plan_id=_resolve_plan_id(context, plan_id),
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Non-root decision",
|
|
chosen_option="Chosen",
|
|
parent_decision_id=str(ULID()),
|
|
)
|
|
context.decision_result = d
|
|
context.recorded_decisions.append(d)
|
|
|
|
|
|
@when("I try to record a decision with whitespace plan_id")
|
|
def step_try_whitespace_plan_id(context: Context) -> None:
|
|
try:
|
|
context.decision_service.record_decision(
|
|
plan_id=" ",
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Q",
|
|
chosen_option="A",
|
|
)
|
|
context.decision_error = None
|
|
except ValidationError as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
@when("I try to record a decision with whitespace question")
|
|
def step_try_whitespace_question(context: Context) -> None:
|
|
try:
|
|
context.decision_service.record_decision(
|
|
plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question=" ",
|
|
chosen_option="A",
|
|
)
|
|
context.decision_error = None
|
|
except ValidationError as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
@when("I try to record a decision with whitespace chosen_option")
|
|
def step_try_whitespace_chosen(context: Context) -> None:
|
|
try:
|
|
context.decision_service.record_decision(
|
|
plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
question="Q",
|
|
chosen_option=" ",
|
|
)
|
|
context.decision_error = None
|
|
except ValidationError as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
@when("I list snapshots for decisions with missing entries")
|
|
def step_list_snapshots_missing(context: Context) -> None:
|
|
"""Call list_for_plan with a Decision whose snapshot is not in the store."""
|
|
fake_decision = Decision(
|
|
plan_id=str(ULID()),
|
|
decision_type=DecisionType.STRATEGY_CHOICE,
|
|
sequence_number=0,
|
|
question="ghost",
|
|
chosen_option="ghost",
|
|
)
|
|
context.standalone_plan_snapshots = context.standalone_store.list_for_plan(
|
|
[fake_decision]
|
|
)
|
|
|
|
|
|
@then("the dsvc standalone plan snapshots should have {count:d} entries")
|
|
def step_standalone_plan_snapshots_count(context: Context, count: int) -> None:
|
|
assert len(context.standalone_plan_snapshots) == count
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Persisted-mode (database-backed) steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a persisted decision service")
|
|
def step_persisted_decision_service(context: Context) -> None:
|
|
"""Create a DecisionService backed by a real SQLite database via UnitOfWork."""
|
|
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
|
|
|
|
db_path = tempfile.mktemp(suffix=".db", prefix="dsvc_persisted_")
|
|
context._dsvc_db_path = db_path
|
|
uow = UnitOfWork(f"sqlite:///{db_path}")
|
|
uow.init_database()
|
|
|
|
context.decision_service = DecisionService(unit_of_work=uow)
|
|
context.recorded_decisions = []
|
|
context.decision_error = None
|
|
context.decision_result = None
|
|
context.decision_list = None
|
|
context.tree_result = None
|
|
context.path_result = None
|
|
context.superseded_result = None
|
|
context.snapshot_result = None
|
|
context.snapshots_dict = None
|
|
context.hash_query_result = None
|
|
context.delete_result = None
|
|
context.explicit_snapshot = None
|
|
context._plan_id_registry = {}
|
|
|
|
# Register cleanup to remove temp DB file
|
|
def _cleanup_db() -> None:
|
|
for suffix in ("", "-journal", "-wal", "-shm"):
|
|
with contextlib.suppress(OSError):
|
|
os.unlink(db_path + suffix)
|
|
|
|
if not hasattr(context, "_cleanup_handlers"):
|
|
context._cleanup_handlers = []
|
|
context._cleanup_handlers.append(_cleanup_db)
|
|
|
|
|
|
@when("I recreate the decision service with the same database")
|
|
def step_recreate_service_same_db(context: Context) -> None:
|
|
"""Destroy the current DecisionService and create a fresh one against the same DB.
|
|
|
|
This simulates a service restart: the in-memory sequence counters
|
|
are lost and must be rehydrated from the database.
|
|
"""
|
|
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
|
|
|
|
db_path = context._dsvc_db_path
|
|
uow = UnitOfWork(f"sqlite:///{db_path}")
|
|
uow.init_database()
|
|
|
|
context.decision_service = DecisionService(unit_of_work=uow)
|
|
# Preserve _plan_id_registry so symbolic IDs ("P1") still resolve
|
|
# to the same ULIDs used before the restart.
|
|
context.recorded_decisions = []
|
|
context.decision_error = None
|
|
context.decision_result = None
|
|
|
|
|
|
@when("I try to store a duplicate of the recorded decision")
|
|
def step_try_store_duplicate(context: Context) -> None:
|
|
"""Attempt to store a decision with the same ID that already exists."""
|
|
try:
|
|
decision = context.decision_result
|
|
context.decision_service._store_decision(decision)
|
|
context.decision_error = None
|
|
except DuplicateDecisionError as exc:
|
|
context.decision_error = exc
|
|
|
|
|
|
@then("a dsvc duplicate decision error should be raised")
|
|
def step_duplicate_error_raised(context: Context) -> None:
|
|
assert context.decision_error is not None
|
|
assert isinstance(context.decision_error, DuplicateDecisionError)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Full context snapshot steps (issue #9056)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a plan lifecycle service with decision service wired")
|
|
def step_lifecycle_with_decision_service(context: Context) -> None:
|
|
"""Create a PlanLifecycleService with a real DecisionService wired in."""
|
|
from cleveragents.application.services.decision_service import DecisionService
|
|
from cleveragents.application.services.plan_lifecycle_service import (
|
|
PlanLifecycleService,
|
|
)
|
|
from cleveragents.config.settings import Settings
|
|
|
|
Settings._instance = None
|
|
settings = Settings()
|
|
context.snapshot_decision_service = DecisionService()
|
|
context.snapshot_lifecycle_service = PlanLifecycleService(
|
|
settings=settings,
|
|
decision_service=context.snapshot_decision_service,
|
|
)
|
|
context.snapshot_plan = None
|
|
context.snapshot_decision = None
|
|
context.snapshot_action_name = None
|
|
|
|
|
|
@given('an action "{action_name}" for strategize snapshot test')
|
|
def step_create_action_for_snapshot_test(context: Context, action_name: str) -> None:
|
|
"""Create an action for the strategize snapshot test."""
|
|
context.snapshot_action_name = action_name
|
|
context.snapshot_lifecycle_service.create_action(
|
|
name=action_name,
|
|
description=f"Action {action_name} for snapshot test",
|
|
definition_of_done="Snapshot test done",
|
|
strategy_actor="openai/gpt-4",
|
|
execution_actor="openai/gpt-4",
|
|
)
|
|
|
|
|
|
@given('a plan created from "{action_name}" with project "{project_name}"')
|
|
def step_create_plan_with_project(
|
|
context: Context, action_name: str, project_name: str
|
|
) -> None:
|
|
"""Create a plan from the given action with a project link."""
|
|
from cleveragents.domain.models.core.plan import ProjectLink
|
|
|
|
context.snapshot_plan = context.snapshot_lifecycle_service.use_action(
|
|
action_name=action_name,
|
|
project_links=[ProjectLink(project_name=project_name)],
|
|
)
|
|
|
|
|
|
@given('a plan created from "{action_name}" without projects')
|
|
def step_create_plan_without_projects(context: Context, action_name: str) -> None:
|
|
"""Create a plan from the given action without any project links."""
|
|
context.snapshot_plan = context.snapshot_lifecycle_service.use_action(
|
|
action_name=action_name,
|
|
project_links=[],
|
|
)
|
|
|
|
|
|
@when("I start strategize for the snapshot test plan")
|
|
def step_start_strategize_snapshot_test(context: Context) -> None:
|
|
"""Start strategize and capture the recorded decision."""
|
|
plan_id = context.snapshot_plan.identity.plan_id
|
|
context.snapshot_lifecycle_service.start_strategize(plan_id)
|
|
|
|
decisions = context.snapshot_decision_service.list_decisions(plan_id)
|
|
assert len(decisions) >= 1, f"Expected at least 1 decision, got {len(decisions)}"
|
|
strategy_decisions = [
|
|
d for d in decisions if d.decision_type.value == "strategy_choice"
|
|
]
|
|
assert len(strategy_decisions) >= 1, "Expected at least 1 strategy_choice decision"
|
|
context.snapshot_decision = strategy_decisions[0]
|
|
|
|
|
|
@then("the strategize decision should have a non-empty hot_context_hash")
|
|
def step_check_snapshot_hash_not_empty(context: Context) -> None:
|
|
"""Verify the context snapshot hash is not empty."""
|
|
snapshot = context.snapshot_decision.context_snapshot
|
|
assert snapshot.hot_context_hash, "hot_context_hash should not be empty"
|
|
|
|
|
|
@then("the strategize decision should have a non-empty hot_context_ref")
|
|
def step_check_snapshot_ref_not_empty(context: Context) -> None:
|
|
"""Verify the context snapshot ref is not empty."""
|
|
snapshot = context.snapshot_decision.context_snapshot
|
|
assert snapshot.hot_context_ref, "hot_context_ref should not be empty"
|
|
|
|
|
|
@then('the strategize decision hot_context_ref should start with "{prefix}"')
|
|
def step_check_snapshot_ref_prefix(context: Context, prefix: str) -> None:
|
|
"""Verify the context snapshot ref starts with the expected prefix."""
|
|
snapshot = context.snapshot_decision.context_snapshot
|
|
assert snapshot.hot_context_ref.startswith(prefix), (
|
|
f"hot_context_ref should start with {prefix!r}"
|
|
)
|
|
|
|
|
|
@then("the strategize decision should have a non-empty actor_state_ref")
|
|
def step_check_snapshot_actor_ref_not_empty(context: Context) -> None:
|
|
"""Verify the actor_state_ref is not empty."""
|
|
snapshot = context.snapshot_decision.context_snapshot
|
|
assert snapshot.actor_state_ref, "actor_state_ref should not be empty"
|
|
|
|
|
|
@then("the strategize decision should have relevant_resources populated")
|
|
def step_check_snapshot_resources_populated(context: Context) -> None:
|
|
"""Verify relevant_resources is not empty."""
|
|
snapshot = context.snapshot_decision.context_snapshot
|
|
assert len(snapshot.relevant_resources) > 0, (
|
|
"relevant_resources should not be empty"
|
|
)
|
|
|
|
|
|
@then('the strategize decision hot_context_hash should start with "{prefix}"')
|
|
def step_check_snapshot_hash_prefix(context: Context, prefix: str) -> None:
|
|
"""Verify the context snapshot hash starts with the expected prefix."""
|
|
snapshot = context.snapshot_decision.context_snapshot
|
|
assert snapshot.hot_context_hash.startswith(prefix), (
|
|
f"hot_context_hash should start with {prefix!r}"
|
|
)
|
|
|
|
|
|
@then("the strategize decision hot_context_hash should be {length:d} characters long")
|
|
def step_check_snapshot_hash_length(context: Context, length: int) -> None:
|
|
"""Verify the context snapshot hash has the expected length."""
|
|
snapshot = context.snapshot_decision.context_snapshot
|
|
actual_length = len(snapshot.hot_context_hash)
|
|
assert actual_length == length, (
|
|
f"hot_context_hash length should be {length}, got {actual_length}"
|
|
)
|
|
|
|
|
|
@then("the strategize decision should have empty relevant_resources")
|
|
def step_check_snapshot_resources_empty(context: Context) -> None:
|
|
"""Verify relevant_resources is empty."""
|
|
snapshot = context.snapshot_decision.context_snapshot
|
|
assert len(snapshot.relevant_resources) == 0, (
|
|
f"relevant_resources should be empty, got {snapshot.relevant_resources}"
|
|
)
|