Files
cleveragents-core/features/llm_actors_coverage.feature
CoreRasurae 3e13411fcf
CI / lint (pull_request) Successful in 1m17s
CI / typecheck (pull_request) Successful in 2m2s
CI / security (pull_request) Successful in 2m5s
CI / quality (pull_request) Successful in 1m12s
CI / push-validation (pull_request) Successful in 47s
CI / helm (pull_request) Successful in 1m1s
CI / build (pull_request) Successful in 1m8s
CI / integration_tests (pull_request) Successful in 4m52s
CI / unit_tests (pull_request) Failing after 6m20s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
fix(actors): distinguish namespace/name from provider/model in actor name parsing
When action YAML references actors using namespace/name format (e.g.
`strategy_actor: local/my-strategist`), `_parse_actor_name()` incorrectly
treated the namespace prefix as a provider name, causing
`ValueError: Unknown provider type: local`.

Changes:

- `_is_known_provider()`: New utility function (strategy_resolution.py)
  that checks whether a slash-separated first segment matches a known
  `ProviderType` value (openai, anthropic, etc.). Consolidated into a
  single implementation; llm_actors.py now imports from
  strategy_resolution.py.

- `_parse_actor_name()`: When the first segment IS a known provider,
  preserves existing behaviour (provider/model). When it is NOT a known
  provider, treats the input as namespace/name and returns the full name
  as the model identifier with the default provider. Callers SHOULD
  pre-resolve namespace/name references via the actor registry before
  calling this function. Both implementations (strategy_resolution.py
  and llm_actors.py) updated; ProviderType import moved to top level.

- `PlanLifecycleService.resolve_actor_provider_model()`: New method
  that resolves a namespaced actor name (e.g. local/my-strategist) to
  provider/model format by looking up the actor record and extracting
  its provider and model fields.

- `LifecycleService` Protocol (strategy_resolution.py): Added
  `resolve_actor_provider_model` to the protocol, eliminating the
  need for `# type: ignore[union-attr]` at call sites.

- `PlanLifecycleProtocol` (llm_actors.py): Added
  `resolve_actor_provider_model` to the protocol.

- Caller pre-resolution: StrategyActor, LLMStrategizeActor,
  LLMExecuteActor, SessionWorkflow._resolve_llm(), and CLI session
  commands now pre-resolve actor names through the lifecycle service
  or via injected actor_resolver callables before calling
  `_parse_actor_name()`, ensuring namespace/name references are
  correctly mapped to their underlying LLM providers.

- `_build_actor_resolver()` (cli/commands/session.py) and
  `_build_actor_resolver_for_session_workflow()` (a2a/facade.py):
  Moved `_is_known_provider` imports to top level; removed redundant
  `except (NotFoundError, Exception)`; added warning logging for
  outer exception handlers.

- `make_mock_lifecycle()` (mock_strategy_llm.py) and
  `_make_mock_lifecycle()` (llm_actors_coverage_steps.py): Added
  `resolve_actor_provider_model` to mock lifecycle services for
  test compatibility.

- Added `@tdd_issue @tdd_issue_11254` tags to all new BDD scenarios
  related to namespace/name disambiguation in llm_actors_coverage,
  strategy_actor_llm, and plan_lifecycle_service_coverage_boost_r4
  feature files.

- Updated docs/CHANGELOG.md with the fix entry.

ISSUES CLOSED: #11254
2026-05-22 20:42:25 +01:00

256 lines
12 KiB
Gherkin

