Files
microdao-daarion/tests/test_stepan_telemetry.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

389 lines
14 KiB
Python
Raw 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.
"""
Unit-тести для telemetry.py (AGX_STEPAN_METRIC tag) — v2.7.2.
Перевіряє:
1. tlog() форматує рядок з тегом AGX_STEPAN_METRIC
2. tlog() коректно серіалізує типи (bool, int, float, list, dict)
3. depth_classifier — логи depth=light/deep мають тег
4. memory_manager.push_recent_topic — topics_push=true/false мають тег
5. Безпека: tlog() не кидає виняток якщо value некоректний
6. PII-safe: user_id/chat_id анонімізуються у tlog() (формат h:xxxxxxxxxx)
7. anonymize_id() — коректність, стабільність, edge cases
"""
import logging
import sys
from pathlib import Path
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.telemetry import tlog, TELEMETRY_TAG, _fmt_value, anonymize_id
from crews.agromatrix_crew.depth_classifier import classify_depth
from crews.agromatrix_crew.memory_manager import (
_default_user_profile,
push_recent_topic,
)
# ─── tlog() unit ─────────────────────────────────────────────────────────────
class _CaptureHandler(logging.Handler):
"""Ловить LogRecord-и для перевірки."""
def __init__(self):
super().__init__()
self.records: list[logging.LogRecord] = []
def emit(self, record: logging.LogRecord) -> None:
self.records.append(record)
@property
def messages(self) -> list[str]:
return [r.getMessage() for r in self.records]
def _capture_logger(name: str) -> tuple[logging.Logger, _CaptureHandler]:
lg = logging.getLogger(name)
lg.setLevel(logging.DEBUG)
h = _CaptureHandler()
lg.addHandler(h)
return lg, h
def test_tlog_contains_tag():
lg, h = _capture_logger("test_tag")
tlog(lg, "depth", depth="light", reason="greeting")
assert any(TELEMETRY_TAG in m for m in h.messages), \
f"Expected {TELEMETRY_TAG!r} in log, got: {h.messages}"
def test_tlog_message_format():
lg, h = _capture_logger("test_fmt")
tlog(lg, "crew_launch", launched=True, depth="deep")
msg = h.messages[-1]
assert msg.startswith(TELEMETRY_TAG)
assert "crew_launch" in msg
assert "launched=true" in msg
assert "depth=deep" in msg
def test_tlog_bool_lowercase():
lg, h = _capture_logger("test_bool")
tlog(lg, "test_event", flag_a=True, flag_b=False)
msg = h.messages[-1]
assert "flag_a=true" in msg
assert "flag_b=false" in msg
def test_tlog_list_joined():
lg, h = _capture_logger("test_list")
tlog(lg, "agents", agents=["ops", "iot", "platform"])
msg = h.messages[-1]
assert "ops,iot,platform" in msg
def test_tlog_dict_compact_json():
lg, h = _capture_logger("test_dict")
tlog(lg, "event", meta={"key": "val"})
msg = h.messages[-1]
assert '"key"' in msg or "key" in msg
def test_tlog_float_formatted():
lg, h = _capture_logger("test_float")
tlog(lg, "confidence", score=0.75)
msg = h.messages[-1]
assert "score=0.75" in msg
def test_tlog_no_kv():
lg, h = _capture_logger("test_nokv")
tlog(lg, "simple_event")
msg = h.messages[-1]
assert TELEMETRY_TAG in msg
assert "simple_event" in msg
def test_tlog_safe_on_bad_value():
"""tlog() не має кидати виняток навіть при некоректному value."""
lg, h = _capture_logger("test_safe")
class _Bad:
def __str__(self):
raise RuntimeError("bad")
# Should NOT raise
tlog(lg, "event", bad=_Bad())
# At least one message logged (fallback)
assert len(h.messages) >= 1
# ─── _fmt_value unit ──────────────────────────────────────────────────────────
def test_fmt_bool_true():
assert _fmt_value(True) == "true"
def test_fmt_bool_false():
assert _fmt_value(False) == "false"
def test_fmt_int():
assert _fmt_value(42) == "42"
def test_fmt_float():
assert _fmt_value(3.14) == "3.14"
def test_fmt_list():
assert _fmt_value(["a", "b", "c"]) == "a,b,c"
def test_fmt_str():
assert _fmt_value("hello") == "hello"
# ─── depth_classifier logs tagged ────────────────────────────────────────────
def test_depth_classifier_greeting_logs_tagged():
"""classify_depth("привіт") має emitнути рядок з TELEMETRY_TAG."""
dc_logger = logging.getLogger("crews.agromatrix_crew.depth_classifier")
dc_logger.setLevel(logging.DEBUG)
h = _CaptureHandler()
dc_logger.addHandler(h)
try:
classify_depth("привіт")
assert any(TELEMETRY_TAG in m for m in h.messages), \
f"No tagged log for greeting. Messages: {h.messages}"
finally:
dc_logger.removeHandler(h)
def test_depth_classifier_deep_logs_tagged():
"""classify_depth("зроби план на тиждень") → тег присутній."""
dc_logger = logging.getLogger("crews.agromatrix_crew.depth_classifier")
dc_logger.setLevel(logging.DEBUG)
h = _CaptureHandler()
dc_logger.addHandler(h)
try:
classify_depth("зроби план на тиждень")
assert any(TELEMETRY_TAG in m for m in h.messages), \
f"No tagged log for deep. Messages: {h.messages}"
finally:
dc_logger.removeHandler(h)
def test_depth_classifier_followup_logs_tagged():
"""classify_depth("а на завтра?", last_topic=...) → тег присутній."""
dc_logger = logging.getLogger("crews.agromatrix_crew.depth_classifier")
dc_logger.setLevel(logging.DEBUG)
h = _CaptureHandler()
dc_logger.addHandler(h)
try:
classify_depth("а на завтра?", last_topic="plan_day")
assert any(TELEMETRY_TAG in m for m in h.messages), \
f"No tagged log for followup. Messages: {h.messages}"
finally:
dc_logger.removeHandler(h)
# ─── memory_manager.push_recent_topic logs tagged ────────────────────────────
def test_push_recent_topic_push_tagged():
"""push_recent_topic → topics_push лог має тег."""
mm_logger = logging.getLogger("crews.agromatrix_crew.memory_manager")
mm_logger.setLevel(logging.DEBUG)
h = _CaptureHandler()
mm_logger.addHandler(h)
try:
profile = _default_user_profile("u_tag_push")
push_recent_topic(profile, "plan_day", "план на завтра")
tagged = [m for m in h.messages if TELEMETRY_TAG in m]
assert tagged, f"No tagged log for topics_push. Messages: {h.messages}"
assert any("topics_push" in m for m in tagged)
assert any("pushed=true" in m for m in tagged)
finally:
mm_logger.removeHandler(h)
def test_push_recent_topic_dedup_tagged():
"""push_recent_topic dedup → topics_push=false лог має тег."""
mm_logger = logging.getLogger("crews.agromatrix_crew.memory_manager")
mm_logger.setLevel(logging.DEBUG)
h = _CaptureHandler()
mm_logger.addHandler(h)
try:
profile = _default_user_profile("u_tag_dedup")
push_recent_topic(profile, "plan_day", "план на завтра")
h.records.clear()
push_recent_topic(profile, "plan_day", "план на завтра") # dedup
tagged = [m for m in h.messages if TELEMETRY_TAG in m]
assert tagged, f"No tagged log for dedup. Messages: {h.messages}"
assert any("pushed=false" in m for m in tagged)
finally:
mm_logger.removeHandler(h)
# ─── Log level passthrough ────────────────────────────────────────────────────
def test_tlog_default_level_is_info():
"""tlog без level= використовує INFO."""
lg, h = _capture_logger("test_level")
tlog(lg, "event")
assert h.records[-1].levelno == logging.INFO
def test_tlog_warning_level():
"""tlog з level=logging.WARNING записує WARNING."""
lg, h = _capture_logger("test_warn_level")
import logging as _logging
tlog(lg, "fallback_event", level=_logging.WARNING, reason="timeout")
assert h.records[-1].levelno == _logging.WARNING
# ─── anonymize_id unit ────────────────────────────────────────────────────────
def test_anonymize_none_returns_none():
assert anonymize_id(None) is None
def test_anonymize_empty_returns_empty():
assert anonymize_id("") == ""
def test_anonymize_format_h_prefix():
result = anonymize_id("123456789")
assert result is not None
assert result.startswith("h:")
assert len(result) == 12 # "h:" + 10 hex chars
def test_anonymize_hex_chars_only():
"""Хеш частина містить тільки hex символи."""
import re
result = anonymize_id("some_user_id")
assert result is not None
hash_part = result[2:] # skip "h:"
assert re.fullmatch(r'[0-9a-f]{10}', hash_part), \
f"Expected 10 lowercase hex chars, got: {hash_part!r}"
def test_anonymize_stable_same_input():
"""Той самий input → той самий псевдонім (детерміновано)."""
a = anonymize_id("user_42")
b = anonymize_id("user_42")
assert a == b
def test_anonymize_different_inputs_different_hashes():
"""Різні inputs → різні псевдоніми (з високою ймовірністю)."""
a = anonymize_id("user_1")
b = anonymize_id("user_2")
assert a != b
def test_anonymize_int_via_str():
"""Типовий user_id як число (stringified)."""
result = anonymize_id("987654321")
assert result is not None
assert result.startswith("h:")
assert len(result) == 12
def test_anonymize_telegram_negative_chat_id():
"""Telegram chat_id може бути від'ємним числом."""
result = anonymize_id("-1001234567890")
assert result is not None
assert result.startswith("h:")
assert len(result) == 12
# ─── tlog PII-safe behavior ───────────────────────────────────────────────────
def test_tlog_user_id_is_anonymized():
"""user_id=<raw> → у лог-рядку з'являється h:... а не сире значення."""
lg, h = _capture_logger("test_pii_uid")
raw_user_id = "987654321"
tlog(lg, "memory_save", user_id=raw_user_id, ok=True)
msg = h.messages[-1]
# Сирого id не має бути у рядку
assert raw_user_id not in msg, \
f"Raw user_id found in log: {msg!r}"
# Замість нього має бути анонімізований псевдонім
assert "user_id=h:" in msg, f"Expected 'user_id=h:' in: {msg!r}"
def test_tlog_chat_id_is_anonymized():
"""chat_id → анонімізується автоматично."""
lg, h = _capture_logger("test_pii_cid")
raw_chat_id = "-1001234567890"
tlog(lg, "memory_save", entity="FarmProfile", chat_id=raw_chat_id, ok=True)
msg = h.messages[-1]
assert raw_chat_id not in msg, \
f"Raw chat_id found in log: {msg!r}"
assert "chat_id=h:" in msg, f"Expected 'chat_id=h:' in: {msg!r}"
def test_tlog_non_pii_key_not_anonymized():
"""Звичайні ключі (не user_id/chat_id) не анонімізуються."""
lg, h = _capture_logger("test_nonpii")
tlog(lg, "depth", depth="light", reason="greeting", words=3)
msg = h.messages[-1]
assert "depth=light" in msg
assert "reason=greeting" in msg
assert "words=3" in msg
def test_tlog_pii_anonymize_format_stable():
"""Один і той самий user_id → той самий псевдонім у різних tlog викликах."""
lg, h = _capture_logger("test_pii_stable")
uid = "user_test_stable"
tlog(lg, "event_a", user_id=uid)
tlog(lg, "event_b", user_id=uid)
# Витягаємо псевдоніми
anon_a = [p for p in h.messages[0].split() if p.startswith("user_id=")][0]
anon_b = [p for p in h.messages[1].split() if p.startswith("user_id=")][0]
assert anon_a == anon_b, \
f"Unstable anonymization: {anon_a!r} vs {anon_b!r}"
def test_tlog_custom_pii_keys():
"""Можна передати власний set pii_keys для додаткових полів."""
lg, h = _capture_logger("test_custom_pii")
tlog(lg, "custom", pii_keys=frozenset({"session_id"}), session_id="abc123xyz")
msg = h.messages[-1]
assert "abc123xyz" not in msg
assert "session_id=h:" in msg
def test_tlog_none_user_id_is_null():
"""None user_id → логується як null (не помилка)."""
lg, h = _capture_logger("test_pii_none")
tlog(lg, "event", user_id=None)
msg = h.messages[-1]
assert "user_id=null" in msg
def test_memory_manager_logs_no_raw_user_id():
"""
Перевіряємо що memory_manager tlog логи не містять сирого user_id.
Симулюємо через push_recent_topic (логи topics_push).
"""
mm_logger = logging.getLogger("crews.agromatrix_crew.memory_manager")
mm_logger.setLevel(logging.DEBUG)
h = _CaptureHandler()
mm_logger.addHandler(h)
try:
from crews.agromatrix_crew.memory_manager import _default_user_profile, push_recent_topic
raw_uid = "99887766554"
profile = _default_user_profile(raw_uid)
push_recent_topic(profile, "plan_day", "план на завтра")
for msg in h.messages:
if TELEMETRY_TAG in msg:
assert raw_uid not in msg, \
f"Raw user_id found in telemetry log: {msg!r}"
finally:
mm_logger.removeHandler(h)