Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: XNLLLLH <XNLLLLH@users.noreply.github.com>
168 lines
5.9 KiB
Python
168 lines
5.9 KiB
Python
"""Tests for core._inject_overnight_digest -- (DAEMON-11).
|
|
|
|
Covers 5 behaviours:
|
|
1. First memory_recall of the day (>18h since last shown) gets overnight_digest.
|
|
2. Second recall within <18h does NOT include overnight_digest.
|
|
3. Empty state / no pending digest -> no overnight_digest key.
|
|
4. Digest is cleared from state after one delivery (D-24 once-per-window).
|
|
5. Exception in get_pending_digest does NOT break memory_recall (silent fail).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def isolated_state(tmp_path, monkeypatch):
|
|
from iai_mcp import daemon_state
|
|
state_path = tmp_path / ".daemon-state.json"
|
|
monkeypatch.setattr(daemon_state, "STATE_PATH", state_path)
|
|
return state_path
|
|
|
|
|
|
# digest shape -- every required field populated.
|
|
_FULL_DIGEST = {
|
|
"rem_cycles_completed": 4,
|
|
"episodes_processed": 10,
|
|
"schemas_induced_tier0": 3,
|
|
"claude_call_used": True,
|
|
"quota_used_pct": 0.003,
|
|
"main_insight_text": "today's unifying insight",
|
|
"sigma_observed": 1.2,
|
|
"s5_drift_alerts": [],
|
|
"daemon_uptime_hours": 8,
|
|
"timed_out_cycles": 0,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 1: first recall of day gets digest
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_first_recall_gets_digest(isolated_state):
|
|
from iai_mcp.core import _inject_overnight_digest
|
|
from iai_mcp.daemon_state import save_state
|
|
|
|
# Seed state: pending digest + last shown 20h ago (past the 18h threshold).
|
|
now = datetime.now(timezone.utc)
|
|
save_state({
|
|
"pending_digest": dict(_FULL_DIGEST),
|
|
"last_digest_shown_at": (now - timedelta(hours=20)).isoformat(),
|
|
})
|
|
|
|
response: dict = {"hits": [], "anti_hits": [], "activation_trace": [], "budget_used": 0}
|
|
_inject_overnight_digest(response)
|
|
|
|
assert "overnight_digest" in response
|
|
dig = response["overnight_digest"]
|
|
# required fields surface.
|
|
assert dig["rem_cycles_completed"] == 4
|
|
assert dig["episodes_processed"] == 10
|
|
assert dig["schemas_induced_tier0"] == 3
|
|
assert dig["claude_call_used"] is True
|
|
assert dig["quota_used_pct"] == 0.003
|
|
assert dig["main_insight_text"] == "today's unifying insight"
|
|
assert dig["sigma_observed"] == 1.2
|
|
assert dig["s5_drift_alerts"] == []
|
|
assert dig["daemon_uptime_hours"] == 8
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 2: second recall within 18h window does NOT include digest
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_not_twice(isolated_state):
|
|
"""the same digest must not appear twice inside the 18h window."""
|
|
from iai_mcp.core import _inject_overnight_digest
|
|
from iai_mcp.daemon_state import save_state
|
|
|
|
now = datetime.now(timezone.utc)
|
|
# last_shown 4h ago -- inside the window.
|
|
save_state({
|
|
"pending_digest": dict(_FULL_DIGEST),
|
|
"last_digest_shown_at": (now - timedelta(hours=4)).isoformat(),
|
|
})
|
|
|
|
response: dict = {"hits": []}
|
|
_inject_overnight_digest(response)
|
|
assert "overnight_digest" not in response
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 3: no pending digest -> no key added
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_no_digest_when_none_pending(isolated_state):
|
|
from iai_mcp.core import _inject_overnight_digest
|
|
from iai_mcp.daemon_state import save_state
|
|
|
|
save_state({}) # empty state
|
|
response: dict = {"hits": []}
|
|
_inject_overnight_digest(response)
|
|
assert "overnight_digest" not in response
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 4: digest cleared from state after one delivery
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_digest_cleared_after_delivery(isolated_state):
|
|
"""after surfacing the digest, state must no longer carry
|
|
pending_digest so a subsequent recall (even after another 18h) does not
|
|
re-show the stale digest."""
|
|
from iai_mcp.core import _inject_overnight_digest
|
|
from iai_mcp.daemon_state import load_state, save_state
|
|
|
|
now = datetime.now(timezone.utc)
|
|
save_state({
|
|
"pending_digest": dict(_FULL_DIGEST),
|
|
"last_digest_shown_at": (now - timedelta(hours=20)).isoformat(),
|
|
})
|
|
|
|
response: dict = {"hits": []}
|
|
_inject_overnight_digest(response)
|
|
assert "overnight_digest" in response
|
|
|
|
# Persisted state: pending_digest consumed.
|
|
on_disk = load_state()
|
|
assert "pending_digest" not in on_disk
|
|
# last_digest_shown_at advanced to roughly now.
|
|
shown_at = datetime.fromisoformat(on_disk["last_digest_shown_at"])
|
|
if shown_at.tzinfo is None:
|
|
shown_at = shown_at.replace(tzinfo=timezone.utc)
|
|
assert shown_at >= now - timedelta(seconds=5)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 5: exception in get_pending_digest does NOT break memory_recall
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_exception_is_silent(isolated_state, monkeypatch):
|
|
"""If get_pending_digest raises (corrupt state, unexpected schema), the
|
|
response must still be returned without an overnight_digest key. The
|
|
memory_recall hot path NEVER breaks on daemon-digest faults."""
|
|
from iai_mcp import core
|
|
|
|
def boom(*args, **kwargs):
|
|
raise RuntimeError("simulated state corruption")
|
|
|
|
monkeypatch.setattr("iai_mcp.core.get_pending_digest", boom)
|
|
|
|
response: dict = {"hits": [], "existing": True}
|
|
# Must not raise.
|
|
core._inject_overnight_digest(response)
|
|
assert response.get("existing") is True
|
|
assert "overnight_digest" not in response
|