Files
roboco/tests/e2e_smoke/test_sequence_claim_gate.py
T

145 lines
4.9 KiB
Python
Raw Normal View History

"""Scenario: the sibling-sequence claim gate holds independent of edges.
CEO directive: sequence is the bar, full stop. A same-parent sibling with a
strictly lower sequence must hold the later sibling's claim even when NO
dependency edge was ever wired between them — the live bug this guardrails:
a PM delegated a batch of revision subtasks by sequence alone (0..3, no
depends_on edges), and a later sequence claimed in parallel with an earlier,
still-open one.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from tests.e2e_smoke.arcs import (
origin_branch,
seed_company,
seed_project,
seed_task,
set_branch_name,
task_state,
)
from tests.e2e_smoke.harness import ScriptedAgent, expect_error
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
from tests.e2e_smoke.harness import E2EStack
# Pydantic's IWillPlanRequest.approach enforces >= 150 chars, and the PM
# sub_tasks gate requires a real (>= 60 char) description, so both blocked
# and unblocked calls need compliant payloads — the sequence gate must be
# the thing that fires, not an earlier content gate.
_APPROACH = (
"Plan revision 1 once revision 0 reaches a terminal state. This batch "
"wires no dependency edges between its siblings, only sequence, so the "
"claim gate alone must hold the delegation order end to end."
)
_SUB_TASKS = [
{
"title": "Land revision 1",
"description": (
"Apply the second sequenced revision once revision 0 is terminal "
"and open its leaf PR against the cell branch."
),
}
]
def _cancel(stack: E2EStack, task_id: Any) -> None:
"""Direct terminal-status write — mirrors dispatcher_assign/set_branch_name's
style (arcs.py): stands in for the real dev->QA->doc->PM chain reaching a
terminal state, out of scope for this gate-only scenario."""
from roboco.db.tables import TaskTable
from roboco.models.base import TaskStatus
from sqlalchemy import select
async def _run(session: AsyncSession) -> None:
row = (
await session.execute(select(TaskTable).where(TaskTable.id == task_id))
).scalar_one()
row.status = TaskStatus.CANCELLED
stack.run_db(_run)
def test_sibling_sequence_blocks_claim_until_earlier_sibling_terminal(
e2e_stack: E2EStack,
) -> None:
from roboco.models import Team
from roboco.models.base import TaskType
stack = e2e_stack
company = seed_company(stack)
project_id, _project_slug = seed_project(stack, company)
main_pm = ScriptedAgent(stack, company.main_pm_id, "main-pm", "main_pm")
parent_id = seed_task(
stack,
title="Revision batch",
description="Coordinates a sequenced batch of revision subtasks.",
acceptance_criteria=["every revision lands"],
task_type=TaskType.PLANNING,
team=Team.MAIN_PM,
project_id=project_id,
created_by=company.main_pm_id,
assigned_to=company.main_pm_id,
)
seq0_id = seed_task(
stack,
title="Revision 0",
description="First sequenced revision subtask.",
acceptance_criteria=["revision 0 lands"],
task_type=TaskType.PLANNING,
team=Team.MAIN_PM,
project_id=project_id,
parent_task_id=parent_id,
sequence=0,
created_by=company.main_pm_id,
assigned_to=company.main_pm_id,
)
seq1_id = seed_task(
stack,
title="Revision 1",
description="Second sequenced revision subtask.",
acceptance_criteria=["revision 1 lands"],
task_type=TaskType.PLANNING,
team=Team.MAIN_PM,
project_id=project_id,
parent_task_id=parent_id,
sequence=1,
created_by=company.main_pm_id,
assigned_to=company.main_pm_id,
)
branch = f"feature/main_pm/{str(seq1_id)[:8]}"
origin_branch(stack, branch, start="master")
set_branch_name(stack, seq1_id, branch)
# No wire_dependency() call anywhere in this test — sequence alone must
# hold the order; seq0 stays PENDING (open, non-terminal).
env = expect_error(
main_pm.flow(
"i_will_plan",
task_id=str(seq1_id),
plan="Land revision 1 once revision 0 is terminal.",
approach=_APPROACH,
sub_tasks=_SUB_TASKS,
),
"sequence_held",
"main_pm i_will_plan seq-1 while seq-0 open (no dependency edge)",
)
assert "Revision 0" in (env.get("message") or "")
assert task_state(stack, seq1_id)["status"] == "pending"
_cancel(stack, seq0_id)
env = main_pm.flow(
"i_will_plan",
task_id=str(seq1_id),
plan="Land revision 1 now that revision 0 is terminal.",
approach=_APPROACH,
sub_tasks=_SUB_TASKS,
)
assert env.get("error") != "invalid_state", env
assert task_state(stack, seq1_id)["status"] == "in_progress", env