Files
roboco/tests/unit/eval/test_fixtures.py
T
10f039c36f feat(eval): golden-task eval harness + doctrine cohort stamp (#655)
* fix(notifications): exponential backoff + CAS claim for expired-unacked re-escalation

The sweep re-escalated every expired unacked ack-required notification
on every ~60s tick, forever — the live incident: 3 fresh blocker
escalations + Telegram DMs per minute from a static stale pile. Now
each notification carries reescalation_count / last_reescalated_at /
reescalation_delivered_count (migration 079): first fire at expiry,
then doubling intervals from 1h capped at 24h, hard stop after
ROBOCO_NOTIFICATION_MAX_REESCALATIONS (default 5) with one permanent
log carrying attempts-vs-delivered so 'seen and ignored' is
distinguishable from 'route never worked'. The due/wait/capped decision
is a pure function in foundation/policy/communications.py.

Per adversarial review, the attempt slot is claimed by compare-and-set
(UPDATE ... WHERE reescalation_count = :n) BEFORE delivery — the
previous draft leaned on the 60s dedup window, which never engages for
BLOCKER_ESCALATION (_LOOP_PRONE_TYPES excludes it), so concurrent
sweeps would have double-delivered. A lost claim skips delivery
outright. Legacy rows read as count=0 and keep today's first-fire
semantics. 61 tests incl. a two-session CAS race and a real alembic
upgrade/downgrade round trip.

* feat(budgets): per-task and per-project cost budgets (flag-gated)

tasks.budget_usd + projects.monthly_budget_usd (migration 080, chained
on 079; adds ix_agent_spawn_sessions_task_id since both enforcement
seams filter on bare task_id). Behind ROBOCO_TASK_BUDGETS_ENABLED
(default off, feature-flags card) — verifiably inert when off.

Claim-time: a project-month-spend guard applies to WORK-STARTING claims
only (i_will_work_on / i_will_plan) — per adversarial review, review/
doc/gate/inbound-PR claims are exempt so in-flight work can always
finish reviewing and merging at cap. Spend counts closed sessions'
estimated_cost_usd PLUS open sessions priced live from token snapshots
(the original closed-only sum read parallel long sessions as $0).

Sweep-side: the existing budget sweep also prices the active task's
spend vs budget_usd (TaskType defaults when null); on breach the task
is BLOCKED (HUMAN resolver, budget marker) BEFORE the graceful stop so
the unclaim no-ops and the dispatcher never respawns onto it, and the
CEO notification names both recovery steps. unblock on a budget-blocked
task re-checks live spend and refuses while still over — no silent
re-breach loop. Panel: budget inputs in both dialogs (0 rejected — a
zero budget silently blocks everything), spend logic consolidated in
TaskService.task_spend_usd. 42 new tests incl. a real-DB spend-query
suite and a two-tick non-refire sweep test.

* feat(eval): golden-task eval harness + doctrine cohort stamp

roboco/eval: 6 BenchTaskSpec fixtures run through the real lifecycle in
a disposable environment (the e2e_smoke harness's fake GitHub + local
git origin + throwaway DB catalog — real isolation, not convention),
scored deterministically (terminal status, revision_count, cycle time,
tokens/cost via the agent_spawn_sessions task_id join) plus a local-
model judge whose output is nested under a non_deterministic-marked
object so cohort diffs don't read judge noise as regression. CLI:
python -m roboco.eval run --role <slug> --cohort <name>. Source-
checkout-only by declared posture (deptry-scoped ignore + a hard
ImportError guard naming why; tests/ never ships in images or wheels).

agent_spawn_sessions.doctrine_version (migration 081, chained on 080)
is stamped at spawn-session finalize from the composed prompt layers —
with the session's model column it identifies a cohort durably.

Per adversarial review: bench runs patch the vault flags off (they were
writing real markdown into the operator's vault), and the real-spawn
OrchestratorStageSpawner is deliberately cut to NotImplementedError —
spawned containers' MCP wiring resolves to the production orchestrator
under real agent UUIDs, so real spawns wait for a dedicated follow-up;
the injectable scripted spawner is the working path. Full suite 13852
passed / 94% coverage in the source worktree; deptry/mypy/xenon clean.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-23 00:06:50 +02:00

75 lines
2.7 KiB
Python

"""Schema checks for the golden-task fixtures (roboco/eval/fixtures.py).
Nothing here touches a DB or the network — these are pure sanity checks on
the static FIXTURES tuple so a malformed fixture (a duplicate key, a fixture
file that escapes its own bench/<key>/ namespace and could collide with
another fixture's repo state, an empty brief) is caught before it ever
reaches the runner.
"""
from __future__ import annotations
import dataclasses
from typing import Any, cast
import pytest
from roboco.eval.fixtures import FIXTURES, BenchTaskSpec
_MIN_FIXTURES = 5
_MAX_FIXTURES = 8
def test_fixture_keys_are_unique() -> None:
keys = [f.key for f in FIXTURES]
assert len(keys) == len(set(keys)), f"duplicate fixture keys: {keys}"
def test_at_least_five_fixtures() -> None:
# The task calls for 5-8 canonical fixtures.
assert _MIN_FIXTURES <= len(FIXTURES) <= _MAX_FIXTURES, len(FIXTURES)
def test_every_fixture_has_a_non_empty_brief() -> None:
for f in FIXTURES:
assert f.title.strip(), f.key
assert f.description.strip(), f.key
assert f.acceptance_criteria, f"{f.key} has no acceptance criteria"
assert all(c.strip() for c in f.acceptance_criteria), f.key
assert f.expectations.strip(), f"{f.key} has no judge expectations note"
def test_repo_files_are_namespaced_under_bench_key() -> None:
"""Every fixture's seeded file lives under bench/<its own key>/ so
sequential fixtures sharing one project's git history never collide."""
for f in FIXTURES:
assert f.repo_files, f"{f.key} seeds no repo files"
prefix = f"bench/{f.key}/"
for rel_path, content in f.repo_files:
assert rel_path.startswith(prefix), (
f"{f.key}: {rel_path!r} escapes its own {prefix!r} namespace"
)
assert ".." not in rel_path, f"{f.key}: {rel_path!r} looks like a traversal"
assert content, f"{f.key}: {rel_path!r} has empty content"
def test_repo_file_paths_within_a_fixture_are_unique() -> None:
for f in FIXTURES:
paths = [rel_path for rel_path, _content in f.repo_files]
assert len(paths) == len(set(paths)), f"{f.key}: duplicate paths {paths}"
def test_target_role_is_developer_for_every_fixture() -> None:
"""Matches EvalRunner.run_cohort's current scope cut (see runner.py's
module docstring) — every fixture must be runnable by the one role the
bench supports today."""
for f in FIXTURES:
assert f.target_role == "developer", f.key
def test_bench_task_spec_is_frozen() -> None:
spec = FIXTURES[0]
assert isinstance(spec, BenchTaskSpec)
mutable_view = cast("Any", spec)
with pytest.raises(dataclasses.FrozenInstanceError):
mutable_view.title = "mutated"