diff --git a/features/steps/tdd_json_decode_crash_persistence_steps.py b/features/steps/tdd_json_decode_crash_persistence_steps.py new file mode 100644 index 000000000..e0d901ec9 --- /dev/null +++ b/features/steps/tdd_json_decode_crash_persistence_steps.py @@ -0,0 +1,98 @@ +"""Steps for TDD bug-capture scenario #989. + +This scenario intentionally fails until the bugfix branch for #989 adds +corruption-specific handling around automation profile JSON decoding. +""" + +from __future__ import annotations + +import json +from datetime import UTC, datetime +from typing import Any + +from behave import given, then, when +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from cleveragents.infrastructure.database.models import ( + AutomationProfileModel, + Base, +) +from cleveragents.infrastructure.database.repositories import ( + AutomationProfileRepository, +) + + +def _now_iso() -> str: + return datetime.now(UTC).isoformat() + + +@given("an automation profile repository row with corrupt safety_json for bug 989") +def step_given_corrupt_safety_json_row(context: Any) -> None: + """Persist a row that reproduces the malformed JSON decode path in _to_domain.""" + engine = create_engine("sqlite+pysqlite:///:memory:", future=True) + Base.metadata.create_all(bind=engine) + factory = sessionmaker(bind=engine, expire_on_commit=False, future=True) + + now_iso = _now_iso() + with factory() as session: + session.add( + AutomationProfileModel( + name="bug-989-corrupt-json", + description="Corrupt JSON regression fixture", + schema_version="1.0", + auto_strategize=0.0, + auto_execute=0.0, + auto_apply=0.0, + auto_decisions_strategize=0.0, + auto_decisions_execute=0.0, + auto_validation_fix=0.0, + auto_strategy_revision=0.0, + auto_reversion_from_apply=0.0, + auto_child_plans=0.0, + auto_retry_transient=0.0, + auto_checkpoint_restore=0.0, + require_sandbox=True, + require_checkpoints=True, + allow_unsafe_tools=False, + # Deliberately malformed JSON (missing closing brace) + safety_json='{"require_sandbox": true', + guards_json=None, + created_at=now_iso, + updated_at=now_iso, + ) + ) + session.commit() + + context.tdd_989_repo = AutomationProfileRepository(factory) + context.tdd_989_error = None + + +@when("the corrupt automation profile is fetched by name for bug 989") +def step_when_fetch_corrupt_profile(context: Any) -> None: + """Execute repository get path that currently leaks JSONDecodeError.""" + repo: AutomationProfileRepository = context.tdd_989_repo + try: + repo.get_by_name("bug-989-corrupt-json") + except Exception as exc: + context.tdd_989_error = exc + + +@then( + "a corruption-specific domain error should be raised instead of JSONDecodeError for bug 989" +) +def step_then_domain_specific_error(context: Any) -> None: + """Assert expected post-fix behavior (fails today while bug exists).""" + err = context.tdd_989_error + assert err is not None, ( + "Expected a corruption-specific domain error, but no error was raised" + ) + assert not isinstance(err, json.JSONDecodeError), ( + "Expected a domain-specific corruption error, " + f"but raw JSONDecodeError leaked: {err}" + ) + name_or_message = f"{type(err).__name__} {err}".lower() + assert "corrupt" in name_or_message, ( + "Expected corruption-specific error semantics (type/message containing 'corrupt'), " + f"got {type(err).__name__}: {err}" + ) diff --git a/features/tdd_json_decode_crash_persistence.feature b/features/tdd_json_decode_crash_persistence.feature new file mode 100644 index 000000000..44de204e3 --- /dev/null +++ b/features/tdd_json_decode_crash_persistence.feature @@ -0,0 +1,14 @@ +@tdd_bug @tdd_bug_989 @tdd_expected_fail +Feature: TDD Bug #989 — automation profile persistence crashes on corrupt JSON + As a developer reading persisted automation profiles + I want corrupt JSON payloads to be handled with a domain-specific error + So that callers do not receive a raw JSONDecodeError crash + + # This test captures bug #989. It intentionally uses @tdd_expected_fail + # until the bugfix for #989 is merged. The underlying assertion currently + # fails (proving the bug exists) and the tag inversion makes CI pass. + + Scenario: Bug #989 — get_by_name should not leak JSONDecodeError for corrupt safety_json + Given an automation profile repository row with corrupt safety_json for bug 989 + When the corrupt automation profile is fetched by name for bug 989 + Then a corruption-specific domain error should be raised instead of JSONDecodeError for bug 989