mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* test: align phase1 smoke mock with the armed team-match gate The8e5f84c4sweep fixed 13 test files' inconsistent-team mocks but ran only the gateway/foundation/runtime subsets; the full gate caught this integration mock whose parent task carried an auto-generated MagicMock team and died on not_authorized before the incomplete_input assertion. * fix(runtime): attribute every agent.spawned audit to its dispatcher A rogue spawner could not be identified live (2026-07-02): agent.spawned rows carry container/model but not which dispatch loop launched them. spawn_agent now takes spawned_by, stamps it into the spawned/spawn_failed audit details, every call site passes its loop name, and an AST sweep test holds future callers to it. * fix(api): admin-complete refuses when the task's PR is still open PATCH status=completed on a task with an OPEN PR stranded its commits unmerged (bit the CEO twice live 2026-07-02). The override now refuses with the PR number/URL and the consequence before the generic hatch text; force:true stays the deliberate, audited escape. * fix(panel): awaiting_ceo_approval offers the working ceo-approve path The header's only approve action was Approve & Merge (POST /approve-and-merge, no notes) which 400s NO_PR on a branchless MegaTask umbrella — the CEO's approve button just failed. Primary action is now Approve & Complete via the CeoApproveDialog (POST /ceo-approve, notes >=20 chars, proven live); Approve & Merge stays for PR-bearing tasks. * test: stop leaking self-heal + rate-limit state into live Redis Two test files wrote real keys into a developer's localhost Redis: self-heal originate tests left self_heal:notified:* (2h TTL) and the i_am_blocked rate-limited tests left a NO-TTL 'anthropic rate-limited' tracker blob — order/state-dependent poison for anything reading the real tracker, and the prime suspect class for the one-off test_self_heal_engine full-run failure (not reproduced in 5x dir runs, adversarial orders, and a green full gate). Both files now point the computed redis_url at an unreachable port; the engines' fail-open paths keep every assertion intact. Leaked keys scrubbed live. * docs: changelog + map delta for the leak-fix batch; mypy-clean attribution test The attribution test's direct method assignments tripped the full gate's mypy (method-assign) — switched to the house monkeypatch idiom, no suppressions. * fix(gate): clear the ten xenon C-ranks; isolate all tests from live Redis Master CI has been red at the phase1 smoke test, so neither CI nor a local full gate had reached the xenon step since the team-match sweep — whose inline 'agent_team=str(agent.team) if ...' kwarg pushed nine verb bodies from B(10) to C(11-12) unseen. A shared actor_context_fields() (_protocol.py) computes (actor_slug, agent_team) once per verb, restoring all nine to B with zero behavior change; the new admin-complete override helper extraction does the same for routes/tasks.py. tests/conftest.py gains an autouse fixture pointing the computed redis_url at an unreachable port for every test — the root fix for the three families caught writing live-Redis keys (self-heal dedupe, rate-limit tracker, notification purpose-dedupe); no test uses a real Redis, and every production path is fail-open by design. * refactor(runtime): delete the never-wired dispatch-time spawn cooldown _safe_spawn / gateway_pre_spawn_check / trigger_filter had no caller in the repo's entire history (87ef42bfonly flipped the flag). Its five rules are superseded: provider parking runs inside spawn_agent, claim freshness is the guards+reaper, runaway respawns are the progress-aware breaker + notification cooldown; the per-task cooldown rule would queue-stall every normal stage handoff if wired today. gateway_triggers table kept inert. Ratified by the CEO over wiring it. * build: serialize uv — gate recipes never implicitly sync the venv Every uv run re-syncs implicitly, so a background make quality plus any foreground uv run raced two writers on one .venv and tore site-packages apart (the recurring rich/pip/bandit ImportError corruption; bit twice today, four times on 2026-07-02's first session). UV_NO_SYNC=1 is now exported Makefile-wide and quality/quality-fast/gate depend on one explicit up-front sync step. * fix(git): PR/merge/branch REST calls honor github_api_base_url Fifteen sites hardcoded https://api.github.com while the CI-run and open-PR-list calls already read settings.github_api_base_url — a GHE or test override silently applied to half the surface. One _api_base() helper keeps them uniform; default behavior unchanged. * ci: split the monolith — backend CI, Panel CI, E2E Smoke ci.yml keeps its file name and the backend quality job only (self-heal / ci-watch / release-readiness default to the ci.yml workflow); the panel job moves to panel-ci.yml scoped to panel/**, and the new scripted-agent lifecycle smoke gets e2e-smoke.yml + a make e2e-smoke target (env-gated out of the default pytest run). Trade: a panel-only red now lands on Panel CI, which the ci.yml-pinned watch engines don't see. * feat(tests): e2e lifecycle smoke harness — scripted agents, real gates tests/e2e_smoke stands up the real API (flow/do routers + middleware on uvicorn) over the ephemeral test Postgres, a local bare origin standing in for GitHub, and a fake GitHub REST layer whose merges are real git merges. A deterministic driver reloads the real MCP flow/do modules per agent and walks claim (real clone + worktree) -> tracing-gap -> note -> plan gate -> commit -> PR -> the full i_am_done ladder -> QA verdicts -> documenter -> awaiting_pm_review in ~5s. Runs via make e2e-smoke + its own CI workflow; skipped (env-gated) in the default suite. The freeze-lift condition's first half: scenario 1 green. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
158 lines
5.4 KiB
Python
158 lines
5.4 KiB
Python
"""Foundation Phase 1 smoke gate.
|
|
|
|
End-to-end: a delegate call that would have produced a skeleton task
|
|
pre-Phase-1 must now return Envelope.incomplete_input with a populated
|
|
field_hints map. No silent fallback; no ``["completed and reviewed by
|
|
assignee"]`` ever lands in the DB.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from roboco.services.gateway.choreographer import (
|
|
Choreographer,
|
|
ChoreographerDeps,
|
|
DelegateInputs,
|
|
)
|
|
|
|
# Lower bound for a useful field hint. The canonical
|
|
# ``_HINT_ACCEPTANCE_CRITERIA`` text is ~200 chars; anything under this
|
|
# would mean the hint string was truncated or replaced with a stub.
|
|
_MIN_FIELD_HINT_LEN = 30
|
|
|
|
|
|
def _make_deps(**overrides: Any) -> ChoreographerDeps:
|
|
"""Build a ChoreographerDeps with AsyncMock services + empty evidence repo.
|
|
|
|
Mirrors ``tests/unit/gateway/test_delegate_incomplete_input.py``: every
|
|
evidence_repo lookup the briefing assembler reaches for must return
|
|
``[]`` so the briefing build does not raise on a coroutine result.
|
|
"""
|
|
base: dict[str, Any] = {
|
|
"task": AsyncMock(),
|
|
"work_session": AsyncMock(),
|
|
"git": AsyncMock(),
|
|
"a2a": AsyncMock(),
|
|
"journal": AsyncMock(),
|
|
"audit": AsyncMock(),
|
|
"evidence_repo": AsyncMock(),
|
|
}
|
|
base.update(overrides)
|
|
repo = base["evidence_repo"]
|
|
for method in (
|
|
"list_unread_a2a",
|
|
"list_unread_mentions",
|
|
"list_pending_notifications",
|
|
"task_metadata_gaps",
|
|
"recent_team_activity",
|
|
"blockers_in_lane",
|
|
"journal_highlights_for_task",
|
|
):
|
|
getattr(repo, method).return_value = []
|
|
# C8: default-fresh journal:decision so PM-decision gate passes for
|
|
# callers that don't override the journal mock.
|
|
_ldef = base["journal"].latest_decision_at.return_value
|
|
if type(_ldef).__name__ in ("MagicMock", "AsyncMock"):
|
|
base["journal"].latest_decision_at.return_value = datetime.now(UTC)
|
|
return ChoreographerDeps(**base)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skeleton_task_path_returns_incomplete_input() -> None:
|
|
"""The 2026-05-10 smoke run produced subtasks with empty
|
|
acceptance_criteria via the gateway. After Phase 1, that exact
|
|
sequence must produce incomplete_input rejections instead.
|
|
"""
|
|
pm_id = uuid4()
|
|
parent = MagicMock(
|
|
id=uuid4(),
|
|
project_id=uuid4(),
|
|
status="in_progress",
|
|
assigned_to=pm_id,
|
|
priority=2,
|
|
team="backend",
|
|
)
|
|
task_svc = AsyncMock()
|
|
task_svc.get.return_value = parent
|
|
task_svc.agent_for.return_value = MagicMock(
|
|
id=pm_id, role="cell_pm", team="backend", slug="be-pm"
|
|
)
|
|
task_svc.get_subtasks.return_value = []
|
|
deps = _make_deps(task=task_svc)
|
|
c = Choreographer(deps)
|
|
|
|
# The exact shape from the failing smoke run: acceptance_criteria
|
|
# missing, optional fields not provided.
|
|
env = await c.delegate(
|
|
pm_id,
|
|
parent.id,
|
|
DelegateInputs(
|
|
title="Branch naming smoke test",
|
|
description=(
|
|
"Verify branch creation follows the feature/team/task convention."
|
|
),
|
|
assigned_to="be-dev-1",
|
|
team="backend",
|
|
task_type="code",
|
|
nature="technical",
|
|
estimated_complexity="medium",
|
|
acceptance_criteria=None,
|
|
),
|
|
)
|
|
body = env.as_dict()
|
|
assert body["error"] == "incomplete_input", (
|
|
f"expected incomplete_input, got: {body}"
|
|
)
|
|
assert "acceptance_criteria" in body["missing"]
|
|
# The agent learns from field_hints what to fill:
|
|
assert "acceptance_criteria" in body["field_hints"]
|
|
assert len(body["field_hints"]["acceptance_criteria"]) > _MIN_FIELD_HINT_LEN
|
|
# Skeleton task never reached the DB.
|
|
task_svc.create_subtask.assert_not_awaited()
|
|
|
|
|
|
def test_no_silent_fallback_phrase_in_repo() -> None:
|
|
"""Production code must not contain the placeholder string outside of
|
|
documented locations.
|
|
|
|
Allowed appearances after Tasks 12 + 18 + 20:
|
|
- ``roboco/foundation/policy/task_completeness.py`` — the DENYLIST
|
|
entry plus the ``_HINT_ACCEPTANCE_CRITERIA`` text that names the
|
|
phrase as a known evasion.
|
|
- ``roboco/api/routes/tasks.py`` — comment in the POST /tasks handler
|
|
explaining why the route runs ``task_completeness.check`` (Task 20).
|
|
- ``roboco/services/task.py`` — docstring on ``create_subtask``
|
|
documenting the Task 18 deletion of the silent fallback.
|
|
|
|
Any other appearance is a real Phase 1 gap.
|
|
"""
|
|
proc = subprocess.run(
|
|
# ``-I`` skips binary files so stray ``__pycache__/*.pyc`` matches
|
|
# (left over from a previous run) don't contaminate the grep.
|
|
["grep", "-rnI", "completed and reviewed by assignee", "roboco/"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
allowed_files = (
|
|
"roboco/foundation/policy/task_completeness.py",
|
|
"roboco/api/routes/tasks.py",
|
|
"roboco/services/task.py",
|
|
)
|
|
suspicious: list[str] = []
|
|
for line in proc.stdout.splitlines():
|
|
if not line:
|
|
continue
|
|
if any(line.startswith(f"{path}:") for path in allowed_files):
|
|
continue
|
|
suspicious.append(line)
|
|
assert suspicious == [], (
|
|
f"silent-fallback phrase still present in production code: {suspicious}"
|
|
)
|