Files
phase-memory/tests/test_management_cli.py
tegwick feb60c1d96 feat(PMEM-WP-0017): federated store management CLI and activity reports
Add phase_memory.management for cross-store discovery and windowed
activity reporting. Extend the phase-memory CLI with stores list and
report commands, plus make report-7d for the default weekly operator view.
2026-07-03 01:24:16 +02:00

139 lines
5.6 KiB
Python

"""Tests for federated store management and reporting."""
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
import pytest
from phase_memory.cli import main
from phase_memory.management import (
FEDERATED_REPORT_SCHEMA,
STORE_KIND_LOCAL_GRAPH,
STORE_KIND_OPS_WARDEN,
build_federated_report,
build_store_list,
classify_store,
discover_memory_stores,
resolve_store_reference,
)
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "management"
WINDOW_END = datetime(2026, 7, 3, 12, 0, tzinfo=timezone.utc)
def _fixture_environ(tmp_path: Path) -> dict[str, str]:
registry = FIXTURES / "registry.json"
ops_store = FIXTURES / "ops-warden-store"
local_store = FIXTURES / "local-store"
return {
"WARDEN_MEMORY_STORE": str(ops_store),
"PHASE_MEMORY_REGISTRY": str(registry),
"PHASE_MEMORY_STORE_PATHS": str(local_store),
"HOME": str(tmp_path),
"XDG_DATA_HOME": str(tmp_path / "xdg"),
}
def test_classify_store_kinds() -> None:
assert classify_store(FIXTURES / "ops-warden-store") == STORE_KIND_OPS_WARDEN
assert classify_store(FIXTURES / "local-store") == STORE_KIND_LOCAL_GRAPH
assert classify_store(FIXTURES / "registry.json") is None
def test_discover_memory_stores_deduplicates_and_merges_registry(tmp_path: Path) -> None:
environ = _fixture_environ(tmp_path)
stores, diagnostics = discover_memory_stores(environ)
assert not any(item.get("severity") == "error" for item in diagnostics)
store_ids = {store.store_id for store in stores}
assert "fixture-local-graph" in store_ids
assert len({str(store.path) for store in stores}) == len(stores)
def test_build_federated_report_aggregate_and_detail(tmp_path: Path) -> None:
environ = _fixture_environ(tmp_path)
report = build_federated_report(days=7, environ=environ, window_end=WINDOW_END)
assert report["schema_version"] == FEDERATED_REPORT_SCHEMA
assert report["valid"] is True
assert report["store_count"] == 2
aggregate = report["aggregate"]
assert aggregate["episode_count"] == 3
assert aggregate["audit_event_count"] == 1
assert aggregate["active_store_count"] == 2
assert aggregate["by_outcome"]["resolved"] == 1
assert aggregate["by_outcome"]["skipped"] == 1
assert aggregate["by_session_kind"]["warden.operator"] == 1
assert aggregate["by_operation"]["audit.query"] == 1
detail = build_federated_report(
days=7,
focus_store_id="fixture-local-graph",
environ=environ,
window_end=WINDOW_END,
)
assert detail["focus_store_id"] == "fixture-local-graph"
assert detail["store_count"] == 1
assert detail["store_detail"]["episode_count"] == 1
assert detail["store_detail"]["audit_event_count"] == 1
def test_resolve_store_reference_by_id_and_path(tmp_path: Path) -> None:
environ = _fixture_environ(tmp_path)
stores, _ = discover_memory_stores(environ)
by_id = resolve_store_reference("fixture-local-graph", stores, environ=environ)
assert by_id is not None
assert by_id.store_kind == STORE_KIND_LOCAL_GRAPH
by_path = resolve_store_reference(str(FIXTURES / "ops-warden-store"), stores, environ=environ)
assert by_path is not None
assert by_path.store_kind == STORE_KIND_OPS_WARDEN
def test_federated_report_aggregate_fixture(tmp_path: Path) -> None:
environ = _fixture_environ(tmp_path)
report = build_federated_report(days=7, environ=environ, window_end=WINDOW_END)
payload = {
"aggregate": report["aggregate"],
"store_count": report["store_count"],
"stores": [
{key: store[key] for key in ("store_id", "store_kind", "episode_count", "audit_event_count")}
for store in report["stores"]
],
}
expected = json.loads((FIXTURES / "federated-report-aggregate.json").read_text(encoding="utf-8"))
assert payload["aggregate"] == expected["aggregate"]
assert payload["store_count"] == expected["store_count"]
actual_stores = sorted(payload["stores"], key=lambda item: item["store_kind"])
expected_stores = sorted(expected["stores"], key=lambda item: item["store_kind"])
for actual, item in zip(actual_stores, expected_stores, strict=True):
assert actual["store_kind"] == item["store_kind"]
assert actual["episode_count"] == item["episode_count"]
assert actual["audit_event_count"] == item["audit_event_count"]
def test_cli_stores_list_and_report(tmp_path: Path, capsys, monkeypatch: pytest.MonkeyPatch) -> None:
environ = _fixture_environ(tmp_path)
monkeypatch.setenv("WARDEN_MEMORY_STORE", environ["WARDEN_MEMORY_STORE"])
monkeypatch.setenv("PHASE_MEMORY_REGISTRY", environ["PHASE_MEMORY_REGISTRY"])
monkeypatch.setenv("PHASE_MEMORY_STORE_PATHS", environ["PHASE_MEMORY_STORE_PATHS"])
assert main(["stores", "list", "--format", "summary"]) == 0
listed = capsys.readouterr().out
assert "Fixture local graph store" in listed
assert main(["report", "--days", "7", "--format", "summary"]) == 0
reported = capsys.readouterr().out
assert "episodes=" in reported
assert "active_stores=" in reported
def test_build_store_list_from_registry_only(tmp_path: Path) -> None:
environ = {
"WARDEN_MEMORY_STORE": str(tmp_path / "missing-ops"),
"PHASE_MEMORY_REGISTRY": str(FIXTURES / "registry.json"),
"PHASE_MEMORY_STORE_PATHS": str(FIXTURES / "local-store"),
}
listing = build_store_list(environ=environ)
assert listing["valid"] is True
assert listing["store_count"] == 1
assert listing["stores"][0]["store_id"] == "fixture-local-graph"