Files
temp/features/steps/tdd_subplan_spawn_orchestration_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

247 lines
9.1 KiB
Python

"""Step definitions for TDD Issue #823 — subplan spawn orchestration.
These steps exercise ``SubplanService.spawn()`` and verify that it creates
real child ``Plan`` domain objects and triggers lifecycle progression.
On ``master`` (before the fix), ``spawn()`` only creates ``SubplanStatus``
records and ``SpawnMetadata`` without creating actual child ``Plan`` objects
or triggering the strategize phase. The assertions in these steps will
**fail** until the bug is fixed, proving the bug exists.
"""
from __future__ import annotations
from unittest.mock import MagicMock
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.subplan_service import (
SpawnEntry,
SpawnResult,
SubplanService,
)
from cleveragents.domain.models.core.decision import (
ContextSnapshot,
Decision,
DecisionType,
)
from cleveragents.domain.models.core.plan import (
ExecutionMode,
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
ProcessingState,
SubplanConfig,
)
_PARENT_PLAN_ID: str = "01HGZ6FE0AQDYTR4BXVQZ6PN00"
_ROOT_PLAN_ID: str = "01HGZ6FE0AQDYTR4BXVQZ6RF00"
_DECISION_ID_1: str = "01HGZ6FE0AQDYTR4BXVQZ6DA00"
_DECISION_ID_2: str = "01HGZ6FE0AQDYTR4BXVQZ6DB00"
def _mock_decision_service() -> MagicMock:
"""Create a mock DecisionService for SubplanService construction."""
svc: MagicMock = MagicMock()
svc.list_by_type = MagicMock(return_value=[])
return svc
def _make_parent_plan() -> Plan:
"""Create a parent plan suitable for subplan spawning."""
return Plan(
identity=PlanIdentity(
plan_id=_PARENT_PLAN_ID,
root_plan_id=_ROOT_PLAN_ID,
),
namespaced_name=NamespacedName(namespace="local", name="parent-plan"),
description="Parent plan for subplan spawn orchestration test",
action_name="local/parent-action",
phase=PlanPhase.STRATEGIZE,
processing_state=ProcessingState.PROCESSING,
subplan_config=SubplanConfig(
execution_mode=ExecutionMode.SEQUENTIAL,
),
)
def _make_spawn_entries() -> list[SpawnEntry]:
"""Create spawn entries for testing."""
dec1: Decision = Decision(
decision_id=_DECISION_ID_1,
plan_id=_PARENT_PLAN_ID,
decision_type=DecisionType.SUBPLAN_SPAWN,
sequence_number=0,
question="Spawn child plan for module A?",
chosen_option="local/sub-action-a",
context_snapshot=ContextSnapshot(),
)
dec2: Decision = Decision(
decision_id=_DECISION_ID_2,
plan_id=_PARENT_PLAN_ID,
decision_type=DecisionType.SUBPLAN_SPAWN,
sequence_number=1,
question="Spawn child plan for module B?",
chosen_option="local/sub-action-b",
context_snapshot=ContextSnapshot(),
)
return [
SpawnEntry(decision=dec1, action_name="local/sub-action-a"),
SpawnEntry(decision=dec2, action_name="local/sub-action-b"),
]
@given("a parent plan configured for subplan spawning")
def step_parent_plan(context: Context) -> None:
"""Set up a parent plan with subplan configuration."""
context.parent_plan = _make_parent_plan()
context.subplan_service = SubplanService(
decision_service=_mock_decision_service(),
)
context.subplan_config = SubplanConfig(
execution_mode=ExecutionMode.SEQUENTIAL,
)
@given("valid spawn entries for the parent plan")
def step_spawn_entries(context: Context) -> None:
"""Create valid spawn entries derived from decisions."""
context.spawn_entries = _make_spawn_entries()
@when("I spawn subplans via SubplanService")
def step_spawn_subplans(context: Context) -> None:
"""Call SubplanService.spawn() with the prepared inputs."""
service: SubplanService = context.subplan_service
parent_plan: Plan = context.parent_plan
config: SubplanConfig = context.subplan_config
entries: list[SpawnEntry] = context.spawn_entries
result: SpawnResult = service.spawn(
parent_plan=parent_plan,
config=config,
spawn_entries=entries,
)
context.spawn_result = result
@then("the spawn result should contain child Plan domain objects")
def step_result_has_child_plans(context: Context) -> None:
"""Assert that SpawnResult includes actual child Plan objects.
Bug #823: SpawnResult only contains SubplanStatus metadata, not
real Plan domain objects. This assertion will fail until the bug
is fixed.
"""
result: SpawnResult = context.spawn_result
# The spawn result should have a way to access child Plan objects.
# Currently SpawnResult only has spawned_statuses (SubplanStatus) and
# metadata (SpawnMetadata) — no child Plan objects are created.
child_plans: object = getattr(result, "child_plans", None)
assert child_plans is not None, (
"SpawnResult does not contain child_plans attribute — "
"spawn() only creates metadata without actual child Plan objects "
"(bug #823)"
)
assert isinstance(child_plans, list), (
"SpawnResult.child_plans should be a list of Plan objects"
)
assert len(child_plans) == len(context.spawn_entries), (
f"Expected {len(context.spawn_entries)} child plans, got {len(child_plans)}"
)
@then("each child Plan should have parent_plan_id set to the parent")
def step_child_plans_have_parent_id(context: Context) -> None:
"""Assert child Plan objects reference the parent plan.
Bug #823: No child Plan objects are created, so this will fail.
"""
result: SpawnResult = context.spawn_result
child_plans: list[Plan] | None = getattr(result, "child_plans", None)
assert child_plans is not None, "No child_plans on SpawnResult (bug #823)"
for child in child_plans:
assert isinstance(child, Plan), (
f"Expected Plan instance, got {type(child).__name__}"
)
assert child.identity.parent_plan_id == _PARENT_PLAN_ID, (
f"Child plan parent_plan_id={child.identity.parent_plan_id!r}, "
f"expected {_PARENT_PLAN_ID!r}"
)
@then("each child plan should be in the strategize phase")
def step_child_plans_strategize(context: Context) -> None:
"""Assert child plans enter the strategize phase after spawn.
Bug #823: No child Plan objects exist to check phase on.
"""
result: SpawnResult = context.spawn_result
child_plans: list[Plan] | None = getattr(result, "child_plans", None)
assert child_plans is not None, (
"No child_plans on SpawnResult — spawn() does not create "
"child Plan objects or trigger lifecycle (bug #823)"
)
for child in child_plans:
assert child.phase == PlanPhase.STRATEGIZE, (
f"Child plan phase={child.phase!r}, expected STRATEGIZE"
)
@then("each child plan processing state should be queued")
def step_child_plans_queued(context: Context) -> None:
"""Assert child plans start in QUEUED processing state.
Bug #823: No child Plan objects exist.
"""
result: SpawnResult = context.spawn_result
child_plans: list[Plan] | None = getattr(result, "child_plans", None)
assert child_plans is not None, "No child_plans on SpawnResult (bug #823)"
for child in child_plans:
assert child.processing_state == ProcessingState.QUEUED, (
f"Child plan state={child.processing_state!r}, expected QUEUED"
)
@then("the parent plan subplan_statuses should reflect child lifecycle")
def step_parent_tracks_lifecycle(context: Context) -> None:
"""Assert parent plan tracks child plan lifecycle progression.
Bug #823: spawn() creates SubplanStatus records with status=QUEUED
but does not update them as child plans progress. Even the basic
expectation that SubplanStatus entries are attached to the parent
plan (not just returned in SpawnResult) is not met.
"""
parent: Plan = context.parent_plan
result: SpawnResult = context.spawn_result
# After spawn, the parent plan's subplan_statuses should be updated
# to include the newly spawned child plans.
assert len(parent.subplan_statuses) == len(context.spawn_entries), (
f"Parent plan has {len(parent.subplan_statuses)} subplan_statuses "
f"but {len(context.spawn_entries)} were spawned — spawn() does not "
f"attach statuses to the parent plan (bug #823). "
f"Statuses are only returned in SpawnResult.spawned_statuses "
f"({result.total_spawned} entries)."
)
@then("the spawn result total_spawned should match child plan count")
def step_total_spawned_matches(context: Context) -> None:
"""Assert total_spawned reflects actual child Plan objects.
Bug #823: total_spawned counts SubplanStatus records, not real
child Plan objects. This step verifies the child_plans list exists
and its length matches total_spawned.
"""
result: SpawnResult = context.spawn_result
child_plans: list[Plan] | None = getattr(result, "child_plans", None)
assert child_plans is not None, (
"No child_plans on SpawnResult — cannot verify count (bug #823)"
)
assert len(child_plans) == result.total_spawned, (
f"child_plans count ({len(child_plans)}) does not match "
f"total_spawned ({result.total_spawned})"
)