a808c395f9
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 17s
CI / typecheck (pull_request) Successful in 35s
CI / security (pull_request) Successful in 50s
CI / unit_tests (pull_request) Successful in 2m46s
CI / integration_tests (pull_request) Successful in 3m16s
CI / docker (pull_request) Successful in 40s
CI / coverage (pull_request) Successful in 5m6s
CI / lint (push) Successful in 13s
CI / quality (push) Successful in 16s
CI / build (push) Successful in 18s
CI / security (push) Successful in 32s
CI / typecheck (push) Successful in 35s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m52s
CI / integration_tests (push) Successful in 3m8s
CI / docker (push) Successful in 39s
CI / coverage (push) Successful in 5m53s
CI / benchmark-publish (push) Successful in 16m55s
CI / benchmark-regression (pull_request) Successful in 33m0s
Add 53 new .feature files and corresponding step definition files targeting uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts in 7 pre-existing step files by disambiguating step text. New tests cover: ACP clients/facade, actor CLI/config, application container, ACMS service/strategies, async worker, automation profile CLI, autonomy guardrail, bridge, change model, config CLI/service, context service, cross-plan correction, database models, decision service, decomposition clustering/service, discovery handler, langchain chat provider, langgraph nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/ preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI, provider registry, reactive application/route, repositories, resolver handler, resource registry service, resume model, retry patterns, sandbox protocol, server CLI, skill CLI/service, skills registry, subplan execution/service, system CLI, UKO loader, UoW, and YAML template engine. Closes #645
301 lines
14 KiB
Gherkin
301 lines
14 KiB
Gherkin
@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
|