test: add TDD bug-capture test for #932 — plan apply missing --yes flag #958

Merged
brent.edwards merged 1 commits from tdd/m4-plan-apply-yes-flag into master 2026-03-21 00:13:18 +00:00
6 changed files with 241 additions and 1 deletions
+1
View File
@@ -48,6 +48,7 @@
auto-discovered with bounded scan_depth. Updated `fs-directory` child types
and auto-discovery. Updated `git-checkout` child types. Includes YAML
configs, Behave BDD tests, Robot tests, and ASV benchmarks. (#330)
- Added TDD bug-capture tests for #932 (plan apply missing --yes flag). (#950)
- Fixed `plan execute` CLI failing with "Plan is not in an executable state
(current: strategize/queued)" after strategize completed successfully.
Root cause: `_get_plan_executor()` created a second `PlanLifecycleService`
@@ -0,0 +1,91 @@
"""Step definitions for TDD Bug #932 — plan apply missing --yes flag.
These steps verify that the ``lifecycle-apply`` CLI command accepts the
``--yes`` / ``-y`` flag as required by the specification. The spec
mandates ``agents plan apply [--yes|-y] <PLAN_ID>`` to skip the
confirmation prompt before applying plan changes, but the current
implementation does not recognise this flag.
When the flag is missing, Typer (Click) rejects the option with
``"No such option: --yes"`` and exit code 2. The assertion that this
error is absent will **fail** — proving the bug. The
``@tdd_expected_fail`` tag on the feature inverts this failure to a
pass while the bug remains open.
"""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner
from cleveragents.cli.commands.plan import app as plan_app
@given("a plan CLI runner for the yes-flag test")
def step_plan_cli_runner(context: Context) -> None:
"""Create a Typer CliRunner for the plan sub-app."""
context.apply_yes_runner = CliRunner()
@when("I invoke lifecycle-apply with --yes flag")
def step_invoke_lifecycle_apply_yes(context: Context) -> None:
"""Invoke ``plan lifecycle-apply --yes <PLAN_ID>``.
We pass a dummy plan ID because we only care whether the ``--yes``
flag is recognised by the CLI framework, not whether the plan
exists. If ``--yes`` is unknown, Typer exits with code 2 *before*
any business logic runs.
"""
context.apply_yes_result = context.apply_yes_runner.invoke(
plan_app,
["lifecycle-apply", "--yes", "DUMMY_PLAN_ID"],
)
@when("I invoke lifecycle-apply with -y flag")
def step_invoke_lifecycle_apply_y(context: Context) -> None:
"""Invoke ``plan lifecycle-apply -y <PLAN_ID>``."""
context.apply_yes_result = context.apply_yes_runner.invoke(
plan_app,
["lifecycle-apply", "-y", "DUMMY_PLAN_ID"],
)
@then("the lifecycle-apply --yes invocation should not report an unknown option")
def step_assert_yes_recognised(context: Context) -> None:
"""Assert the output does NOT contain the Click/Typer unknown-option error.
When ``--yes`` is not defined on the command, Click produces::
Error: No such option: --yes
and returns exit code 2. If the flag is properly defined, the
command proceeds past option parsing (and may fail later due to
the dummy plan ID, but that is irrelevant — we only test flag
recognition).
"""
output = context.apply_yes_result.output
exit_code = context.apply_yes_result.exit_code
# Check both the text message and the Click/Typer usage-error exit code.
# Exit code 2 specifically means "unrecognised option" in Click.
assert exit_code != 2 and "No such option" not in output, (
f"The --yes flag was not recognised by lifecycle-apply.\n"
f"Exit code: {exit_code}\n"
f"Output:\n{output}"
)
@then("the lifecycle-apply -y invocation should not report an unknown option")
def step_assert_y_recognised(context: Context) -> None:
"""Assert the output does NOT contain the Click/Typer unknown-option error.
Same check as the ``--yes`` variant but for the ``-y`` short flag.
"""
output = context.apply_yes_result.output
exit_code = context.apply_yes_result.exit_code
assert exit_code != 2 and "No such option" not in output, (
f"The -y flag was not recognised by lifecycle-apply.\n"
f"Exit code: {exit_code}\n"
f"Output:\n{output}"
)
+22
View File
@@ -0,0 +1,22 @@
@tdd_expected_fail @tdd_bug @tdd_bug_932
Feature: TDD Bug #932 — plan apply missing --yes flag
As a developer
I want to verify that `agents plan lifecycle-apply` accepts the --yes
flag required by the specification
So that the bug is captured and will be caught by a regression test
The specification mandates `agents plan apply [--yes|-y] <PLAN_ID>`
but the current `lifecycle-apply` implementation does not accept
`--yes` or `-y`. Other destructive CLI commands (`session delete`,
`project delete`, `plan correct`, `plan rollback`) correctly implement
the `--yes`/`-y` pattern to skip confirmation prompts.
Scenario: lifecycle-apply recognises the --yes long flag
Given a plan CLI runner for the yes-flag test
When I invoke lifecycle-apply with --yes flag
Then the lifecycle-apply --yes invocation should not report an unknown option
Scenario: lifecycle-apply recognises the -y short flag
Given a plan CLI runner for the yes-flag test
When I invoke lifecycle-apply with -y flag
Then the lifecycle-apply -y invocation should not report an unknown option
+1 -1
View File
@@ -1060,7 +1060,7 @@ def benchmark_regression(session: nox.Session):
"""Run Airspeed Velocity benchmarks regression test."""
session.install("-e", ".[tests]")
config_path = "asv.conf.json"
asv_base_sha = os.environ.get("ASV_BASE_SHA")
asv_base_sha = os.environ.get("ASV_BASE_SHA", "master")
session.run(
"asv",
"machine",
+93
View File
@@ -0,0 +1,93 @@
"""Helper script for tdd_plan_apply_yes_flag.robot smoke tests.
Each subcommand exercises the real CLI path (no mocks) to reproduce bug #932.
The ``lifecycle-apply`` command is invoked with ``--yes`` or ``-y`` to verify
that the flag is recognised by the CLI framework.
The helper reports the **real** outcome: it exits 0 and prints the sentinel
when the flag is accepted (bug is fixed), and exits 1 when the flag is
rejected (bug still present). The ``tdd_expected_fail_listener`` on the
Robot side handles pass/fail inversion while the bug remains open.
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from pathlib import Path
from typing import NoReturn
# Ensure local source tree is importable.
_ROOT = Path(__file__).resolve().parents[1]
_SRC = str(_ROOT / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from typer.testing import CliRunner # noqa: E402
from cleveragents.cli.commands.plan import app as plan_app # noqa: E402
runner = CliRunner()
def _fail(message: str) -> NoReturn:
"""Print an error message to stderr and exit with code 1."""
print(message, file=sys.stderr)
sys.exit(1)
# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------
def check_yes_long() -> None:
"""Invoke ``lifecycle-apply --yes DUMMY`` and verify the flag is accepted.
Exits 0 with sentinel when ``--yes`` is recognised (bug fixed).
Exits 1 when Typer rejects the flag (bug still present).
"""
result = runner.invoke(plan_app, ["lifecycle-apply", "--yes", "DUMMY_PLAN_ID"])
if "No such option" in result.output or result.exit_code == 2:
_fail(
f"lifecycle-apply rejected --yes flag.\n"
f"Exit code: {result.exit_code}\n"
f"Output: {result.output}"
)
print("tdd-plan-apply-yes-flag-long-ok")
def check_yes_short() -> None:
"""Invoke ``lifecycle-apply -y DUMMY`` and verify the flag is accepted.
Exits 0 with sentinel when ``-y`` is recognised (bug fixed).
Exits 1 when Typer rejects the flag (bug still present).
"""
result = runner.invoke(plan_app, ["lifecycle-apply", "-y", "DUMMY_PLAN_ID"])
if "No such option" in result.output or result.exit_code == 2:
_fail(
f"lifecycle-apply rejected -y flag.\n"
f"Exit code: {result.exit_code}\n"
f"Output: {result.output}"
)
print("tdd-plan-apply-yes-flag-short-ok")
# ---------------------------------------------------------------------------
# Dispatcher
# ---------------------------------------------------------------------------
_COMMANDS: dict[str, Callable[[], None]] = {
"check-yes-long": check_yes_long,
"check-yes-short": check_yes_short,
}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(
f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>",
file=sys.stderr,
)
sys.exit(1)
cmd = _COMMANDS[sys.argv[1]]
cmd()
+33
View File
@@ -0,0 +1,33 @@
*** Settings ***
Documentation TDD Bug #932 — plan lifecycle-apply missing --yes flag
... Integration smoke tests verifying that the lifecycle-apply
... command accepts the --yes / -y flag required by the
... specification. The spec mandates
... ``agents plan apply [--yes|-y] <PLAN_ID>`` to skip the
... confirmation prompt, but the current implementation does not
... recognise this flag.
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_tdd_plan_apply_yes_flag.py
*** Test Cases ***
TDD Plan Apply Yes Long Flag Via CLI
[Documentation] Verify that ``lifecycle-apply --yes`` is recognised
[Tags] tdd_expected_fail tdd_bug tdd_bug_932
${result}= Run Process ${PYTHON} ${HELPER} check-yes-long cwd=${WORKSPACE} timeout=30s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} tdd-plan-apply-yes-flag-long-ok
TDD Plan Apply Yes Short Flag Via CLI
[Documentation] Verify that ``lifecycle-apply -y`` is recognised
[Tags] tdd_expected_fail tdd_bug tdd_bug_932
${result}= Run Process ${PYTHON} ${HELPER} check-yes-short cwd=${WORKSPACE} timeout=30s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} tdd-plan-apply-yes-flag-short-ok