Files
temp/features/steps/tdd_plan_explain_plan_id_steps.py
hurui200320 1878998b7a refactor(testing): rename tdd_bug/tdd_bug_N tags to tdd_issue/tdd_issue_N
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
2026-03-27 05:58:35 +00:00

182 lines
6.3 KiB
Python

"""Step definitions for tdd_plan_explain_plan_id.feature.
TDD issue-capture test for bug #968: ``plan explain`` expects a decision_id
as its first positional argument, but the M3 acceptance test passes a plan_id.
Because ``svc.get_decision(plan_id)`` raises ``DecisionNotFoundError`` (the
plan ID is not a decision ID), the command exits with rc=1 and "Decision not
found" error.
These steps use the ``@tdd_expected_fail`` tag so that the assertions — which
expect the *fixed* behaviour (rc=0 with decision details) — do not fail CI
while the bug is still unfixed. Once bug #968 is fixed the tag will be
removed and the tests will run normally.
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner
from ulid import ULID
from cleveragents.application.services.decision_service import (
DecisionNotFoundError,
)
from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.domain.models.core.decision import (
Decision,
DecisionType,
)
runner = CliRunner()
_PATCH_CONTAINER = "cleveragents.application.container.get_container"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_decision(
decision_id: str | None = None,
plan_id: str | None = None,
sequence: int = 0,
parent_id: str | None = None,
dtype: DecisionType = DecisionType.PROMPT_DEFINITION,
question: str = "What should we build?",
chosen: str = "A REST API",
) -> Decision:
"""Build a minimal Decision with sensible defaults."""
did = decision_id or str(ULID())
pid = plan_id or str(ULID())
kwargs: dict = {
"decision_id": did,
"plan_id": pid,
"sequence_number": sequence,
"decision_type": dtype,
"question": question,
"chosen_option": chosen,
}
if parent_id is not None:
kwargs["parent_decision_id"] = parent_id
elif dtype != DecisionType.PROMPT_DEFINITION:
kwargs["parent_decision_id"] = str(ULID())
return Decision(**kwargs)
def _mock_container_with_decision_svc(svc_mock: MagicMock) -> MagicMock:
"""Create a mock container whose ``decision_service()`` returns *svc_mock*."""
container = MagicMock()
container.decision_service.return_value = svc_mock
return container
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given(
"tdd968 a mock DecisionService where get_decision raises DecisionNotFoundError for a plan id"
)
def step_tdd968_mock_get_decision_none(context: Context) -> None:
"""Simulate the current buggy behaviour: plan ID is not a decision ID."""
context.tdd968_plan_id = str(ULID())
svc = MagicMock()
# get_decision raises DecisionNotFoundError when called with a plan_id (the bug)
svc.get_decision.side_effect = DecisionNotFoundError(context.tdd968_plan_id)
context.tdd968_svc = svc
context.tdd968_container = _mock_container_with_decision_svc(svc)
@given(
"tdd968 the same mock DecisionService returns decisions "
"via list_decisions for the plan id"
)
def step_tdd968_mock_list_decisions(context: Context) -> None:
"""Set up list_decisions to return real decisions for the plan.
After the fix, the command should fall back to this lookup when
get_decision raises DecisionNotFoundError.
"""
root_id = str(ULID())
child_id = str(ULID())
pid = context.tdd968_plan_id
context.tdd968_root_question = "What should we build?"
decisions = [
_make_decision(
decision_id=root_id,
plan_id=pid,
sequence=0,
question=context.tdd968_root_question,
chosen="A REST API",
),
_make_decision(
decision_id=child_id,
plan_id=pid,
parent_id=root_id,
sequence=1,
dtype=DecisionType.STRATEGY_CHOICE,
question="Which framework?",
chosen="FastAPI",
),
]
context.tdd968_svc.list_decisions.return_value = decisions
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when("tdd968 I invoke plan explain with the plan id")
def step_tdd968_invoke_explain(context: Context) -> None:
"""Invoke ``plan explain <plan_id>`` via CliRunner with mocked container."""
with patch(_PATCH_CONTAINER, return_value=context.tdd968_container):
result = runner.invoke(
plan_app,
["explain", context.tdd968_plan_id, "--format", "json"],
)
context.tdd968_result = result
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then("tdd968 the command should exit with return code 0")
def step_tdd968_rc_zero(context: Context) -> None:
"""Assert rc=0 — this will FAIL while bug #968 is unfixed (rc=1)."""
assert context.tdd968_result.exit_code == 0, (
f"Expected exit code 0 but got {context.tdd968_result.exit_code}. "
f"Output: {context.tdd968_result.output}"
)
@then("tdd968 the output should contain decision details")
def step_tdd968_output_has_details(context: Context) -> None:
"""Assert the output contains decision fields — fails while bug exists."""
output = context.tdd968_result.output
# When the bug is fixed, the output should contain decision fields
assert "decision_id" in output and "question" in output, (
f"Expected decision details in output, got: {output}"
)
# Verify that the fix called list_decisions with the correct plan_id
context.tdd968_svc.list_decisions.assert_called_once_with(
context.tdd968_plan_id,
)
@then("tdd968 the output should contain the root decision question")
def step_tdd968_output_has_question(context: Context) -> None:
"""Assert the root decision question appears — fails while bug exists."""
output = context.tdd968_result.output
assert context.tdd968_root_question in output, (
f"Expected root question '{context.tdd968_root_question}' "
f"in output, got: {output}"
)