f678d611bb
CI / benchmark-publish (pull_request) Has been skipped
CI / typecheck (pull_request) Successful in 58s
CI / quality (pull_request) Successful in 57s
CI / security (pull_request) Successful in 1m3s
CI / build (pull_request) Successful in 25s
CI / helm (pull_request) Successful in 34s
CI / lint (pull_request) Successful in 3m20s
CI / integration_tests (pull_request) Successful in 4m2s
CI / unit_tests (pull_request) Successful in 4m27s
CI / docker (pull_request) Successful in 1m18s
CI / coverage (pull_request) Successful in 8m39s
CI / e2e_tests (pull_request) Successful in 12m37s
CI / status-check (pull_request) Successful in 1s
CI / typecheck (push) Successful in 47s
CI / lint (push) Successful in 3m16s
CI / security (push) Successful in 4m7s
CI / quality (push) Successful in 3m46s
CI / build (push) Successful in 21s
CI / helm (push) Successful in 24s
CI / unit_tests (push) Successful in 4m19s
CI / integration_tests (push) Successful in 3m55s
CI / docker (push) Successful in 1m19s
CI / e2e_tests (push) Successful in 12m30s
CI / coverage (push) Successful in 11m48s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Successful in 29m36s
CI / benchmark-regression (push) Has been skipped
CI / benchmark-regression (pull_request) Successful in 56m32s
Added CorrectionAttemptModel SQLAlchemy model with all spec-defined columns
(correction_attempt_id, plan_id, original_decision_id, new_decision_id,
mode, guidance, archived_artifacts_path, state, created_at, completed_at).
Added FK constraints to v3_plans and decisions tables. Created Alembic
migration and idx_corrections_plan index. Added repository layer for
CRUD operations.
Addressed code review feedback (rounds 1-12):
- Replaced Any type annotations with typed CorrectionAttemptRecord
signatures using TYPE_CHECKING imports.
- Replaced fragile time.sleep with deterministic timestamps.
- Added cascade deletion test with PRAGMA foreign_keys=ON.
- Changed update_state() to accept typed enum and datetime params.
- Added guidance non-empty validator with max_length=10_000.
- Added spec-aligned server_default for created_at column.
- Fixed timezone handling in to_domain() and from_domain().
- Added spec-defined lifecycle state transition validation via
validate_correction_state_transition() domain function.
- Improved FK-violation error messages in create() and update_state().
- Normalised timestamp to millisecond precision matching SQLite
server_default strftime('%f') output.
- Added auto-set completed_at on terminal transitions.
- Added CorrectionAttemptRecord field validators (strip, non-empty).
- Changed original_decision_id FK from CASCADE to RESTRICT matching
spec DDL default, preserving correction audit trail.
- Added input validation in update_state() for new_decision_id and
archived_artifacts_path (empty/whitespace rejection).
- Fixed dirty-session bug by moving validation before ORM mutations.
- Extracted _SQLITE_TIMESTAMP_MS_LEN constant for timestamp truncation.
Addressed thirteenth code review feedback:
- Changed InvalidCorrectionStateTransitionError base class from
DatabaseError to BusinessRuleViolation per CONTRIBUTING.md exception
semantics (state transition is a business rule, not a database error;
prevents incorrect retries by @database_retry decorator).
- Changed new_decision_id FK from SET NULL to RESTRICT matching the
spec DDL default (no ON DELETE clause) and consistent with the
RESTRICT approach used for original_decision_id.
- Changed update_state() input validation for new_decision_id and
archived_artifacts_path from DatabaseError to ValueError per
CONTRIBUTING.md argument validation guidelines.
- Defensive to_domain() coercion now defaults corrupted state to
'failed' (terminal) instead of 'pending', preventing re-execution
of completed/failed corrections with corrupted DB values.
- Extracted format_sqlite_timestamp() helper and SQLITE_TIMESTAMP_MS_LEN
public constant, removing duplicated timestamp formatting logic
between from_domain() and update_state().
- Added code comment explaining CASCADE on plan_id FK as a codebase
convention deviation from spec DDL default.
- Added BDD scenario verifying RESTRICT FK on original_decision_id
blocks decision deletion.
- Replaced weak cross-plan isolation test with stronger two-plan
scenario verifying list_by_plan returns only each plan's attempts.
- Fixed hardcoded assertion in step_check_archived_path to use
context variable.
- 45 BDD scenarios and 5 Robot integration tests.
Addressed fourteenth code review feedback:
- Added ORM-level relationship(cascade="all, delete-orphan") on
LifecyclePlanModel for CorrectionAttemptModel, consistent with all
other v3_plans child tables, ensuring ORM-level cascade deletes
work even when SQLite FK enforcement is disabled.
- Added defensive to_domain() coercion for corrupted guidance column
(defaults to "[corrupted]" with warning log), consistent with
existing mode/state coercion pattern.
- Added ValueError guard in format_sqlite_timestamp() rejecting naive
datetimes per CONTRIBUTING.md fail-fast argument validation.
- Fixed stale spec DDL line reference in CorrectionAttemptModel
docstring.
- Fixed duplicated docstring on SQLITE_TIMESTAMP_MS_LEN constant.
Addressed fifteenth code review feedback:
- Fixed update_state() to defensively handle corrupted DB state values
via try/except ValueError coercion to FAILED terminal state with
warning log, consistent with to_domain() defensive coercion pattern.
- Strengthened RESTRICT FK BDD assertion to verify exception type
(IntegrityError/DatabaseError) instead of only checking presence.
- Split multi-When/Then cross-plan isolation BDD scenario into
idiomatic single-When/Then scenarios per Gherkin best practice.
- 53 BDD scenarios (was 45) and 5 Robot integration tests.
ISSUES CLOSED: #920
402 lines
19 KiB
Gherkin
402 lines
19 KiB
Gherkin
Feature: Correction attempt persistence via CorrectionAttemptRepository
|
|
As a developer
|
|
I want correction attempt records to persist correctly
|
|
So that decision correction workflows are durable across operations
|
|
|
|
Background:
|
|
Given a fresh in-memory correction attempt database
|
|
And a prerequisite action "local/correction-action" exists for correction attempts
|
|
And a prerequisite plan exists for correction attempts
|
|
And a prerequisite root decision exists for correction attempts
|
|
|
|
# ------------------------------------------------------------------
|
|
# Create and retrieve
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Create a correction attempt and retrieve it by ID
|
|
Given a new correction attempt in revert mode
|
|
When I persist the correction attempt via the repository
|
|
Then I can retrieve the correction attempt by its ID
|
|
And the persisted correction attempt mode should be "revert"
|
|
And the persisted correction attempt state should be "pending"
|
|
|
|
Scenario: Create an append-mode correction attempt
|
|
Given a new correction attempt in append mode
|
|
When I persist the correction attempt via the repository
|
|
Then I can retrieve the correction attempt by its ID
|
|
And the persisted correction attempt mode should be "append"
|
|
|
|
Scenario: Persisted correction attempt preserves all fields
|
|
Given a new correction attempt with all fields populated
|
|
When I persist the correction attempt via the repository
|
|
Then I can retrieve the correction attempt by its ID
|
|
And the persisted correction attempt guidance should match
|
|
And the persisted correction attempt plan_id should match
|
|
And the persisted correction attempt original_decision_id should match
|
|
|
|
# ------------------------------------------------------------------
|
|
# List by plan
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: List correction attempts by plan returns ordered results
|
|
Given 3 persisted correction attempts for the same plan
|
|
When I list correction attempts by plan ID
|
|
Then I should get 3 correction attempts in creation order
|
|
|
|
# ------------------------------------------------------------------
|
|
# Update state
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Update correction attempt state to executing
|
|
Given a persisted correction attempt in pending state
|
|
When I update the correction attempt state to "executing"
|
|
Then the correction attempt state should be "executing"
|
|
|
|
Scenario: Update correction attempt state to complete with timestamp
|
|
Given a persisted correction attempt in executing state
|
|
When I update the correction attempt to complete with timestamp
|
|
Then the correction attempt state should be "complete"
|
|
And the correction attempt completed_at should be set
|
|
|
|
Scenario: Invalid state transition from pending to complete is rejected
|
|
Given a persisted correction attempt in pending state
|
|
When I try to update the correction attempt state to "complete"
|
|
Then an InvalidCorrectionStateTransitionError should be raised
|
|
|
|
Scenario: Update correction attempt with new decision ID
|
|
Given a persisted correction attempt in pending state
|
|
When I update the correction attempt with a new decision ID
|
|
Then the correction attempt new_decision_id should be set
|
|
|
|
Scenario: Update correction attempt with archived artifacts path
|
|
Given a persisted correction attempt in pending state
|
|
When I update the correction attempt with archived artifacts path
|
|
Then the correction attempt archived_artifacts_path should be set
|
|
|
|
# ------------------------------------------------------------------
|
|
# Delete
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Delete a correction attempt by ID
|
|
Given a persisted correction attempt in pending state
|
|
When I delete the correction attempt
|
|
Then the correction attempt should no longer exist
|
|
|
|
Scenario: Delete a non-existent correction attempt returns false
|
|
When I try to delete a non-existent correction attempt
|
|
Then the correction attempt delete result should be false
|
|
|
|
# ------------------------------------------------------------------
|
|
# Duplicate detection
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Creating a duplicate correction attempt raises error
|
|
Given a persisted correction attempt in pending state
|
|
When I try to persist the same correction attempt again
|
|
Then a DuplicateCorrectionAttemptError should be raised
|
|
|
|
# ------------------------------------------------------------------
|
|
# Cascade deletion
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Deleting a plan cascades to its correction attempts
|
|
Given a persisted correction attempt in pending state
|
|
When the parent plan is deleted
|
|
Then the correction attempt should have been cascade deleted
|
|
|
|
# ------------------------------------------------------------------
|
|
# Not found
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Getting a non-existent correction attempt raises error
|
|
When I try to get a non-existent correction attempt
|
|
Then a CorrectionAttemptNotFoundError should be raised
|
|
|
|
# ------------------------------------------------------------------
|
|
# Edge cases
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: List correction attempts for a plan with no attempts
|
|
When I list correction attempts for a plan with no correction attempts
|
|
Then I should get 0 correction attempts
|
|
|
|
# ------------------------------------------------------------------
|
|
# Additional state transition coverage
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Update correction attempt state to failed with timestamp
|
|
Given a persisted correction attempt in executing state
|
|
When I update the correction attempt to failed with timestamp
|
|
Then the correction attempt state should be "failed"
|
|
And the correction attempt completed_at should be set
|
|
|
|
Scenario: Transition from complete state is rejected
|
|
Given a persisted correction attempt in complete state
|
|
When I try to update the correction attempt state to "executing"
|
|
Then an InvalidCorrectionStateTransitionError should be raised
|
|
|
|
Scenario: Transition from failed state is rejected
|
|
Given a persisted correction attempt in failed state
|
|
When I try to update the correction attempt state to "pending"
|
|
Then an InvalidCorrectionStateTransitionError should be raised
|
|
|
|
Scenario: Setting completed_at on non-terminal transition is rejected
|
|
Given a persisted correction attempt in pending state
|
|
When I try to update the correction attempt to executing with completed_at
|
|
Then an InvalidCorrectionStateTransitionError should be raised
|
|
|
|
# ------------------------------------------------------------------
|
|
# Domain model guidance validation
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Empty guidance is rejected at domain model level
|
|
When I try to create a correction attempt with empty guidance
|
|
Then a guidance validation error should be raised
|
|
|
|
Scenario: Whitespace-only guidance is rejected at domain model level
|
|
When I try to create a correction attempt with whitespace-only guidance
|
|
Then a guidance validation error should be raised
|
|
|
|
# ------------------------------------------------------------------
|
|
# Additional test coverage
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Updating state of a non-existent correction attempt raises error
|
|
When I try to update the state of a non-existent correction attempt
|
|
Then a CorrectionAttemptNotFoundError should be raised
|
|
|
|
Scenario: Updating with non-existent new_decision_id raises FK error
|
|
Given a persisted correction attempt in pending state
|
|
When I try to update the correction attempt with a non-existent decision ID
|
|
Then a DatabaseError should be raised
|
|
|
|
Scenario: Guidance at max length is accepted
|
|
Given a new correction attempt with guidance at max length
|
|
When I persist the correction attempt via the repository
|
|
Then I can retrieve the correction attempt by its ID
|
|
|
|
Scenario: Guidance exceeding max length is rejected at domain model level
|
|
When I try to create a correction attempt with guidance exceeding max length
|
|
Then a guidance validation error should be raised
|
|
|
|
# ------------------------------------------------------------------
|
|
# Timezone normalization coverage (M3)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Non-UTC timezone datetimes are normalised to UTC on persist
|
|
Given a new correction attempt with non-UTC timezone timestamps
|
|
When I persist the correction attempt via the repository
|
|
Then I can retrieve the correction attempt by its ID
|
|
And the persisted correction attempt created_at should be in UTC
|
|
|
|
# ------------------------------------------------------------------
|
|
# archived_artifacts_path round-trip on create (M4)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Correction attempt with archived_artifacts_path round-trips on create
|
|
Given a new correction attempt with archived_artifacts_path set
|
|
When I persist the correction attempt via the repository
|
|
Then I can retrieve the correction attempt by its ID
|
|
And the persisted correction attempt archived_artifacts_path should match
|
|
|
|
# ------------------------------------------------------------------
|
|
# Minimum-boundary guidance (L7)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Single-character guidance is accepted
|
|
Given a new correction attempt with single-character guidance
|
|
When I persist the correction attempt via the repository
|
|
Then I can retrieve the correction attempt by its ID
|
|
And the persisted correction attempt guidance should be a single character
|
|
|
|
# ------------------------------------------------------------------
|
|
# Delete in non-pending state (L4)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Delete a correction attempt in complete state
|
|
Given a persisted correction attempt in complete state
|
|
When I delete the correction attempt
|
|
Then the correction attempt should no longer exist
|
|
|
|
# ------------------------------------------------------------------
|
|
# Cross-plan isolation for list_by_plan (L5)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: list_by_plan returns only attempts for the specified plan
|
|
Given a persisted correction attempt in pending state
|
|
When I list correction attempts for a plan with no correction attempts
|
|
Then I should get 0 correction attempts
|
|
|
|
# ------------------------------------------------------------------
|
|
# Auto-set completed_at for terminal states (T-1)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Terminal transition auto-sets completed_at when not provided
|
|
Given a persisted correction attempt in executing state
|
|
When I update the correction attempt state to "complete" without completed_at
|
|
Then the correction attempt state should be "complete"
|
|
And the correction attempt completed_at should be set
|
|
|
|
Scenario: Terminal transition to failed auto-sets completed_at when not provided
|
|
Given a persisted correction attempt in executing state
|
|
When I update the correction attempt state to "failed" without completed_at
|
|
Then the correction attempt state should be "failed"
|
|
And the correction attempt completed_at should be set
|
|
|
|
# ------------------------------------------------------------------
|
|
# FK violation on create (T-2)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Creating a correction attempt with non-existent plan_id raises error
|
|
When I try to create a correction attempt with a non-existent plan_id
|
|
Then a DatabaseError should be raised
|
|
|
|
Scenario: Creating a correction attempt with non-existent original_decision_id raises error
|
|
When I try to create a correction attempt with a non-existent original_decision_id
|
|
Then a DatabaseError should be raised
|
|
|
|
# ------------------------------------------------------------------
|
|
# Invalid mode at domain model level (T-3)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Invalid mode value is rejected at domain model level
|
|
When I try to create a correction attempt with an invalid mode
|
|
Then a mode validation error should be raised
|
|
|
|
# ------------------------------------------------------------------
|
|
# Empty/whitespace new_decision_id via update_state (M-1)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Whitespace-only new_decision_id is rejected in update_state
|
|
Given a persisted correction attempt in pending state
|
|
When I try to update the correction attempt with whitespace-only new_decision_id
|
|
Then a correction attempt ValueError should be raised
|
|
|
|
Scenario: Empty new_decision_id is rejected in update_state
|
|
Given a persisted correction attempt in pending state
|
|
When I try to update the correction attempt with empty new_decision_id
|
|
Then a correction attempt ValueError should be raised
|
|
|
|
# ------------------------------------------------------------------
|
|
# Combined field update (L-5)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Update with both new_decision_id and archived_artifacts_path
|
|
Given a persisted correction attempt in pending state
|
|
When I update the correction attempt with new_decision_id and archived_artifacts_path
|
|
Then the correction attempt new_decision_id should be set
|
|
And the correction attempt archived_artifacts_path should be set
|
|
|
|
# ------------------------------------------------------------------
|
|
# Self-transition rejection (L-3)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Self-transition from pending to pending is rejected
|
|
Given a persisted correction attempt in pending state
|
|
When I try to update the correction attempt state to "pending"
|
|
Then an InvalidCorrectionStateTransitionError should be raised
|
|
|
|
Scenario: Self-transition from executing to executing is rejected
|
|
Given a persisted correction attempt in executing state
|
|
When I try to update the correction attempt state to "executing"
|
|
Then an InvalidCorrectionStateTransitionError should be raised
|
|
|
|
# ------------------------------------------------------------------
|
|
# Empty/whitespace archived_artifacts_path via update_state (L-2)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Empty archived_artifacts_path is rejected in update_state
|
|
Given a persisted correction attempt in pending state
|
|
When I try to update the correction attempt with empty archived_artifacts_path
|
|
Then a correction attempt ValueError should be raised
|
|
|
|
Scenario: Whitespace-only archived_artifacts_path is rejected in update_state
|
|
Given a persisted correction attempt in pending state
|
|
When I try to update the correction attempt with whitespace-only archived_artifacts_path
|
|
Then a correction attempt ValueError should be raised
|
|
|
|
# ------------------------------------------------------------------
|
|
# Whitespace-padded archived_artifacts_path stripping (L-6)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Whitespace-padded archived_artifacts_path is stripped on update
|
|
Given a persisted correction attempt in pending state
|
|
When I update the correction attempt with whitespace-padded archived_artifacts_path
|
|
Then the correction attempt archived_artifacts_path should be stripped
|
|
|
|
# ------------------------------------------------------------------
|
|
# RESTRICT FK on original_decision_id (R-1)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: Deleting a decision referenced by original_decision_id is blocked
|
|
Given a persisted correction attempt in pending state
|
|
When I try to delete the original decision
|
|
Then a database integrity error should be raised for the decision
|
|
And the correction attempt should still exist
|
|
|
|
# ------------------------------------------------------------------
|
|
# Stronger cross-plan isolation (I-1)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: list_by_plan returns only first plan's attempts
|
|
Given a second prerequisite plan exists for correction attempts
|
|
And a persisted correction attempt for the first plan
|
|
And a persisted correction attempt for the second plan
|
|
When I list correction attempts for the first plan
|
|
Then I should get exactly 1 correction attempt for the first plan
|
|
|
|
Scenario: list_by_plan returns only second plan's attempts
|
|
Given a second prerequisite plan exists for correction attempts
|
|
And a persisted correction attempt for the first plan
|
|
And a persisted correction attempt for the second plan
|
|
When I list correction attempts for the second plan
|
|
Then I should get exactly 1 correction attempt for the second plan
|
|
|
|
# ------------------------------------------------------------------
|
|
# Defensive to_domain() coercion for corrupted DB data (DC-1)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: to_domain coerces unknown mode to revert
|
|
Given a raw correction attempt row with corrupted mode "unknown_mode"
|
|
When I retrieve the corrupted correction attempt by ID
|
|
Then the retrieved correction attempt mode should be "revert"
|
|
|
|
Scenario: to_domain coerces unknown state to failed
|
|
Given a raw correction attempt row with corrupted state "unknown_state"
|
|
When I retrieve the corrupted correction attempt by ID
|
|
Then the retrieved correction attempt state should be "failed"
|
|
|
|
Scenario: to_domain coerces empty guidance to corrupted marker
|
|
Given a raw correction attempt row with empty guidance
|
|
When I retrieve the corrupted correction attempt by ID
|
|
Then the retrieved correction attempt guidance should be "[corrupted]"
|
|
|
|
# ------------------------------------------------------------------
|
|
# format_sqlite_timestamp naive datetime rejection (TS-1)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: format_sqlite_timestamp rejects naive datetimes
|
|
When I call format_sqlite_timestamp with a naive datetime
|
|
Then a ValueError should be raised for naive datetime
|
|
|
|
# ------------------------------------------------------------------
|
|
# Domain model naive datetime normalisation (TS-2)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: CorrectionAttemptRecord normalizes naive created_at to UTC
|
|
When I create a CorrectionAttemptRecord with naive created_at
|
|
Then the record created_at should have UTC timezone
|
|
|
|
Scenario: CorrectionAttemptRecord normalizes naive completed_at to UTC
|
|
When I create a CorrectionAttemptRecord with naive completed_at
|
|
Then the record completed_at should have UTC timezone
|
|
|
|
# ------------------------------------------------------------------
|
|
# Corrupted DB state in update_state (DC-2)
|
|
# ------------------------------------------------------------------
|
|
|
|
Scenario: update_state treats corrupted DB state as terminal failed
|
|
Given a raw correction attempt row with corrupted state "garbage_state"
|
|
When I try to update the corrupted correction attempt state to "executing"
|
|
Then an InvalidCorrectionStateTransitionError should be raised
|