mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
42bf9aecaf
Round-2 review caught 11 confirmed findings on the 3-commit metacog stack;
this commit applies them.
* **bug-1 (major)**: Wake source tag was leaking onto real user messages
flushed during a wake send. ``_append_user_turn`` and ``send`` now
take an explicit ``from_wake: bool`` parameter — only the wake's
synthesized first turn passes True, so ``_flush_queued_messages``'s
real user input no longer inherits the audit tag. Regression test
pins the contract.
* **perf-1 (major)**: ``CoordinatorIdleObserver._maybe_enqueue`` was
issuing list_workstreams + visible_memory_count storage queries
before the cheap cooldown gate could short-circuit. New
``_cooldown_allows`` read-only peek runs first; storage queries only
fire when cooldown actually allows the nudge.
* **q-1 (major)**: Added the missing coord-side integration test that
exercises ``CoordinatorIdleObserver`` + ``IdleNudgeWatcher`` together
in the production install order against a real ``SessionManager``,
protecting the subscription-order contract from silent regression.
* **perf-2/3 (minor)**: Cap check moved above ``_last_assistant_used_wait``;
``_fire_counts`` restructured as ``dict[str, dict[str, int]]`` keyed by
ws_id so the leave-IDLE existence check is O(1).
* **perf-4 (minor)**: ``NudgeQueue.drain`` fast-paths the all-match
case (the common one for chat-loop drain seams) by swapping
``self._items`` directly instead of allocating a fresh ``kept``
deque + per-entry append.
* **perf-5 (minor)**: Wake's synthesized empty user turn no longer
writes a content-empty row to the conversations table — the
``_source`` audit tag isn't column-backed and the side-channel
reminder is stripped before persist, so the row would carry nothing.
* **q-3 (minor)**: Split ``IdleNudgeWatcher`` + ``install_*`` /
``shutdown_*`` helpers out of ``metacognition.py`` into the new
``turnstone/core/idle_nudge_watcher.py``; metacog stays a
static-template module.
* **sec-1 (nit)**: Widened ``_sanitize_child_name``'s control-char
regex to cover Unicode bidi-overrides, zero-width chars,
line/paragraph separators, BOM, and tag chars.
* **q-4/q-5 (nits)**: Docstring referenced the wrong peek primitive
(``has_pending`` → ``len()``); ``_last_assistant_used_wait``'s
``session`` parameter now typed ``ChatSession``.
5571 non-live tests pass; ruff + mypy clean.
(cherry picked from commit 3f106f98b2)
166 lines
6.1 KiB
Python
166 lines
6.1 KiB
Python
"""Unit tests for :class:`IdleNudgeWatcher`.
|
|
|
|
Drives a fake :class:`SessionManager` that mimics the real one's
|
|
``subscribe_to_state`` / ``get`` contract. The watcher itself
|
|
dispatches via ``turnstone.core.session_worker.send``; we patch that
|
|
module-level function to capture calls without spawning real threads.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import threading
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from turnstone.core.idle_nudge_watcher import IdleNudgeWatcher
|
|
from turnstone.core.nudge_queue import NudgeQueue
|
|
from turnstone.core.workstream import WorkstreamState
|
|
|
|
|
|
class _FakeSession:
|
|
def __init__(self) -> None:
|
|
self._nudge_queue = NudgeQueue()
|
|
self.deliver_wake_nudge_from_queue_called = 0
|
|
|
|
def deliver_wake_nudge_from_queue(self) -> None:
|
|
self.deliver_wake_nudge_from_queue_called += 1
|
|
|
|
|
|
class _FakeWorkstream:
|
|
def __init__(self, ws_id: str = "ws-test") -> None:
|
|
self.id = ws_id
|
|
self.session: _FakeSession | None = _FakeSession()
|
|
self._lock = threading.Lock()
|
|
self._worker_running = False
|
|
self._closed = False
|
|
self.worker_thread: Any = None
|
|
|
|
|
|
class _FakeManager:
|
|
"""Mimics SessionManager's subscribe-to-state surface without a DB."""
|
|
|
|
def __init__(self) -> None:
|
|
self._workstreams: dict[str, _FakeWorkstream] = {}
|
|
self._subscribers: list[Any] = []
|
|
self._subscribers_lock = threading.Lock()
|
|
|
|
def add_ws(self, ws: _FakeWorkstream) -> None:
|
|
self._workstreams[ws.id] = ws
|
|
|
|
def get(self, ws_id: str) -> _FakeWorkstream | None:
|
|
return self._workstreams.get(ws_id)
|
|
|
|
def subscribe_to_state(self, callback: Any) -> None:
|
|
with self._subscribers_lock:
|
|
self._subscribers.append(callback)
|
|
|
|
def unsubscribe_from_state(self, callback: Any) -> None:
|
|
with self._subscribers_lock, contextlib.suppress(ValueError):
|
|
self._subscribers.remove(callback)
|
|
|
|
def fire_state(self, ws_id: str, state: WorkstreamState) -> None:
|
|
"""Mirror SessionManager.set_state's subscriber-fan-out behaviour."""
|
|
with self._subscribers_lock:
|
|
subs = list(self._subscribers)
|
|
for cb in subs:
|
|
# Match contextlib.suppress(Exception) in real SessionManager.
|
|
with contextlib.suppress(Exception):
|
|
cb(ws_id, state)
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_mgr_and_ws() -> tuple[_FakeManager, _FakeWorkstream]:
|
|
mgr = _FakeManager()
|
|
ws = _FakeWorkstream()
|
|
mgr.add_ws(ws)
|
|
return mgr, ws
|
|
|
|
|
|
class TestIdleNudgeWatcher:
|
|
def test_idle_event_with_empty_queue_no_op(self, fake_mgr_and_ws):
|
|
mgr, ws = fake_mgr_and_ws
|
|
watcher = IdleNudgeWatcher(mgr)
|
|
watcher.start()
|
|
with patch("turnstone.core.session_worker.send") as mock_send:
|
|
mgr.fire_state(ws.id, WorkstreamState.IDLE)
|
|
assert mock_send.call_count == 0
|
|
|
|
def test_idle_event_with_pending_nudge_dispatches(self, fake_mgr_and_ws):
|
|
mgr, ws = fake_mgr_and_ws
|
|
ws.session._nudge_queue.enqueue("idle_children", "your kids", "any")
|
|
watcher = IdleNudgeWatcher(mgr)
|
|
watcher.start()
|
|
with patch("turnstone.core.session_worker.send") as mock_send:
|
|
mgr.fire_state(ws.id, WorkstreamState.IDLE)
|
|
assert mock_send.call_count == 1
|
|
kwargs = mock_send.call_args.kwargs
|
|
# `enqueue=lambda: None` — verify by calling and checking no-op.
|
|
assert kwargs["enqueue"]() is None
|
|
# `run` should call deliver_wake_nudge_from_queue when invoked.
|
|
kwargs["run"]()
|
|
assert ws.session.deliver_wake_nudge_from_queue_called == 1
|
|
assert kwargs["thread_name"].startswith("wake-nudge-")
|
|
|
|
def test_non_idle_state_ignored(self, fake_mgr_and_ws):
|
|
mgr, ws = fake_mgr_and_ws
|
|
ws.session._nudge_queue.enqueue("foo", "bar", "any")
|
|
watcher = IdleNudgeWatcher(mgr)
|
|
watcher.start()
|
|
with patch("turnstone.core.session_worker.send") as mock_send:
|
|
for state in (
|
|
WorkstreamState.RUNNING,
|
|
WorkstreamState.THINKING,
|
|
WorkstreamState.ATTENTION,
|
|
WorkstreamState.ERROR,
|
|
):
|
|
mgr.fire_state(ws.id, state)
|
|
assert mock_send.call_count == 0
|
|
|
|
def test_unknown_ws_ignored(self, fake_mgr_and_ws):
|
|
mgr, _ws = fake_mgr_and_ws
|
|
watcher = IdleNudgeWatcher(mgr)
|
|
watcher.start()
|
|
with patch("turnstone.core.session_worker.send") as mock_send:
|
|
mgr.fire_state("ghost", WorkstreamState.IDLE)
|
|
assert mock_send.call_count == 0
|
|
|
|
def test_session_none_ignored(self, fake_mgr_and_ws):
|
|
mgr, ws = fake_mgr_and_ws
|
|
ws.session = None # workstream loaded but session not yet built
|
|
watcher = IdleNudgeWatcher(mgr)
|
|
watcher.start()
|
|
with patch("turnstone.core.session_worker.send") as mock_send:
|
|
mgr.fire_state(ws.id, WorkstreamState.IDLE)
|
|
assert mock_send.call_count == 0
|
|
|
|
def test_start_is_idempotent(self, fake_mgr_and_ws):
|
|
mgr, ws = fake_mgr_and_ws
|
|
watcher = IdleNudgeWatcher(mgr)
|
|
watcher.start()
|
|
watcher.start() # no-op
|
|
ws.session._nudge_queue.enqueue("foo", "bar", "any")
|
|
with patch("turnstone.core.session_worker.send") as mock_send:
|
|
mgr.fire_state(ws.id, WorkstreamState.IDLE)
|
|
# Only one subscriber was registered despite the double-start.
|
|
assert mock_send.call_count == 1
|
|
|
|
def test_shutdown_unsubscribes(self, fake_mgr_and_ws):
|
|
mgr, ws = fake_mgr_and_ws
|
|
ws.session._nudge_queue.enqueue("foo", "bar", "any")
|
|
watcher = IdleNudgeWatcher(mgr)
|
|
watcher.start()
|
|
watcher.shutdown()
|
|
with patch("turnstone.core.session_worker.send") as mock_send:
|
|
mgr.fire_state(ws.id, WorkstreamState.IDLE)
|
|
assert mock_send.call_count == 0
|
|
|
|
def test_shutdown_is_idempotent(self, fake_mgr_and_ws):
|
|
mgr, _ws = fake_mgr_and_ws
|
|
watcher = IdleNudgeWatcher(mgr)
|
|
watcher.start()
|
|
watcher.shutdown()
|
|
watcher.shutdown() # no error
|