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
247 lines
9.1 KiB
Python
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})"
|
|
)
|