Files
temp/features/steps/llm_actors_coverage_steps.py
aditya f0442e835d feat(acms): plan execution leverages ACMS context for LLM calls
Integrate ACMS execute-phase context assembly into LLMExecuteActor and inject assembled context into execute prompts with resilient fallback when assembly fails or returns empty output.

Wire the plan CLI executor to use an ACMS-backed execute context assembler, add Behave coverage for context injection/fallback/empty context, and extend Robot M5 verification helpers to assert execute-phase ACMS context usage.

ISSUES CLOSED: #850
2026-03-30 09:21:51 +00:00

781 lines
27 KiB
Python

"""Step definitions for llm_actors_coverage.feature.
These steps exercise uncovered code paths in llm_actors.py including:
- _parse_actor_name helper (lines 45-50)
- LLMStrategizeActor.__init__ validation (lines 62-73)
- LLMStrategizeActor.execute full flow (lines 75-182)
- LLMStrategizeActor._parse_decisions (lines 184-204)
- LLMExecuteActor.__init__ validation (lines 215-226)
- LLMExecuteActor.execute full flow (lines 228-333)
- LLMExecuteActor._parse_file_blocks (lines 335-356)
- LLMExecuteActor._write_to_sandbox (lines 358-384)
"""
import os
import shutil
import tempfile
from types import SimpleNamespace
from unittest.mock import MagicMock
from behave import given, then, when
from cleveragents.application.services.llm_actors import (
LLMExecuteActor,
LLMStrategizeActor,
_parse_actor_name,
)
from cleveragents.application.services.plan_executor import (
ExecuteResult,
StrategizeResult,
StrategyDecision,
)
from cleveragents.core.exceptions import ValidationError
from cleveragents.domain.models.acms.crp import (
AssembledContext,
ContextFragment,
FragmentProvenance,
)
from cleveragents.domain.models.core.plan import InvariantSource, PlanInvariant
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_mock_plan(action_name="test-action"):
"""Create a mock plan with the given action_name."""
plan = SimpleNamespace(action_name=action_name)
return plan
def _make_mock_action(strategy_actor=None, execution_actor=None):
"""Create a mock action with optional actor names."""
action = SimpleNamespace(
strategy_actor=strategy_actor,
execution_actor=execution_actor,
)
return action
def _make_mock_llm_response(content):
"""Create a mock LLM response with a .content attribute."""
response = SimpleNamespace(content=content)
return response
def _make_mock_llm_response_no_content(fallback_str):
"""Create a mock LLM response without .content attribute."""
class NoContentResponse:
def __str__(self):
return fallback_str
return NoContentResponse()
def _make_mock_registry(llm_response):
"""Create a mock ProviderRegistry that returns a mock LLM."""
mock_llm = MagicMock()
mock_llm.invoke.return_value = llm_response
registry = MagicMock()
registry.create_llm.return_value = mock_llm
return registry
def _make_mock_lifecycle(strategy_actor=None, execution_actor=None):
"""Create a mock lifecycle service."""
plan = _make_mock_plan()
action = _make_mock_action(
strategy_actor=strategy_actor,
execution_actor=execution_actor,
)
lifecycle = SimpleNamespace(
get_plan=MagicMock(return_value=plan),
get_action=MagicMock(return_value=action),
)
return lifecycle
def _sample_decisions():
"""Return a list of sample StrategyDecision objects."""
return [
StrategyDecision(
decision_id="DEC001",
step_text="Create the main module",
sequence=0,
parent_id=None,
),
StrategyDecision(
decision_id="DEC002",
step_text="Add unit tests",
sequence=1,
parent_id="DEC001",
),
]
class _StubContextAssembler:
"""Simple execute-context assembler test double."""
def __init__(self, assembled: AssembledContext | None = None, fail: bool = False):
self._assembled = assembled
self._fail = fail
def assemble(self, plan):
if self._fail:
raise RuntimeError("context assembly failed")
return self._assembled
LLM_NUMBERED_RESPONSE = "1. Create the module\n2. Write unit tests\n3. Update docs"
LLM_FILE_BLOCKS_RESPONSE = (
"FILE: src/main.py\n```python\nprint('hello')\n```\n\n"
"FILE: tests/test_main.py\n```python\ndef test_main(): pass\n```\n"
)
# ---------------------------------------------------------------------------
# _parse_actor_name
# ---------------------------------------------------------------------------
@when('I parse actor name "{name}"')
def step_parse_actor_name(context, name):
context.parsed_provider, context.parsed_model = _parse_actor_name(name)
@when('I parse actor name ""')
def step_parse_actor_name_empty(context):
context.parsed_provider, context.parsed_model = _parse_actor_name("")
@then('the parsed provider should be "{expected}"')
def step_check_parsed_provider(context, expected):
assert context.parsed_provider == expected, (
f"Expected provider '{expected}', got '{context.parsed_provider}'"
)
@then('the parsed model should be "{expected}"')
def step_check_parsed_model(context, expected):
assert context.parsed_model == expected, (
f"Expected model '{expected}', got '{context.parsed_model}'"
)
# ---------------------------------------------------------------------------
# LLMStrategizeActor.__init__ validation
# ---------------------------------------------------------------------------
@when("I create an LLMStrategizeActor with None provider_registry")
def step_create_strategize_none_registry(context):
try:
LLMStrategizeActor(provider_registry=None, lifecycle_service=MagicMock())
context.caught_error = None
except ValidationError as exc:
context.caught_error = exc
@when("I create an LLMStrategizeActor with None lifecycle_service")
def step_create_strategize_none_lifecycle(context):
try:
LLMStrategizeActor(provider_registry=MagicMock(), lifecycle_service=None)
context.caught_error = None
except ValidationError as exc:
context.caught_error = exc
@then('lacov a ValidationError should be raised with message "{msg}"')
def step_check_validation_error(context, msg):
assert context.caught_error is not None, (
"Expected a ValidationError but none was raised"
)
assert msg in str(context.caught_error), (
f"Expected message containing '{msg}', got '{context.caught_error}'"
)
@given("a mock provider registry for strategize")
def step_mock_registry_for_strategize(context):
context.mock_registry = MagicMock()
@given("a mock lifecycle service for strategize")
def step_mock_lifecycle_for_strategize(context):
context.mock_lifecycle = MagicMock()
@when("I create an LLMStrategizeActor with valid dependencies")
def step_create_valid_strategize_actor(context):
context.strategize_actor = LLMStrategizeActor(
provider_registry=context.mock_registry,
lifecycle_service=context.mock_lifecycle,
)
@then("the LLMStrategizeActor should be created successfully")
def step_verify_strategize_actor_created(context):
assert context.strategize_actor is not None
assert isinstance(context.strategize_actor, LLMStrategizeActor)
# ---------------------------------------------------------------------------
# LLMStrategizeActor.execute
# ---------------------------------------------------------------------------
@given("a valid LLMStrategizeActor")
def step_setup_valid_strategize_actor(context):
llm_response = _make_mock_llm_response(LLM_NUMBERED_RESPONSE)
context.mock_registry = _make_mock_registry(llm_response)
context.mock_lifecycle = _make_mock_lifecycle(strategy_actor="openai/gpt-4")
context.strategize_actor = LLMStrategizeActor(
provider_registry=context.mock_registry,
lifecycle_service=context.mock_lifecycle,
)
@given("the LLM returns a numbered step list for strategize")
def step_llm_returns_numbered_list(context):
llm_response = _make_mock_llm_response(LLM_NUMBERED_RESPONSE)
context.mock_registry = _make_mock_registry(llm_response)
context.mock_lifecycle = _make_mock_lifecycle(strategy_actor="anthropic/claude-3")
context.strategize_actor = LLMStrategizeActor(
provider_registry=context.mock_registry,
lifecycle_service=context.mock_lifecycle,
)
@given("the LLM returns a response without content attribute")
def step_llm_returns_no_content(context):
response = _make_mock_llm_response_no_content("1. Fallback step text")
mock_llm = MagicMock()
mock_llm.invoke.return_value = response
context.mock_registry = MagicMock()
context.mock_registry.create_llm.return_value = mock_llm
context.mock_lifecycle = _make_mock_lifecycle(strategy_actor="openai/gpt-4")
context.strategize_actor = LLMStrategizeActor(
provider_registry=context.mock_registry,
lifecycle_service=context.mock_lifecycle,
)
@when("I call strategize execute with empty plan_id")
def step_strategize_execute_empty_plan_id(context):
try:
context.strategize_actor.execute(plan_id="", definition_of_done="some task")
context.caught_error = None
except ValidationError as exc:
context.caught_error = exc
@when('I call strategize execute with plan_id "{pid}" and a stream callback')
def step_strategize_execute_with_callback(context, pid):
context.stream_events = []
def callback(event_type, data):
context.stream_events.append({"type": event_type, "data": data})
context.strategize_result = context.strategize_actor.execute(
plan_id=pid,
definition_of_done="Build a REST API",
stream_callback=callback,
)
@when('I call strategize execute with plan_id "{pid}" and no stream callback')
def step_strategize_execute_no_callback(context, pid):
context.strategize_result = context.strategize_actor.execute(
plan_id=pid,
definition_of_done="Build a REST API",
stream_callback=None,
)
@when('I call strategize execute with plan_id "{pid}" and invariants')
def step_strategize_execute_with_invariants(context, pid):
invariants = [
PlanInvariant(text="Must use Python 3.12+", source=InvariantSource.PLAN),
PlanInvariant(text="No external dependencies", source=InvariantSource.ACTION),
]
context.strategize_result = context.strategize_actor.execute(
plan_id=pid,
definition_of_done="Build a module",
invariants=invariants,
stream_callback=None,
)
@when('I call strategize execute with plan_id "{pid}" and None definition_of_done')
def step_strategize_execute_none_dod(context, pid):
context.strategize_result = context.strategize_actor.execute(
plan_id=pid,
definition_of_done=None,
stream_callback=None,
)
@then("the strategize result should contain decisions")
def step_verify_strategize_decisions(context):
assert isinstance(context.strategize_result, StrategizeResult)
assert len(context.strategize_result.decisions) > 0
assert context.strategize_result.decision_root_id is not None
@then('the stream callback should have received "{event_type}"')
def step_verify_stream_callback_event(context, event_type):
event_types = [e["type"] for e in context.stream_events]
assert event_type in event_types, f"Expected event '{event_type}' in {event_types}"
@then("the strategize result should contain invariant records")
def step_verify_invariant_records(context):
assert isinstance(context.strategize_result, StrategizeResult)
assert len(context.strategize_result.invariant_records) == 2
assert (
context.strategize_result.invariant_records[0]["text"]
== "Must use Python 3.12+"
)
assert context.strategize_result.invariant_records[0]["source"] == "plan"
assert context.strategize_result.invariant_records[1]["source"] == "action"
# ---------------------------------------------------------------------------
# LLMStrategizeActor._parse_decisions
# ---------------------------------------------------------------------------
@when('I parse LLM decisions from numbered list "{text}"')
def step_parse_decisions_numbered(context, text):
raw = text.replace("\\n", "\n")
context.parsed_decisions = LLMStrategizeActor._parse_decisions(raw)
@when('I parse LLM decisions from bullet list "{text}"')
def step_parse_decisions_bullets(context, text):
raw = text.replace("\\n", "\n")
context.parsed_decisions = LLMStrategizeActor._parse_decisions(raw)
@when("I parse LLM decisions from empty string")
def step_parse_decisions_empty(context):
context.parsed_decisions = LLMStrategizeActor._parse_decisions("")
@when("I parse LLM decisions from text with blank lines")
def step_parse_decisions_blank_lines(context):
text = "1. Step one\n\n\n2. Step two\n\n"
context.parsed_decisions = LLMStrategizeActor._parse_decisions(text)
@when('I parse LLM decisions from "{text}"')
def step_parse_decisions_generic(context, text):
raw = text.replace("\\n", "\n")
context.parsed_decisions = LLMStrategizeActor._parse_decisions(raw)
@then("I should get {count:d} parsed decisions")
def step_verify_parsed_decision_count(context, count):
assert len(context.parsed_decisions) == count, (
f"Expected {count} decisions, got {len(context.parsed_decisions)}: "
f"{context.parsed_decisions}"
)
@then('parsed decision {idx:d} should be "{expected}"')
def step_verify_parsed_decision_text(context, idx, expected):
assert context.parsed_decisions[idx] == expected, (
f"Expected decision[{idx}]='{expected}', got '{context.parsed_decisions[idx]}'"
)
@then('I should get the default decision "{expected}"')
def step_verify_default_decision(context, expected):
assert len(context.parsed_decisions) == 1
assert context.parsed_decisions[0] == expected
@then("blank lines should be skipped in the result")
def step_verify_blank_lines_skipped(context):
assert len(context.parsed_decisions) == 2
assert context.parsed_decisions[0] == "Step one"
assert context.parsed_decisions[1] == "Step two"
# ---------------------------------------------------------------------------
# LLMExecuteActor.__init__ validation
# ---------------------------------------------------------------------------
@when("I create an LLMExecuteActor with None provider_registry")
def step_create_execute_none_registry(context):
try:
LLMExecuteActor(provider_registry=None, lifecycle_service=MagicMock())
context.caught_error = None
except ValidationError as exc:
context.caught_error = exc
@when("I create an LLMExecuteActor with None lifecycle_service")
def step_create_execute_none_lifecycle(context):
try:
LLMExecuteActor(provider_registry=MagicMock(), lifecycle_service=None)
context.caught_error = None
except ValidationError as exc:
context.caught_error = exc
@given("a mock provider registry for execute")
def step_mock_registry_for_execute(context):
context.mock_registry = MagicMock()
@given("a mock lifecycle service for execute")
def step_mock_lifecycle_for_execute(context):
context.mock_lifecycle = MagicMock()
@when("I create an LLMExecuteActor with valid dependencies")
def step_create_valid_execute_actor(context):
context.execute_actor = LLMExecuteActor(
provider_registry=context.mock_registry,
lifecycle_service=context.mock_lifecycle,
)
@then("the LLMExecuteActor should be created successfully")
def step_verify_execute_actor_created(context):
assert context.execute_actor is not None
assert isinstance(context.execute_actor, LLMExecuteActor)
# ---------------------------------------------------------------------------
# LLMExecuteActor.execute
# ---------------------------------------------------------------------------
@given("a valid LLMExecuteActor")
def step_setup_valid_execute_actor(context):
llm_response = _make_mock_llm_response(LLM_FILE_BLOCKS_RESPONSE)
context.mock_registry = _make_mock_registry(llm_response)
context.mock_lifecycle = _make_mock_lifecycle(execution_actor="openai/gpt-4")
context.execute_actor = LLMExecuteActor(
provider_registry=context.mock_registry,
lifecycle_service=context.mock_lifecycle,
)
context.sandbox_dir = None
@given("a valid LLMExecuteActor with assembled execute-phase context")
def step_setup_execute_actor_with_context(context):
llm_response = _make_mock_llm_response(LLM_FILE_BLOCKS_RESPONSE)
context.mock_registry = _make_mock_registry(llm_response)
context.mock_lifecycle = _make_mock_lifecycle(execution_actor="openai/gpt-4")
assembled = AssembledContext(
fragments=(
ContextFragment(
uko_node="resource://src/main.py",
content="def run():\n return 'ok'",
detail_depth=2,
token_count=12,
relevance_score=0.9,
provenance=FragmentProvenance(
resource_uri="local/large-repo",
location="src/main.py",
strategy="execute_phase_context",
),
),
),
total_tokens=12,
budget_used=0.1,
strategies_used=("relevance",),
context_hash="ctxhash123",
preamble="execute phase context",
provenance_map={},
)
context.execute_actor = LLMExecuteActor(
provider_registry=context.mock_registry,
lifecycle_service=context.mock_lifecycle,
context_assembler=_StubContextAssembler(assembled=assembled),
)
@given("a valid LLMExecuteActor with failing context assembly")
def step_setup_execute_actor_with_failing_context(context):
llm_response = _make_mock_llm_response(LLM_FILE_BLOCKS_RESPONSE)
context.mock_registry = _make_mock_registry(llm_response)
context.mock_lifecycle = _make_mock_lifecycle(execution_actor="openai/gpt-4")
context.execute_actor = LLMExecuteActor(
provider_registry=context.mock_registry,
lifecycle_service=context.mock_lifecycle,
context_assembler=_StubContextAssembler(fail=True),
)
@given("a valid LLMExecuteActor with empty assembled context")
def step_setup_execute_actor_with_empty_context(context):
llm_response = _make_mock_llm_response(LLM_FILE_BLOCKS_RESPONSE)
context.mock_registry = _make_mock_registry(llm_response)
context.mock_lifecycle = _make_mock_lifecycle(execution_actor="openai/gpt-4")
context.execute_actor = LLMExecuteActor(
provider_registry=context.mock_registry,
lifecycle_service=context.mock_lifecycle,
context_assembler=_StubContextAssembler(assembled=None),
)
@given("the LLM returns file blocks for execute")
def step_llm_returns_file_blocks(context):
llm_response = _make_mock_llm_response(LLM_FILE_BLOCKS_RESPONSE)
context.mock_registry = _make_mock_registry(llm_response)
context.mock_lifecycle = _make_mock_lifecycle(execution_actor="anthropic/claude-3")
context_assembler = None
if hasattr(context, "execute_actor") and context.execute_actor is not None:
context_assembler = getattr(context.execute_actor, "_context_assembler", None)
context.execute_actor = LLMExecuteActor(
provider_registry=context.mock_registry,
lifecycle_service=context.mock_lifecycle,
context_assembler=context_assembler,
)
@given("the LLM returns empty response for execute")
def step_llm_returns_empty_for_execute(context):
llm_response = _make_mock_llm_response("No files to generate.")
context.mock_registry = _make_mock_registry(llm_response)
context.mock_lifecycle = _make_mock_lifecycle(execution_actor="openai/gpt-4")
context.execute_actor = LLMExecuteActor(
provider_registry=context.mock_registry,
lifecycle_service=context.mock_lifecycle,
)
@when("I call execute actor with empty plan_id")
def step_execute_actor_empty_plan_id(context):
try:
context.execute_actor.execute(plan_id="", decisions=_sample_decisions())
context.caught_error = None
except ValidationError as exc:
context.caught_error = exc
@when('I call execute actor with plan_id "{pid}" and a stream callback')
def step_execute_actor_with_callback(context, pid):
context.exec_stream_events = []
def callback(event_type, data):
context.exec_stream_events.append({"type": event_type, "data": data})
context.execute_result = context.execute_actor.execute(
plan_id=pid,
decisions=_sample_decisions(),
stream_callback=callback,
)
@when('I call execute actor with plan_id "{pid}" and no stream callback')
def step_execute_actor_no_callback(context, pid):
context.execute_result = context.execute_actor.execute(
plan_id=pid,
decisions=_sample_decisions(),
stream_callback=None,
)
llm = context.mock_registry.create_llm.return_value
call_args = llm.invoke.call_args
assert call_args is not None
context.execute_prompt_text = call_args.args[0][0].content
@when('I call execute actor with plan_id "{pid}" and a sandbox root')
def step_execute_actor_with_sandbox(context, pid):
context.sandbox_dir = tempfile.mkdtemp(prefix="llm_actors_test_")
def cleanup():
if context.sandbox_dir and os.path.exists(context.sandbox_dir):
shutil.rmtree(context.sandbox_dir)
context.add_cleanup(cleanup)
context.execute_result = context.execute_actor.execute(
plan_id=pid,
decisions=_sample_decisions(),
sandbox_root=context.sandbox_dir,
stream_callback=None,
)
@when('I call execute actor with plan_id "{pid}" sandbox root and read_only')
def step_execute_actor_sandbox_readonly(context, pid):
context.sandbox_dir = tempfile.mkdtemp(prefix="llm_actors_test_ro_")
def cleanup():
if context.sandbox_dir and os.path.exists(context.sandbox_dir):
shutil.rmtree(context.sandbox_dir)
context.add_cleanup(cleanup)
context.execute_result = context.execute_actor.execute(
plan_id=pid,
decisions=_sample_decisions(),
sandbox_root=context.sandbox_dir,
read_only=True,
stream_callback=None,
)
@then("the execute result should contain a changeset")
def step_verify_execute_changeset(context):
assert isinstance(context.execute_result, ExecuteResult)
assert context.execute_result.changeset is not None
assert context.execute_result.changeset_id is not None
@then('the execute stream callback should have received "{event_type}"')
def step_verify_exec_stream_event(context, event_type):
event_types = [e["type"] for e in context.exec_stream_events]
assert event_type in event_types, f"Expected event '{event_type}' in {event_types}"
@then("the execute result should have sandbox refs")
def step_verify_sandbox_refs(context):
assert isinstance(context.execute_result, ExecuteResult)
assert len(context.execute_result.sandbox_refs) > 0
@then("no files should be written to sandbox")
def step_verify_no_sandbox_writes(context):
# In read_only mode, _write_to_sandbox is not called, so the sandbox
# directory should be empty (only the dir itself exists, no generated files).
contents = os.listdir(context.sandbox_dir)
assert len(contents) == 0, (
f"Expected empty sandbox in read_only mode, found: {contents}"
)
@then("the execute result should have zero entries")
def step_verify_zero_entries(context):
assert isinstance(context.execute_result, ExecuteResult)
assert context.execute_result.tool_calls_count == 0
@then('the execute prompt should contain "{needle}"')
def step_execute_prompt_contains(context, needle):
assert needle in context.execute_prompt_text, (
f"Expected prompt to contain '{needle}', got: {context.execute_prompt_text}"
)
@then('the execute prompt should not contain "{needle}"')
def step_execute_prompt_not_contains(context, needle):
assert needle not in context.execute_prompt_text, (
f"Expected prompt not to contain '{needle}', got: {context.execute_prompt_text}"
)
# ---------------------------------------------------------------------------
# LLMExecuteActor._parse_file_blocks
# ---------------------------------------------------------------------------
@when("I parse file blocks from LLM output with two files")
def step_parse_file_blocks_two_files(context):
context.parsed_entries = LLMExecuteActor._parse_file_blocks(
LLM_FILE_BLOCKS_RESPONSE, "PLAN_TEST"
)
@when("I parse file blocks from empty LLM output")
def step_parse_file_blocks_empty(context):
context.parsed_entries = LLMExecuteActor._parse_file_blocks(
"No files here.", "PLAN_EMPTY"
)
@then("I should get {count:d} changeset entries")
def step_verify_changeset_entry_count(context, count):
assert len(context.parsed_entries) == count, (
f"Expected {count} entries, got {len(context.parsed_entries)}"
)
@then('changeset entry {idx:d} path should be "{expected}"')
def step_verify_changeset_entry_path(context, idx, expected):
assert context.parsed_entries[idx].path == expected, (
f"Expected path '{expected}', got '{context.parsed_entries[idx].path}'"
)
# ---------------------------------------------------------------------------
# LLMExecuteActor._write_to_sandbox
# ---------------------------------------------------------------------------
@given("lacov a temporary sandbox directory")
def step_create_temp_sandbox(context):
context.sandbox_dir = tempfile.mkdtemp(prefix="llm_sandbox_test_")
def cleanup():
if context.sandbox_dir and os.path.exists(context.sandbox_dir):
shutil.rmtree(context.sandbox_dir)
context.add_cleanup(cleanup)
@when("I write generated files to the sandbox")
def step_write_files_to_sandbox(context):
entries = LLMExecuteActor._parse_file_blocks(LLM_FILE_BLOCKS_RESPONSE, "PLAN_WR")
LLMExecuteActor._write_to_sandbox(
entries, context.sandbox_dir, LLM_FILE_BLOCKS_RESPONSE
)
@then("the sandbox should contain the generated files")
def step_verify_sandbox_files(context):
main_path = os.path.join(context.sandbox_dir, "src", "main.py")
test_path = os.path.join(context.sandbox_dir, "tests", "test_main.py")
assert os.path.exists(main_path), f"Expected file at {main_path}"
assert os.path.exists(test_path), f"Expected file at {test_path}"
with open(main_path) as f:
content = f.read()
assert "print('hello')" in content
@given("lacov a temporary sandbox directory that is read-only")
def step_create_readonly_sandbox(context):
context.sandbox_dir = tempfile.mkdtemp(prefix="llm_sandbox_ro_")
# Pre-create the subdirectory so makedirs succeeds, then make the
# target *file* location unwritable by making the leaf dir read-only.
context.readonly_leaf = os.path.join(context.sandbox_dir, "src")
os.makedirs(context.readonly_leaf, exist_ok=True)
# Make the leaf dir read-only so open() inside it fails with OSError
os.chmod(context.readonly_leaf, 0o555)
def cleanup():
if context.sandbox_dir and os.path.exists(context.sandbox_dir):
os.chmod(context.readonly_leaf, 0o755)
shutil.rmtree(context.sandbox_dir)
context.add_cleanup(cleanup)
@when("I write generated files to the read-only sandbox")
def step_write_to_readonly_sandbox(context):
# FILE path puts the file inside "src/" which is read-only
llm_output = "FILE: src/fail.py\n```python\nprint('fail')\n```\n"
entries = LLMExecuteActor._parse_file_blocks(llm_output, "PLAN_RO")
# Should not raise - OSError is caught and logged inside _write_to_sandbox
context.write_exception = None
try:
LLMExecuteActor._write_to_sandbox(entries, context.sandbox_dir, llm_output)
except Exception as exc:
context.write_exception = exc
@then("the write should not raise an exception")
def step_verify_no_write_exception(context):
assert context.write_exception is None, (
f"Expected no exception, got: {context.write_exception}"
)