Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
"""Host-level concurrency accounting + review-lane fairness (OOF-30 review).
|
||||
|
||||
Three gaps found in review of the original memory-guard PR:
|
||||
|
||||
1. The standalone daemon path (``hermes kanban daemon --force`` /
|
||||
:func:`hermes_cli.kanban_db.run_daemon`) never resolved
|
||||
``kanban.max_in_progress`` at all — the one shipped entry point that
|
||||
could still fan out an entire backlog in a single tick.
|
||||
2. ``max_in_progress`` was enforced per-board while the gateway dispatcher
|
||||
ticks every active board — N boards multiplied the host budget by N.
|
||||
3. The ready loop consumed the entire shared spawn budget before the
|
||||
review loop ran, so a sustained ready backlog starved autonomous
|
||||
reviews indefinitely.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli import kanban_db as kb
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kanban_home(tmp_path, monkeypatch):
|
||||
"""Isolated HERMES_HOME with an empty kanban DB."""
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
kb.init_db()
|
||||
return home
|
||||
|
||||
|
||||
def _set_task_status(conn: sqlite3.Connection, task_id: str, status: str) -> None:
|
||||
conn.execute("UPDATE tasks SET status = ? WHERE id = ?", (status, task_id))
|
||||
|
||||
|
||||
def _fake_spawn_factory(spawns: list):
|
||||
def fake_spawn(task, workspace, board=None):
|
||||
spawns.append(task.id)
|
||||
return 42
|
||||
return fake_spawn
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Standalone daemon resolves max_in_progress (P1a)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_run_daemon_resolves_and_passes_max_in_progress(
|
||||
kanban_home, monkeypatch,
|
||||
):
|
||||
"""The daemon tick must pass a resolved cap into dispatch_once.
|
||||
|
||||
Regression guard for the OOF-30 review finding: ``run_daemon`` only
|
||||
forwarded ``max_spawn`` — with no explicit ``--max`` (the shipped
|
||||
systemd shape) nothing capped the tick even though the gateway and
|
||||
``hermes kanban dispatch`` paths both resolved the memory-derived
|
||||
default.
|
||||
"""
|
||||
captured: dict = {}
|
||||
stop = threading.Event()
|
||||
|
||||
def fake_dispatch_once(conn, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return kb.DispatchResult()
|
||||
|
||||
monkeypatch.setattr(kb, "dispatch_once", fake_dispatch_once)
|
||||
# No explicit config → the derived default must flow through.
|
||||
monkeypatch.setattr(kb, "configured_max_in_progress", lambda: None)
|
||||
monkeypatch.setattr(kb, "derive_default_max_in_progress", lambda sample=None: 3)
|
||||
|
||||
def on_tick(res):
|
||||
stop.set()
|
||||
|
||||
kb.run_daemon(interval=0.01, stop_event=stop, on_tick=on_tick)
|
||||
|
||||
assert captured.get("max_in_progress") == 3
|
||||
|
||||
|
||||
def test_run_daemon_explicit_config_wins(kanban_home, monkeypatch):
|
||||
captured: dict = {}
|
||||
stop = threading.Event()
|
||||
|
||||
def fake_dispatch_once(conn, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return kb.DispatchResult()
|
||||
|
||||
monkeypatch.setattr(kb, "dispatch_once", fake_dispatch_once)
|
||||
monkeypatch.setattr(kb, "configured_max_in_progress", lambda: 7)
|
||||
monkeypatch.setattr(
|
||||
kb, "derive_default_max_in_progress",
|
||||
lambda sample=None: pytest.fail("derived default must not be consulted"),
|
||||
)
|
||||
|
||||
def on_tick(res):
|
||||
stop.set()
|
||||
|
||||
kb.run_daemon(interval=0.01, stop_event=stop, on_tick=on_tick)
|
||||
|
||||
assert captured.get("max_in_progress") == 7
|
||||
|
||||
|
||||
def test_configured_max_in_progress_parsing(monkeypatch):
|
||||
import hermes_cli.config as cfgmod
|
||||
|
||||
cases = [
|
||||
({"kanban": {"max_in_progress": 4}}, 4),
|
||||
({"kanban": {"max_in_progress": "5"}}, 5),
|
||||
({"kanban": {"max_in_progress": 0}}, None),
|
||||
({"kanban": {"max_in_progress": -2}}, None),
|
||||
({"kanban": {"max_in_progress": "lots"}}, None),
|
||||
({"kanban": {}}, None),
|
||||
({}, None),
|
||||
]
|
||||
for config, expected in cases:
|
||||
monkeypatch.setattr(
|
||||
cfgmod, "load_config_readonly", lambda c=config: c
|
||||
)
|
||||
assert kb.configured_max_in_progress() == expected, config
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. max_in_progress counts running work on ALL boards (P1b)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_max_in_progress_counts_other_boards(
|
||||
kanban_home, all_assignees_spawnable,
|
||||
):
|
||||
"""Workers running on another board consume the same host budget."""
|
||||
kb.create_board("second")
|
||||
|
||||
# Two workers already running on the second board.
|
||||
with kb.connect(board="second") as conn:
|
||||
for title in ("busy-1", "busy-2"):
|
||||
tid = kb.create_task(conn, title=title, assignee="alice")
|
||||
assert kb.claim_task(conn, tid) is not None
|
||||
|
||||
spawns: list = []
|
||||
with kb.connect() as conn:
|
||||
kb.create_task(conn, title="wants-to-run", assignee="alice")
|
||||
res = kb.dispatch_once(
|
||||
conn, spawn_fn=_fake_spawn_factory(spawns), max_in_progress=2,
|
||||
)
|
||||
|
||||
# Host budget (2) already consumed by the second board → nothing spawns.
|
||||
assert not spawns
|
||||
assert not res.spawned
|
||||
|
||||
|
||||
def test_max_in_progress_partial_budget_across_boards(
|
||||
kanban_home, all_assignees_spawnable,
|
||||
):
|
||||
kb.create_board("second")
|
||||
|
||||
with kb.connect(board="second") as conn:
|
||||
tid = kb.create_task(conn, title="busy", assignee="alice")
|
||||
assert kb.claim_task(conn, tid) is not None
|
||||
|
||||
spawns: list = []
|
||||
with kb.connect() as conn:
|
||||
for title in ("a", "b", "c"):
|
||||
kb.create_task(conn, title=title, assignee="alice")
|
||||
res = kb.dispatch_once(
|
||||
conn, spawn_fn=_fake_spawn_factory(spawns), max_in_progress=2,
|
||||
)
|
||||
|
||||
# 1 running elsewhere + budget 2 → exactly one new spawn here.
|
||||
assert len(spawns) == 1
|
||||
assert len(res.spawned) == 1
|
||||
|
||||
|
||||
def test_count_running_tasks_other_boards_fails_open(
|
||||
kanban_home, monkeypatch,
|
||||
):
|
||||
"""A broken board enumeration must not brick dispatch (returns 0)."""
|
||||
monkeypatch.setattr(
|
||||
kb, "list_boards",
|
||||
lambda **k: (_ for _ in ()).throw(RuntimeError("boom")),
|
||||
)
|
||||
assert kb.count_running_tasks_other_boards() == 0
|
||||
|
||||
|
||||
def test_max_spawn_stays_per_board(kanban_home, all_assignees_spawnable):
|
||||
"""``max_spawn`` keeps its historical per-board semantics."""
|
||||
kb.create_board("second")
|
||||
with kb.connect(board="second") as conn:
|
||||
tid = kb.create_task(conn, title="busy", assignee="alice")
|
||||
assert kb.claim_task(conn, tid) is not None
|
||||
|
||||
spawns: list = []
|
||||
with kb.connect() as conn:
|
||||
kb.create_task(conn, title="a", assignee="alice")
|
||||
res = kb.dispatch_once(
|
||||
conn, spawn_fn=_fake_spawn_factory(spawns), max_spawn=1,
|
||||
)
|
||||
|
||||
# The other board's worker does NOT count against max_spawn.
|
||||
assert len(spawns) == 1
|
||||
assert len(res.spawned) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Review lane cannot be starved by a sustained ready backlog (P2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _park_in_review(conn: sqlite3.Connection, title: str, assignee: str) -> str:
|
||||
tid = kb.create_task(conn, title=title, assignee=assignee)
|
||||
_set_task_status(conn, tid, "review")
|
||||
return tid
|
||||
|
||||
|
||||
def test_review_lane_gets_reserved_slot_under_ready_backlog(
|
||||
kanban_home, all_assignees_spawnable, monkeypatch,
|
||||
):
|
||||
import hermes_cli.config as cfgmod
|
||||
monkeypatch.setattr(
|
||||
cfgmod, "load_config",
|
||||
lambda *a, **k: {"kanban": {"review_dispatch": True}},
|
||||
)
|
||||
|
||||
spawns: list = []
|
||||
with kb.connect() as conn:
|
||||
for title in ("ready-1", "ready-2", "ready-3"):
|
||||
kb.create_task(conn, title=title, assignee="alice")
|
||||
review_id = _park_in_review(conn, "review-me", "reviewer")
|
||||
res = kb.dispatch_once(
|
||||
conn, spawn_fn=_fake_spawn_factory(spawns), max_in_progress=2,
|
||||
)
|
||||
|
||||
spawned_ids = [s[0] for s in res.spawned]
|
||||
# Budget 2: one ready + the reserved review slot — never 2×ready.
|
||||
assert len(spawned_ids) == 2
|
||||
assert review_id in spawned_ids
|
||||
|
||||
|
||||
def test_review_reservation_released_when_no_review_work(
|
||||
kanban_home, all_assignees_spawnable, monkeypatch,
|
||||
):
|
||||
import hermes_cli.config as cfgmod
|
||||
monkeypatch.setattr(
|
||||
cfgmod, "load_config",
|
||||
lambda *a, **k: {"kanban": {"review_dispatch": True}},
|
||||
)
|
||||
|
||||
spawns: list = []
|
||||
with kb.connect() as conn:
|
||||
for title in ("ready-1", "ready-2", "ready-3"):
|
||||
kb.create_task(conn, title=title, assignee="alice")
|
||||
res = kb.dispatch_once(
|
||||
conn, spawn_fn=_fake_spawn_factory(spawns), max_in_progress=2,
|
||||
)
|
||||
|
||||
# No review work → ready lane keeps the full budget.
|
||||
assert len(res.spawned) == 2
|
||||
|
||||
|
||||
def test_nonspawnable_review_does_not_tax_ready_budget(
|
||||
kanban_home, monkeypatch,
|
||||
):
|
||||
"""Review tasks parked for humans (no real profile) release the slot."""
|
||||
import hermes_cli.config as cfgmod
|
||||
import hermes_cli.profiles as profmod
|
||||
|
||||
monkeypatch.setattr(
|
||||
cfgmod, "load_config",
|
||||
lambda *a, **k: {"kanban": {"review_dispatch": True}},
|
||||
)
|
||||
# Only 'alice' is a real profile; the review assignee is a human lane.
|
||||
monkeypatch.setattr(
|
||||
profmod, "profile_exists", lambda name: name == "alice"
|
||||
)
|
||||
|
||||
spawns: list = []
|
||||
with kb.connect() as conn:
|
||||
for title in ("ready-1", "ready-2"):
|
||||
kb.create_task(conn, title=title, assignee="alice")
|
||||
_park_in_review(conn, "human-review", "some-human")
|
||||
res = kb.dispatch_once(
|
||||
conn, spawn_fn=_fake_spawn_factory(spawns), max_in_progress=2,
|
||||
)
|
||||
|
||||
# Human-lane review is not spawnable → no reservation, ready gets both.
|
||||
assert len(res.spawned) == 2
|
||||
|
||||
|
||||
def test_review_budget_still_bounded_by_shared_cap(
|
||||
kanban_home, all_assignees_spawnable, monkeypatch,
|
||||
):
|
||||
"""The reservation caps the ready lane; it grants review no extra slots."""
|
||||
import hermes_cli.config as cfgmod
|
||||
monkeypatch.setattr(
|
||||
cfgmod, "load_config",
|
||||
lambda *a, **k: {"kanban": {"review_dispatch": True}},
|
||||
)
|
||||
|
||||
spawns: list = []
|
||||
with kb.connect() as conn:
|
||||
kb.create_task(conn, title="ready-1", assignee="alice")
|
||||
for i in range(3):
|
||||
_park_in_review(conn, f"review-{i}", "reviewer")
|
||||
res = kb.dispatch_once(
|
||||
conn, spawn_fn=_fake_spawn_factory(spawns), max_in_progress=2,
|
||||
)
|
||||
|
||||
# Budget 2 total across both lanes, reservation notwithstanding.
|
||||
assert len(res.spawned) == 2
|
||||
Reference in New Issue
Block a user