mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
test(e2e): feature-spotlight end-to-end scenario (catches the unregistered-verb gap)
Drives the Head of Marketing calling propose_feature_spotlight through the real
do_server -> /api/v1/do route -> ContentActions -> XEngine, then asserts a held
x_feature draft is created (confirmed_by_human=False) and the exploration task
completes. Red-then-green verified: reverting the do_server registration
(3f90d382) makes this fail with the exact live symptom. This is the e2e coverage
the wiring gap should have had (standing rule: gaps become e2e smoke).
This commit is contained in:
@@ -27,6 +27,7 @@ class Company:
|
|||||||
main_pm_id: Any
|
main_pm_id: Any
|
||||||
pr_reviewer_id: Any
|
pr_reviewer_id: Any
|
||||||
ceo_id: Any
|
ceo_id: Any
|
||||||
|
hom_id: Any # Head of Marketing — Board role
|
||||||
|
|
||||||
|
|
||||||
_COMPANY_CACHE: dict[str, Company] = {}
|
_COMPANY_CACHE: dict[str, Company] = {}
|
||||||
@@ -67,6 +68,7 @@ def seed_company(stack: E2EStack) -> Company:
|
|||||||
main_pm = agent("main-pm", AgentRole.MAIN_PM, None)
|
main_pm = agent("main-pm", AgentRole.MAIN_PM, None)
|
||||||
reviewer = agent("pr-reviewer-1", AgentRole.PR_REVIEWER, None)
|
reviewer = agent("pr-reviewer-1", AgentRole.PR_REVIEWER, None)
|
||||||
ceo = agent("ceo", AgentRole.CEO, None)
|
ceo = agent("ceo", AgentRole.CEO, None)
|
||||||
|
hom = agent("head-marketing", AgentRole.HEAD_MARKETING, Team.BOARD)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
out.ceo_id = ceo.id
|
out.ceo_id = ceo.id
|
||||||
out.dev_id = dev.id
|
out.dev_id = dev.id
|
||||||
@@ -75,6 +77,7 @@ def seed_company(stack: E2EStack) -> Company:
|
|||||||
out.cell_pm_id = cell_pm.id
|
out.cell_pm_id = cell_pm.id
|
||||||
out.main_pm_id = main_pm.id
|
out.main_pm_id = main_pm.id
|
||||||
out.pr_reviewer_id = reviewer.id
|
out.pr_reviewer_id = reviewer.id
|
||||||
|
out.hom_id = hom.id
|
||||||
|
|
||||||
stack.run_db(_run)
|
stack.run_db(_run)
|
||||||
_COMPANY_CACHE["company"] = out
|
_COMPANY_CACHE["company"] = out
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
"""Scenario: the Head of Marketing's feature-spotlight draft, end to end.
|
||||||
|
|
||||||
|
Regression coverage for the class of bug where a new content verb is wired at
|
||||||
|
the role-config + content-actions + route layers but never added to
|
||||||
|
do_server's ``_TOOLS`` registry — ``_register_tools()`` then registers only
|
||||||
|
the intersection of granted verbs and ``_TOOLS``, silently dropping the verb
|
||||||
|
so the agent can never call it. This is exactly what happened to
|
||||||
|
``propose_feature_spotlight`` in the v0.18.0 feature-spotlight work (see
|
||||||
|
``tests/unit/mcp_servers/test_do_server_tool_coverage.py`` for the equivalent
|
||||||
|
unit-level registry audit across every role).
|
||||||
|
|
||||||
|
Drives the REAL do_server module — reloaded with a Head-of-Marketing
|
||||||
|
manifest built from the REAL role_config, exactly as ScriptedAgent does for
|
||||||
|
every other role — through the REAL /api/v1/do/propose_feature_spotlight
|
||||||
|
route, REAL ContentActions, and REAL XEngine, then asserts the held X-queue
|
||||||
|
draft (source=x_feature, confirmed_by_human=False) exists and the
|
||||||
|
exploration task completed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
from tests.e2e_smoke.arcs import seed_company, seed_project, seed_task, task_state
|
||||||
|
from tests.e2e_smoke.harness import ScriptedAgent, expect_ok
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from tests.e2e_smoke.arcs import Company
|
||||||
|
from tests.e2e_smoke.harness import E2EStack
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_system_and_secretary(stack: E2EStack) -> None:
|
||||||
|
"""Seed ``system`` + ``secretary-1`` at their FIXED foundation UUIDs.
|
||||||
|
|
||||||
|
``XEngine.materialize_feature_spotlight`` writes the held draft's
|
||||||
|
``created_by`` / ``assigned_to`` straight from the static identity
|
||||||
|
registry (not a DB lookup keyed by role), so those exact ids must exist
|
||||||
|
as real agent rows for the FK to resolve — ``seed_company``'s random
|
||||||
|
``uuid4()`` agents don't cover this. Idempotent (mirrors
|
||||||
|
``tests/unit/services/test_x_engine.py``'s ``_seed``): safe if ever
|
||||||
|
called more than once against the same stack.
|
||||||
|
"""
|
||||||
|
from roboco.db.tables import AgentTable
|
||||||
|
from roboco.foundation import identity as _foundation
|
||||||
|
from roboco.models import AgentRole, AgentStatus
|
||||||
|
|
||||||
|
async def _run(session: AsyncSession) -> None:
|
||||||
|
for agent_uuid, slug, role in (
|
||||||
|
(_foundation.AGENTS["system"].uuid, "system", AgentRole.SYSTEM),
|
||||||
|
(
|
||||||
|
_foundation.AGENTS["secretary-1"].uuid,
|
||||||
|
"secretary-1",
|
||||||
|
AgentRole.SECRETARY,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
if await session.get(AgentTable, agent_uuid) is not None:
|
||||||
|
continue
|
||||||
|
session.add(
|
||||||
|
AgentTable(
|
||||||
|
id=agent_uuid,
|
||||||
|
name=slug,
|
||||||
|
slug=slug,
|
||||||
|
role=role,
|
||||||
|
team=None,
|
||||||
|
status=AgentStatus.ACTIVE,
|
||||||
|
model_config={},
|
||||||
|
system_prompt=slug,
|
||||||
|
capabilities=[],
|
||||||
|
permissions={},
|
||||||
|
metrics={},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
stack.run_db(_run)
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_feature_exploration(
|
||||||
|
stack: E2EStack, company: Company, project_id: Any
|
||||||
|
) -> Any:
|
||||||
|
"""A held ``x_feature_exploration`` task assigned to the Head of Marketing.
|
||||||
|
|
||||||
|
Mirrors ``XEngine._originate_feature_exploration``'s shape (team/
|
||||||
|
task_type/nature/complexity/source/confirmed_by_human), seeded directly
|
||||||
|
the same way ``seed_hierarchy`` stands in for the orchestrator's own
|
||||||
|
task creation.
|
||||||
|
"""
|
||||||
|
from roboco.models import Team
|
||||||
|
from roboco.models.base import Complexity, TaskNature, TaskStatus, TaskType
|
||||||
|
from roboco.services.task import X_FEATURE_EXPLORATION_SOURCE
|
||||||
|
|
||||||
|
return seed_task(
|
||||||
|
stack,
|
||||||
|
title="X feature-spotlight exploration",
|
||||||
|
description=(
|
||||||
|
"Investigate shipped capabilities (CHANGELOG, feature flags, "
|
||||||
|
"docs/map, charter, KB) and propose ONE feature-spotlight post "
|
||||||
|
"for an under-publicized, not-yet-covered capability."
|
||||||
|
),
|
||||||
|
acceptance_criteria=[
|
||||||
|
"propose_feature_spotlight() is called once with an "
|
||||||
|
"under-publicized, not-yet-covered feature"
|
||||||
|
],
|
||||||
|
task_type=TaskType.ADMINISTRATIVE,
|
||||||
|
nature=TaskNature.NON_TECHNICAL,
|
||||||
|
estimated_complexity=Complexity.LOW,
|
||||||
|
team=Team.BOARD,
|
||||||
|
project_id=project_id,
|
||||||
|
created_by=company.main_pm_id,
|
||||||
|
assigned_to=company.hom_id,
|
||||||
|
status=TaskStatus.PENDING,
|
||||||
|
source=X_FEATURE_EXPLORATION_SOURCE,
|
||||||
|
confirmed_by_human=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _x_draft_state(stack: E2EStack, task_id: UUID) -> dict[str, Any]:
|
||||||
|
"""``source`` / ``confirmed_by_human`` for the materialized X-queue draft
|
||||||
|
— the held-artifact fields ``task_state()`` doesn't carry."""
|
||||||
|
from roboco.db.tables import TaskTable
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
async def _run(session: AsyncSession) -> dict[str, Any]:
|
||||||
|
row = (
|
||||||
|
await session.execute(select(TaskTable).where(TaskTable.id == task_id))
|
||||||
|
).scalar_one()
|
||||||
|
return {
|
||||||
|
"status": str(row.status),
|
||||||
|
"source": row.source,
|
||||||
|
"confirmed_by_human": row.confirmed_by_human,
|
||||||
|
}
|
||||||
|
|
||||||
|
state: dict[str, Any] = stack.run_db(_run)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def test_feature_spotlight_proposal_creates_held_draft(e2e_stack: E2EStack) -> None:
|
||||||
|
stack = e2e_stack
|
||||||
|
company = seed_company(stack)
|
||||||
|
_seed_system_and_secretary(stack)
|
||||||
|
project_id, _project_slug = seed_project(stack, company)
|
||||||
|
exploration_id = _seed_feature_exploration(stack, company, project_id)
|
||||||
|
|
||||||
|
hom = ScriptedAgent(stack, company.hom_id, "head-marketing", "head_marketing")
|
||||||
|
|
||||||
|
# The bug's exact surface: propose_feature_spotlight shipped granted
|
||||||
|
# (role_config) + implemented (ContentActions) + routed (api/v1/do), but
|
||||||
|
# do_server carried neither the wrapper function nor the _TOOLS entry, so
|
||||||
|
# no spawned Head of Marketing could ever reach it over MCP. Asserting
|
||||||
|
# the registry directly — not just relying on the call below succeeding
|
||||||
|
# — means a future regression that drops ONLY the _TOOLS entry (keeping
|
||||||
|
# the wrapper function) still fails here, even though that narrower
|
||||||
|
# variant would not raise AttributeError from the getattr() in .do().
|
||||||
|
do_module = hom._module("roboco.mcp.do_server")
|
||||||
|
assert "propose_feature_spotlight" in do_module._TOOLS, (
|
||||||
|
"propose_feature_spotlight missing from do_server._TOOLS — the MCP "
|
||||||
|
"server has no way to expose it to any role"
|
||||||
|
)
|
||||||
|
assert "propose_feature_spotlight" in do_module._REGISTERED_TOOLS, (
|
||||||
|
"propose_feature_spotlight is granted to head_marketing in "
|
||||||
|
"role_config but absent from this agent's _register_tools() output "
|
||||||
|
"— the manifest -> _register_tools -> callable chain dropped it"
|
||||||
|
)
|
||||||
|
|
||||||
|
feature_slug = f"e2e-feature-{uuid4().hex[:8]}"
|
||||||
|
env = expect_ok(
|
||||||
|
hom.do(
|
||||||
|
"propose_feature_spotlight",
|
||||||
|
feature_slug=feature_slug,
|
||||||
|
feature_title="Organizational Memory Loop",
|
||||||
|
body=(
|
||||||
|
"Did you know RoboCo agents distill one lesson per completed "
|
||||||
|
"task and reuse it on the next matching claim? Institutional "
|
||||||
|
"memory, built in."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"hom propose_feature_spotlight",
|
||||||
|
)
|
||||||
|
assert env.get("status") == "feature_spotlight_proposed", env
|
||||||
|
task_id_str = env.get("task_id")
|
||||||
|
assert task_id_str and task_id_str != str(exploration_id), (
|
||||||
|
f"expected a NEW draft task id distinct from the exploration task: {env}"
|
||||||
|
)
|
||||||
|
|
||||||
|
draft = _x_draft_state(stack, UUID(task_id_str))
|
||||||
|
assert draft["source"] == "x_feature", draft
|
||||||
|
assert draft["confirmed_by_human"] is False, draft
|
||||||
|
assert draft["status"] == "pending", draft # held, awaiting the CEO's review
|
||||||
|
|
||||||
|
assert task_state(stack, exploration_id)["status"] == "completed"
|
||||||
Reference in New Issue
Block a user