forked from HAL9000/cleveragents-core
a4a6b061a6
Aligned v3_plans table with specification DDL:
1. Added effective_profile_snapshot column (TEXT NOT NULL) for
storing frozen JSON snapshot of automation profile at plan
creation time. Added Pydantic field_validator ensuring the
value is well-formed JSON. Validator catches RecursionError
for deeply nested JSON, consistent with automation_profile
deserialization hardening. Validator error message uses
length-only to avoid potential information disclosure.
Documented that the default "{}" exists for backward
compatibility; new plans should explicitly set the snapshot.
2. Made root_plan_id NOT NULL — root plans self-reference their
own plan_id, child plans reference the root ancestor. Added
explicit ondelete="RESTRICT" FK policy for consistency with
other FKs in the model. Documented known FK policy drift
between ORM model (RESTRICT) and migrated databases (retained
SET NULL) in the migration; data integrity is preserved by
the NOT NULL constraint regardless. Moved root_plan_id
self-reference resolution into a PlanIdentity model_validator
so the domain model is consistent with the DB NOT NULL
constraint before and after persistence (previously the
resolution only happened in from_domain(), creating an
asymmetry where root_plan_id was None in-memory but non-null
after round-tripping through the database).
3. Made automation_profile NOT NULL with default "balanced".
4. Documented intentional deviation: phase default is "action"
(code) vs "strategize" (spec) because the Action phase was
added as a pre-Strategize setup step.
5. Created Alembic migration with backfill logic for existing
rows. Root-ancestor backfill uses level-by-level propagation
with a parent-readiness guard to correctly resolve plans at
arbitrary hierarchy depth (3+ levels). Added safety bound
(max 100 iterations) with logged error on exhaustion to guard
against cycles in parent_plan_id. Merged batch_alter_table
operations to avoid redundant full-table copies in SQLite
batch mode. Migration backfill also handles empty-string
automation_profile values. Documented downgrade limitation
(backfill is not reversible). Orphan-row fallback now logs
affected row count at WARNING level. Migration cycle-detection
now logs affected plan_id values before the orphan fallback
overwrites them. All migration SQL uses sa.text() for
consistency with SQLAlchemy best practices.
6. Hardened automation_profile deserialization in to_domain() to
catch ValueError (invalid StrEnum provenance), Pydantic
ValidationError, and RecursionError (deeply nested JSON) in
addition to JSONDecodeError and KeyError, preventing
unreadable plans from corrupted DB rows. Applied the same
defensive deserialization pattern to effective_profile_snapshot
in to_domain(): corrupted JSON falls back to '{}' with a
WARNING log instead of crashing the read path. Added TypeError
to the effective_profile_snapshot exception list in to_domain()
for consistency with the Pydantic validator. Logging of
unparseable values uses length only to avoid potential
information disclosure.
7. Used explicit None check (is not None) instead of truthiness
for root_plan_id resolution in from_domain(), for
effective_profile_snapshot in to_domain(), and in
_serialize_automation_profile() for consistency.
8. Documented intentional column naming conventions vs spec DDL
(e.g. automation_profile vs automation_profile_name, *_actor
vs *_actor_name, processing_state vs state, v3_plans vs
plans). Documented the semantic difference: automation_profile
stores either a bare name or structured JSON with provenance,
whereas the spec automation_profile_name stores a plain name.
9. Fixed benchmark plan constructors
(plan_phase_migration_bench.py) that were missing the now-
required root_plan_id and effective_profile_snapshot fields.
10. Replaced defensive getattr() with direct attribute access for
effective_profile_snapshot in from_domain() and update(),
since the field is now defined on the Plan domain model.
11. Fixed Any type annotation in test helper _make_plan() to use
AutomationProfileRef | None for proper type safety.
12. Added BDD scenarios for PlanIdentity self-reference
resolution, NULL effective_profile_snapshot constraint
enforcement, valid-JSON-missing-profile_name-key
deserialization, invalid-JSON and empty-string snapshot
rejection by Pydantic validator, and corrupted
effective_profile_snapshot DB fallback in to_domain().
13. Extracted default automation profile name to a module-level
constant (DEFAULT_AUTOMATION_PROFILE) to reduce sentinel
duplication across models.py and repositories.py.
14. Centralised automation-profile serialisation into
LifecyclePlanModel._serialize_automation_profile() to
eliminate duplication between from_domain() and
LifecyclePlanRepository.update().
15. Fixed to_domain() root_plan_id type cast from str | None
to str, reflecting the NOT NULL column constraint.
16. Added PlanIdentity model_validator that resolves None
root_plan_id to plan_id at domain construction time, ensuring
the domain model honours the spec DDL NOT NULL constraint
regardless of persistence state. Simplified from_domain()
root resolution accordingly.
ISSUES CLOSED: #921
615 lines
20 KiB
Python
615 lines
20 KiB
Python
"""Step definitions for database_models_coverage_r2.feature.
|
|
|
|
Targets uncovered lines in
|
|
src/cleveragents/infrastructure/database/models.py
|
|
identified from build/coverage.xml (round 2).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import UTC, datetime
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
from behave import given, then, when # type: ignore[import-untyped]
|
|
|
|
from cleveragents.domain.models.core.plan import (
|
|
AutomationProfileProvenance,
|
|
AutomationProfileRef,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
NOW = datetime(2025, 6, 1, 12, 0, 0, tzinfo=UTC)
|
|
NOW_ISO = NOW.isoformat()
|
|
|
|
|
|
def _make_ulid(seed: int = 1) -> str:
|
|
"""Return a fake but valid-length 26-char ULID."""
|
|
return f"01ABCDEFGH{seed:016d}"
|
|
|
|
|
|
def _minimal_plan_timestamps() -> SimpleNamespace:
|
|
return SimpleNamespace(
|
|
created_at=NOW,
|
|
updated_at=NOW,
|
|
strategize_started_at=None,
|
|
strategize_completed_at=None,
|
|
execute_started_at=None,
|
|
execute_completed_at=None,
|
|
apply_started_at=None,
|
|
applied_at=None,
|
|
)
|
|
|
|
|
|
def _minimal_plan_identity(plan_id: str | None = None) -> SimpleNamespace:
|
|
return SimpleNamespace(
|
|
plan_id=plan_id or _make_ulid(1),
|
|
parent_plan_id=None,
|
|
root_plan_id=None,
|
|
attempt=1,
|
|
)
|
|
|
|
|
|
def _minimal_namespaced_name() -> SimpleNamespace:
|
|
return SimpleNamespace(
|
|
server=None,
|
|
namespace="local",
|
|
name="test-action",
|
|
__str__=lambda self: f"{self.namespace}/{self.name}",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LifecycleActionModel — safety_profile round-trip (lines 376, 427)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a LifecycleActionModel with safety_profile_json populated")
|
|
def step_action_model_with_safety(context: Any) -> None:
|
|
from cleveragents.infrastructure.database.models import (
|
|
LifecycleActionModel,
|
|
)
|
|
|
|
safety_data = {
|
|
"require_sandbox": True,
|
|
"require_checkpoints": False,
|
|
"allow_unsafe_tools": False,
|
|
}
|
|
model = LifecycleActionModel(
|
|
namespaced_name="local/test-action",
|
|
namespace="local",
|
|
name="test-action",
|
|
description="desc",
|
|
definition_of_done="dod",
|
|
strategy_actor="strat",
|
|
execution_actor="exec",
|
|
reusable=True,
|
|
read_only=False,
|
|
state="available",
|
|
tags_json="[]",
|
|
created_at=NOW_ISO,
|
|
updated_at=NOW_ISO,
|
|
safety_profile_json=json.dumps(safety_data),
|
|
)
|
|
# Ensure relationship lists exist
|
|
model.arguments_rel = []
|
|
model.invariants_rel = []
|
|
context.action_model = model
|
|
|
|
|
|
@when("I convert the action model to domain via to_domain")
|
|
def step_action_to_domain(context: Any) -> None:
|
|
context.domain_action = context.action_model.to_domain()
|
|
|
|
|
|
@then("the domain action has a non-null safety_profile object")
|
|
def step_check_safety_profile_not_none(context: Any) -> None:
|
|
sp = context.domain_action.safety_profile
|
|
assert sp is not None, "safety_profile should be deserialized"
|
|
assert sp.require_sandbox is True
|
|
|
|
|
|
@given("an Action domain object with a SafetyProfile attached")
|
|
def step_action_domain_with_safety(context: Any) -> None:
|
|
from cleveragents.domain.models.core.safety_profile import SafetyProfile
|
|
|
|
context.domain_action_input = SimpleNamespace(
|
|
namespaced_name=_minimal_namespaced_name(),
|
|
description="desc",
|
|
long_description=None,
|
|
definition_of_done="dod",
|
|
strategy_actor="strat",
|
|
execution_actor="exec",
|
|
review_actor=None,
|
|
apply_actor=None,
|
|
estimation_actor=None,
|
|
invariant_actor=None,
|
|
automation_profile=None,
|
|
safety_profile=SafetyProfile(
|
|
require_sandbox=True,
|
|
require_checkpoints=False,
|
|
allow_unsafe_tools=True,
|
|
),
|
|
reusable=True,
|
|
read_only=False,
|
|
inputs_schema=None,
|
|
state=SimpleNamespace(value="available"),
|
|
created_by=None,
|
|
tags=[],
|
|
arguments=[],
|
|
invariants=[],
|
|
created_at=NOW,
|
|
updated_at=NOW,
|
|
)
|
|
|
|
|
|
@when("I convert the action to a model via from_domain")
|
|
def step_action_from_domain(context: Any) -> None:
|
|
from cleveragents.infrastructure.database.models import (
|
|
LifecycleActionModel,
|
|
)
|
|
|
|
context.action_model_out = LifecycleActionModel.from_domain(
|
|
context.domain_action_input,
|
|
)
|
|
|
|
|
|
@then("the model safety_profile_json contains serialized SafetyProfile data")
|
|
def step_check_safety_json(context: Any) -> None:
|
|
raw = context.action_model_out.safety_profile_json
|
|
assert raw is not None, "safety_profile_json must not be None"
|
|
parsed = json.loads(raw)
|
|
assert parsed["require_sandbox"] is True
|
|
assert parsed["allow_unsafe_tools"] is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LifecycleActionModel — string arg_type/requirement (lines 466, 469)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given(
|
|
"an Action domain object whose arguments have plain-string arg_type and requirement"
|
|
)
|
|
def step_action_plain_string_args(context: Any) -> None:
|
|
arg = SimpleNamespace(
|
|
name="my_arg",
|
|
arg_type="string", # plain string, no .value
|
|
requirement="required", # plain string, no .value
|
|
description="a test arg",
|
|
default_value=None,
|
|
min_value=None,
|
|
max_value=None,
|
|
validation_pattern=None,
|
|
)
|
|
context.domain_action_input = SimpleNamespace(
|
|
namespaced_name=_minimal_namespaced_name(),
|
|
description="desc",
|
|
long_description=None,
|
|
definition_of_done="dod",
|
|
strategy_actor="strat",
|
|
execution_actor="exec",
|
|
review_actor=None,
|
|
apply_actor=None,
|
|
estimation_actor=None,
|
|
invariant_actor=None,
|
|
automation_profile=None,
|
|
safety_profile=None,
|
|
reusable=True,
|
|
read_only=False,
|
|
inputs_schema=None,
|
|
state=SimpleNamespace(value="available"),
|
|
created_by=None,
|
|
tags=[],
|
|
arguments=[arg],
|
|
invariants=[],
|
|
created_at=NOW,
|
|
updated_at=NOW,
|
|
)
|
|
|
|
|
|
@when("I convert that action to a model via from_domain")
|
|
def step_that_action_from_domain(context: Any) -> None:
|
|
from cleveragents.infrastructure.database.models import (
|
|
LifecycleActionModel,
|
|
)
|
|
|
|
context.action_model_out = LifecycleActionModel.from_domain(
|
|
context.domain_action_input,
|
|
)
|
|
|
|
|
|
@then("the model argument rows use the plain-string values directly")
|
|
def step_check_arg_strings(context: Any) -> None:
|
|
args = context.action_model_out.arguments_rel
|
|
assert len(args) == 1
|
|
assert args[0].arg_type == "string"
|
|
assert args[0].requirement == "required"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LifecyclePlanModel — string processing_state (line 882)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _base_plan_ns(
|
|
*,
|
|
processing_state: Any = "queued",
|
|
automation_profile: AutomationProfileRef | None = None,
|
|
namespaced_name: Any = None,
|
|
error_details: Any = None,
|
|
) -> SimpleNamespace:
|
|
"""Build a minimal Plan-like namespace for from_domain tests."""
|
|
ns_name = namespaced_name or _minimal_namespaced_name()
|
|
return SimpleNamespace(
|
|
identity=_minimal_plan_identity(),
|
|
namespaced_name=ns_name,
|
|
action_name="local/test-action",
|
|
description="plan desc",
|
|
definition_of_done="dod",
|
|
phase=SimpleNamespace(value="action"),
|
|
processing_state=processing_state,
|
|
automation_profile=automation_profile,
|
|
strategy_actor=None,
|
|
execution_actor=None,
|
|
review_actor=None,
|
|
apply_actor=None,
|
|
estimation_actor=None,
|
|
invariant_actor=None,
|
|
execution_environment=None,
|
|
execution_env_priority=None,
|
|
project_links=[],
|
|
invariants=[],
|
|
arguments={},
|
|
arguments_order=[],
|
|
changeset_id=None,
|
|
sandbox_refs=[],
|
|
validation_summary=None,
|
|
decision_root_id=None,
|
|
timestamps=_minimal_plan_timestamps(),
|
|
error_message=None,
|
|
error_details=error_details,
|
|
created_by=None,
|
|
tags=[],
|
|
reusable=True,
|
|
read_only=False,
|
|
effective_profile_snapshot="{}",
|
|
)
|
|
|
|
|
|
@given("a Plan domain object whose processing_state is a plain string")
|
|
def step_plan_string_state(context: Any) -> None:
|
|
context.plan_domain = _base_plan_ns(processing_state="complete")
|
|
|
|
|
|
@when("I convert the plan to a model via from_domain")
|
|
def step_plan_from_domain(context: Any) -> None:
|
|
from cleveragents.infrastructure.database.models import (
|
|
LifecyclePlanModel,
|
|
)
|
|
|
|
context.plan_model_out = LifecyclePlanModel.from_domain(context.plan_domain)
|
|
|
|
|
|
@then("the model processing_state equals the plain string value")
|
|
def step_check_plan_state(context: Any) -> None:
|
|
assert context.plan_model_out.processing_state == "complete"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LifecyclePlanModel — automation_profile (line 890)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a Plan domain object with a non-null automation_profile")
|
|
def step_plan_with_automation_profile(context: Any) -> None:
|
|
profile = AutomationProfileRef(
|
|
profile_name="strict",
|
|
provenance=AutomationProfileProvenance.ACTION,
|
|
)
|
|
context.plan_domain = _base_plan_ns(automation_profile=profile)
|
|
|
|
|
|
@then("the model automation_profile column contains JSON with profile_name")
|
|
def step_check_automation_profile_json(context: Any) -> None:
|
|
raw = context.plan_model_out.automation_profile
|
|
assert raw is not None
|
|
parsed = json.loads(raw)
|
|
assert parsed["profile_name"] == "strict"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LifecyclePlanModel — fallback namespace (line 906)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a Plan domain object whose namespaced_name has no namespace attribute")
|
|
def step_plan_no_namespace(context: Any) -> None:
|
|
# Use a plain string that has no .namespace attribute
|
|
context.plan_domain = _base_plan_ns(namespaced_name="local/test-plan")
|
|
|
|
|
|
@then('the model namespace equals "local"')
|
|
def step_check_namespace_local(context: Any) -> None:
|
|
assert context.plan_model_out.namespace == "local"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LifecyclePlanModel — error_details (line 923)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a Plan domain object with non-null error_details")
|
|
def step_plan_with_error_details(context: Any) -> None:
|
|
context.plan_domain = _base_plan_ns(
|
|
error_details={"code": "E001", "msg": "something failed"},
|
|
)
|
|
|
|
|
|
@then("the model error_details_json is a JSON string of the details dict")
|
|
def step_check_error_details_json(context: Any) -> None:
|
|
raw = context.plan_model_out.error_details_json
|
|
assert raw is not None
|
|
parsed = json.loads(raw)
|
|
assert parsed["code"] == "E001"
|
|
assert parsed["msg"] == "something failed"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SkillModel — include with overrides (line 2327)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a Skill domain object with an include that has overrides")
|
|
def step_skill_with_include_overrides(context: Any) -> None:
|
|
include = SimpleNamespace(
|
|
name="other-skill/base",
|
|
overrides={"timeout": 120},
|
|
)
|
|
context.skill_domain = SimpleNamespace(
|
|
name="local/my-skill",
|
|
description="a skill",
|
|
tool_refs=[],
|
|
includes=[include],
|
|
anonymous_tools=[],
|
|
mcp_servers=[],
|
|
agent_skills=[],
|
|
overrides={},
|
|
version=None,
|
|
)
|
|
|
|
|
|
@when("I convert the skill to a model via from_domain")
|
|
def step_skill_from_domain(context: Any) -> None:
|
|
from cleveragents.infrastructure.database.models import SkillModel
|
|
|
|
context.skill_model_out = SkillModel.from_domain(context.skill_domain)
|
|
|
|
|
|
@then("the include item_config contains the serialized overrides")
|
|
def step_check_include_overrides(context: Any) -> None:
|
|
items = context.skill_model_out.items_rel
|
|
include_items = [i for i in items if i.item_type == "include"]
|
|
assert len(include_items) == 1
|
|
config = json.loads(include_items[0].item_config)
|
|
assert config["overrides"]["timeout"] == 120
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DecisionModel — string decision_type (line 2698)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a Decision domain object whose decision_type is a plain string")
|
|
def step_decision_string_type(context: Any) -> None:
|
|
from cleveragents.domain.models.core.decision import (
|
|
ContextSnapshot,
|
|
)
|
|
|
|
context.decision_domain = SimpleNamespace(
|
|
decision_id=_make_ulid(10),
|
|
plan_id=_make_ulid(11),
|
|
parent_decision_id=None,
|
|
sequence_number=1,
|
|
decision_type="strategy_choice", # plain string, no .value
|
|
question="Which strategy?",
|
|
chosen_option="Option A",
|
|
alternatives_considered=["Option B"],
|
|
confidence_score=0.9,
|
|
context_snapshot=ContextSnapshot(
|
|
hot_context_hash="abc",
|
|
hot_context_ref="ref1",
|
|
relevant_resources=[],
|
|
actor_state_ref="actor1",
|
|
),
|
|
rationale="because",
|
|
actor_reasoning=None,
|
|
downstream_decision_ids=[],
|
|
downstream_plan_ids=[],
|
|
artifacts_produced=[],
|
|
created_at=NOW,
|
|
is_correction=False,
|
|
corrects_decision_id=None,
|
|
correction_reason=None,
|
|
superseded_by=None,
|
|
)
|
|
|
|
|
|
@when("I convert the decision to a model via from_domain")
|
|
def step_decision_from_domain(context: Any) -> None:
|
|
from cleveragents.infrastructure.database.models import DecisionModel
|
|
|
|
context.decision_model_out = DecisionModel.from_domain(
|
|
context.decision_domain,
|
|
)
|
|
|
|
|
|
@then("the model decision_type equals the plain string")
|
|
def step_check_decision_type_string(context: Any) -> None:
|
|
assert context.decision_model_out.decision_type == "strategy_choice"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CheckpointModel — to_domain (lines 2823-2852)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a CheckpointModel with metadata_json containing reason and phase")
|
|
def step_checkpoint_model_with_meta(context: Any) -> None:
|
|
from cleveragents.infrastructure.database.models import CheckpointModel
|
|
|
|
context.checkpoint_model = CheckpointModel(
|
|
checkpoint_id=_make_ulid(20),
|
|
plan_id=_make_ulid(21),
|
|
sandbox_ref="abc123commit",
|
|
decision_id=None,
|
|
checkpoint_type="pre_write",
|
|
resource_id=None,
|
|
filesystem_path="checkpoints/cp1",
|
|
size_bytes=1024,
|
|
created_at=NOW_ISO,
|
|
metadata_json=json.dumps(
|
|
{
|
|
"reason": "before write",
|
|
"source_tool": "file_write",
|
|
"phase": "execute",
|
|
}
|
|
),
|
|
)
|
|
|
|
|
|
@when("I convert the checkpoint model to domain via to_domain")
|
|
def step_checkpoint_to_domain(context: Any) -> None:
|
|
context.domain_checkpoint = context.checkpoint_model.to_domain()
|
|
|
|
|
|
@then("the domain checkpoint has the correct metadata fields")
|
|
def step_check_checkpoint_metadata(context: Any) -> None:
|
|
cp = context.domain_checkpoint
|
|
assert cp.checkpoint_id == _make_ulid(20)
|
|
assert cp.plan_id == _make_ulid(21)
|
|
assert cp.sandbox_ref == "abc123commit"
|
|
assert cp.checkpoint_type == "pre_write"
|
|
assert cp.filesystem_path == "checkpoints/cp1"
|
|
assert cp.size_bytes == 1024
|
|
assert cp.metadata.reason == "before write"
|
|
assert cp.metadata.source_tool == "file_write"
|
|
assert cp.metadata.phase == "execute"
|
|
|
|
|
|
@given("a CheckpointModel with invalid JSON in metadata_json")
|
|
def step_checkpoint_bad_json(context: Any) -> None:
|
|
from cleveragents.infrastructure.database.models import CheckpointModel
|
|
|
|
context.checkpoint_model = CheckpointModel(
|
|
checkpoint_id=_make_ulid(30),
|
|
plan_id=_make_ulid(31),
|
|
sandbox_ref="deadbeef",
|
|
checkpoint_type="manual",
|
|
filesystem_path="",
|
|
size_bytes=None,
|
|
created_at=NOW_ISO,
|
|
metadata_json="NOT VALID JSON {{{",
|
|
)
|
|
|
|
|
|
@then("the domain checkpoint metadata is empty defaults")
|
|
def step_check_empty_metadata(context: Any) -> None:
|
|
cp = context.domain_checkpoint
|
|
assert cp.metadata.reason == ""
|
|
assert cp.metadata.source_tool == ""
|
|
assert cp.metadata.phase == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CheckpointModel — from_domain (lines 2865-2879)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a Checkpoint domain object with full metadata")
|
|
def step_checkpoint_domain(context: Any) -> None:
|
|
from cleveragents.domain.models.core.checkpoint import (
|
|
Checkpoint,
|
|
CheckpointMetadata,
|
|
)
|
|
|
|
context.checkpoint_domain = Checkpoint(
|
|
checkpoint_id=_make_ulid(40),
|
|
plan_id=_make_ulid(41),
|
|
sandbox_ref="commitsha",
|
|
decision_id=_make_ulid(42),
|
|
checkpoint_type="post_step",
|
|
resource_id=_make_ulid(43),
|
|
filesystem_path="cp/path",
|
|
size_bytes=2048,
|
|
created_at=NOW,
|
|
metadata=CheckpointMetadata(
|
|
reason="post step save",
|
|
source_tool="apply_tool",
|
|
phase="apply",
|
|
),
|
|
)
|
|
|
|
|
|
@when("I convert the checkpoint to a model via from_domain")
|
|
def step_checkpoint_from_domain(context: Any) -> None:
|
|
from cleveragents.infrastructure.database.models import CheckpointModel
|
|
|
|
context.checkpoint_model_out = CheckpointModel.from_domain(
|
|
context.checkpoint_domain,
|
|
)
|
|
|
|
|
|
@then("the model has correct checkpoint_id plan_id and metadata_json")
|
|
def step_check_checkpoint_model_fields(context: Any) -> None:
|
|
m = context.checkpoint_model_out
|
|
assert m.checkpoint_id == _make_ulid(40)
|
|
assert m.plan_id == _make_ulid(41)
|
|
assert m.sandbox_ref == "commitsha"
|
|
assert m.decision_id == _make_ulid(42)
|
|
assert m.checkpoint_type == "post_step"
|
|
assert m.resource_id == _make_ulid(43)
|
|
assert m.filesystem_path == "cp/path"
|
|
assert m.size_bytes == 2048
|
|
assert m.created_at == NOW_ISO
|
|
raw = m.metadata_json
|
|
assert raw is not None
|
|
parsed = json.loads(raw)
|
|
assert parsed["reason"] == "post step save"
|
|
assert parsed["source_tool"] == "apply_tool"
|
|
assert parsed["phase"] == "apply"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_session (lines 2907-2908)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("an in-memory SQLAlchemy engine")
|
|
def step_create_engine(context: Any) -> None:
|
|
from cleveragents.infrastructure.database.models import init_database
|
|
|
|
context.engine = init_database("sqlite:///:memory:")
|
|
|
|
|
|
@when("I call get_session with that engine")
|
|
def step_call_get_session(context: Any) -> None:
|
|
from cleveragents.infrastructure.database.models import get_session
|
|
|
|
context.session = get_session(context.engine)
|
|
|
|
|
|
@then("I receive a valid SQLAlchemy session object")
|
|
def step_check_session(context: Any) -> None:
|
|
from sqlalchemy.orm import Session
|
|
|
|
assert isinstance(context.session, Session)
|
|
# Verify it can execute a simple query
|
|
result = context.session.execute(__import__("sqlalchemy").text("SELECT 1"))
|
|
assert result.scalar() == 1
|
|
context.session.close()
|