Files
roboco/tests/e2e_smoke/test_feature_spotlight.py
T
Renn F da0fa2e33a 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).
2026-07-04 09:30:09 +02:00

192 lines
7.8 KiB
Python

"""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"