mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* [927e64d5] Backend slice: auditor scheduled trigger and reactive alert producers (#496) * [1f2cdb4b] Reactive alert producers at QA-fail and rework (#492) * [1f2cdb4b] feat(services): add auditor-targeted rework alert producers at QA-fail and rework chokepoints * [1f2cdb4b] test(services): fix mypy typing in auditor alert producer unit tests * [1f2cdb4b] docs(backend): document reactive auditor rework alert producers in map and role docs --------- Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> * [5173415f] Scheduled audit trigger, config, and sweep prompt (#493) * [5173415f] Add scheduled audit trigger, interval config, sweep prompt, and focused tests * [5173415f] Allow ROBOCO_AUDIT_INTERVAL_SECONDS=0 to disable scheduled sweeps * [5173415f] docs(audit): document scheduled auditor sweeps and ROBOCO_AUDIT_INTERVAL_SECONDS --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> * [a26c18b9] E2E smoke test for auditor triggers (#495) * [a26c18b9] Add e2e smoke test for auditor scheduled and reactive triggers * [a26c18b9] docs(tests): add e2e smoke test catalog and changelog entry for auditor triggers --------- Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> * [3bc47cdc] Fix _fresh_orchestrator state for auditor trigger e2e tests (#497) * [3bc47cdc] fix(tests): initialize orchestrator state in _fresh_orchestrator helper * [3bc47cdc] docs(changelog): add _fresh_orchestrator test harness fix entry --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> * [8323cd50] Fix e2e smoke regression on assembled cell PR #496 (#498) * [8323cd50] fix(e2e_smoke): repair auditor-trigger smoke tests and harden harness * [8323cd50] docs(tests): document e2e smoke harness hardening for PR #498 --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> --------- Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> * [37e6d999] Backend: repair failing CI checks on auditor revival PR #499 (#503) * [6e79bada] Triage and fix Python quality gate and Analyze (python) failures (#501) * [6e79bada] fix(task): replace type ignore with forward-reference cast for SQLAlchemy Mapped UUID in get_all_descendants * [6e79bada] fix(notification_delivery): add generic type arguments to dict return types in get_ack_status and get_delivery_summary * [6e79bada] docs(changelog): add Python quality gate type-hygiene fixes to Unreleased --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> * [50e7e104] Triage Analyze (javascript-typescript) failure on backend-only diff (#500) * [50e7e104] Split CodeQL workflow so JS/TS analyzer only runs on panel changes * [50e7e104] docs(backend): document split CodeQL workflow triggers and branch protection notes --------- Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> * [203c426b] Triage and fix e2e lifecycle smoke (scripted agents) failure (#502) * [203c426b] fix(orchestrator): pre-initialize _instances in __new__ so __init__-bypass tests survive _dispatch_audit_work; allow audit_interval_seconds=0; mount /api/notifications in e2e harness * [203c426b] fix(e2e_smoke): restore ROBOCO_AGENT_TOKEN isolation and clarify /api/notifications mount comment * [203c426b] docs(map): document orchestrator __new__ pre-init and e2e harness token isolation for auditor-revival smoke fix --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> * [48cb05c2] Fix remaining e2e lifecycle smoke (scripted agents) failure on auditor-revival PR #503 (#504) * [48cb05c2] Harden AgentOrchestrator __new__ pre-init for auditor dispatch state * [48cb05c2] Document auditor-dispatch pre-init rationale in AgentOrchestrator __new__ * [48cb05c2] docs(orchestrator): extend __new__ pre-init docs for auditor-dispatch state --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech> * [90c9474c] intake: ambient workspace note + dedupe scope clones by git_url Two intake follow-ups folded into 90c9474c's spec Notes: (a) _resolve_intake_ambient now prepends a workspace note so the intake agent knows its cwd holds clones of every project in the scope (the primary at cwd, siblings alongside under /data/workspaces) and drafts against the real trees via Grep/Glob/Read, not from memory. (b) _clone_intake_scope dedupes slugs by git_url before cloning. A multi-project scope can list several projects pointing at one repo (a monorepo's cell-projects share a git_url); cloning each produced redundant identical workspaces. Mirrors CI-watch's per-git_url dedupe: keep the first slug per non-empty git_url; a project with no/empty git_url is never collapsed onto another so distinct local repos still clone. The dedupe is a pure static helper (_dedupe_slugs_by_git_url) with unit coverage. --------- Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Renn F <rennf93@users.noreply.github.com>
182 lines
6.5 KiB
Python
182 lines
6.5 KiB
Python
"""Scheduled audit trigger in _dispatch_audit_work.
|
|
|
|
Covers the interval cooldown (ROBOCO_AUDIT_INTERVAL_SECONDS) and the
|
|
active-agent breaker that prevents auditor spawn storms.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from datetime import UTC, datetime, timedelta
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from roboco.config import Settings, settings
|
|
from roboco.runtime.orchestrator import AgentOrchestrator
|
|
|
|
|
|
@pytest.fixture
|
|
def orch() -> AgentOrchestrator:
|
|
orch = AgentOrchestrator.__new__(AgentOrchestrator)
|
|
orch._notification_spawn_at = {}
|
|
orch._last_audit_spawn_at = None
|
|
return orch
|
|
|
|
|
|
async def _run_dispatch(
|
|
orch: AgentOrchestrator, *, tasks: list[dict] | None = None
|
|
) -> MagicMock:
|
|
"""Run _dispatch_audit_work with notifications empty and return the spawn mock."""
|
|
client = MagicMock()
|
|
with (
|
|
patch.object(orch, "_fetch_notifications", new=AsyncMock(return_value=[])),
|
|
patch.object(orch, "_fetch_tasks", new=AsyncMock(return_value=tasks or [])),
|
|
patch.object(orch, "_is_agent_active", return_value=False),
|
|
patch.object(orch, "spawn_agent", new=AsyncMock()) as spawn_mock,
|
|
):
|
|
await orch._dispatch_audit_work(client)
|
|
return spawn_mock
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_spawns_when_overdue_and_delivery_activity(
|
|
orch: AgentOrchestrator,
|
|
) -> None:
|
|
"""Interval elapsed + recent tasks -> auditor sweep is spawned."""
|
|
now = datetime.now(UTC)
|
|
with (
|
|
patch.object(settings, "audit_interval_seconds", 21600),
|
|
patch("roboco.runtime.orchestrator.datetime", wraps=datetime) as dt_mock,
|
|
):
|
|
dt_mock.now.return_value = now
|
|
orch._last_audit_spawn_at = now - timedelta(seconds=21601)
|
|
spawn_mock = await _run_dispatch(
|
|
orch,
|
|
tasks=[
|
|
{
|
|
"id": "t1",
|
|
"status": "in_progress",
|
|
"updated_at": "2026-07-13T00:00:00Z",
|
|
}
|
|
],
|
|
)
|
|
assert spawn_mock.await_count == 1
|
|
call_kwargs = spawn_mock.await_args.kwargs if spawn_mock.await_args else {}
|
|
assert call_kwargs.get("agent_id") == "auditor"
|
|
assert call_kwargs.get("spawned_by") == "_dispatch_audit_work"
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_skips_when_interval_not_elapsed(orch: AgentOrchestrator) -> None:
|
|
"""Cooldown: last spawn was recent -> no scheduled sweep."""
|
|
now = datetime.now(UTC)
|
|
with (
|
|
patch.object(settings, "audit_interval_seconds", 21600),
|
|
patch("roboco.runtime.orchestrator.datetime", wraps=datetime) as dt_mock,
|
|
):
|
|
dt_mock.now.return_value = now
|
|
orch._last_audit_spawn_at = now - timedelta(seconds=1800)
|
|
spawn_mock = await _run_dispatch(
|
|
orch,
|
|
tasks=[
|
|
{
|
|
"id": "t1",
|
|
"status": "in_progress",
|
|
"updated_at": "2026-07-13T00:00:00Z",
|
|
}
|
|
],
|
|
)
|
|
assert spawn_mock.await_count == 0
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_skips_when_no_delivery_activity(orch: AgentOrchestrator) -> None:
|
|
"""No active tasks and no recently completed tasks -> no sweep."""
|
|
now = datetime.now(UTC)
|
|
with (
|
|
patch.object(settings, "audit_interval_seconds", 21600),
|
|
patch("roboco.runtime.orchestrator.datetime", wraps=datetime) as dt_mock,
|
|
):
|
|
dt_mock.now.return_value = now
|
|
orch._last_audit_spawn_at = now - timedelta(seconds=21601)
|
|
spawn_mock = await _run_dispatch(orch, tasks=[])
|
|
assert spawn_mock.await_count == 0
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_breaker_skips_when_auditor_active(orch: AgentOrchestrator) -> None:
|
|
"""Active-agent breaker prevents a second auditor container."""
|
|
client = MagicMock()
|
|
now = datetime.now(UTC)
|
|
with (
|
|
patch.object(settings, "audit_interval_seconds", 21600),
|
|
patch("roboco.runtime.orchestrator.datetime", wraps=datetime) as dt_mock,
|
|
patch.object(orch, "_fetch_notifications", new=AsyncMock(return_value=[])),
|
|
patch.object(orch, "_fetch_tasks", new=AsyncMock(return_value=[])),
|
|
patch.object(orch, "_is_agent_active", return_value=True),
|
|
patch.object(orch, "spawn_agent", new=AsyncMock()) as spawn_mock,
|
|
):
|
|
dt_mock.now.return_value = now
|
|
orch._last_audit_spawn_at = now - timedelta(seconds=21601)
|
|
await orch._dispatch_audit_work(client)
|
|
assert spawn_mock.await_count == 0
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_reactive_alert_stamps_last_spawn_and_blocks_scheduled(
|
|
orch: AgentOrchestrator,
|
|
) -> None:
|
|
"""A reactive alert spawn records _last_audit_spawn_at and returns early."""
|
|
client = MagicMock()
|
|
alert = {
|
|
"id": "a1",
|
|
"to_agents": ["auditor"],
|
|
"subject": "Coverage gap",
|
|
"body": "Test",
|
|
}
|
|
now = datetime.now(UTC)
|
|
with (
|
|
patch.object(settings, "audit_interval_seconds", 21600),
|
|
patch("roboco.runtime.orchestrator.datetime", wraps=datetime) as dt_mock,
|
|
patch.object(orch, "_fetch_notifications", new=AsyncMock(return_value=[alert])),
|
|
patch.object(orch, "_fetch_tasks", new=AsyncMock(return_value=[])),
|
|
patch.object(orch, "_is_agent_active", return_value=False),
|
|
patch.object(orch, "spawn_agent", new=AsyncMock()) as spawn_mock,
|
|
):
|
|
dt_mock.now.return_value = now
|
|
await orch._dispatch_audit_work(client)
|
|
assert spawn_mock.await_count == 1
|
|
assert orch._last_audit_spawn_at == now
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_cooldown_zero_disables_scheduled_sweeps(orch: AgentOrchestrator) -> None:
|
|
"""ROBOCO_AUDIT_INTERVAL_SECONDS=0 disables scheduled sweeps entirely."""
|
|
now = datetime.now(UTC)
|
|
with (
|
|
patch.object(settings, "audit_interval_seconds", 0),
|
|
patch("roboco.runtime.orchestrator.datetime", wraps=datetime) as dt_mock,
|
|
):
|
|
dt_mock.now.return_value = now
|
|
orch._last_audit_spawn_at = None
|
|
spawn_mock = await _run_dispatch(
|
|
orch,
|
|
tasks=[
|
|
{
|
|
"id": "t1",
|
|
"status": "in_progress",
|
|
"updated_at": "2026-07-13T00:00:00Z",
|
|
}
|
|
],
|
|
)
|
|
assert spawn_mock.await_count == 0
|
|
|
|
|
|
def test_env_override_zero_is_accepted() -> None:
|
|
"""ROBOCO_AUDIT_INTERVAL_SECONDS=0 is valid Pydantic input, matching the
|
|
documented disable sentinel and the runtime gate in _audit_spawn_cooled().
|
|
"""
|
|
with patch.dict(os.environ, {"ROBOCO_AUDIT_INTERVAL_SECONDS": "0"}, clear=False):
|
|
s = Settings()
|
|
assert s.audit_interval_seconds == 0
|