Independent FastAPI backend for the MaskanX agentic growth platform. Includes the agent runtime, MCP client integrations (Meta Ads, LinkedIn, HubSpot, Tavily, Exa, xAI, Citedy, image generation), PostgreSQL storage for chats and cron jobs, provider and secret management, and the CLI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
657 lines
23 KiB
Python
657 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests for 2c: Session Context Management — Smart Compaction.
|
|
|
|
Covers all 4 modules:
|
|
- importance.py: message importance classification
|
|
- tiered_compaction.py: L0/L1/L2 survival logic
|
|
- topic_summarizer.py: topic clustering + structured prompts
|
|
- session_bridge.py: cross-session context injection
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import List
|
|
|
|
from adclaw.agents.memory.importance import (
|
|
Importance,
|
|
classify_importance,
|
|
tag_messages,
|
|
)
|
|
from adclaw.agents.memory.tiered_compaction import (
|
|
IMPORTANCE_TO_TIER,
|
|
plan_compaction,
|
|
)
|
|
from adclaw.agents.memory.topic_summarizer import (
|
|
TopicCluster,
|
|
build_structured_summary_prompt,
|
|
cluster_by_topic,
|
|
)
|
|
from adclaw.agents.memory.session_bridge import (
|
|
SessionSummary,
|
|
build_prior_knowledge_section,
|
|
build_session_summary,
|
|
extract_tagged_lines,
|
|
staleness_label,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class FakeMsg:
|
|
"""Minimal Msg stand-in for testing."""
|
|
|
|
id: str = "test"
|
|
role: str = "user"
|
|
content: str | list = ""
|
|
|
|
def get_text_content(self) -> str:
|
|
if self.content is None:
|
|
return ""
|
|
if isinstance(self.content, str):
|
|
return self.content
|
|
# Extract text from content blocks
|
|
parts: List[str] = []
|
|
for block in self.content:
|
|
if isinstance(block, str):
|
|
parts.append(block)
|
|
elif isinstance(block, dict) and "text" in block:
|
|
parts.append(block["text"])
|
|
return " ".join(parts)
|
|
|
|
|
|
def _msg(
|
|
content: str = "",
|
|
role: str = "user",
|
|
msg_id: str = "test",
|
|
) -> FakeMsg:
|
|
return FakeMsg(id=msg_id, role=role, content=content)
|
|
|
|
|
|
def _tool_msg(
|
|
tool_name: str = "execute_shell_command",
|
|
msg_id: str = "tool1",
|
|
) -> FakeMsg:
|
|
return FakeMsg(
|
|
id=msg_id,
|
|
role="assistant",
|
|
content=[{"type": "tool_use", "name": tool_name, "input": {}}],
|
|
)
|
|
|
|
|
|
# ===================================================================
|
|
# 1. Importance Classification
|
|
# ===================================================================
|
|
|
|
|
|
class TestClassifyImportance:
|
|
def test_system_messages_always_critical(self):
|
|
msg = _msg("You are a helpful assistant", role="system")
|
|
assert classify_importance(msg) == Importance.CRITICAL
|
|
|
|
def test_decision_keywords(self):
|
|
msg = _msg("I decided to use glm-5 for production")
|
|
assert classify_importance(msg) == Importance.CRITICAL
|
|
|
|
def test_config_change_is_critical(self):
|
|
msg = _msg("Config changed: max_input_length set to 64000")
|
|
assert classify_importance(msg) == Importance.CRITICAL
|
|
|
|
def test_never_avoid_rule(self):
|
|
msg = _msg("Never use qwen-max, always use glm-5 to avoid timeouts")
|
|
assert classify_importance(msg) == Importance.CRITICAL
|
|
|
|
def test_from_now_on_is_critical(self):
|
|
msg = _msg("From now on, deploy to staging first")
|
|
assert classify_importance(msg) == Importance.CRITICAL
|
|
|
|
def test_error_is_high(self):
|
|
msg = _msg("Error: connection refused on port 8088")
|
|
assert classify_importance(msg) == Importance.HIGH
|
|
|
|
def test_failed_attempt_is_high(self):
|
|
msg = _msg("Tried qwen-max but it didn't work on Coding Plan")
|
|
assert classify_importance(msg) == Importance.HIGH
|
|
|
|
def test_fix_is_high(self):
|
|
msg = _msg("Fixed the deployment script issue")
|
|
assert classify_importance(msg) == Importance.HIGH
|
|
|
|
def test_todo_is_high(self):
|
|
msg = _msg("TODO: update the API key before next deploy")
|
|
assert classify_importance(msg) == Importance.HIGH
|
|
|
|
def test_acknowledgment_is_low(self):
|
|
msg = _msg("ok thanks", role="user")
|
|
assert classify_importance(msg) == Importance.LOW
|
|
|
|
def test_yes_is_low(self):
|
|
msg = _msg("yes", role="user")
|
|
assert classify_importance(msg) == Importance.LOW
|
|
|
|
def test_done_is_low(self):
|
|
msg = _msg("done", role="user")
|
|
assert classify_importance(msg) == Importance.LOW
|
|
|
|
def test_short_assistant_response_is_low(self):
|
|
msg = _msg("Done.", role="assistant")
|
|
assert classify_importance(msg) == Importance.LOW
|
|
|
|
def test_normal_message_is_medium(self):
|
|
msg = _msg("Can you help me set up the deployment pipeline?")
|
|
assert classify_importance(msg) == Importance.MEDIUM
|
|
|
|
def test_tool_message_is_medium(self):
|
|
msg = _tool_msg()
|
|
assert classify_importance(msg) == Importance.MEDIUM
|
|
|
|
def test_long_low_pattern_not_matched(self):
|
|
# "ok" at start but message is > 100 chars — should not match LOW
|
|
msg = _msg("ok " + "x" * 100, role="user")
|
|
assert classify_importance(msg) == Importance.MEDIUM
|
|
|
|
def test_slash_command_is_critical(self):
|
|
msg = _msg("/compact the memory")
|
|
assert classify_importance(msg) == Importance.CRITICAL
|
|
|
|
def test_empty_message_is_trivial(self):
|
|
msg = _msg("", role="user")
|
|
assert classify_importance(msg) == Importance.TRIVIAL
|
|
|
|
def test_whitespace_only_is_trivial(self):
|
|
msg = _msg(" ", role="assistant")
|
|
assert classify_importance(msg) == Importance.TRIVIAL
|
|
|
|
def test_single_char_is_trivial(self):
|
|
msg = _msg(".", role="user")
|
|
assert classify_importance(msg) == Importance.TRIVIAL
|
|
|
|
|
|
class TestTagMessages:
|
|
def test_returns_dict_with_correct_ids(self):
|
|
msgs = [
|
|
_msg("decided to use X", msg_id="1"),
|
|
_msg("ok", msg_id="2"),
|
|
_msg("error in build", msg_id="3"),
|
|
]
|
|
tags = tag_messages(msgs)
|
|
assert tags["1"] == Importance.CRITICAL
|
|
assert tags["2"] == Importance.LOW
|
|
assert tags["3"] == Importance.HIGH
|
|
|
|
def test_empty_list(self):
|
|
assert tag_messages([]) == {}
|
|
|
|
|
|
# ===================================================================
|
|
# 2. Tiered Compaction
|
|
# ===================================================================
|
|
|
|
|
|
class TestTieredCompaction:
|
|
def test_l2_compacted_immediately(self):
|
|
msgs = [
|
|
_msg("ok thanks", msg_id="ack1"),
|
|
_msg("sure", msg_id="ack2"),
|
|
_msg("I decided to use glm-5", msg_id="decision1"),
|
|
]
|
|
plan = plan_compaction(
|
|
messages=msgs,
|
|
cycle_counts={"ack1": 0, "ack2": 0, "decision1": 0},
|
|
current_cycle=0,
|
|
)
|
|
compact_ids = {m.id for m in plan.to_compact}
|
|
assert "ack1" in compact_ids
|
|
assert "ack2" in compact_ids
|
|
# L0 (CRITICAL) preserved
|
|
assert "decision1" not in compact_ids
|
|
|
|
def test_l1_survives_two_cycles(self):
|
|
msgs = [_msg("Error: connection failed", msg_id="err1")]
|
|
|
|
# Cycle 0: should survive
|
|
plan = plan_compaction(msgs, {"err1": 0}, current_cycle=0)
|
|
assert len(plan.to_compact) == 0
|
|
|
|
# Cycle 1: should still survive
|
|
plan = plan_compaction(msgs, {"err1": 0}, current_cycle=1)
|
|
assert len(plan.to_compact) == 0
|
|
|
|
# Cycle 2: should be compacted
|
|
plan = plan_compaction(msgs, {"err1": 0}, current_cycle=2)
|
|
assert len(plan.to_compact) == 1
|
|
|
|
def test_l0_never_compacted(self):
|
|
msgs = [_msg("I decided to deploy to production", msg_id="d1")]
|
|
plan = plan_compaction(msgs, {"d1": 0}, current_cycle=100)
|
|
assert len(plan.to_compact) == 0
|
|
assert len(plan.to_preserve) == 1
|
|
|
|
def test_new_messages_default_to_current_cycle(self):
|
|
msgs = [_msg("ok", msg_id="new1")]
|
|
# Not in cycle_counts — defaults to current_cycle
|
|
plan = plan_compaction(msgs, {}, current_cycle=5)
|
|
# L2 with age=0, survival=0 → compacted
|
|
compact_ids = {m.id for m in plan.to_compact}
|
|
assert "new1" in compact_ids
|
|
|
|
def test_stats_populated(self):
|
|
msgs = [
|
|
_msg("decided X", msg_id="c1"),
|
|
_msg("error Y", msg_id="h1"),
|
|
_msg("ok", msg_id="l1"),
|
|
]
|
|
plan = plan_compaction(msgs, {}, current_cycle=0)
|
|
assert plan.stats["total"] == 3
|
|
assert plan.stats["tier_L0"] == 1
|
|
assert plan.stats["tier_L1"] == 1
|
|
assert plan.stats["tier_L2"] == 1
|
|
|
|
def test_empty_messages(self):
|
|
plan = plan_compaction([], {}, current_cycle=0)
|
|
assert plan.to_compact == []
|
|
assert plan.to_preserve == []
|
|
|
|
def test_importance_to_tier_complete(self):
|
|
for imp in Importance:
|
|
assert imp in IMPORTANCE_TO_TIER
|
|
|
|
|
|
# ===================================================================
|
|
# 3. Topic Clustering & Summarization
|
|
# ===================================================================
|
|
|
|
|
|
class TestTopicClustering:
|
|
def test_groups_consecutive_same_topic(self):
|
|
msgs = [
|
|
_msg("There's a bug in the config", msg_id="1"),
|
|
_msg("Error: missing key in config.json", msg_id="2"),
|
|
_msg("Let me deploy the fix", msg_id="3"),
|
|
]
|
|
clusters = cluster_by_topic(msgs)
|
|
assert len(clusters) >= 1
|
|
# All messages should be covered
|
|
total = sum(len(c.messages) for c in clusters)
|
|
assert total == 3
|
|
|
|
def test_failure_detection(self):
|
|
msgs = [_msg("Tried X but it failed with error Y", msg_id="1")]
|
|
clusters = cluster_by_topic(msgs)
|
|
assert clusters[0].has_failure is True
|
|
|
|
def test_no_failure_when_clean(self):
|
|
msgs = [_msg("Deployed the new version successfully", msg_id="1")]
|
|
clusters = cluster_by_topic(msgs)
|
|
assert clusters[0].has_failure is False
|
|
|
|
def test_tool_messages_clustered(self):
|
|
msgs = [
|
|
_tool_msg("read_file", msg_id="t1"),
|
|
_tool_msg("write_file", msg_id="t2"),
|
|
_msg("Checking the tests", msg_id="m1"),
|
|
]
|
|
clusters = cluster_by_topic(msgs)
|
|
# First two should be in "file-ops" cluster
|
|
assert clusters[0].topic == "file-ops"
|
|
assert len(clusters[0].messages) == 2
|
|
|
|
def test_empty_messages(self):
|
|
assert not cluster_by_topic([])
|
|
|
|
def test_single_message(self):
|
|
msgs = [_msg("hello world", msg_id="1")]
|
|
clusters = cluster_by_topic(msgs)
|
|
assert len(clusters) == 1
|
|
assert clusters[0].topic == "general"
|
|
|
|
def test_general_fallback_when_no_hints(self):
|
|
msgs = [
|
|
_msg("How are you?", msg_id="1"),
|
|
_msg("I'm fine", msg_id="2"),
|
|
]
|
|
clusters = cluster_by_topic(msgs)
|
|
assert all(c.topic == "general" for c in clusters)
|
|
|
|
|
|
class TestStructuredSummaryPrompt:
|
|
def test_basic_prompt_structure(self):
|
|
cluster = TopicCluster(
|
|
topic="debugging",
|
|
messages=[_msg("Error in deployment", msg_id="1")],
|
|
max_importance=Importance.HIGH,
|
|
has_failure=True,
|
|
)
|
|
prompt = build_structured_summary_prompt([cluster])
|
|
assert "debugging" in prompt
|
|
assert "[CONTAINS FAILURES]" in prompt
|
|
assert "DECISION:" in prompt
|
|
assert "FAILED:" in prompt
|
|
assert "Topic Map" in prompt
|
|
|
|
def test_previous_summary_included(self):
|
|
cluster = TopicCluster(
|
|
topic="general",
|
|
messages=[_msg("hello", msg_id="1")],
|
|
max_importance=Importance.MEDIUM,
|
|
)
|
|
prompt = build_structured_summary_prompt(
|
|
[cluster],
|
|
previous_summary="User was working on deployment",
|
|
)
|
|
assert "Prior Context" in prompt
|
|
assert "deployment" in prompt
|
|
|
|
def test_no_message_content_in_prompt(self):
|
|
"""Prompt should NOT embed message content to avoid double tokens."""
|
|
cluster = TopicCluster(
|
|
topic="general",
|
|
messages=[_msg("secret message content xyz", msg_id="1")],
|
|
max_importance=Importance.MEDIUM,
|
|
)
|
|
prompt = build_structured_summary_prompt([cluster])
|
|
assert "secret message content xyz" not in prompt
|
|
assert "1 messages" in prompt
|
|
|
|
def test_empty_clusters(self):
|
|
prompt = build_structured_summary_prompt([])
|
|
assert "Summarize" in prompt
|
|
|
|
|
|
# ===================================================================
|
|
# 4. Session Bridge
|
|
# ===================================================================
|
|
|
|
|
|
class TestStalenessLabel:
|
|
def test_just_now(self):
|
|
assert staleness_label(time.time() - 60) == "just now"
|
|
|
|
def test_hours_ago(self):
|
|
label = staleness_label(time.time() - 7200)
|
|
assert "h ago" in label
|
|
|
|
def test_days_ago(self):
|
|
label = staleness_label(time.time() - 172800)
|
|
assert "d ago" in label
|
|
|
|
def test_weeks_ago_outdated(self):
|
|
label = staleness_label(time.time() - 1209600)
|
|
assert "outdated" in label
|
|
|
|
def test_minimum_1h(self):
|
|
label = staleness_label(time.time() - 3700)
|
|
assert label == "1h ago"
|
|
|
|
def test_minimum_1d(self):
|
|
label = staleness_label(time.time() - 90000)
|
|
assert label == "1d ago"
|
|
|
|
|
|
class TestExtractTaggedLines:
|
|
def test_extracts_decisions(self):
|
|
text = (
|
|
"Some intro\nDECISION: Use glm-5\n"
|
|
"More text\nDECISION: Deploy to staging"
|
|
)
|
|
decisions = extract_tagged_lines(text, "DECISION")
|
|
assert decisions == ["Use glm-5", "Deploy to staging"]
|
|
|
|
def test_case_insensitive(self):
|
|
text = "decision: lowercase works too"
|
|
assert extract_tagged_lines(text, "DECISION") == [
|
|
"lowercase works too"
|
|
]
|
|
|
|
def test_no_matches(self):
|
|
assert not extract_tagged_lines("no tagged lines here", "DECISION")
|
|
|
|
def test_failed_prefix(self):
|
|
text = "FAILED: qwen-max on coding plan"
|
|
assert extract_tagged_lines(text, "FAILED") == [
|
|
"qwen-max on coding plan"
|
|
]
|
|
|
|
|
|
class TestBuildSessionSummary:
|
|
def test_extracts_decisions_and_failures(self):
|
|
text = (
|
|
"## Topic: config\n"
|
|
"DECISION: Use glm-5 for speed\n"
|
|
"FAILED: qwen-max timed out\n"
|
|
"ACTION: Update API key"
|
|
)
|
|
summary = build_session_summary(
|
|
session_id="s1",
|
|
summary_text=text,
|
|
topic_tags=["config"],
|
|
)
|
|
assert summary.session_id == "s1"
|
|
assert any("glm-5 for speed" in d for d in summary.decisions)
|
|
assert "qwen-max timed out" in summary.failures
|
|
assert summary.topic_tags == ["config"]
|
|
assert summary.timestamp > 0
|
|
|
|
def test_custom_timestamp(self):
|
|
summary = build_session_summary("s1", "text", timestamp=1000.0)
|
|
assert summary.timestamp == 1000.0
|
|
|
|
|
|
class TestBuildPriorKnowledge:
|
|
def test_empty_inputs(self):
|
|
result = build_prior_knowledge_section([], [])
|
|
assert result == ""
|
|
|
|
def test_aom_memories_included(self):
|
|
result = build_prior_knowledge_section(
|
|
session_summaries=[],
|
|
aom_memories=["glm-5 is 6x faster than qwen3.5-plus"],
|
|
)
|
|
assert "glm-5" in result
|
|
assert "Prior Knowledge" in result
|
|
|
|
def test_session_summaries_with_staleness(self):
|
|
summary = SessionSummary(
|
|
session_id="s1",
|
|
timestamp=time.time() - 60,
|
|
summary_text="Worked on deployment",
|
|
decisions=["Use Docker"],
|
|
failures=["Nginx config failed"],
|
|
)
|
|
result = build_prior_knowledge_section([summary], [])
|
|
assert "just now" in result
|
|
assert "Use Docker" in result
|
|
assert "Nginx config failed" in result
|
|
|
|
def test_respects_token_budget(self):
|
|
long_memories = [f"Memory item {i} " * 50 for i in range(20)]
|
|
result = build_prior_knowledge_section(
|
|
[],
|
|
long_memories,
|
|
token_budget=100,
|
|
)
|
|
# Should be tiered down to fit
|
|
est_tokens = len(result) // 4
|
|
assert est_tokens <= 200 # some overhead tolerance
|
|
|
|
def test_max_5_sessions(self):
|
|
summaries = [
|
|
SessionSummary(
|
|
session_id=f"s{i}",
|
|
timestamp=time.time() - i * 3600,
|
|
summary_text=f"Session {i}",
|
|
)
|
|
for i in range(10)
|
|
]
|
|
result = build_prior_knowledge_section(summaries, [])
|
|
# Should only include first 5
|
|
assert "Session 0" in result
|
|
assert "Session 4" in result
|
|
# Session 5+ should not be present
|
|
assert "Session 5" not in result
|
|
|
|
|
|
# ===================================================================
|
|
# 5. Critical Gap Tests
|
|
# ===================================================================
|
|
|
|
|
|
class TestClassifyImportanceNoneAndEmptyContent:
|
|
"""Gap #4: None/empty content in classify_importance.
|
|
|
|
Messages with None content or content that returns None from
|
|
get_text_content must not crash and should classify gracefully.
|
|
"""
|
|
|
|
def test_none_content_does_not_crash(self):
|
|
"""msg_text() guards with `or ""` but we need to verify the
|
|
full classify_importance path handles the resulting empty string."""
|
|
msg = FakeMsg(id="n1", role="user", content=None)
|
|
# content=None -> get_text_content returns None ->
|
|
# msg_text should return ""
|
|
result = classify_importance(msg)
|
|
assert result == Importance.TRIVIAL
|
|
|
|
def test_content_list_with_no_text_blocks(self):
|
|
"""Content is a list but contains no extractable text."""
|
|
msg = FakeMsg(
|
|
id="n2",
|
|
role="assistant",
|
|
content=[{"type": "image", "url": "http://example.com/img.png"}],
|
|
)
|
|
result = classify_importance(msg)
|
|
# get_text_content returns "" for a list with no text blocks
|
|
assert result == Importance.TRIVIAL
|
|
|
|
def test_content_list_with_empty_strings(self):
|
|
msg = FakeMsg(id="n3", role="user", content=["", ""])
|
|
result = classify_importance(msg)
|
|
assert result == Importance.TRIVIAL
|
|
|
|
def test_none_returning_get_text_content(self):
|
|
"""Simulate a Msg subclass whose get_text_content returns None."""
|
|
|
|
class NoneTextMsg(FakeMsg):
|
|
def get_text_content(self):
|
|
return None
|
|
|
|
msg = NoneTextMsg(id="n4", role="user", content="anything")
|
|
result = classify_importance(msg)
|
|
assert result == Importance.TRIVIAL
|
|
|
|
|
|
class TestTopicClusteringInterleavedHints:
|
|
"""Gap #5: Topic clustering with interleaved known/unknown hints.
|
|
|
|
Verifies forward-fill, backward-fill, and the "general" fallback
|
|
when known-topic messages are interleaved with hint-less messages.
|
|
"""
|
|
|
|
def test_forward_fill_propagates_topic_to_following_none(self):
|
|
"""A message with a known topic should forward-fill the next
|
|
message that has no topic hint."""
|
|
msgs = [
|
|
_msg("There's a bug in the system", msg_id="1"), # debugging
|
|
_msg("How are you?", msg_id="2"), # no hint -> inherits debugging
|
|
]
|
|
clusters = cluster_by_topic(msgs)
|
|
# Both should end up in the same cluster due to forward fill
|
|
assert len(clusters) == 1
|
|
assert clusters[0].topic == "debugging"
|
|
assert len(clusters[0].messages) == 2
|
|
|
|
def test_backward_fill_for_leading_nones(self):
|
|
"""Leading messages with no hint should backward-fill from
|
|
the first message that has a hint."""
|
|
msgs = [
|
|
_msg("How are you?", msg_id="1"), # no hint
|
|
_msg("Let me check the config", msg_id="2"), # configuration
|
|
]
|
|
clusters = cluster_by_topic(msgs)
|
|
# Backward fill: msg 1 inherits "configuration" from msg 2
|
|
assert len(clusters) == 1
|
|
assert clusters[0].topic == "configuration"
|
|
|
|
def test_interleaved_known_unknown_known(self):
|
|
"""Known -> unknown -> different known creates proper splits."""
|
|
msgs = [
|
|
_msg("Error: connection failed", msg_id="1"), # debugging
|
|
_msg("Let me think about this", msg_id="2"), # no hint -> fwd fill = debugging
|
|
_msg("Now let me deploy the fix", msg_id="3"), # deployment
|
|
]
|
|
clusters = cluster_by_topic(msgs)
|
|
# msgs 1+2 should cluster as debugging, msg 3 as deployment
|
|
assert len(clusters) == 2
|
|
assert clusters[0].topic == "debugging"
|
|
assert len(clusters[0].messages) == 2
|
|
assert clusters[1].topic == "deployment"
|
|
|
|
def test_all_none_hints_become_general(self):
|
|
"""When no message has any topic hint, all become 'general'."""
|
|
msgs = [
|
|
_msg("Hello there", msg_id="1"),
|
|
_msg("Nice weather", msg_id="2"),
|
|
_msg("Indeed it is", msg_id="3"),
|
|
]
|
|
clusters = cluster_by_topic(msgs)
|
|
assert len(clusters) == 1
|
|
assert clusters[0].topic == "general"
|
|
assert len(clusters[0].messages) == 3
|
|
|
|
def test_tool_msgs_interleaved_with_text(self):
|
|
"""Tool messages with known topics interleaved with plain text."""
|
|
msgs = [
|
|
_tool_msg("read_file", msg_id="t1"), # file-ops
|
|
_msg("The file looks correct", msg_id="m1"), # no hint -> fwd fill
|
|
_tool_msg("browser_use", msg_id="t2"), # web-research
|
|
_msg("Found the answer", msg_id="m2"), # no hint -> fwd fill
|
|
]
|
|
clusters = cluster_by_topic(msgs)
|
|
# t1 + m1 = file-ops, t2 + m2 = web-research
|
|
assert len(clusters) == 2
|
|
assert clusters[0].topic == "file-ops"
|
|
assert len(clusters[0].messages) == 2
|
|
assert clusters[1].topic == "web-research"
|
|
assert len(clusters[1].messages) == 2
|
|
|
|
|
|
class TestTieredCompactionEdgeCases:
|
|
"""Additional tiered compaction edge cases."""
|
|
|
|
def test_all_critical_messages_nothing_compacted(self):
|
|
"""Gap #2 partial: when every message is CRITICAL, nothing
|
|
gets compacted regardless of cycle."""
|
|
msgs = [
|
|
_msg("I decided to use glm-5", msg_id="d1"),
|
|
_msg("From now on, use staging", msg_id="d2"),
|
|
_msg("Config changed: port set to 8088", msg_id="d3"),
|
|
]
|
|
plan = plan_compaction(
|
|
msgs, {"d1": 0, "d2": 0, "d3": 0}, current_cycle=100
|
|
)
|
|
assert len(plan.to_compact) == 0
|
|
assert len(plan.to_preserve) == 3
|
|
assert plan.stats["tier_L0"] == 3
|
|
|
|
def test_mixed_l1_ages_partial_compaction(self):
|
|
"""L1 messages at different ages: only the old ones compact."""
|
|
msgs = [
|
|
_msg("Error: disk full", msg_id="e1"), # HIGH -> L1
|
|
_msg("Fixed the issue", msg_id="e2"), # HIGH -> L1
|
|
]
|
|
# e1 was first seen at cycle 0, e2 at cycle 2
|
|
plan = plan_compaction(
|
|
msgs, {"e1": 0, "e2": 2}, current_cycle=2
|
|
)
|
|
compact_ids = {m.id for m in plan.to_compact}
|
|
preserve_ids = {m.id for m in plan.to_preserve}
|
|
# e1: age=2, survival=2 -> compacted
|
|
assert "e1" in compact_ids
|
|
# e2: age=0, survival=2 -> preserved
|
|
assert "e2" in preserve_ids
|