deer-flow/backend/tests/test_schedule_thread_lookup.py
rayhpeng c38d291505 refactor(schedule): standardize the module on the hexagonal architecture
Replaces the pre-hexagonal scheduled-task implementation with a slice
built to the layering spec: a pure domain (two aggregates, two state
machines, the policy value object), output ports it declares itself,
SQL/launcher/thread adapters implementing them under `app/adapters/`,
and a composition root that is the one place any of them is
instantiated.

The old implementation mixed all of that into `app/scheduler/service.py`
and a router that reached straight into repositories, so the rules that
matter -- overlap policy, lease handling, which write owns which
timestamp -- were only reachable through a live database. They are now
unit-assertable on in-memory fakes, with the contract suite running each
port against both the fake and real sqlite, and the concurrency
invariants pinned by dedicated race tests.

Two bugs the old shape hid are fixed on the way: a completion hook that
replayed a stale snapshot and rolled back the launch write, and a
corrupt stored row surfacing to the client as a 4xx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 11:16:46 +08:00

72 lines
3.1 KiB
Python

"""Contract tests for the thread-lookup anti-corruption layer.
The port asks one question -- "does this thread exist AND may this user use
it?" -- and deliberately answers both halves with a single bool, so a caller
cannot probe for the existence of threads they cannot see. These tests pin that
the adapter does not accidentally widen it back into two answers.
"""
from __future__ import annotations
import pytest
from app.adapters.schedule.thread_lookup import ThreadStoreThreadLookup
class _RecordingThreadStore:
"""Stands in for `ThreadMetaStore`, recording how it was asked."""
def __init__(self, owners: dict[str, str] | None = None) -> None:
self._owners = dict(owners or {})
self.calls: list[tuple[str, str, bool]] = []
async def check_access(self, thread_id: str, user_id: str, *, require_existing: bool = False) -> bool:
self.calls.append((thread_id, user_id, require_existing))
owner = self._owners.get(thread_id)
if owner is None:
# Mirrors the real store: absent rows pass a non-strict check.
return not require_existing
return owner == user_id
class TestTheOneQuestion:
@pytest.mark.asyncio
async def test_an_owned_thread_exists_for_its_user(self):
lookup = ThreadStoreThreadLookup(_RecordingThreadStore({"thread-1": "user-1"}))
assert await lookup.exists_for_user("thread-1", "user-1") is True
@pytest.mark.asyncio
async def test_someone_elses_thread_does_not(self):
lookup = ThreadStoreThreadLookup(_RecordingThreadStore({"thread-1": "user-1"}))
assert await lookup.exists_for_user("thread-1", "user-2") is False
@pytest.mark.asyncio
async def test_a_missing_thread_does_not(self):
"""This is the half `require_existing` buys: without it the store
treats an absent row as accessible, and a task could be bound to a
thread that does not exist."""
lookup = ThreadStoreThreadLookup(_RecordingThreadStore())
assert await lookup.exists_for_user("thread-nope", "user-1") is False
@pytest.mark.asyncio
async def test_missing_and_forbidden_are_indistinguishable(self):
lookup = ThreadStoreThreadLookup(_RecordingThreadStore({"thread-1": "user-1"}))
missing = await lookup.exists_for_user("thread-nope", "user-2")
forbidden = await lookup.exists_for_user("thread-1", "user-2")
assert missing == forbidden is False
class TestHowTheStoreIsAsked:
@pytest.mark.asyncio
async def test_require_existing_is_always_set(self):
store = _RecordingThreadStore({"thread-1": "user-1"})
await ThreadStoreThreadLookup(store).exists_for_user("thread-1", "user-1")
assert store.calls == [("thread-1", "user-1", True)]
@pytest.mark.asyncio
async def test_the_result_is_a_real_bool(self):
"""The port is typed `bool`; a truthy row object leaking through would
satisfy the domain's `if` and still be the wrong contract."""
store = _RecordingThreadStore({"thread-1": "user-1"})
assert await ThreadStoreThreadLookup(store).exists_for_user("thread-1", "user-1") is True