Feature: LLM Actors Coverage
Exercises previously uncovered code paths in the llm_actors module,
including _parse_actor_name, LLMStrategizeActor, and LLMExecuteActor.
# ---------------------------------------------------------------
# _parse_actor_name helper
# ---------------------------------------------------------------
Scenario: Parse actor name with provider/model format
When I parse actor name "openai/gpt-4"
Then the parsed provider should be "openai"
And the parsed model should be "gpt-4"
Scenario: Parse actor name with only model (no slash)
When I parse actor name "gpt-4"
Then the parsed provider should be "openai"
And the parsed model should be "gpt-4"
Scenario: Parse actor name with empty string defaults to openai/gpt-4
When I parse actor name ""
Then the parsed provider should be "openai"
And the parsed model should be "gpt-4"
Scenario: Parse actor name with anthropic provider
When I parse actor name "anthropic/claude-3"
Then the parsed provider should be "anthropic"
And the parsed model should be "claude-3"
@tdd_issue @tdd_issue_11254
Scenario: Parse actor name with namespace/name format
When I parse actor name "local/my-strategist"
Then the parsed provider should be "openai"
And the parsed model should be "local/my-strategist"
@tdd_issue @tdd_issue_11254
Scenario: Parse actor name with org namespace/name format
When I parse actor name "cleverthis/my-executor"
Then the parsed provider should be "openai"
And the parsed model should be "cleverthis/my-executor"
@tdd_issue @tdd_issue_11254
Scenario: Parse actor name with unknown-provider-like segment
When I parse actor name "unknown/gpt-4"
Then the parsed provider should be "openai"
And the parsed model should be "unknown/gpt-4"
# ---------------------------------------------------------------
# LLMStrategizeActor.__init__ validation
# ---------------------------------------------------------------
Scenario: LLMStrategizeActor rejects None provider_registry
When I create an LLMStrategizeActor with None provider_registry
Then lacov a ValidationError should be raised with message "provider_registry must not be None"
Scenario: LLMStrategizeActor rejects None lifecycle_service
When I create an LLMStrategizeActor with None lifecycle_service
Then lacov a ValidationError should be raised with message "lifecycle_service must not be None"
Scenario: LLMStrategizeActor initializes with valid dependencies
Given a mock provider registry for strategize
And a mock lifecycle service for strategize
When I create an LLMStrategizeActor with valid dependencies
Then the LLMStrategizeActor should be created successfully
# ---------------------------------------------------------------
# LLMStrategizeActor.execute
# ---------------------------------------------------------------
Scenario: LLMStrategizeActor rejects empty plan_id
Given a valid LLMStrategizeActor
When I call strategize execute with empty plan_id
Then lacov a ValidationError should be raised with message "plan_id must not be empty"
Scenario: LLMStrategizeActor executes with stream callback
Given a valid LLMStrategizeActor
And the LLM returns a numbered step list for strategize
When I call strategize execute with plan_id "PLAN123" and a stream callback
Then the strategize result should contain decisions
And the stream callback should have received "strategize_started"
And the stream callback should have received "strategize_decisions"
And the stream callback should have received "strategize_complete"
Scenario: LLMStrategizeActor executes without stream callback
Given a valid LLMStrategizeActor
And the LLM returns a numbered step list for strategize
When I call strategize execute with plan_id "PLAN456" and no stream callback
Then the strategize result should contain decisions
Scenario: LLMStrategizeActor executes with invariants
Given a valid LLMStrategizeActor
And the LLM returns a numbered step list for strategize
When I call strategize execute with plan_id "PLAN789" and invariants
Then the strategize result should contain invariant records
Scenario: LLMStrategizeActor executes with None definition_of_done
Given a valid LLMStrategizeActor
And the LLM returns a numbered step list for strategize
When I call strategize execute with plan_id "PLANX" and None definition_of_done
Then the strategize result should contain decisions
# ---------------------------------------------------------------
# LLMStrategizeActor._parse_decisions
# ---------------------------------------------------------------
Scenario: Parse decisions from numbered list
When I parse LLM decisions from numbered list "1. Create file\n2. Add tests\n3. Run CI"
Then I should get 3 parsed decisions
And parsed decision 0 should be "Create file"
Scenario: Parse decisions from bullet list
When I parse LLM decisions from bullet list "- Create file\n* Add tests\n Run CI"
Then I should get 3 parsed decisions
Scenario: Parse decisions from empty string
When I parse LLM decisions from empty string
Then I should get the default decision "Complete the plan objectives"
Scenario: Parse decisions strips blank lines
When I parse LLM decisions from text with blank lines
Then blank lines should be skipped in the result
Scenario: Parse decisions with numbered colon prefix
When I parse LLM decisions from "1: Create file\n2: Add tests"
Then I should get 2 parsed decisions
And parsed decision 0 should be "Create file"
# ---------------------------------------------------------------
# LLMExecuteActor.__init__ validation
# ---------------------------------------------------------------
Scenario: LLMExecuteActor rejects None provider_registry
When I create an LLMExecuteActor with None provider_registry
Then lacov a ValidationError should be raised with message "provider_registry must not be None"
Scenario: LLMExecuteActor rejects None lifecycle_service
When I create an LLMExecuteActor with None lifecycle_service
Then lacov a ValidationError should be raised with message "lifecycle_service must not be None"
Scenario: LLMExecuteActor initializes with valid dependencies
Given a mock provider registry for execute
And a mock lifecycle service for execute
When I create an LLMExecuteActor with valid dependencies
Then the LLMExecuteActor should be created successfully
# ---------------------------------------------------------------
# LLMExecuteActor.execute
# ---------------------------------------------------------------
Scenario: LLMExecuteActor rejects empty plan_id
Given a valid LLMExecuteActor
When I call execute actor with empty plan_id
Then lacov a ValidationError should be raised with message "plan_id must not be empty"
Scenario: LLMExecuteActor executes with stream callback
Given a valid LLMExecuteActor
And the LLM returns file blocks for execute
When I call execute actor with plan_id "EXEC01" and a stream callback
Then the execute result should contain a changeset
And the execute stream callback should have received "execute_started"
And the execute stream callback should have received "execute_step"
And the execute stream callback should have received "execute_complete"
Scenario: LLMExecuteActor executes without stream callback
Given a valid LLMExecuteActor
And the LLM returns file blocks for execute
When I call execute actor with plan_id "EXEC02" and no stream callback
Then the execute result should contain a changeset
Scenario: LLMExecuteActor executes with sandbox_root
Given a valid LLMExecuteActor
And the LLM returns file blocks for execute
When I call execute actor with plan_id "EXEC03" and a sandbox root
Then the execute result should have sandbox refs
Scenario: LLMExecuteActor executes with sandbox_root in read_only mode
Given a valid LLMExecuteActor
And the LLM returns file blocks for execute
When I call execute actor with plan_id "EXEC04" sandbox root and read_only
Then the execute result should have sandbox refs
And no files should be written to sandbox
Scenario: LLMExecuteActor executes with no file blocks from LLM
Given a valid LLMExecuteActor
And the LLM returns empty response for execute
When I call execute actor with plan_id "EXEC05" and no stream callback
Then the execute result should have zero entries
Scenario: LLMExecuteActor injects assembled execute-phase context into prompt
Given a valid LLMExecuteActor with assembled execute-phase context
And the LLM returns file blocks for execute
When I call execute actor with plan_id "EXEC06" and no stream callback
Then the execute prompt should contain "ACMS Execute-Phase Context"
And the execute prompt should contain "src/main.py"
Scenario: LLMExecuteActor falls back when context assembly fails
Given a valid LLMExecuteActor with failing context assembly
And the LLM returns file blocks for execute
When I call execute actor with plan_id "EXEC07" and no stream callback
Then the execute result should contain a changeset
And the execute prompt should not contain "ACMS Execute-Phase Context"
Scenario: LLMExecuteActor handles empty assembled context
Given a valid LLMExecuteActor with empty assembled context
And the LLM returns file blocks for execute
When I call execute actor with plan_id "EXEC08" and no stream callback
Then the execute result should contain a changeset
And the execute prompt should not contain "ACMS Execute-Phase Context"
# ---------------------------------------------------------------
# LLMExecuteActor._parse_file_blocks
# ---------------------------------------------------------------
Scenario: Parse file blocks from LLM output with FILE markers
When I parse file blocks from LLM output with two files
Then I should get 2 changeset entries
And changeset entry 0 path should be "src/main.py"
Scenario: Parse file blocks from LLM output with no files
When I parse file blocks from empty LLM output
Then I should get 0 changeset entries
# M7 fix: <CAFS>/<CAFE> short-form and >>>>>>>> non-conflicting formats
# had zero test coverage. These scenarios exercise both new patterns.
Scenario: Parse file blocks using CAFS short delimiter format
When I parse file blocks using the CAFS short delimiter format
Then I should get 1 changeset entry with path "src/cafs_example.py"
Scenario: Parse file blocks using the non-conflicting arrow delimiter format
When I parse file blocks using the new arrow delimiter format
Then I should get 1 changeset entry with path "src/arrow_example.py"
# ---------------------------------------------------------------
# LLMExecuteActor._write_to_sandbox
# ---------------------------------------------------------------
Scenario: Write to sandbox creates files in the directory
Given lacov a temporary sandbox directory
When I write generated files to the sandbox
Then the sandbox should contain the generated files
Scenario: Write to sandbox handles OSError gracefully
Given lacov a temporary sandbox directory that is read-only
When I write generated files to the read-only sandbox
Then the write should not raise an exception
# ---------------------------------------------------------------
# LLM response without .content attribute
# ---------------------------------------------------------------
Scenario: Strategize actor handles LLM response without content attribute
Given a valid LLMStrategizeActor
And the LLM returns a response without content attribute
When I call strategize execute with plan_id "NOCONTENT" and no stream callback
Then the strategize result should contain decisions