test(plan): TDD failing tests for subplan spawn orchestration (bug #823)
CI / lint (pull_request) Successful in 1m2s
CI / quality (pull_request) Successful in 39s
CI / typecheck (pull_request) Successful in 2m21s
CI / benchmark-publish (pull_request) Has been skipped
CI / security (pull_request) Successful in 2m36s
CI / build (pull_request) Successful in 21s
CI / e2e_tests (pull_request) Successful in 2m6s
CI / integration_tests (pull_request) Successful in 3m57s
CI / unit_tests (pull_request) Successful in 4m5s
CI / docker (pull_request) Successful in 58s
CI / coverage (pull_request) Successful in 6m37s
CI / benchmark-regression (pull_request) Successful in 37m33s
CI / lint (pull_request) Successful in 1m2s
CI / quality (pull_request) Successful in 39s
CI / typecheck (pull_request) Successful in 2m21s
CI / benchmark-publish (pull_request) Has been skipped
CI / security (pull_request) Successful in 2m36s
CI / build (pull_request) Successful in 21s
CI / e2e_tests (pull_request) Successful in 2m6s
CI / integration_tests (pull_request) Successful in 3m57s
CI / unit_tests (pull_request) Successful in 4m5s
CI / docker (pull_request) Successful in 58s
CI / coverage (pull_request) Successful in 6m37s
CI / benchmark-regression (pull_request) Successful in 37m33s
Write Behave scenario and Robot Framework test proving that SubplanService.spawn() only creates metadata (SubplanStatus records and SpawnMetadata) without creating real child Plan domain objects or triggering lifecycle progression. Tests are tagged @tdd_expected_fail so CI passes via result inversion. ISSUES CLOSED: #838
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
"""Step definitions for TDD Bug #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})"
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
@tdd_expected_fail @tdd_bug @tdd_bug_823 @mock_only
|
||||
Feature: TDD Bug #823 — subplan spawn creates metadata but does not orchestrate child plan execution
|
||||
As a developer
|
||||
I want to verify that SubplanService.spawn() creates real child Plan
|
||||
domain objects and triggers their lifecycle progression
|
||||
So that the bug is captured and will be caught by a regression test
|
||||
|
||||
SubplanService.spawn() currently creates SubplanStatus records and
|
||||
SpawnMetadata but does NOT create actual child Plan domain objects,
|
||||
trigger the strategize phase on child plans, set up inter-plan
|
||||
communication, merge results, or handle child plan failures. These
|
||||
tests assert the expected behaviour and will FAIL until the bug is
|
||||
fixed. The @tdd_expected_fail tag inverts the result so CI passes.
|
||||
|
||||
Scenario: Spawn result contains child Plan domain objects
|
||||
Given a parent plan configured for subplan spawning
|
||||
And valid spawn entries for the parent plan
|
||||
When I spawn subplans via SubplanService
|
||||
Then the spawn result should contain child Plan domain objects
|
||||
And each child Plan should have parent_plan_id set to the parent
|
||||
|
||||
Scenario: Child plans enter the strategize phase after spawn
|
||||
Given a parent plan configured for subplan spawning
|
||||
And valid spawn entries for the parent plan
|
||||
When I spawn subplans via SubplanService
|
||||
Then each child plan should be in the strategize phase
|
||||
And each child plan processing state should be queued
|
||||
|
||||
Scenario: Parent plan tracks child plan lifecycle status
|
||||
Given a parent plan configured for subplan spawning
|
||||
And valid spawn entries for the parent plan
|
||||
When I spawn subplans via SubplanService
|
||||
Then the parent plan subplan_statuses should reflect child lifecycle
|
||||
And the spawn result total_spawned should match child plan count
|
||||
@@ -0,0 +1,250 @@
|
||||
"""Helper script for tdd_subplan_spawn_orchestration.robot smoke tests.
|
||||
|
||||
Each subcommand exercises SubplanService.spawn() to reproduce bug #823.
|
||||
The helper reports the **real** outcome: it exits 0 and prints the sentinel
|
||||
when the expected behaviour is observed (bug fixed), and exits 1 when the
|
||||
bug is 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
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Ensure local source tree is importable.
|
||||
_ROOT: Path = Path(__file__).resolve().parents[1]
|
||||
_SRC: str = str(_ROOT / "src")
|
||||
_ROBOT: str = str(_ROOT / "robot")
|
||||
for _p in (_SRC, _ROBOT):
|
||||
if _p not in sys.path:
|
||||
sys.path.insert(0, _p)
|
||||
|
||||
from cleveragents.application.services.subplan_service import ( # noqa: E402
|
||||
SpawnEntry,
|
||||
SpawnResult,
|
||||
SubplanService,
|
||||
)
|
||||
from cleveragents.domain.models.core.decision import ( # noqa: E402
|
||||
ContextSnapshot,
|
||||
Decision,
|
||||
DecisionType,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import ( # noqa: E402
|
||||
ExecutionMode,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
ProcessingState,
|
||||
SubplanConfig,
|
||||
)
|
||||
|
||||
_PARENT_PLAN_ID: str = "01HGZ6FE0AQDYTR4BXVQZ6PN00"
|
||||
_ROOT_PLAN_ID: str = "01HGZ6FE0AQDYTR4BXVQZ6RF00"
|
||||
_DEC_ID1: str = "01HGZ6FE0AQDYTR4BXVQZ6DA00"
|
||||
_DEC_ID2: str = "01HGZ6FE0AQDYTR4BXVQZ6DB00"
|
||||
|
||||
|
||||
def _fail(msg: str) -> NoReturn:
|
||||
"""Print error message to stderr and exit with code 1."""
|
||||
print(msg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _mock_decision_service() -> MagicMock:
|
||||
"""Create a mock DecisionService."""
|
||||
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=_DEC_ID1,
|
||||
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=_DEC_ID2,
|
||||
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"),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Subcommands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _spawn_child_plans() -> None:
|
||||
"""Verify spawn() returns actual child Plan domain objects.
|
||||
|
||||
Bug #823: SpawnResult only has spawned_statuses (SubplanStatus) and
|
||||
metadata (SpawnMetadata). No child_plans attribute exists.
|
||||
"""
|
||||
svc: SubplanService = SubplanService(
|
||||
decision_service=_mock_decision_service(),
|
||||
)
|
||||
parent: Plan = _make_parent_plan()
|
||||
config: SubplanConfig = SubplanConfig(
|
||||
execution_mode=ExecutionMode.SEQUENTIAL,
|
||||
)
|
||||
entries: list[SpawnEntry] = _make_spawn_entries()
|
||||
|
||||
result: SpawnResult = svc.spawn(
|
||||
parent_plan=parent,
|
||||
config=config,
|
||||
spawn_entries=entries,
|
||||
)
|
||||
|
||||
# Assert that SpawnResult contains child Plan objects
|
||||
child_plans: object = getattr(result, "child_plans", None)
|
||||
if child_plans is None:
|
||||
_fail(
|
||||
"SpawnResult does not contain child_plans attribute — "
|
||||
"spawn() only creates metadata without actual child Plan "
|
||||
"objects (bug #823)"
|
||||
)
|
||||
if not isinstance(child_plans, list):
|
||||
_fail("SpawnResult.child_plans should be a list of Plan objects")
|
||||
if len(child_plans) != len(entries):
|
||||
_fail(f"Expected {len(entries)} child plans, got {len(child_plans)}")
|
||||
# Verify each child has correct parent_plan_id
|
||||
for child in child_plans:
|
||||
if not isinstance(child, Plan):
|
||||
_fail(f"Expected Plan instance, got {type(child).__name__}")
|
||||
if child.identity.parent_plan_id != _PARENT_PLAN_ID:
|
||||
_fail(
|
||||
f"Child plan parent_plan_id={child.identity.parent_plan_id!r}, "
|
||||
f"expected {_PARENT_PLAN_ID!r}"
|
||||
)
|
||||
print("tdd-spawn-child-plans-ok")
|
||||
|
||||
|
||||
def _spawn_lifecycle() -> None:
|
||||
"""Verify child plans enter the strategize phase after spawn.
|
||||
|
||||
Bug #823: No child Plan objects are created, so lifecycle is not
|
||||
triggered.
|
||||
"""
|
||||
svc: SubplanService = SubplanService(
|
||||
decision_service=_mock_decision_service(),
|
||||
)
|
||||
parent: Plan = _make_parent_plan()
|
||||
config: SubplanConfig = SubplanConfig(
|
||||
execution_mode=ExecutionMode.SEQUENTIAL,
|
||||
)
|
||||
entries: list[SpawnEntry] = _make_spawn_entries()
|
||||
|
||||
result: SpawnResult = svc.spawn(
|
||||
parent_plan=parent,
|
||||
config=config,
|
||||
spawn_entries=entries,
|
||||
)
|
||||
|
||||
# Assert that child plans exist and are in strategize phase
|
||||
child_plans: object = getattr(result, "child_plans", None)
|
||||
if child_plans is None:
|
||||
_fail(
|
||||
"No child_plans on SpawnResult — spawn() does not create "
|
||||
"child Plan objects or trigger lifecycle (bug #823)"
|
||||
)
|
||||
if not isinstance(child_plans, list):
|
||||
_fail("child_plans is not a list")
|
||||
for child in child_plans:
|
||||
if not isinstance(child, Plan):
|
||||
_fail(f"Expected Plan, got {type(child).__name__}")
|
||||
if child.phase != PlanPhase.STRATEGIZE:
|
||||
_fail(f"Child plan phase={child.phase!r}, expected STRATEGIZE")
|
||||
if child.processing_state != ProcessingState.QUEUED:
|
||||
_fail(f"Child plan state={child.processing_state!r}, expected QUEUED")
|
||||
print("tdd-spawn-lifecycle-ok")
|
||||
|
||||
|
||||
def _spawn_parent_tracking() -> None:
|
||||
"""Verify parent plan tracks spawned child plan statuses.
|
||||
|
||||
Bug #823: spawn() creates SubplanStatus records in SpawnResult but
|
||||
does not attach them to the parent plan's subplan_statuses list.
|
||||
"""
|
||||
svc: SubplanService = SubplanService(
|
||||
decision_service=_mock_decision_service(),
|
||||
)
|
||||
parent: Plan = _make_parent_plan()
|
||||
config: SubplanConfig = SubplanConfig(
|
||||
execution_mode=ExecutionMode.SEQUENTIAL,
|
||||
)
|
||||
entries: list[SpawnEntry] = _make_spawn_entries()
|
||||
|
||||
result: SpawnResult = svc.spawn(
|
||||
parent_plan=parent,
|
||||
config=config,
|
||||
spawn_entries=entries,
|
||||
)
|
||||
|
||||
# After spawn(), parent plan's subplan_statuses should be populated
|
||||
if len(parent.subplan_statuses) != len(entries):
|
||||
_fail(
|
||||
f"Parent plan has {len(parent.subplan_statuses)} subplan_statuses "
|
||||
f"but {len(entries)} were spawned — spawn() does not attach "
|
||||
f"statuses to the parent plan (bug #823). "
|
||||
f"Statuses only in SpawnResult.spawned_statuses "
|
||||
f"({result.total_spawned} entries)."
|
||||
)
|
||||
print("tdd-spawn-parent-tracking-ok")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dispatcher
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_COMMANDS: dict[str, Callable[[], None]] = {
|
||||
"spawn-child-plans": _spawn_child_plans,
|
||||
"spawn-lifecycle": _spawn_lifecycle,
|
||||
"spawn-parent-tracking": _spawn_parent_tracking,
|
||||
}
|
||||
|
||||
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: Callable[[], None] = _COMMANDS[sys.argv[1]]
|
||||
cmd()
|
||||
@@ -0,0 +1,44 @@
|
||||
*** Settings ***
|
||||
Documentation TDD Bug #823 — subplan spawn creates metadata but does not orchestrate
|
||||
... child plan execution. Integration smoke tests verifying that
|
||||
... SubplanService.spawn() creates real child Plan domain objects and
|
||||
... triggers lifecycle progression. Currently spawn() only creates
|
||||
... SubplanStatus records and SpawnMetadata without creating child Plans.
|
||||
... Tests are tagged tdd_expected_fail so CI passes via result inversion.
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} ${CURDIR}/helper_tdd_subplan_spawn_orchestration.py
|
||||
|
||||
*** Test Cases ***
|
||||
TDD Spawn Result Contains Child Plan Objects
|
||||
[Documentation] Verify that spawn() returns actual child Plan domain objects,
|
||||
... not just SubplanStatus metadata.
|
||||
[Tags] tdd_expected_fail tdd_bug tdd_bug_823
|
||||
${result}= Run Process ${PYTHON} ${HELPER} spawn-child-plans cwd=${WORKSPACE} timeout=30s
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-spawn-child-plans-ok
|
||||
|
||||
TDD Child Plans Enter Strategize Phase
|
||||
[Documentation] Verify that spawned child plans enter the strategize phase
|
||||
... and have their lifecycle triggered.
|
||||
[Tags] tdd_expected_fail tdd_bug tdd_bug_823
|
||||
${result}= Run Process ${PYTHON} ${HELPER} spawn-lifecycle cwd=${WORKSPACE} timeout=30s
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-spawn-lifecycle-ok
|
||||
|
||||
TDD Parent Plan Tracks Child Status
|
||||
[Documentation] Verify that spawn() attaches SubplanStatus entries to
|
||||
... the parent plan so lifecycle can be tracked.
|
||||
[Tags] tdd_expected_fail tdd_bug tdd_bug_823
|
||||
${result}= Run Process ${PYTHON} ${HELPER} spawn-parent-tracking cwd=${WORKSPACE} timeout=30s
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-spawn-parent-tracking-ok
|
||||
Reference in New Issue
Block a user