mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(board): materialize program items as Main-PM roots, make reports actionable (#711)
Two coupled gaps in the Board Program output path. Approved items were created unowned and in BACKLOG. Nothing dispatches BACKLOG, and once activated a cell PM claimed the parentless task as a root, where _cell_pm_complete resolves its merge target through resolve_parent_branch — which for a parentless task falls through to the project head rung. The result was a cell branch merging straight into the trunk, bypassing the Main-PM root, the root->master PR and the CEO gate (live: PRs #703 and #704 both targeted slave directly). All eight materializers now create a PENDING, main-pm-assigned root with team=Team.MAIN_PM, matching what approve_and_start does for an intake draft. The team is load-bearing, not cosmetic: _next_hint_pr_fail, _deliver_pr_fail_to_owner, delegate's wave-chain dispatch and the PR layer label all key on it, and a cell-teamed root drops the 'do NOT re-submit the root' steer that exists because of PR #138's infinite pr_fail loop. The item's own cell survives as a delegation hint in the description, which is what the Main PM's briefing renders. Periscope, Sentinel and Coroner produced artifacts with no way to act on them — three panel surfaces carried explicit 'no approve/reject UI' comments while each item already held a machine-readable suggested action. They now have per-item approve and dismiss, modelled on the roadmap queue: idempotent per item, CEO-gated, deep-copy-before-mutate so SQLAlchemy's dirty check still fires, and every decision recorded through record_decision so it reaches the next cycle's prompt. Approving materializes through the same corrected Main-PM-owned path. Target project resolves to each engine's own existing anchor — RoboCo's project for Periscope and Sentinel, the incident's project for Coroner — and fails with a clean invalid_state naming what is unresolvable rather than guessing at a repo. Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -25,6 +25,7 @@ from sqlalchemy import update
|
||||
_SEED_GIT_URL = "https://example.com/roboco-api-dogfood.git"
|
||||
|
||||
CEO_UUID = _foundation.AGENTS["ceo"].uuid
|
||||
MAIN_PM_UUID = _foundation.AGENTS["main-pm"].uuid
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
@@ -74,10 +75,40 @@ async def _seed_ceo(session: AsyncSession) -> None:
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def _seed_main_pm(session: AsyncSession) -> None:
|
||||
"""The Main PM row matching ``MAIN_PM_UUID`` — approving an item now
|
||||
assigns this id to the materialized task (an FK to ``agents``). The
|
||||
slug must be the exact ``"main-pm"`` (not a randomized suffix like the
|
||||
other seed helpers here use): ``TaskService.approve_and_start`` and
|
||||
other call sites resolve the Main PM by an EXACT slug lookup, and this
|
||||
row's id is the fixed, cross-test-shared foundation UUID — a wrong slug
|
||||
here would permanently squat that id with an unresolvable row for every
|
||||
other test in the shared suite run."""
|
||||
if await session.get(AgentTable, MAIN_PM_UUID) is not None:
|
||||
return
|
||||
session.add(
|
||||
AgentTable(
|
||||
id=MAIN_PM_UUID,
|
||||
name="main-pm",
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=Team.MAIN_PM,
|
||||
status=AgentStatus.ACTIVE,
|
||||
model_config={},
|
||||
system_prompt="x",
|
||||
capabilities=[],
|
||||
permissions={},
|
||||
metrics={},
|
||||
)
|
||||
)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def _seed_cycle(session: AsyncSession) -> tuple[TaskTable, ProjectTable]:
|
||||
system = await _seed_agent(session, AgentRole.SYSTEM, "system")
|
||||
po = await _seed_agent(session, AgentRole.PRODUCT_OWNER, "product-owner")
|
||||
await _seed_ceo(session)
|
||||
await _seed_main_pm(session)
|
||||
project = ProjectTable(
|
||||
id=uuid4(),
|
||||
name="RoboCo API",
|
||||
@@ -192,9 +223,10 @@ async def test_list_cycles_returns_authored_cycle(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approve_item_materializes_backlog_task(
|
||||
async def test_approve_item_materializes_main_pm_owned_task(
|
||||
db_session: AsyncSession, ceo_client: AsyncClient
|
||||
) -> None:
|
||||
"""Defect fix: see test_roadmap_routes.py's identical assertion update."""
|
||||
task, _project = await _seed_cycle(db_session)
|
||||
resp = await ceo_client.post(f"/api/dogfood/cycles/{task.id}/items/item-0/approve")
|
||||
assert resp.status_code == HTTPStatus.OK
|
||||
@@ -204,7 +236,9 @@ async def test_approve_item_materializes_backlog_task(
|
||||
|
||||
materialized = await db_session.get(TaskTable, UUID(body["materialized_task_id"]))
|
||||
assert materialized is not None
|
||||
assert materialized.status == TaskStatus.BACKLOG
|
||||
assert materialized.status == TaskStatus.PENDING
|
||||
assert materialized.assigned_to == MAIN_PM_UUID
|
||||
assert materialized.parent_task_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -35,6 +35,7 @@ from roboco.models.base import (
|
||||
TaskType,
|
||||
Team,
|
||||
)
|
||||
from roboco.seeds.initial_data import AGENT_UUIDS
|
||||
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
|
||||
from roboco.services.task import TaskService
|
||||
|
||||
@@ -825,20 +826,28 @@ async def test_pm_escalate_to_ceo_path(
|
||||
project = lifecycle_setup["project"]
|
||||
system_agent = lifecycle_setup["system_agent"]
|
||||
|
||||
main_pm_agent = AgentTable(
|
||||
id=uuid4(),
|
||||
name="Main PM",
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=None,
|
||||
status=AgentStatus.ACTIVE,
|
||||
model_config={},
|
||||
system_prompt="main_pm",
|
||||
capabilities=["coord"],
|
||||
permissions={},
|
||||
metrics={},
|
||||
)
|
||||
db_session.add(main_pm_agent)
|
||||
# Keyed on the fixed foundation UUID + an existence check (mirrors
|
||||
# test_task_service_transitions.py's identical seeding pattern): the
|
||||
# cross-test-shared DB already has other tests seeding this exact
|
||||
# "main-pm" slug, and an unconditional insert with a fresh random id
|
||||
# would collide on the slug's unique index.
|
||||
main_pm_id = UUID(AGENT_UUIDS["main-pm"])
|
||||
if await db_session.get(AgentTable, main_pm_id) is None:
|
||||
db_session.add(
|
||||
AgentTable(
|
||||
id=main_pm_id,
|
||||
name="Main PM",
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=None,
|
||||
status=AgentStatus.ACTIVE,
|
||||
model_config={},
|
||||
system_prompt="main_pm",
|
||||
capabilities=["coord"],
|
||||
permissions={},
|
||||
metrics={},
|
||||
)
|
||||
)
|
||||
await db_session.flush()
|
||||
del project, system_agent # only needed for fixture wiring above.
|
||||
|
||||
@@ -849,7 +858,7 @@ async def test_pm_escalate_to_ceo_path(
|
||||
task.qa_verified = True
|
||||
task.docs_complete = True
|
||||
task.parent_task_id = None # explicit — escalate_to_ceo refuses subtasks.
|
||||
task.assigned_to = main_pm_agent.id
|
||||
task.assigned_to = main_pm_id
|
||||
task.commits = [
|
||||
{"sha": uuid4().hex[:40], "message": "feat: /healthz", "task_id": str(task.id)}
|
||||
]
|
||||
@@ -862,7 +871,7 @@ async def test_pm_escalate_to_ceo_path(
|
||||
# cast for mypy under the project's strict config — the values are
|
||||
# already real ``uuid.UUID`` at runtime.
|
||||
env = await c.complete(
|
||||
UUID(str(main_pm_agent.id)),
|
||||
main_pm_id,
|
||||
UUID(str(task.id)),
|
||||
notes="Root task ready for CEO approval — escalating.",
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ from sqlalchemy import update
|
||||
_SEED_GIT_URL = "https://example.com/backend-svc-mirror.git"
|
||||
|
||||
CEO_UUID = _foundation.AGENTS["ceo"].uuid
|
||||
MAIN_PM_UUID = _foundation.AGENTS["main-pm"].uuid
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
@@ -74,10 +75,40 @@ async def _seed_ceo(session: AsyncSession) -> None:
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def _seed_main_pm(session: AsyncSession) -> None:
|
||||
"""The Main PM row matching ``MAIN_PM_UUID`` — approving an item now
|
||||
assigns this id to the materialized task (an FK to ``agents``). The
|
||||
slug must be the exact ``"main-pm"`` (not a randomized suffix like the
|
||||
other seed helpers here use): ``TaskService.approve_and_start`` and
|
||||
other call sites resolve the Main PM by an EXACT slug lookup, and this
|
||||
row's id is the fixed, cross-test-shared foundation UUID — a wrong slug
|
||||
here would permanently squat that id with an unresolvable row for every
|
||||
other test in the shared suite run."""
|
||||
if await session.get(AgentTable, MAIN_PM_UUID) is not None:
|
||||
return
|
||||
session.add(
|
||||
AgentTable(
|
||||
id=MAIN_PM_UUID,
|
||||
name="main-pm",
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=Team.MAIN_PM,
|
||||
status=AgentStatus.ACTIVE,
|
||||
model_config={},
|
||||
system_prompt="x",
|
||||
capabilities=[],
|
||||
permissions={},
|
||||
metrics={},
|
||||
)
|
||||
)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def _seed_cycle(session: AsyncSession) -> tuple[TaskTable, ProjectTable]:
|
||||
system = await _seed_agent(session, AgentRole.SYSTEM, "system")
|
||||
hom = await _seed_agent(session, AgentRole.HEAD_MARKETING, "head-marketing")
|
||||
await _seed_ceo(session)
|
||||
await _seed_main_pm(session)
|
||||
project = ProjectTable(
|
||||
id=uuid4(),
|
||||
name="Backend Service",
|
||||
@@ -192,9 +223,12 @@ async def test_list_cycles_returns_authored_cycle(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approve_item_materializes_backlog_task(
|
||||
async def test_approve_item_materializes_main_pm_owned_task(
|
||||
db_session: AsyncSession, ceo_client: AsyncClient
|
||||
) -> None:
|
||||
"""Defect fix: see test_roadmap_routes.py's identical assertion update.
|
||||
Documentation type is untouched by the main-pm code->planning coercion
|
||||
(that only retypes ``code``, never ``documentation``)."""
|
||||
task, _project = await _seed_cycle(db_session)
|
||||
resp = await ceo_client.post(f"/api/mirror/cycles/{task.id}/items/item-0/approve")
|
||||
assert resp.status_code == HTTPStatus.OK
|
||||
@@ -204,7 +238,9 @@ async def test_approve_item_materializes_backlog_task(
|
||||
|
||||
materialized = await db_session.get(TaskTable, UUID(body["materialized_task_id"]))
|
||||
assert materialized is not None
|
||||
assert materialized.status == TaskStatus.BACKLOG
|
||||
assert materialized.status == TaskStatus.PENDING
|
||||
assert materialized.assigned_to == MAIN_PM_UUID
|
||||
assert materialized.parent_task_id is None
|
||||
assert materialized.task_type == TaskType.DOCUMENTATION
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from sqlalchemy import update
|
||||
_SEED_GIT_URL = "https://example.com/backend-svc.git"
|
||||
|
||||
CEO_UUID = _foundation.AGENTS["ceo"].uuid
|
||||
MAIN_PM_UUID = _foundation.AGENTS["main-pm"].uuid
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
@@ -74,10 +75,40 @@ async def _seed_ceo(session: AsyncSession) -> None:
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def _seed_main_pm(session: AsyncSession) -> None:
|
||||
"""The Main PM row matching ``MAIN_PM_UUID`` — approving an item now
|
||||
assigns this id to the materialized task (an FK to ``agents``). The
|
||||
slug must be the exact ``"main-pm"`` (not a randomized suffix like the
|
||||
other seed helpers here use): ``TaskService.approve_and_start`` and
|
||||
other call sites resolve the Main PM by an EXACT slug lookup, and this
|
||||
row's id is the fixed, cross-test-shared foundation UUID — a wrong slug
|
||||
here would permanently squat that id with an unresolvable row for every
|
||||
other test in the shared suite run."""
|
||||
if await session.get(AgentTable, MAIN_PM_UUID) is not None:
|
||||
return
|
||||
session.add(
|
||||
AgentTable(
|
||||
id=MAIN_PM_UUID,
|
||||
name="main-pm",
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=Team.MAIN_PM,
|
||||
status=AgentStatus.ACTIVE,
|
||||
model_config={},
|
||||
system_prompt="x",
|
||||
capabilities=[],
|
||||
permissions={},
|
||||
metrics={},
|
||||
)
|
||||
)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def _seed_cycle(session: AsyncSession) -> tuple[TaskTable, ProjectTable]:
|
||||
system = await _seed_agent(session, AgentRole.SYSTEM, "system")
|
||||
po = await _seed_agent(session, AgentRole.PRODUCT_OWNER, "product-owner")
|
||||
await _seed_ceo(session)
|
||||
await _seed_main_pm(session)
|
||||
project = ProjectTable(
|
||||
id=uuid4(),
|
||||
name="Backend Service",
|
||||
@@ -190,9 +221,10 @@ async def test_list_cycles_returns_authored_cycle(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approve_item_materializes_backlog_task(
|
||||
async def test_approve_item_materializes_main_pm_owned_task(
|
||||
db_session: AsyncSession, ceo_client: AsyncClient
|
||||
) -> None:
|
||||
"""Defect fix: see test_roadmap_routes.py's identical assertion update."""
|
||||
task, _project = await _seed_cycle(db_session)
|
||||
resp = await ceo_client.post(
|
||||
f"/api/pest-control/cycles/{task.id}/items/item-0/approve"
|
||||
@@ -204,7 +236,9 @@ async def test_approve_item_materializes_backlog_task(
|
||||
|
||||
materialized = await db_session.get(TaskTable, UUID(body["materialized_task_id"]))
|
||||
assert materialized is not None
|
||||
assert materialized.status == TaskStatus.BACKLOG
|
||||
assert materialized.status == TaskStatus.PENDING
|
||||
assert materialized.assigned_to == MAIN_PM_UUID
|
||||
assert materialized.parent_task_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -2,13 +2,14 @@ from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from uuid import uuid4
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from roboco.db.tables import AgentTable, ProjectTable, TaskTable
|
||||
from roboco.models import AgentRole, AgentStatus, Team
|
||||
from roboco.models.base import TaskNature, TaskStatus, TaskType
|
||||
from roboco.seeds.initial_data import AGENT_UUIDS
|
||||
from roboco.services.base import NotFoundError
|
||||
from roboco.services.prompter import (
|
||||
PrompterService,
|
||||
@@ -138,9 +139,28 @@ async def redraft_setup(db_session: AsyncSession) -> AsyncIterator[dict]:
|
||||
metrics={},
|
||||
)
|
||||
|
||||
main_pm = _agent("main-pm", AgentRole.MAIN_PM)
|
||||
# merge() with the fixed AGENT_UUIDS id: idempotent whether or not another
|
||||
# test already committed this exact "main-pm"-slugged row on the shared
|
||||
# session-scoped test DB (mirrors test_prompter.py's identical upsert) —
|
||||
# an unconditional insert with a fresh random id here would collide with
|
||||
# any other test's row on the slug's unique index.
|
||||
main_pm = await db_session.merge(
|
||||
AgentTable(
|
||||
id=UUID(AGENT_UUIDS["main-pm"]),
|
||||
name="main-pm",
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=None,
|
||||
status=AgentStatus.ACTIVE,
|
||||
model_config={},
|
||||
system_prompt="x",
|
||||
capabilities=[],
|
||||
permissions={},
|
||||
metrics={},
|
||||
)
|
||||
)
|
||||
po = _agent(f"product-owner-{uuid4().hex[:4]}", AgentRole.PRODUCT_OWNER)
|
||||
db_session.add_all([main_pm, po])
|
||||
db_session.add(po)
|
||||
await db_session.flush()
|
||||
project = ProjectTable(
|
||||
id=uuid4(),
|
||||
|
||||
@@ -21,6 +21,7 @@ from roboco.models.permissions import AgentContext
|
||||
from roboco.services.task import ROADMAP_SOURCE
|
||||
|
||||
CEO_UUID = _foundation.AGENTS["ceo"].uuid
|
||||
MAIN_PM_UUID = _foundation.AGENTS["main-pm"].uuid
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
@@ -70,10 +71,40 @@ async def _seed_ceo(session: AsyncSession) -> None:
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def _seed_main_pm(session: AsyncSession) -> None:
|
||||
"""The Main PM row matching ``MAIN_PM_UUID`` — approving an item now
|
||||
assigns this id to the materialized task (an FK to ``agents``). The
|
||||
slug must be the exact ``"main-pm"`` (not a randomized suffix like the
|
||||
other seed helpers here use): ``TaskService.approve_and_start`` and
|
||||
other call sites resolve the Main PM by an EXACT slug lookup, and this
|
||||
row's id is the fixed, cross-test-shared foundation UUID — a wrong slug
|
||||
here would permanently squat that id with an unresolvable row for every
|
||||
other test in the shared suite run."""
|
||||
if await session.get(AgentTable, MAIN_PM_UUID) is not None:
|
||||
return
|
||||
session.add(
|
||||
AgentTable(
|
||||
id=MAIN_PM_UUID,
|
||||
name="main-pm",
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=Team.MAIN_PM,
|
||||
status=AgentStatus.ACTIVE,
|
||||
model_config={},
|
||||
system_prompt="x",
|
||||
capabilities=[],
|
||||
permissions={},
|
||||
metrics={},
|
||||
)
|
||||
)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def _seed_cycle(session: AsyncSession) -> tuple[TaskTable, ProjectTable]:
|
||||
system = await _seed_agent(session, AgentRole.SYSTEM, "system")
|
||||
po = await _seed_agent(session, AgentRole.PRODUCT_OWNER, "product-owner")
|
||||
await _seed_ceo(session)
|
||||
await _seed_main_pm(session)
|
||||
project = ProjectTable(
|
||||
id=uuid4(),
|
||||
name="Backend Service",
|
||||
@@ -170,9 +201,13 @@ async def test_list_cycles_returns_authored_cycle(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approve_item_materializes_backlog_task(
|
||||
async def test_approve_item_materializes_main_pm_owned_task(
|
||||
db_session: AsyncSession, ceo_client: AsyncClient
|
||||
) -> None:
|
||||
"""Defect fix (#703/#704): approval used to materialize an unowned
|
||||
BACKLOG task; see test_roadmap_service.py's identical assertion update
|
||||
for the full rationale. It now materializes PENDING + assigned_to=
|
||||
main-pm."""
|
||||
task, _project = await _seed_cycle(db_session)
|
||||
resp = await ceo_client.post(f"/api/roadmap/cycles/{task.id}/items/item-0/approve")
|
||||
assert resp.status_code == HTTPStatus.OK
|
||||
@@ -182,7 +217,9 @@ async def test_approve_item_materializes_backlog_task(
|
||||
|
||||
materialized = await db_session.get(TaskTable, UUID(body["materialized_task_id"]))
|
||||
assert materialized is not None
|
||||
assert materialized.status == TaskStatus.BACKLOG
|
||||
assert materialized.status == TaskStatus.PENDING
|
||||
assert materialized.assigned_to == MAIN_PM_UUID
|
||||
assert materialized.parent_task_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -25,6 +25,7 @@ from sqlalchemy import update
|
||||
_SEED_GIT_URL = "https://example.com/backend-svc-spackle.git"
|
||||
|
||||
CEO_UUID = _foundation.AGENTS["ceo"].uuid
|
||||
MAIN_PM_UUID = _foundation.AGENTS["main-pm"].uuid
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
@@ -74,10 +75,40 @@ async def _seed_ceo(session: AsyncSession) -> None:
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def _seed_main_pm(session: AsyncSession) -> None:
|
||||
"""The Main PM row matching ``MAIN_PM_UUID`` — approving an item now
|
||||
assigns this id to the materialized task (an FK to ``agents``). The
|
||||
slug must be the exact ``"main-pm"`` (not a randomized suffix like the
|
||||
other seed helpers here use): ``TaskService.approve_and_start`` and
|
||||
other call sites resolve the Main PM by an EXACT slug lookup, and this
|
||||
row's id is the fixed, cross-test-shared foundation UUID — a wrong slug
|
||||
here would permanently squat that id with an unresolvable row for every
|
||||
other test in the shared suite run."""
|
||||
if await session.get(AgentTable, MAIN_PM_UUID) is not None:
|
||||
return
|
||||
session.add(
|
||||
AgentTable(
|
||||
id=MAIN_PM_UUID,
|
||||
name="main-pm",
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=Team.MAIN_PM,
|
||||
status=AgentStatus.ACTIVE,
|
||||
model_config={},
|
||||
system_prompt="x",
|
||||
capabilities=[],
|
||||
permissions={},
|
||||
metrics={},
|
||||
)
|
||||
)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def _seed_cycle(session: AsyncSession) -> tuple[TaskTable, ProjectTable]:
|
||||
system = await _seed_agent(session, AgentRole.SYSTEM, "system")
|
||||
po = await _seed_agent(session, AgentRole.PRODUCT_OWNER, "product-owner")
|
||||
await _seed_ceo(session)
|
||||
await _seed_main_pm(session)
|
||||
project = ProjectTable(
|
||||
id=uuid4(),
|
||||
name="Backend Service",
|
||||
@@ -192,9 +223,10 @@ async def test_list_cycles_returns_authored_cycle(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approve_item_materializes_backlog_task(
|
||||
async def test_approve_item_materializes_main_pm_owned_task(
|
||||
db_session: AsyncSession, ceo_client: AsyncClient
|
||||
) -> None:
|
||||
"""Defect fix: see test_roadmap_routes.py's identical assertion update."""
|
||||
task, _project = await _seed_cycle(db_session)
|
||||
resp = await ceo_client.post(f"/api/spackle/cycles/{task.id}/items/item-0/approve")
|
||||
assert resp.status_code == HTTPStatus.OK
|
||||
@@ -204,7 +236,9 @@ async def test_approve_item_materializes_backlog_task(
|
||||
|
||||
materialized = await db_session.get(TaskTable, UUID(body["materialized_task_id"]))
|
||||
assert materialized is not None
|
||||
assert materialized.status == TaskStatus.BACKLOG
|
||||
assert materialized.status == TaskStatus.PENDING
|
||||
assert materialized.assigned_to == MAIN_PM_UUID
|
||||
assert materialized.parent_task_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -2048,21 +2048,29 @@ async def test_activate_batch_root_subtasks_retypes_code_to_planning(
|
||||
when flipping team to main_pm, mirroring approve_and_start, or the
|
||||
main_pm+code combo recurs."""
|
||||
svc = task_setup["svc"]
|
||||
# approve_and_start resolves the main-pm agent by slug — seed it.
|
||||
main_pm = AgentTable(
|
||||
id=uuid4(),
|
||||
name="Main PM",
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=Team.MAIN_PM,
|
||||
status=AgentStatus.ACTIVE,
|
||||
model_config={},
|
||||
system_prompt="main-pm",
|
||||
capabilities=[],
|
||||
permissions={},
|
||||
metrics={},
|
||||
)
|
||||
db_session.add(main_pm)
|
||||
# approve_and_start resolves the main-pm agent by slug — seed it. Keyed
|
||||
# on the fixed foundation UUID + an existence check (mirrors
|
||||
# test_ceo_reject_routes_coordination_task_to_main_pm above): the
|
||||
# cross-test-shared DB already has other tests seeding this exact
|
||||
# "main-pm" slug, and a second unconditional insert with a fresh random
|
||||
# id would collide on the slug's unique index.
|
||||
main_pm_id = UUID(AGENT_UUIDS["main-pm"])
|
||||
if await db_session.get(AgentTable, main_pm_id) is None:
|
||||
db_session.add(
|
||||
AgentTable(
|
||||
id=main_pm_id,
|
||||
name="Main PM",
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=Team.MAIN_PM,
|
||||
status=AgentStatus.ACTIVE,
|
||||
model_config={},
|
||||
system_prompt="main-pm",
|
||||
capabilities=[],
|
||||
permissions={},
|
||||
metrics={},
|
||||
)
|
||||
)
|
||||
await db_session.flush()
|
||||
|
||||
batch = uuid4()
|
||||
|
||||
Reference in New Issue
Block a user