Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: XNLLLLH <XNLLLLH@users.noreply.github.com>
248 lines
8.6 KiB
Python
248 lines
8.6 KiB
Python
"""Tests for the session-start assembler (D-10, OPS-01, OPS-05).
|
|
|
|
Plan 05-03 D5-02: the DEFAULT wake_depth flipped to `minimal` (lazy <=30
|
|
tok payload). Tests that assert Phase-1 eager-dump behaviour now pass
|
|
``profile_state={"wake_depth": "standard"}`` explicitly to continue
|
|
exercising the back-compat legacy path.
|
|
|
|
Covers:
|
|
- Graceful empty-store path (total_cached_tokens == 0, l0 == "").
|
|
- L0 identity rendering -- "IAI-MCP" appears in payload.l0 when seeded.
|
|
- Total cached budget respected (<= 2000 tok) on realistic pinned content.
|
|
- L2 community cap at 7 (CONN-01 Yeo-like).
|
|
- Rich-club segment truncation at 1500-tok budget.
|
|
- core.py `session_start_payload` dispatch wiring.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from uuid import UUID, uuid4
|
|
|
|
from iai_mcp.community import CommunityAssignment
|
|
from iai_mcp.core import _seed_l0_identity, dispatch
|
|
from iai_mcp.session import (
|
|
L0_RECORD_UUID,
|
|
L2_COMMUNITY_CAP,
|
|
RICH_CLUB_BUDGET_TOKENS,
|
|
TOTAL_CACHED_BUDGET,
|
|
SessionStartPayload,
|
|
_approx_tokens,
|
|
assemble_session_start,
|
|
)
|
|
from iai_mcp.store import MemoryStore
|
|
from iai_mcp.types import EMBED_DIM, MemoryRecord
|
|
|
|
|
|
# D5-02: Phase-1 eager behaviour lives behind wake_depth="standard"
|
|
# now that the default flipped to "minimal". Legacy tests opt in explicitly.
|
|
_STANDARD = {"wake_depth": "standard"}
|
|
|
|
|
|
# ------------------------------------------------------------- helpers
|
|
|
|
|
|
def _l0_record(store: MemoryStore) -> None:
|
|
"""Seed the fixed-UUID L0 identity record (matches core._seed_l0_identity)."""
|
|
_seed_l0_identity(store)
|
|
|
|
|
|
def _pinned_record(
|
|
store: MemoryStore,
|
|
text: str,
|
|
community_id: UUID | None = None,
|
|
tags: list[str] | None = None,
|
|
) -> MemoryRecord:
|
|
r = MemoryRecord(
|
|
id=uuid4(),
|
|
tier="semantic",
|
|
literal_surface=text,
|
|
aaak_index="",
|
|
embedding=[0.1] * EMBED_DIM,
|
|
community_id=community_id,
|
|
centrality=0.5,
|
|
detail_level=5,
|
|
pinned=True,
|
|
stability=0.0,
|
|
difficulty=0.0,
|
|
last_reviewed=None,
|
|
never_decay=True,
|
|
never_merge=False,
|
|
provenance=[],
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc),
|
|
tags=list(tags) if tags else [],
|
|
language="en",
|
|
)
|
|
store.insert(r)
|
|
return r
|
|
|
|
|
|
# -------------------------------------------------- graceful empty-store path
|
|
|
|
|
|
def test_empty_store_graceful(tmp_path):
|
|
"""Empty store -> all segments empty, token totals zero on cached.
|
|
|
|
assert on the standard (Phase-1) path — minimal mode would
|
|
emit pointer handles even on empty stores, which is by design.
|
|
"""
|
|
store = MemoryStore(path=tmp_path)
|
|
payload = assemble_session_start(
|
|
store, CommunityAssignment(), [], profile_state=_STANDARD,
|
|
)
|
|
assert payload.l0 == ""
|
|
assert payload.l1 == ""
|
|
assert payload.l2 == []
|
|
assert payload.rich_club == ""
|
|
assert payload.total_cached_tokens == 0
|
|
# Dynamic tail is a fixed reserve even on empty stores.
|
|
assert payload.total_dynamic_tokens > 0
|
|
|
|
|
|
# ---------------------------------------------------------- identity
|
|
|
|
|
|
def test_l0_renders_identity(tmp_path):
|
|
"""a seeded L0 record puts 'IAI-MCP' into the L0 segment (standard mode)."""
|
|
store = MemoryStore(path=tmp_path)
|
|
_l0_record(store)
|
|
payload = assemble_session_start(
|
|
store, CommunityAssignment(), [], profile_state=_STANDARD,
|
|
)
|
|
assert "IAI-MCP" in payload.l0
|
|
|
|
|
|
def test_l0_uses_fixed_uuid(tmp_path):
|
|
"""The assembler MUST read from the canonical L0 UUID. Standard mode."""
|
|
store = MemoryStore(path=tmp_path)
|
|
_l0_record(store)
|
|
# Confirm the seed landed at the fixed UUID, not some random new UUID.
|
|
assert store.get(L0_RECORD_UUID) is not None
|
|
payload = assemble_session_start(
|
|
store, CommunityAssignment(), [], profile_state=_STANDARD,
|
|
)
|
|
assert payload.l0 != ""
|
|
|
|
|
|
def test_l0_segment_excludes_literal_only_at_cap(tmp_path):
|
|
"""L0 segment contains aaak_index header plus the literal (truncated if long)."""
|
|
store = MemoryStore(path=tmp_path)
|
|
_l0_record(store)
|
|
payload = assemble_session_start(
|
|
store, CommunityAssignment(), [], profile_state=_STANDARD,
|
|
)
|
|
# The L0 record's aaak_index is stamped at seed time -> shows up in payload.
|
|
assert "W:" in payload.l0 # wing marker from generate_aaak_index
|
|
|
|
|
|
# ------------------------------------------------------------- budget
|
|
|
|
|
|
def test_total_cached_budget_respected(tmp_path):
|
|
"""L0 + L1 + L2 + rich_club <= TOTAL_CACHED_BUDGET (2000 tok)."""
|
|
store = MemoryStore(path=tmp_path)
|
|
_l0_record(store)
|
|
# 10 pinned L1 records with reasonable short content.
|
|
for i in range(10):
|
|
_pinned_record(store, f"Pinned fact #{i}: short verbatim content here.")
|
|
payload = assemble_session_start(store, CommunityAssignment(), [])
|
|
assert payload.total_cached_tokens <= TOTAL_CACHED_BUDGET
|
|
|
|
|
|
def test_l1_caps_at_max_records(tmp_path):
|
|
"""L1 segment stays bounded even with many pinned records (10-entry cap)."""
|
|
store = MemoryStore(path=tmp_path)
|
|
_l0_record(store)
|
|
# Seed 20 pinned records -- L1 should truncate to 10.
|
|
for i in range(20):
|
|
_pinned_record(store, f"Pinned fact #{i}")
|
|
payload = assemble_session_start(store, CommunityAssignment(), [])
|
|
l1_lines = payload.l1.split("\n") if payload.l1 else []
|
|
assert len(l1_lines) <= 10
|
|
|
|
|
|
# -------------------------------------------------- CONN-01 Yeo-like cap
|
|
|
|
|
|
def test_l2_capped_at_seven(tmp_path):
|
|
"""CONN-01: L2 summaries never exceed 7 regardless of input community count."""
|
|
store = MemoryStore(path=tmp_path)
|
|
# Create 10 fake communities each with one member record.
|
|
assignment = CommunityAssignment()
|
|
for i in range(10):
|
|
cid = uuid4()
|
|
rec = _pinned_record(
|
|
store, f"member of community {i}", community_id=cid
|
|
)
|
|
assignment.top_communities.append(cid)
|
|
assignment.mid_regions[cid] = [rec.id]
|
|
assignment.community_centroids[cid] = [0.0] * EMBED_DIM
|
|
payload = assemble_session_start(store, assignment, [])
|
|
assert len(payload.l2) <= L2_COMMUNITY_CAP
|
|
assert L2_COMMUNITY_CAP == 7
|
|
|
|
|
|
# ----------------------------------------------- rich-club budget truncation
|
|
|
|
|
|
def test_rich_club_truncation_under_budget(tmp_path):
|
|
"""Passing 50 records with long surfaces still keeps rich_club <= 1500 tok."""
|
|
store = MemoryStore(path=tmp_path)
|
|
# Build 50 records with ~300 chars each (~75 tok each).
|
|
rich_uuids: list[UUID] = []
|
|
for i in range(50):
|
|
r = _pinned_record(store, f"rich-club entry {i}: " + ("x" * 280))
|
|
rich_uuids.append(r.id)
|
|
payload = assemble_session_start(store, CommunityAssignment(), rich_uuids)
|
|
assert _approx_tokens(payload.rich_club) <= RICH_CLUB_BUDGET_TOKENS
|
|
|
|
|
|
# ---------------------------------------------- core.py dispatch integration
|
|
|
|
|
|
def test_session_start_payload_dispatch_empty(tmp_path):
|
|
"""core.dispatch('session_start_payload') returns the canonical shape even on empty store."""
|
|
store = MemoryStore(path=tmp_path)
|
|
result = dispatch(store, "session_start_payload", {})
|
|
# Shape keys are all present regardless of whether the store is populated.
|
|
for key in (
|
|
"l0",
|
|
"l1",
|
|
"l2",
|
|
"rich_club",
|
|
"total_cached_tokens",
|
|
"total_dynamic_tokens",
|
|
"breakpoint_marker",
|
|
):
|
|
assert key in result
|
|
# On a fresh store the L0 segment is empty (no seed yet).
|
|
assert result["l0"] == ""
|
|
assert result["total_cached_tokens"] == 0
|
|
|
|
|
|
def test_session_start_payload_dispatch_with_l0(tmp_path):
|
|
"""Once L0 is seeded, dispatch returns identity content.
|
|
|
|
D5-10: per-process wake_depth stays at the 'minimal' default,
|
|
so we temporarily flip it to 'standard' for this back-compat assertion
|
|
and restore afterwards. Thread-safety is not a concern for unit tests.
|
|
"""
|
|
import iai_mcp.core as core
|
|
original = core._profile_state.get("wake_depth", "minimal")
|
|
core._profile_state["wake_depth"] = "standard"
|
|
try:
|
|
store = MemoryStore(path=tmp_path)
|
|
_seed_l0_identity(store)
|
|
result = dispatch(store, "session_start_payload", {})
|
|
assert "IAI-MCP" in result["l0"]
|
|
assert result["breakpoint_marker"] == "--<cache-breakpoint>--"
|
|
finally:
|
|
core._profile_state["wake_depth"] = original
|
|
|
|
|
|
def test_payload_type_is_session_start_payload(tmp_path):
|
|
"""Direct assemble_session_start returns a SessionStartPayload instance."""
|
|
store = MemoryStore(path=tmp_path)
|
|
payload = assemble_session_start(store, CommunityAssignment(), [])
|
|
assert isinstance(payload, SessionStartPayload)
|
|
assert payload.breakpoint_marker == "--<cache-breakpoint>--"
|