8c9a174ddc
R1: Add unit tests (22 total, all passing) - tests/test_queue.py: enqueue/persist, is_issue_pending_or_running for all statuses, update_status mutate+persist, save/load roundtrip, corrupt file handling, pending() filtering - tests/test_watcher.py: FakeMulticaClient drives poll_once across first observation, same updated_at dedupe, updated_at change while pending/running dedupe, re-enqueue after done, multi-issue, mix of new and seen Refactor: extract poll_once(client, state, queue, logger) from watch_loop so tests can call it directly without mocking time.sleep. R2: Document known race near is_issue_pending_or_running — comment added noting that orchestrator marking round done before updating issue status can fire a second round; WYL-45 must resolve atomically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
163 lines
5.5 KiB
Python
163 lines
5.5 KiB
Python
"""Unit tests for the poll_once watcher logic."""
|
|
import logging
|
|
import pathlib
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from coordinator.__main__ import poll_once
|
|
from coordinator.queue import DebateQueue
|
|
from coordinator.state import SeenState
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fake client
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class FakeMulticaClient:
|
|
"""Minimal stand-in for MulticaClient.
|
|
|
|
``responses`` is a list of lists; each call to ``list_issues_by_status``
|
|
pops the next item from the front. If the list is exhausted the last
|
|
entry is repeated (steady-state behaviour).
|
|
"""
|
|
|
|
def __init__(self, responses: list[list[dict[str, Any]]]):
|
|
self._responses = list(responses)
|
|
|
|
def list_issues_by_status(self, status: str, limit: int = 200) -> list[dict[str, Any]]:
|
|
if len(self._responses) > 1:
|
|
return self._responses.pop(0)
|
|
return self._responses[0]
|
|
|
|
|
|
def _issue(iid: str, updated_at: str, identifier: str = "WYL-X", title: str = "T") -> dict:
|
|
return {
|
|
"id": iid,
|
|
"updated_at": updated_at,
|
|
"identifier": identifier,
|
|
"title": title,
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def setup(tmp_path):
|
|
"""Return (state, queue, logger) wired to tmp_path."""
|
|
state = SeenState.load(tmp_path / "seen.json")
|
|
queue = DebateQueue.load(tmp_path / "queue.json")
|
|
logger = logging.getLogger("test.watcher")
|
|
return state, queue, logger
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Scenario (a): first observation of a new in_review issue → enqueue
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_first_observation_enqueues(setup):
|
|
state, queue, logger = setup
|
|
client = FakeMulticaClient([[_issue("id-1", "2026-01-01T00:00:00Z", "WYL-1", "Title A")]])
|
|
|
|
poll_once(client, state, queue, logger)
|
|
|
|
assert len(queue.rounds) == 1
|
|
assert queue.rounds[0].issue_id == "id-1"
|
|
assert queue.rounds[0].identifier == "WYL-1"
|
|
assert queue.rounds[0].status == "pending"
|
|
assert state.last_seen("id-1") == "2026-01-01T00:00:00Z"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Scenario (b): same issue at same updated_at → no second enqueue
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_same_updated_at_no_reenqueue(setup):
|
|
state, queue, logger = setup
|
|
issue = _issue("id-1", "2026-01-01T00:00:00Z")
|
|
client = FakeMulticaClient([[issue]]) # steady-state
|
|
|
|
poll_once(client, state, queue, logger)
|
|
poll_once(client, state, queue, logger)
|
|
|
|
assert len(queue.rounds) == 1 # only the first poll fired
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Scenario (c): updated_at changes while round still pending → dedupe, no re-enqueue
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_updated_at_changes_while_pending_no_reenqueue(setup):
|
|
state, queue, logger = setup
|
|
client = FakeMulticaClient([
|
|
[_issue("id-1", "2026-01-01T00:00:00Z")], # first poll: enqueue
|
|
[_issue("id-1", "2026-01-01T00:01:00Z")], # second poll: new updated_at, but round pending
|
|
])
|
|
|
|
poll_once(client, state, queue, logger)
|
|
assert len(queue.rounds) == 1
|
|
|
|
poll_once(client, state, queue, logger)
|
|
assert len(queue.rounds) == 1 # dedupe fired, no second round
|
|
|
|
|
|
def test_updated_at_changes_while_running_no_reenqueue(setup):
|
|
state, queue, logger = setup
|
|
client = FakeMulticaClient([
|
|
[_issue("id-1", "2026-01-01T00:00:00Z")],
|
|
[_issue("id-1", "2026-01-01T00:01:00Z")],
|
|
])
|
|
|
|
poll_once(client, state, queue, logger)
|
|
queue.update_status(queue.rounds[0].round_id, "running")
|
|
|
|
poll_once(client, state, queue, logger)
|
|
assert len(queue.rounds) == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# After round completes, a *new* updated_at triggers a fresh round
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_reenqueue_after_round_done(setup):
|
|
state, queue, logger = setup
|
|
client = FakeMulticaClient([
|
|
[_issue("id-1", "2026-01-01T00:00:00Z")], # first poll
|
|
[_issue("id-1", "2026-01-01T00:02:00Z")], # third poll: new updated_at, round done
|
|
])
|
|
|
|
poll_once(client, state, queue, logger)
|
|
queue.update_status(queue.rounds[0].round_id, "done")
|
|
|
|
poll_once(client, state, queue, logger)
|
|
assert len(queue.rounds) == 2
|
|
assert queue.rounds[1].status == "pending"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Multiple distinct issues in one poll
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_multiple_issues_each_enqueued_once(setup):
|
|
state, queue, logger = setup
|
|
client = FakeMulticaClient([[
|
|
_issue("id-1", "2026-01-01T00:00:00Z", "WYL-1"),
|
|
_issue("id-2", "2026-01-01T00:00:00Z", "WYL-2"),
|
|
_issue("id-3", "2026-01-01T00:00:00Z", "WYL-3"),
|
|
]])
|
|
|
|
poll_once(client, state, queue, logger)
|
|
assert len(queue.rounds) == 3
|
|
|
|
|
|
def test_mix_new_and_seen(setup):
|
|
state, queue, logger = setup
|
|
client = FakeMulticaClient([
|
|
[_issue("id-1", "t1"), _issue("id-2", "t2")], # first poll: both new
|
|
[_issue("id-1", "t1"), _issue("id-2", "t2")], # second poll: same updated_at
|
|
])
|
|
|
|
poll_once(client, state, queue, logger)
|
|
assert len(queue.rounds) == 2
|
|
|
|
poll_once(client, state, queue, logger)
|
|
assert len(queue.rounds) == 2 # nothing new
|