"""Unit tests for navi.core.task_notes — pending background-task completion notes."""
import pytest
from navi.core import task_notes
class FakeKv:
"""In-memory stand-in for KvStore.get/set."""
def __init__(self):
self.data = {}
async def get(self, user_id, session_id, scope, key):
return self.data.get((user_id, session_id, scope, key))
async def set(self, user_id, session_id, scope, key, value):
self.data[(user_id, session_id, scope, key)] = value
def patch_settings(monkeypatch, **overrides):
"""Replace the frozen settings singleton with an overridden copy."""
import navi.config as config_mod
from navi.config import Settings
new_settings = Settings(**overrides)
monkeypatch.setattr(config_mod, "settings", new_settings)
return new_settings
@pytest.fixture(autouse=True)
def kv(monkeypatch):
store = FakeKv()
task_notes.set_kv_store(store)
yield store
task_notes.set_kv_store(None)
def make_job(task_id="bt-ab12", session_id="s1", status="completed",
preview_text="done 42", tokens=None):
class Job:
pass
job = Job()
job.task_id = task_id
job.session_id = session_id
job.tool = "terminal"
job.status = status
job.subagent_tokens = tokens
job.preview = lambda limit=800: preview_text
return job
class TestAddNote:
async def test_note_recorded(self, kv):
await task_notes.add_note(make_job())
assert await task_notes.pending_count("s1") == 1
async def test_cap_drops_oldest(self, kv, monkeypatch):
patch_settings(monkeypatch, task_notes_max_pending=2)
for i in range(4):
await task_notes.add_note(make_job(task_id=f"bt-{i}"))
assert await task_notes.pending_count("s1") == 2
notes = await task_notes._load("s1")
assert [n["task_id"] for n in notes] == ["bt-2", "bt-3"]
async def test_no_store_is_noop(self, monkeypatch):
task_notes.set_kv_store(None)
await task_notes.add_note(make_job()) # must not raise
assert await task_notes.drain("s1") is None
class TestDrain:
async def test_drain_coalesces_and_empties(self, kv):
await task_notes.add_note(make_job(task_id="bt-a", preview_text="42 files"))
await task_notes.add_note(make_job(task_id="bt-b", status="failed",
preview_text="boom"))
text = await task_notes.drain("s1")
assert text is not None
assert "[Background task results]" in text
assert "bt-a (terminal) completed: 42 files" in text
assert "bt-b (terminal) failed: boom" in text
assert "Do not start new background tasks" in text
# queue is now empty
assert await task_notes.drain("s1") is None
assert await task_notes.pending_count("s1") == 0
async def test_drain_limits_per_turn(self, kv, monkeypatch):
patch_settings(monkeypatch, task_notes_per_turn=2)
for i in range(3):
await task_notes.add_note(make_job(task_id=f"bt-{i}"))
text = await task_notes.drain("s1")
assert "bt-0" in text and "bt-1" in text
assert "bt-2" not in text.split("older result")[0]
assert "1 older result(s)" in text
# remaining note survives for the next turn
assert await task_notes.pending_count("s1") == 1
text2 = await task_notes.drain("s1")
assert "bt-2" in text2
async def test_drain_scoped_by_session(self, kv):
await task_notes.add_note(make_job(session_id="s1"))
assert await task_notes.drain("s2") is None
assert await task_notes.drain("s1") is not None
async def test_empty_preview_placeholder(self, kv):
await task_notes.add_note(make_job(preview_text=""))
text = await task_notes.drain("s1")
assert "(no output)" in text
async def test_subagent_tokens_recorded(self, kv):
await task_notes.add_note(make_job(tokens=1234))
notes = await task_notes._load("s1")
assert notes[0]["subagent_tokens"] == 1234