Files
temp/features/steps/cli_init_yes_flag_steps.py
brent.edwards 4e3bf7d3ad test(cli): add failing tests for agents init --yes missing option
Add TDD-style Behave BDD tests for the missing agents init --yes flag
(bug #522). Five Gherkin scenarios cover: exit code validation, prompt
suppression, -y alias, output summary fields, and interactive-mode
regression guard. Includes Robot Framework smoke tests (tagged @wip)
and ASV benchmarks.

Configure behave.ini to exclude @wip scenarios globally and noxfile.py
to exclude wip-tagged Robot suites, so TDD-failing tests do not break CI.

Review feedback addressed:
- Remove unnecessary # type: ignore from benchmark (outside Pyright scope)
- Fix Then...Then to Then...And in Gherkin (L1)
- Fix CHANGELOG 'three scenarios' to 'five scenarios' (L2)
- Add behave.ini documentation for @wip workaround (Aditya F1)
- Rename Scenario 2 title to 'suppresses interactive prompts' (Aditya F2)

Closes #536
2026-03-06 20:28:37 +00:00

211 lines
7.6 KiB
Python

"""Step definitions for CLI init --yes flag tests (bug #522).
These tests are TDD-style: they assert the CORRECT expected behaviour of
``agents init --yes`` and are expected to FAIL until the bug fix is applied.
"""
from __future__ import annotations
import os
import re
import shutil
import tempfile
from pathlib import Path
from unittest.mock import create_autospec, patch
from behave import given, then, when
from typer.testing import CliRunner
from cleveragents.application.services.project_service import ProjectService
from cleveragents.cli.main import app
def _restore_cwd(context):
"""Restore the original working directory, env var, and clean up."""
os.chdir(context._init_original_cwd)
if context._init_original_home is None:
os.environ.pop("CLEVERAGENTS_HOME", None)
else:
os.environ["CLEVERAGENTS_HOME"] = context._init_original_home
shutil.rmtree(context.temp_dir, ignore_errors=True)
def _create_init_mocks(context):
"""Create and configure mocked container, service, and project.
Returns ``(patcher, mock_service)`` where *patcher* is the started
``patch`` context manager.
The service mock uses ``create_autospec(ProjectService)`` so that
attribute access and method calls are validated against the real
``ProjectService`` interface. The project mock uses a typed
``_MockProject`` class instead of ``MagicMock`` because the
spec-required output fields (``data_dir``, ``config_path``,
``database_status``, ``directories``) do not yet exist on the legacy
``Project`` model — they will be added when the #522 fix aligns the
model with ``docs/specification.md:1381-1386``. Using
``create_autospec(Project)`` would reject those attribute assignments.
"""
mock_service = create_autospec(ProjectService, instance=True)
class _MockProject:
"""Lightweight stand-in for the Project model with spec fields."""
name: str
path: Path
data_dir: Path
config_path: Path
database_status: str
directories: list[str]
mock_project = _MockProject()
mock_project.name = Path(context.temp_dir).name
mock_project.path = Path(context.temp_dir)
# Pre-populate spec-required fields (specification.md:1381-1386) so
# that when the #522 fix lands, output assertions fail for the right
# reason (real bugs) rather than MagicMock stringification artefacts.
mock_project.data_dir = Path(context.temp_dir)
mock_project.config_path = Path(context.temp_dir) / "config.toml"
mock_project.database_status = "initialized (schema v3)"
mock_project.directories = ["logs", "cache", "sessions", "contexts"]
mock_service.initialize_project.return_value = mock_project
patcher = patch("cleveragents.application.container.get_container")
mock_container = patcher.start()
mock_container.return_value.project_service.return_value = mock_service
return patcher, mock_service
@given("I have a temporary project directory for init")
def step_temp_project_directory(context):
"""Create a temporary directory and store it on *context*."""
context.temp_dir = tempfile.mkdtemp()
# Use step-private attribute names (prefixed with _init_) to avoid
# colliding with environment.py's context.original_cwd (str).
# Match the framework's type (str via os.getcwd()) for consistency.
context._init_original_cwd = os.getcwd()
context._init_original_home = os.environ.get("CLEVERAGENTS_HOME")
os.environ["CLEVERAGENTS_HOME"] = context.temp_dir
os.chdir(context.temp_dir)
context.add_cleanup(_restore_cwd, context)
def _run_init_with_flag(context, flag: str) -> None:
"""Invoke ``agents init`` with the given flag via the Typer test runner."""
runner = CliRunner()
patcher, mock_service = _create_init_mocks(context)
try:
result = runner.invoke(app, ["init", flag])
finally:
patcher.stop()
context.init_yes_result = {
"exit_code": result.exit_code,
"output": result.output,
}
context.init_yes_raw_result = result
context.init_yes_mock_service = mock_service
@when("I run agents init with the --yes flag")
def step_run_init_yes(context):
"""Invoke ``agents init --yes`` via the Typer test runner."""
_run_init_with_flag(context, "--yes")
@when("I run agents init with the -y flag")
def step_run_init_short_yes(context):
"""Invoke ``agents init -y`` via the Typer test runner."""
_run_init_with_flag(context, "-y")
@then("the init command should exit with code {code:d}")
def step_init_exit_code(context, code):
"""Assert the init command exited with the expected code."""
actual = context.init_yes_result["exit_code"]
assert actual == code, (
f"Expected exit code {code}, got {actual}. "
f"Output: {context.init_yes_result['output']}"
)
@then('the init output should contain "{text}"')
def step_init_output_contains(context, text):
"""Assert that the init command output contains *text*."""
output = context.init_yes_result["output"]
assert text in output, f"Expected '{text}' in output:\n{output}"
@then("the project service initialize_project should have been called")
def step_initialize_project_called(context):
"""Assert the mock project service's initialize_project was invoked."""
context.init_yes_mock_service.initialize_project.assert_called_once()
@when("I run agents init without the --yes flag")
def step_run_init_no_yes(context):
"""Invoke ``agents init`` without --yes (interactive mode)."""
runner = CliRunner()
patcher, _mock_service = _create_init_mocks(context)
try:
result = runner.invoke(app, ["init"])
finally:
patcher.stop()
context.init_yes_result = {
"exit_code": result.exit_code,
"output": result.output,
}
context.init_yes_raw_result = result
@then("the init output should indicate interactive mode")
def step_output_indicates_interactive(context):
"""Assert that without --yes the output does NOT contain the
non-interactive marker, indicating the command ran in interactive mode.
This is the negative complement to the --yes scenarios: when the fix
lands, ``agents init`` (without --yes) should either present a prompt
or omit the ``Initialized (non-interactive)`` marker.
"""
output = context.init_yes_result["output"]
assert "Initialized (non-interactive)" not in output, (
"Expected interactive mode but output contains "
f"'Initialized (non-interactive)':\n{output}"
)
@then("no interactive prompt should have been presented")
def step_no_interactive_prompt(context):
"""Assert that no interactive prompt was presented.
Verifies that the command output does not contain common prompt-like
tokens, which would indicate the ``--yes`` flag failed to suppress
interactive prompts.
"""
output = context.init_yes_result["output"]
prompt_tokens = (
"[Y/n]",
"[y/N]",
"Continue? ",
"Proceed? ",
"Confirm ",
"(yes/no)",
)
for token in prompt_tokens:
assert token not in output, (
f"Unexpected prompt token '{token}' found in output:\n{output}"
)
# Regex catches "Enter project name:", "Enter path:", "Enter value?"
# without false-positiving on "Entered configuration" or "Enterprise".
# Non-greedy .*? stops at the first : or ? to avoid over-matching
# when the line contains colons in non-prompt contexts (e.g. "Config: /path").
enter_prompt = re.search(r"Enter\s+\S+.*?[:?]", output)
assert enter_prompt is None, (
f"Unexpected Enter-style prompt found in output: "
f"'{enter_prompt.group()}'\n{output}"
)