Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""Tests for the ``on_kanban_dispatch_tick`` observer hook.
|
||||
|
||||
Re-port of PR #56066 per the #64231 batch disposition: renamed to the
|
||||
taxonomy form and fired by ``kanban_db.dispatch_once`` strictly AFTER the
|
||||
board's single-writer dispatch lock has been released — the original fired
|
||||
inside ``_dispatch_tick_lock``, so a slow subscriber could extend the
|
||||
critical section and stall a sibling dispatcher. Verifies the post-lock
|
||||
contract directly, the ``outcome`` classification (including idle and
|
||||
contended ticks), ``dry_run`` propagation, and that a misbehaving
|
||||
subscriber never breaks the dispatcher.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli import kanban_db as kb
|
||||
from hermes_cli.plugins import VALID_HOOKS, get_plugin_manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kanban_home(tmp_path, monkeypatch):
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
kb.init_db()
|
||||
return home
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def captured_ticks(monkeypatch):
|
||||
"""Register a capturing callback for the dispatch tick hook."""
|
||||
mgr = get_plugin_manager()
|
||||
events: list[dict] = []
|
||||
saved = {k: list(v) for k, v in mgr._hooks.items()}
|
||||
mgr._hooks.setdefault("on_kanban_dispatch_tick", []).append(
|
||||
lambda **kw: events.append(kw)
|
||||
)
|
||||
try:
|
||||
yield events
|
||||
finally:
|
||||
mgr._hooks = saved
|
||||
|
||||
def test_active_tick_fires_hook_with_outcome_ok(
|
||||
kanban_home, all_assignees_spawnable, captured_ticks,
|
||||
):
|
||||
"""A tick that spawns a worker fires the hook with outcome='ok'."""
|
||||
conn = kb.connect()
|
||||
try:
|
||||
tid = kb.create_task(conn, title="t", assignee="alice")
|
||||
kb.dispatch_once(conn, spawn_fn=lambda *a, **k: 4242)
|
||||
finally:
|
||||
conn.close()
|
||||
ok_events = [kw for kw in captured_ticks if kw["outcome"] == "ok"]
|
||||
assert ok_events, (
|
||||
f"expected an ok tick, got {[kw['outcome'] for kw in captured_ticks]}"
|
||||
)
|
||||
result = ok_events[-1]["result"]
|
||||
assert any(row[0] == tid for row in result.spawned)
|
||||
|
||||
def test_tick_hook_fires_after_dispatch_lock_released(kanban_home):
|
||||
"""The #56066 sweeper finding, as a contract: subscribers run OUTSIDE
|
||||
the single-writer critical section. From the callback, acquiring the
|
||||
board's dispatch lock must succeed — flock conflicts across file
|
||||
descriptors within one process, so this fails if the hook still fires
|
||||
while ``dispatch_once`` holds the lock."""
|
||||
db_path = kb.kanban_db_path()
|
||||
mgr = get_plugin_manager()
|
||||
saved = {k: list(v) for k, v in mgr._hooks.items()}
|
||||
acquired: list[bool] = []
|
||||
|
||||
def _probe_lock(**kw):
|
||||
with kb._dispatch_tick_lock(db_path) as held:
|
||||
acquired.append(held)
|
||||
|
||||
mgr._hooks.setdefault("on_kanban_dispatch_tick", []).append(_probe_lock)
|
||||
try:
|
||||
conn = kb.connect()
|
||||
try:
|
||||
kb.dispatch_once(conn, spawn_fn=lambda *a, **k: 1)
|
||||
finally:
|
||||
conn.close()
|
||||
finally:
|
||||
mgr._hooks = saved
|
||||
assert acquired == [True]
|
||||
|
||||
|
||||
def test_misbehaving_subscriber_does_not_break_dispatcher(kanban_home):
|
||||
"""A hook callback that raises must not break the dispatch tick."""
|
||||
mgr = get_plugin_manager()
|
||||
saved = {k: list(v) for k, v in mgr._hooks.items()}
|
||||
|
||||
def _boom(**kw):
|
||||
raise RuntimeError("subscriber exploded")
|
||||
|
||||
mgr._hooks.setdefault("on_kanban_dispatch_tick", []).append(_boom)
|
||||
try:
|
||||
conn = kb.connect()
|
||||
try:
|
||||
result = kb.dispatch_once(conn, spawn_fn=lambda *a, **k: 1)
|
||||
assert isinstance(result, kb.DispatchResult)
|
||||
finally:
|
||||
conn.close()
|
||||
finally:
|
||||
mgr._hooks = saved
|
||||
|
||||
|
||||
def test_no_subscriber_short_circuits_tick_hook(kanban_home, monkeypatch):
|
||||
"""With nothing registered, the tick observer is never invoked."""
|
||||
from hermes_cli import lifecycle
|
||||
|
||||
invoked: list[str] = []
|
||||
real_invoke = lifecycle.invoke_hook
|
||||
|
||||
def _spy(hook_name, **kw):
|
||||
invoked.append(hook_name)
|
||||
return real_invoke(hook_name, **kw)
|
||||
|
||||
monkeypatch.setattr(lifecycle, "invoke_hook", _spy)
|
||||
conn = kb.connect()
|
||||
try:
|
||||
kb.dispatch_once(conn, spawn_fn=lambda *a, **k: 1)
|
||||
finally:
|
||||
conn.close()
|
||||
assert "on_kanban_dispatch_tick" not in invoked
|
||||
Reference in New Issue
Block a user