forked from HAL9000/cleveragents-core
1878998b7a
Rename the TDD tag system from tdd_bug/tdd_bug_<N> to tdd_issue/tdd_issue_<N> across the entire codebase. The tdd_expected_fail tag is unchanged. The TDD expected-failure workflow is not limited to bug fixes — it applies equally to any issue type (features, tasks, refactors). The _bug suffix was misleading and narrowed the perceived scope. The new _issue suffix accurately reflects that the TDD tagging system applies to any Forgejo issue. Changes span 92 files: - features/environment.py: validate_tdd_tags(), should_invert_result(), and apply_tdd_inversion() updated — regex, variables, error messages - robot/tdd_expected_fail_listener.py: _validate_tdd_tags(), _should_invert_result(), start_test(), end_test() updated consistently - 33 Behave .feature files: all @tdd_bug/@tdd_bug_<N> tags renamed - 29 Robot .robot files: all tdd_bug/tdd_bug_<N> tags renamed - 3 Robot fixture files renamed (tdd_bug_alone, tdd_missing_tdd_bug, tdd_expected_fail_missing_bug_n) with content and references updated - Tag validation tests and helpers updated (function names, command dispatch keys, output strings, fixture references) - CONTRIBUTING.md: section renamed from 'TDD Bug Test Tags' to 'TDD Issue Test Tags', all tag references and examples updated - noxfile.py: comment references updated - Step definition files, mock helpers, and benchmark files: docstring references updated ISSUES CLOSED: #965
227 lines
8.9 KiB
Python
227 lines
8.9 KiB
Python
"""Step definitions for TDD Issue #783 — init --yes should not require user input.
|
|
|
|
These steps exercise the *real* ``agents init --yes`` code path while
|
|
simulating a TTY environment by replacing the migration runner's
|
|
``_default_prompt_for_migration`` with a function that declines
|
|
migration (simulating a user on a real terminal providing no input).
|
|
The ``@tdd_expected_fail`` tag on the scenarios inverts the result:
|
|
these tests **pass** CI while the bug is present and will **fail** once
|
|
the bug is fixed (signalling that the tag should be removed).
|
|
|
|
Root cause
|
|
~~~~~~~~~~
|
|
``init_command()`` in ``project.py`` receives the ``--yes`` flag but
|
|
does not forward it to the migration runner. The migration runner's
|
|
``_default_prompt_for_migration()`` calls ``typer.confirm()`` when a
|
|
TTY is detected, blocking the command for user input even though
|
|
``--yes`` was specified. In a non-TTY environment (like CliRunner),
|
|
the prompt is silently skipped — the bug only surfaces in real terminal
|
|
sessions. We replace the prompt function to reproduce the TTY code
|
|
path.
|
|
|
|
Mock strategy
|
|
~~~~~~~~~~~~~
|
|
CliRunner replaces ``sys.stdin`` during ``invoke()``, so patching
|
|
``sys.stdin.isatty()`` alone is ineffective. Instead we:
|
|
|
|
1. Patch ``sys.stdin`` on the real ``sys`` module (documents intent
|
|
and is the correct target per the project's established pattern in
|
|
``features/steps/migration_runner_steps.py``).
|
|
2. Patch ``MigrationRunner._default_prompt_for_migration`` with a
|
|
replacement that returns ``False`` — simulating a TTY prompt where
|
|
the user declines migration. This ensures the prompt code path is
|
|
exercised deterministically regardless of CliRunner's stdin
|
|
replacement.
|
|
3. Set ``CLEVERAGENTS_DATABASE_URL`` to a path inside our tmp directory
|
|
whose filename does **not** match the ``before_scenario`` template-DB
|
|
prefixes (``cleveragents_*``, ``test_*``, ``db.*``). This forces the
|
|
migration runner's ``init_or_upgrade`` through the real Alembic path
|
|
instead of the test fast-path that copies a pre-migrated template.
|
|
|
|
With bug #783 present, ``require_confirmation=True`` is hardcoded in
|
|
``UnitOfWork._ensure_database_initialized()``, so the prompt fires and
|
|
returns ``False``, causing ``MigrationNotApprovedError``. After the
|
|
fix, ``--yes`` should bypass the prompt entirely.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.application.container import reset_container
|
|
from cleveragents.cli.commands.project import app as project_app
|
|
from cleveragents.infrastructure.database.migration_runner import MigrationRunner
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
def _tty_prompt_declines(_message: str) -> bool:
|
|
"""Simulate a TTY migration prompt that declines (no user input).
|
|
|
|
Replaces ``_default_prompt_for_migration`` to reproduce the TTY
|
|
code path without depending on ``sys.stdin.isatty()``, which
|
|
CliRunner overrides during ``invoke()``.
|
|
"""
|
|
return False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a fresh environment for tdd-init-yes-no-input")
|
|
def step_fresh_environment(context: Context) -> None:
|
|
"""Set up a fresh temporary environment with no existing database.
|
|
|
|
Creates a temporary directory to serve as the working directory so
|
|
that ``agents init`` creates a fresh ``.cleveragents`` directory.
|
|
Disables auto-apply environment variables so the real prompt
|
|
behaviour is exercised.
|
|
"""
|
|
context.tdd_init_tmpdir = tempfile.mkdtemp(prefix="tdd_init_yes_")
|
|
|
|
# Save env vars that would bypass the prompt or redirect the
|
|
# database, and remove them. ``CLEVERAGENTS_DATABASE_URL`` and
|
|
# ``CLEVERAGENTS_TEST_DATABASE_URL`` are set by ``before_scenario``
|
|
# to pre-migrated scenario databases — removing them forces the
|
|
# container to derive a fresh DB path from ``CLEVERAGENTS_HOME``.
|
|
context.tdd_init_saved_env = {
|
|
k: os.environ.pop(k, None)
|
|
for k in (
|
|
"CLEVERAGENTS_AUTO_APPLY_MIGRATIONS",
|
|
"CI",
|
|
"BEHAVE_TESTING",
|
|
"CLEVERAGENTS_DATABASE_URL",
|
|
"CLEVERAGENTS_TEST_DATABASE_URL",
|
|
)
|
|
}
|
|
|
|
# Register cleanup to restore env vars and remove tmpdir even if
|
|
# subsequent steps are never reached (e.g. hook errors).
|
|
saved_env = context.tdd_init_saved_env
|
|
tmpdir = context.tdd_init_tmpdir
|
|
|
|
def _cleanup() -> None:
|
|
for key, val in saved_env.items():
|
|
if val is not None:
|
|
os.environ[key] = val
|
|
else:
|
|
os.environ.pop(key, None)
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
reset_container()
|
|
|
|
context.add_cleanup(_cleanup)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I invoke agents init --yes with no stdin")
|
|
def step_invoke_init_yes_no_stdin(context: Context) -> None:
|
|
"""Invoke ``agents init --yes`` simulating a TTY with no real input.
|
|
|
|
Uses ``CliRunner`` with ``input=""`` to provide no input. Patches
|
|
``sys.stdin`` on the real ``sys`` module for intent documentation
|
|
and replaces ``MigrationRunner._default_prompt_for_migration`` with
|
|
a function that returns ``False`` (simulating a TTY prompt where the
|
|
user declines). See module docstring for the full mock strategy.
|
|
"""
|
|
# Create a mock stdin that reports as a TTY but has no data
|
|
mock_stdin = MagicMock(spec=sys.stdin)
|
|
mock_stdin.isatty.return_value = True
|
|
|
|
# Reset the global container to ensure a fresh UoW is created
|
|
# using our env var overrides (no stale CLEVERAGENTS_DATABASE_URL).
|
|
reset_container()
|
|
|
|
# Use a DB path whose filename (``init_yes_test.sqlite``) does
|
|
# NOT start with the template-fast-path prefixes
|
|
# (``cleveragents_*``, ``test_*``, ``db.*``) so the real Alembic
|
|
# migration path runs instead of copying a pre-migrated template.
|
|
# Do NOT pre-create directories — ``init_command`` expects a
|
|
# fresh path without an existing ``.cleveragents`` directory.
|
|
tdd_db_path = os.path.join(
|
|
context.tdd_init_tmpdir, ".cleveragents", "init_yes_test.sqlite"
|
|
)
|
|
tdd_db_url = f"sqlite:///{tdd_db_path}"
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"CLEVERAGENTS_HOME": context.tdd_init_tmpdir,
|
|
"CLEVERAGENTS_DATABASE_URL": tdd_db_url,
|
|
},
|
|
):
|
|
# Remove auto-apply env vars inside the patched environment
|
|
for key in (
|
|
"CLEVERAGENTS_AUTO_APPLY_MIGRATIONS",
|
|
"CI",
|
|
"BEHAVE_TESTING",
|
|
):
|
|
os.environ.pop(key, None)
|
|
|
|
# Patch sys.stdin on the real sys module (correct target per
|
|
# project convention) and replace the prompt function to
|
|
# simulate a TTY decline. See module docstring for why
|
|
# both patches are needed.
|
|
with (
|
|
patch.object(sys, "stdin", mock_stdin),
|
|
patch.object(
|
|
MigrationRunner,
|
|
"_default_prompt_for_migration",
|
|
staticmethod(_tty_prompt_declines),
|
|
),
|
|
):
|
|
context.tdd_init_result = runner.invoke(
|
|
project_app,
|
|
["init", "--yes", "--path", context.tdd_init_tmpdir],
|
|
input="",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the tdd-init-yes-no-input command should exit successfully")
|
|
def step_exit_success(context: Context) -> None:
|
|
"""Assert the init command exited with code 0."""
|
|
result = context.tdd_init_result
|
|
assert result is not None, "init --yes was not invoked"
|
|
actual = result.exit_code
|
|
assert actual == 0, f"Expected exit code 0, got {actual}.\nOutput:\n{result.output}"
|
|
|
|
|
|
@then('the tdd-init-yes-no-input output should not contain "{text}"')
|
|
def step_output_not_contains(context: Context, text: str) -> None:
|
|
"""Assert the given text does NOT appear in the command output."""
|
|
result = context.tdd_init_result
|
|
assert result is not None, "init --yes was not invoked"
|
|
output = result.output
|
|
assert text.lower() not in output.lower(), (
|
|
f"Did NOT expect '{text}' in output but found it:\n{output}"
|
|
)
|
|
|
|
|
|
@then('the tdd-init-yes-no-input output should contain "{text}"')
|
|
def step_output_contains(context: Context, text: str) -> None:
|
|
"""Assert the given text appears in the command output."""
|
|
result = context.tdd_init_result
|
|
assert result is not None, "init --yes was not invoked"
|
|
output = result.output
|
|
assert text.lower() in output.lower(), (
|
|
f"Expected '{text}' in output but did not find it:\n{output}"
|
|
)
|