@unit @repository @coverage
Feature: Repository coverage round 2 - Skill, Decision, Checkpoint, and ToolRepository
  Exercise uncovered lines in repositories.py for SkillRepository flattened-tool
  cache methods, DecisionRepository full CRUD, CheckpointRepository full CRUD,
  and ToolRepository legacy wrapper error paths.

  # ===========================================================================
  # SkillRepository – flattened tool cache methods (lines 4656-4821)
  # ===========================================================================

  Background:
    Given r2cov a fresh in-memory database with all tables

  # --- update_flattened_tools ------------------------------------------------

  @skill @cache
  Scenario: SkillRepository.update_flattened_tools persists cache data for an existing skill
    Given r2cov a skill "local/my-skill" exists in the database
    When r2cov update_flattened_tools is called for "local/my-skill" with valid cache data
    Then r2cov the skill row should have non-null flattened_tools_json
    And r2cov the skill row should have the expected flattening_hash

  @skill @cache @error
  Scenario: SkillRepository.update_flattened_tools raises SkillNotFoundError for missing skill
    When r2cov update_flattened_tools is called for "local/nonexistent" with valid cache data
    Then r2cov a SkillNotFoundError should be raised

  # --- get_flattened_tools ---------------------------------------------------

  @skill @cache
  Scenario: SkillRepository.get_flattened_tools returns cache dict for existing skill
    Given r2cov a skill "local/cached-skill" exists in the database
    And r2cov the skill "local/cached-skill" has flattened tools cached
    When r2cov get_flattened_tools is called for "local/cached-skill"
    Then r2cov the result should be a dict with flattened_tools_json key
    And r2cov the result flattening_hash should not be None

  @skill @cache @error
  Scenario: SkillRepository.get_flattened_tools raises SkillNotFoundError for missing skill
    When r2cov get_flattened_tools is called for "local/ghost-skill"
    Then r2cov a SkillNotFoundError should be raised

  # --- needs_refresh ---------------------------------------------------------

  @skill @cache
  Scenario: SkillRepository.needs_refresh returns True when hash is missing
    Given r2cov a skill "local/stale-skill" exists in the database
    When r2cov needs_refresh is called for "local/stale-skill" with hash "abc123"
    Then r2cov the result should be True

  @skill @cache
  Scenario: SkillRepository.needs_refresh returns False when hash matches
    Given r2cov a skill "local/fresh-skill" exists in the database
    And r2cov the skill "local/fresh-skill" has flattening_hash "abc123"
    When r2cov needs_refresh is called for "local/fresh-skill" with hash "abc123"
    Then r2cov the result should be False

  @skill @cache
  Scenario: SkillRepository.needs_refresh returns True when hash differs
    Given r2cov a skill "local/changed-skill" exists in the database
    And r2cov the skill "local/changed-skill" has flattening_hash "old_hash"
    When r2cov needs_refresh is called for "local/changed-skill" with hash "new_hash"
    Then r2cov the result should be True

  @skill @cache @error
  Scenario: SkillRepository.needs_refresh raises SkillNotFoundError for missing skill
    When r2cov needs_refresh is called for "local/missing" with hash "abc"
    Then r2cov a SkillNotFoundError should be raised

  # --- recompute_flattening_hash ---------------------------------------------

  @skill @cache
  Scenario: SkillRepository.recompute_flattening_hash computes and stores SHA-256
    Given r2cov a skill "local/hash-skill" exists in the database
    When r2cov recompute_flattening_hash is called for "local/hash-skill" with yaml "name: test"
    Then r2cov the returned hash should be the SHA-256 of "name: test"
    And r2cov the skill row "local/hash-skill" should have the recomputed hash stored

  @skill @cache @error
  Scenario: SkillRepository.recompute_flattening_hash raises SkillNotFoundError for missing skill
    When r2cov recompute_flattening_hash is called for "local/nope" with yaml "x"
    Then r2cov a SkillNotFoundError should be raised

  # --- invalidate_cached_summaries -------------------------------------------

  @skill @cache
  Scenario: SkillRepository.invalidate_cached_summaries nulls cached fields
    Given r2cov a skill "local/inv-skill" exists in the database
    And r2cov the skill "local/inv-skill" has flattened tools cached
    When r2cov invalidate_cached_summaries is called for "local/inv-skill"
    Then r2cov the skill row "local/inv-skill" should have null flattened_tools_json
    And r2cov the skill row "local/inv-skill" should have null flattening_hash

  @skill @cache @error
  Scenario: SkillRepository.invalidate_cached_summaries raises SkillNotFoundError for missing skill
    When r2cov invalidate_cached_summaries is called for "local/void"
    Then r2cov a SkillNotFoundError should be raised

  # ===========================================================================
  # DecisionRepository CRUD (lines 4841-5255)
  # ===========================================================================

  # --- create ----------------------------------------------------------------

  @decision
  Scenario: DecisionRepository.create persists a new decision
    Given r2cov a lifecycle plan exists for decisions
    When r2cov a decision is created for that plan
    Then r2cov the decision should be retrievable by ID

  @decision @error
  Scenario: DecisionRepository.create raises DuplicateDecisionError on duplicate
    Given r2cov a lifecycle plan exists for decisions
    And r2cov a decision "01HGZ6FE0AQDYTR4BX000DEC01" exists for that plan
    When r2cov creating a decision with the same ID is attempted
    Then r2cov a DuplicateDecisionError should be raised

  # --- get -------------------------------------------------------------------

  @decision
  Scenario: DecisionRepository.get returns None for missing decision
    When r2cov getting decision "01HGZ6FE0AQDYTR4BX000MISS0" is attempted
    Then r2cov the result should be None

  # --- get_by_plan -----------------------------------------------------------

  @decision
  Scenario: DecisionRepository.get_by_plan returns decisions ordered by sequence
    Given r2cov a lifecycle plan exists for decisions
    And r2cov decisions with sequence 0, 1, 2 exist for that plan
    When r2cov get_by_plan is called for that plan
    Then r2cov 3 decisions should be returned in sequence order

  # --- get_tree --------------------------------------------------------------

  @decision
  Scenario: DecisionRepository.get_tree returns BFS-ordered tree
    Given r2cov a lifecycle plan exists for decisions
    And r2cov a decision tree with root and two children exists
    When r2cov get_tree is called with the root decision ID
    Then r2cov 3 decisions should be returned with root first

  @decision @error
  Scenario: DecisionRepository.get_tree raises DecisionNotFoundError for missing root
    When r2cov get_tree is called with a nonexistent root ID
    Then r2cov a DecisionNotFoundError should be raised

  # --- get_path_to_root ------------------------------------------------------

  @decision
  Scenario: DecisionRepository.get_path_to_root walks from leaf to root
    Given r2cov a lifecycle plan exists for decisions
    And r2cov a decision chain root -> mid -> leaf exists
    When r2cov get_path_to_root is called with the leaf decision ID
    Then r2cov the path should contain 3 decisions starting with leaf ending with root

  @decision @error
  Scenario: DecisionRepository.get_path_to_root raises DecisionNotFoundError for missing start
    When r2cov get_path_to_root is called with a nonexistent ID
    Then r2cov a DecisionNotFoundError should be raised

  # --- get_superseded --------------------------------------------------------

  @decision
  Scenario: DecisionRepository.get_superseded returns superseded decisions
    Given r2cov a lifecycle plan exists for decisions
    And r2cov a decision that has been superseded exists
    When r2cov get_superseded is called for that plan
    Then r2cov exactly 1 superseded decision should be returned

  # --- update_superseded_by --------------------------------------------------

  @decision
  Scenario: DecisionRepository.update_superseded_by marks a decision as superseded
    Given r2cov a lifecycle plan exists for decisions
    And r2cov an original decision and a replacement decision exist
    When r2cov update_superseded_by is called on the original with the replacement ID
    Then r2cov the original decision should have superseded_by set

  @decision @error
  Scenario: DecisionRepository.update_superseded_by raises DecisionNotFoundError for missing
    When r2cov update_superseded_by is called with a nonexistent decision ID
    Then r2cov a DecisionNotFoundError should be raised

  # --- list_by_type ----------------------------------------------------------

  @decision
  Scenario: DecisionRepository.list_by_type filters decisions by type
    Given r2cov a lifecycle plan exists for decisions
    And r2cov decisions of type "strategy_choice" and "implementation_choice" exist
    When r2cov list_by_type is called with "strategy_choice"
    Then r2cov only strategy_choice decisions should be returned

  # --- delete ----------------------------------------------------------------

  @decision
  Scenario: DecisionRepository.delete removes a decision
    Given r2cov a lifecycle plan exists for decisions
    And r2cov a single decision exists for deletion
    When r2cov delete is called on the decision
    Then r2cov delete should return True
    And r2cov the decision should no longer be retrievable

  @decision
  Scenario: DecisionRepository.delete returns False for missing decision
    When r2cov delete is called with a nonexistent decision ID
    Then r2cov delete should return False

  # --- get_max_sequence_number -----------------------------------------------

  @decision
  Scenario: DecisionRepository.get_max_sequence_number returns highest sequence
    Given r2cov a lifecycle plan exists for decisions
    And r2cov decisions with sequence 0, 1, 2 exist for that plan
    When r2cov get_max_sequence_number is called for that plan
    Then r2cov the result should be 2

  # --- count -----------------------------------------------------------------

  @decision
  Scenario: DecisionRepository.count returns number of decisions
    Given r2cov a lifecycle plan exists for decisions
    And r2cov decisions with sequence 0, 1, 2 exist for that plan
    When r2cov count is called for that plan
    Then r2cov the count result should be 3

  # ===========================================================================
  # CheckpointRepository CRUD (lines 5279-5453)
  # ===========================================================================

  # --- create ----------------------------------------------------------------

  @checkpoint
  Scenario: CheckpointRepository.create persists a new checkpoint
    Given r2cov a lifecycle plan exists for checkpoints
    When r2cov a checkpoint is created for that plan
    Then r2cov the checkpoint should be retrievable by ID

  # --- get_by_id -------------------------------------------------------------

  @checkpoint @error
  Scenario: CheckpointRepository.get_by_id raises CheckpointNotFoundError for missing
    When r2cov getting checkpoint "01HGZ6FE0AQDYTR4BXCHK00000" is attempted
    Then r2cov a CheckpointNotFoundError should be raised

  # --- list_by_plan ----------------------------------------------------------

  @checkpoint
  Scenario: CheckpointRepository.list_by_plan returns checkpoints ordered by creation time
    Given r2cov a lifecycle plan exists for checkpoints
    And r2cov 3 checkpoints exist for that plan
    When r2cov list_by_plan is called for that plan
    Then r2cov 3 checkpoints should be returned in creation order

  # --- delete ----------------------------------------------------------------

  @checkpoint
  Scenario: CheckpointRepository.delete removes a checkpoint
    Given r2cov a lifecycle plan exists for checkpoints
    And r2cov a single checkpoint exists for deletion
    When r2cov delete_checkpoint is called on the checkpoint
    Then r2cov checkpoint delete should return True

  @checkpoint
  Scenario: CheckpointRepository.delete returns False for missing checkpoint
    When r2cov delete_checkpoint is called with a nonexistent ID
    Then r2cov checkpoint delete should return False

  # --- prune -----------------------------------------------------------------

  @checkpoint
  Scenario: CheckpointRepository.prune removes excess interior checkpoints
    Given r2cov a lifecycle plan exists for checkpoints
    And r2cov 5 checkpoints exist for that plan
    When r2cov prune is called with max_checkpoints 3
    Then r2cov 2 checkpoint IDs should be returned as pruned
    And r2cov only 3 checkpoints should remain for the plan

  @checkpoint
  Scenario: CheckpointRepository.prune does nothing when under limit
    Given r2cov a lifecycle plan exists for checkpoints
    And r2cov 2 checkpoints exist for that plan
    When r2cov prune is called with max_checkpoints 5
    Then r2cov 0 checkpoint IDs should be returned as pruned

  # ===========================================================================
  # ToolRepository legacy wrapper error paths (lines 3568-3595)
  # ===========================================================================

  @tool @error
  Scenario: ToolRepository.get_by_name raises DatabaseError on OperationalError
    Given r2cov a tool repository with a broken session
    When r2cov ToolRepository.get_by_name is called with "local/broken"
    Then r2cov a DatabaseError should be raised containing "Failed to get tool"

  @tool @error
  Scenario: ToolRepository.remove raises ToolNotFoundError when tool does not exist
    Given r2cov a tool repository backed by an empty database
    When r2cov ToolRepository.remove is called with "local/nonexistent"
    Then r2cov a ToolNotFoundError should be raised
