Files
microdao-daarion/tests/test_stepan_v29_consolidation.py
Apple 129e4ea1fc feat(platform): add new services, tools, tests and crews modules
New router intelligence modules (26 files): alert_ingest/store, audit_store,
architecture_pressure, backlog_generator/store, cost_analyzer, data_governance,
dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment,
platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files),
signature_state_store, sofiia_auto_router, tool_governance

New services:
- sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static
- memory-service: integration_endpoints, integrations, voice_endpoints, static UI
- aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents)
- sofiia-supervisor: new supervisor service
- aistalk-bridge-lite: Telegram bridge lite
- calendar-service: CalDAV calendar service with reminders
- mlx-stt-service / mlx-tts-service: Apple Silicon speech services
- binance-bot-monitor: market monitor service
- node-worker: STT/TTS memory providers

New tools (9): agent_email, browser_tool, contract_tool, observability_tool,
oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault

New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus,
farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine,
session_context, style_adapter, telemetry)

Tests: 85+ test files for all new modules
Made-with: Cursor
2026-03-03 07:14:14 -08:00

414 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Tests для Humanized Stepan v2.9 — Memory Consolidation.
Покриває:
1. consolidate_user_profile: limits, dedup, preferences whitelist, summary cap
2. consolidate_farm_profile: field_ids/crop_ids/active_integrations limits
3. Trigger logic: periodic (%25) і hard_trigger (overflow * 1.5)
4. Idempotency
5. Fail-safe: виняток → профіль повертається без змін, warning у лог
6. Telemetry: memory_consolidated лог присутній з тегом
"""
import logging
import sys
from copy import deepcopy
from pathlib import Path
from unittest.mock import patch, MagicMock
root = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(root))
sys.path.insert(0, str(root / 'packages' / 'agromatrix-tools'))
from crews.agromatrix_crew.memory_manager import (
_default_user_profile,
_default_farm_profile,
consolidate_user_profile,
consolidate_farm_profile,
_should_consolidate,
_cap_summary,
_trim_dedup,
_LIMIT_CONTEXT_NOTES,
_LIMIT_KNOWN_INTENTS,
_LIMIT_FIELD_IDS,
_LIMIT_CROP_IDS,
_LIMIT_ACTIVE_INTEG,
_SUMMARY_MAX_CHARS,
_CONSOLIDATION_PERIOD,
_PREF_WHITELIST,
)
from crews.agromatrix_crew.telemetry import TELEMETRY_TAG
# ─── _cap_summary ─────────────────────────────────────────────────────────────
def test_cap_summary_short_unchanged():
s = "Короткий текст"
assert _cap_summary(s) == s
def test_cap_summary_long_cuts_at_word_boundary():
words = ["слово"] * 100
long_text = " ".join(words)
result = _cap_summary(long_text)
assert len(result) <= _SUMMARY_MAX_CHARS
assert not result.endswith("сло") # no mid-word cut
def test_cap_summary_no_trailing_space():
long_text = "a " * 200
result = _cap_summary(long_text)
assert len(result) <= _SUMMARY_MAX_CHARS
assert not result.endswith(" ")
def test_cap_summary_exactly_at_limit():
text = "x" * _SUMMARY_MAX_CHARS
assert _cap_summary(text) == text
def test_cap_summary_over_limit_no_mid_word():
text = "абвгд " * 50 # repeating 6-char + space = 7 chars per word
result = _cap_summary(text)
assert len(result) <= _SUMMARY_MAX_CHARS
# result must end at a space boundary (last char is not a partial word)
assert " " not in result or result == result.strip()
# ─── _trim_dedup ──────────────────────────────────────────────────────────────
def test_trim_dedup_removes_duplicates():
data = ["a", "b", "a", "c", "b"]
result = _trim_dedup(data, 10)
assert result == ["a", "b", "c"]
def test_trim_dedup_preserves_order():
data = list(range(10))
assert _trim_dedup(data, 10) == list(range(10))
def test_trim_dedup_limits_to_max():
data = list(range(50))
result = _trim_dedup(data, 20)
assert len(result) == 20
# Keeps the LAST N (most recent)
assert result == list(range(30, 50))
def test_trim_dedup_empty_list():
assert _trim_dedup([], 10) == []
# ─── consolidate_user_profile: limits ────────────────────────────────────────
def test_user_context_notes_trimmed():
p = _default_user_profile("u1")
p["context_notes"] = [f"note_{i}" for i in range(50)]
result = consolidate_user_profile(p)
assert len(result["context_notes"]) <= _LIMIT_CONTEXT_NOTES
def test_user_context_notes_deduped():
p = _default_user_profile("u2")
p["context_notes"] = ["note_A"] * 10 + ["note_B"] * 10
result = consolidate_user_profile(p)
notes = result["context_notes"]
assert notes.count("note_A") == 1
assert notes.count("note_B") == 1
def test_user_known_intents_trimmed():
p = _default_user_profile("u3")
p["known_intents"] = [f"intent_{i}" for i in range(100)]
result = consolidate_user_profile(p)
assert len(result["known_intents"]) <= _LIMIT_KNOWN_INTENTS
def test_user_known_intents_deduped():
p = _default_user_profile("u4")
p["known_intents"] = ["plan_day"] * 20
result = consolidate_user_profile(p)
assert result["known_intents"].count("plan_day") == 1
# ─── consolidate_user_profile: preferences whitelist ─────────────────────────
def test_preferences_whitelist_removes_extra_keys():
p = _default_user_profile("u5")
p["preferences"]["arbitrary_key"] = "value"
p["preferences"]["another_extra"] = 123
result = consolidate_user_profile(p)
prefs = result["preferences"]
for k in prefs:
assert k in _PREF_WHITELIST, f"Unexpected key in preferences: {k!r}"
def test_preferences_whitelist_keeps_valid_keys():
p = _default_user_profile("u6")
p["preferences"]["units"] = "ha"
p["preferences"]["language"] = "uk"
result = consolidate_user_profile(p)
assert result["preferences"].get("units") == "ha"
assert result["preferences"].get("language") == "uk"
def test_preferences_tone_constraints_normalized():
p = _default_user_profile("u7")
p["preferences"]["tone_constraints"] = {
"no_emojis": 1, # int, not bool
"no_exclamations": "yes", # str, not bool
"unknown_key": "remove_me",
}
result = consolidate_user_profile(p)
tc = result["preferences"]["tone_constraints"]
assert isinstance(tc["no_emojis"], bool)
assert isinstance(tc["no_exclamations"], bool)
assert "unknown_key" not in tc
def test_preferences_tone_constraints_preserved_if_missing():
"""Якщо tone_constraints відсутній у префах — залишається без змін."""
p = _default_user_profile("u8")
p["preferences"].pop("tone_constraints", None)
result = consolidate_user_profile(p)
# After consolidation either has defaults or is absent — no crash
assert "preferences" in result
# ─── consolidate_user_profile: interaction_summary ───────────────────────────
def test_summary_capped_at_220():
p = _default_user_profile("u9")
p["interaction_summary"] = "Дуже довгий текст. " * 50 # ~1000 chars
result = consolidate_user_profile(p)
assert len(result["interaction_summary"]) <= _SUMMARY_MAX_CHARS
def test_summary_no_mid_word_cut():
p = _default_user_profile("u10")
p["interaction_summary"] = ("слово " * 50).strip()
result = consolidate_user_profile(p)
s = result["interaction_summary"]
# Should not end mid-word (should end at a complete word)
assert not s.endswith("сло")
assert not s.endswith("сл")
def test_summary_whitespace_normalized():
p = _default_user_profile("u11")
p["interaction_summary"] = "Іван агроном. Часто питає."
result = consolidate_user_profile(p)
assert " " not in result["interaction_summary"]
def test_summary_none_untouched():
p = _default_user_profile("u12")
p["interaction_summary"] = None
result = consolidate_user_profile(p)
assert result["interaction_summary"] is None
# ─── consolidate_user_profile: idempotency ───────────────────────────────────
def test_consolidation_idempotent():
p = _default_user_profile("u_idem")
p["context_notes"] = ["note_x"] * 5
p["interaction_summary"] = "Іван агроном, короткі відповіді."
p["preferences"]["units"] = "ha"
once = consolidate_user_profile(p)
twice = consolidate_user_profile(once)
assert once == twice, "Consolidation not idempotent"
# ─── consolidate_farm_profile ─────────────────────────────────────────────────
def test_farm_field_ids_trimmed():
f = _default_farm_profile("chat_f1")
f["field_ids"] = [f"field_{i}" for i in range(500)]
result = consolidate_farm_profile(f)
assert len(result["field_ids"]) <= _LIMIT_FIELD_IDS
def test_farm_crop_ids_trimmed():
f = _default_farm_profile("chat_f2")
f["crop_ids"] = [f"crop_{i}" for i in range(200)]
result = consolidate_farm_profile(f)
assert len(result["crop_ids"]) <= _LIMIT_CROP_IDS
def test_farm_active_integrations_trimmed():
f = _default_farm_profile("chat_f3")
f["active_integrations"] = [f"svc_{i}" for i in range(50)]
result = consolidate_farm_profile(f)
assert len(result["active_integrations"]) <= _LIMIT_ACTIVE_INTEG
def test_farm_crops_legacy_trimmed():
"""Legacy 'crops' field also capped."""
f = _default_farm_profile("chat_f4")
f["crops"] = [f"crop_{i}" for i in range(200)]
result = consolidate_farm_profile(f)
assert len(result["crops"]) <= _LIMIT_CROP_IDS
def test_farm_consolidation_preserves_chat_id():
f = _default_farm_profile("chat_preserve")
f["field_ids"] = [f"x_{i}" for i in range(500)]
result = consolidate_farm_profile(f)
assert result["chat_id"] == "chat_preserve"
def test_farm_consolidation_preserves_version():
f = _default_farm_profile("chat_ver")
f["field_ids"] = [f"y_{i}" for i in range(500)]
result = consolidate_farm_profile(f)
assert result["_version"] == f["_version"]
def test_farm_consolidation_idempotent():
f = _default_farm_profile("chat_idem")
f["field_ids"] = [f"f_{i}" for i in range(500)]
once = consolidate_farm_profile(f)
twice = consolidate_farm_profile(once)
assert once == twice
# ─── _should_consolidate triggers ────────────────────────────────────────────
def test_periodic_trigger_at_25():
run, reason = _should_consolidate(25, {})
assert run is True
assert reason == "periodic"
def test_periodic_trigger_at_50():
run, reason = _should_consolidate(50, {})
assert run is True
assert reason == "periodic"
def test_no_trigger_at_24():
run, _ = _should_consolidate(24, {})
assert run is False
def test_no_trigger_at_26():
run, _ = _should_consolidate(26, {})
assert run is False
def test_no_trigger_at_zero():
run, _ = _should_consolidate(0, {})
assert run is False
def test_hard_trigger_on_context_notes_overflow():
profile = {"context_notes": ["n"] * int(_LIMIT_CONTEXT_NOTES * 1.6)}
run, reason = _should_consolidate(7, profile) # not a %25 count
assert run is True
assert reason == "hard_trigger"
def test_hard_trigger_on_known_intents_overflow():
profile = {"known_intents": ["x"] * int(_LIMIT_KNOWN_INTENTS * 1.6)}
run, reason = _should_consolidate(7, profile)
assert run is True
assert reason == "hard_trigger"
def test_no_hard_trigger_under_1_5x():
profile = {"context_notes": ["n"] * (_LIMIT_CONTEXT_NOTES + 1)}
run, _ = _should_consolidate(7, profile)
assert run is False
# ─── fail-safe ───────────────────────────────────────────────────────────────
def test_consolidate_user_profile_fail_safe():
"""Якщо _trim_dedup кидає — повертає profile без змін."""
p = _default_user_profile("u_safe")
p["context_notes"] = ["a", "b"]
with patch(
"crews.agromatrix_crew.memory_manager._trim_dedup",
side_effect=RuntimeError("simulated crash"),
):
result = consolidate_user_profile(p)
# Must not raise; returns original
assert result["user_id"] == "u_safe"
def test_consolidate_farm_profile_fail_safe():
"""Якщо deepcopy кидає — повертає profile без змін (fallback)."""
f = _default_farm_profile("chat_safe")
with patch(
"crews.agromatrix_crew.memory_manager.deepcopy",
side_effect=RuntimeError("simulated crash"),
):
result = consolidate_farm_profile(f)
assert result is not None
# ─── telemetry: memory_consolidated tag present ───────────────────────────────
class _CaptureHandler(logging.Handler):
def __init__(self):
super().__init__()
self.records: list[logging.LogRecord] = []
def emit(self, record):
self.records.append(record)
@property
def messages(self):
return [r.getMessage() for r in self.records]
def test_consolidation_telemetry_tagged():
"""
consolidate_user_profile + tlog → лог-рядок містить AGX_STEPAN_METRIC memory_consolidated.
Перевіряємо напряму через tlog (не через thread).
"""
from crews.agromatrix_crew.telemetry import tlog
mm_logger = logging.getLogger("crews.agromatrix_crew.memory_manager")
mm_logger.setLevel(logging.DEBUG)
h = _CaptureHandler()
mm_logger.addHandler(h)
try:
p = _default_user_profile("u_tlog_test")
p["context_notes"] = ["a"] * 5
p_before = deepcopy(p)
result = consolidate_user_profile(p)
changed = (result != p_before)
tlog(mm_logger, "memory_consolidated", entity="user_profile",
user_id="u_tlog_test", changed=changed, reason="periodic")
tagged = [m for m in h.messages if TELEMETRY_TAG in m]
assert any("memory_consolidated" in m for m in tagged), \
f"Expected memory_consolidated telemetry. Got: {tagged}"
# Verify user_id is anonymized (no raw value)
for m in tagged:
if "memory_consolidated" in m:
assert "u_tlog_test" not in m, f"Raw user_id in telemetry: {m!r}"
assert "user_id=h:" in m
finally:
mm_logger.removeHandler(h)
def test_consolidation_period_constant():
"""CONSOLIDATION_PERIOD має бути 25."""
assert _CONSOLIDATION_PERIOD == 25
def test_consolidation_summary_limit_constant():
"""_SUMMARY_MAX_CHARS має бути 220."""
assert _SUMMARY_MAX_CHARS == 220