feat(memory): test(memory): improve line coverage on handler files

Co-Authored-By: @memory <memory@aipass>
This commit is contained in:
AIOSAI
2026-04-26 02:11:03 -07:00
co-authored by @memory
parent 93da405096
commit 093fde79bd
5 changed files with 3842 additions and 0 deletions
@@ -0,0 +1,425 @@
# ===================AIPASS====================
# Name: tests/test_manager_vectorize.py
# Date: 2026-04-26
# Version: 1.0.0
# Category: memory/tests
# =============================================
"""Tests for manager vectorization and location helpers -- line coverage.
Covers: from aipass.memory.apps.handlers.learnings.manager import process_all_branches
"""
import sys
import json
import subprocess
from unittest.mock import MagicMock, patch
# ---------------------------------------------------------------------------
# Helper: import learnings manager with mocked dependencies
# ---------------------------------------------------------------------------
def _import_manager(monkeypatch):
"""Import manager with mocked memory_files dependency."""
mock_memory_files = MagicMock()
monkeypatch.setitem(
sys.modules,
"aipass.memory.apps.handlers.json.memory_files",
mock_memory_files,
)
sys.modules.pop("aipass.memory.apps.handlers.learnings.manager", None)
parent = sys.modules.get("aipass.memory.apps.handlers.learnings")
if parent is not None and hasattr(parent, "manager"):
delattr(parent, "manager")
from aipass.memory.apps.handlers.learnings import manager
return manager, mock_memory_files
# ===========================================================================
# _find_learnings_location
# ===========================================================================
class TestFindLearningsLocation:
"""Tests for _find_learnings_location helper."""
def test_find_learnings_at_root(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"key_learnings": {"a": "val"}}
parent, loc = mgr._find_learnings_location(data)
assert parent is data
assert loc == "root"
def test_find_learnings_in_active_tasks(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"active_tasks": {"key_learnings": {"b": "val2"}}}
parent, loc = mgr._find_learnings_location(data)
assert parent is data["active_tasks"]
assert loc == "active_tasks"
def test_find_learnings_not_found(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"other_key": 1}
parent, loc = mgr._find_learnings_location(data)
assert parent is None
assert loc == ""
# ===========================================================================
# _get_learnings / _set_learnings
# ===========================================================================
class TestGetSetLearnings:
"""Tests for _get_learnings and _set_learnings helpers."""
def test_get_learnings_returns_dict(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"key_learnings": {"x": "y [2026-01-01]"}}
result = mgr._get_learnings(data)
assert result == {"x": "y [2026-01-01]"}
def test_get_learnings_empty(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"something_else": True}
result = mgr._get_learnings(data)
assert result == {}
def test_set_learnings_existing(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"key_learnings": {"old": "val"}}
ok = mgr._set_learnings(data, {"new": "val2"})
assert ok is True
assert data["key_learnings"] == {"new": "val2"}
def test_set_learnings_creates_at_root(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"no_learnings_here": True}
ok = mgr._set_learnings(data, {"fresh": "entry"})
assert ok is True
assert data["key_learnings"] == {"fresh": "entry"}
# ===========================================================================
# _find_recently_completed_location
# ===========================================================================
class TestFindRecentlyCompletedLocation:
"""Tests for _find_recently_completed_location helper."""
def test_find_recently_completed_at_root(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"recently_completed": ["task1"]}
parent, loc = mgr._find_recently_completed_location(data)
assert parent is data
assert loc == "root"
def test_find_recently_completed_in_active_tasks(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"active_tasks": {"recently_completed": ["task2"]}}
parent, loc = mgr._find_recently_completed_location(data)
assert parent is data["active_tasks"]
assert loc == "active_tasks"
def test_find_recently_completed_not_found(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"nothing": 0}
parent, loc = mgr._find_recently_completed_location(data)
assert parent is None
assert loc == ""
# ===========================================================================
# _get_recently_completed / _set_recently_completed
# ===========================================================================
class TestGetSetRecentlyCompleted:
"""Tests for _get_recently_completed and _set_recently_completed helpers."""
def test_get_recently_completed_returns_list(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"recently_completed": ["a", "b"]}
result = mgr._get_recently_completed(data)
assert result == ["a", "b"]
def test_get_recently_completed_empty(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {}
result = mgr._get_recently_completed(data)
assert result == []
def test_set_recently_completed_existing(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"recently_completed": ["old"]}
ok = mgr._set_recently_completed(data, ["new1", "new2"])
assert ok is True
assert data["recently_completed"] == ["new1", "new2"]
def test_set_recently_completed_creates_at_root(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
data = {"other": True}
ok = mgr._set_recently_completed(data, ["fresh"])
assert ok is True
assert data["recently_completed"] == ["fresh"]
# ===========================================================================
# _vectorize_learnings
# ===========================================================================
def _mock_embedder(monkeypatch, encode_return):
"""Install a mock embedder in sys.modules so the in-function import succeeds."""
mock_emb = MagicMock()
mock_emb.encode_batch = MagicMock(return_value=encode_return)
vector_pkg = MagicMock()
vector_pkg.embedder = mock_emb
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.vector", vector_pkg)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.vector.embedder", mock_emb)
return mock_emb
class TestVectorizeLearnings:
"""Tests for _vectorize_learnings."""
def test_empty_learnings(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
result = mgr._vectorize_learnings("BRANCH", [])
assert result["success"] is True
assert "No learnings" in result["message"]
def test_success(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": [[0.1, 0.2]]})
mock_completed = MagicMock()
mock_completed.returncode = 0
mock_completed.stdout = json.dumps({"success": True, "stored": 1})
with patch.object(subprocess, "run", return_value=mock_completed) as mock_run:
result = mgr._vectorize_learnings("TEST", [("key1", "value1 [2026-01-01]")])
assert result["success"] is True
assert mock_run.called
def test_embedding_fails(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": False, "error": "model not found"})
result = mgr._vectorize_learnings("BRANCH", [("k", "v")])
assert result["success"] is False
assert "Embedding failed" in result["error"]
def test_no_embeddings(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": []})
result = mgr._vectorize_learnings("BRANCH", [("k", "v")])
assert result["success"] is False
assert "No embeddings" in result["error"]
def test_embedding_import_exception(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
# Make the import itself raise by inserting a broken module
broken = MagicMock()
broken.embedder = MagicMock()
broken.embedder.encode_batch = MagicMock(side_effect=RuntimeError("import boom"))
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.vector", broken)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.vector.embedder", broken.embedder)
result = mgr._vectorize_learnings("BRANCH", [("k", "v")])
assert result["success"] is False
assert "import boom" in result["error"]
def test_subprocess_timeout(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": [[0.1]]})
with patch.object(subprocess, "run", side_effect=subprocess.TimeoutExpired(cmd="x", timeout=60)):
result = mgr._vectorize_learnings("BR", [("k", "v")])
assert result["success"] is False
assert "timed out" in result["error"]
def test_subprocess_bad_json(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": [[0.1]]})
mock_proc = MagicMock()
mock_proc.returncode = 0
mock_proc.stdout = "NOT JSON {"
with patch.object(subprocess, "run", return_value=mock_proc):
result = mgr._vectorize_learnings("BR", [("k", "v")])
assert result["success"] is False
assert "Invalid JSON" in result["error"]
def test_subprocess_nonzero_return(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": [[0.1]]})
mock_proc = MagicMock()
mock_proc.returncode = 1
mock_proc.stderr = "segfault"
with patch.object(subprocess, "run", return_value=mock_proc):
result = mgr._vectorize_learnings("BR", [("k", "v")])
assert result["success"] is False
assert "segfault" in result["error"]
def test_subprocess_generic_exception(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": [[0.1]]})
with patch.object(subprocess, "run", side_effect=OSError("disk full")):
result = mgr._vectorize_learnings("BR", [("k", "v")])
assert result["success"] is False
assert "disk full" in result["error"]
def test_numpy_tolist_conversion(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
# Simulate numpy-like objects with a tolist() method
class FakeNdarray:
def __init__(self, data):
self._data = data
def tolist(self):
return self._data
_mock_embedder(
monkeypatch,
{"success": True, "embeddings": [FakeNdarray([0.3, 0.4])]},
)
mock_proc = MagicMock()
mock_proc.returncode = 0
mock_proc.stdout = json.dumps({"success": True})
with patch.object(subprocess, "run", return_value=mock_proc) as mock_run:
result = mgr._vectorize_learnings("BR", [("k", "v [2026-01-01]")])
assert result["success"] is True
# Verify the input sent to subprocess had plain lists, not FakeNdarray
call_kwargs = mock_run.call_args
sent_input = json.loads(call_kwargs.kwargs.get("input") or call_kwargs[1].get("input"))
assert sent_input["embeddings"] == [[0.3, 0.4]]
# ===========================================================================
# _vectorize_completed_tasks
# ===========================================================================
class TestVectorizeCompletedTasks:
"""Tests for _vectorize_completed_tasks."""
def test_empty_tasks(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
result = mgr._vectorize_completed_tasks("BRANCH", [])
assert result["success"] is True
assert "No tasks" in result["message"]
def test_success(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": [[0.5, 0.6]]})
mock_proc = MagicMock()
mock_proc.returncode = 0
mock_proc.stdout = json.dumps({"success": True, "stored": 1})
with patch.object(subprocess, "run", return_value=mock_proc):
result = mgr._vectorize_completed_tasks("TEST", ["did thing [2026-01-01]"])
assert result["success"] is True
def test_embedding_fails(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": False, "error": "no model"})
result = mgr._vectorize_completed_tasks("BR", ["task1"])
assert result["success"] is False
assert "Embedding failed" in result["error"]
def test_no_embeddings(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": []})
result = mgr._vectorize_completed_tasks("BR", ["task1"])
assert result["success"] is False
assert "No embeddings" in result["error"]
def test_embedding_import_exception(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
broken = MagicMock()
broken.embedder = MagicMock()
broken.embedder.encode_batch = MagicMock(side_effect=ValueError("bad weights"))
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.vector", broken)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.vector.embedder", broken.embedder)
result = mgr._vectorize_completed_tasks("BR", ["task1"])
assert result["success"] is False
assert "bad weights" in result["error"]
def test_subprocess_timeout(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": [[0.1]]})
with patch.object(subprocess, "run", side_effect=subprocess.TimeoutExpired(cmd="x", timeout=60)):
result = mgr._vectorize_completed_tasks("BR", ["task1"])
assert result["success"] is False
assert "timed out" in result["error"]
def test_subprocess_bad_json(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": [[0.1]]})
mock_proc = MagicMock()
mock_proc.returncode = 0
mock_proc.stdout = "<<<BAD"
with patch.object(subprocess, "run", return_value=mock_proc):
result = mgr._vectorize_completed_tasks("BR", ["task1"])
assert result["success"] is False
assert "Invalid JSON" in result["error"]
def test_subprocess_nonzero_return(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": [[0.1]]})
mock_proc = MagicMock()
mock_proc.returncode = 1
mock_proc.stderr = "oom killed"
with patch.object(subprocess, "run", return_value=mock_proc):
result = mgr._vectorize_completed_tasks("BR", ["task1"])
assert result["success"] is False
assert "oom killed" in result["error"]
def test_subprocess_generic_exception(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
_mock_embedder(monkeypatch, {"success": True, "embeddings": [[0.1]]})
with patch.object(subprocess, "run", side_effect=PermissionError("no access")):
result = mgr._vectorize_completed_tasks("BR", ["task1"])
assert result["success"] is False
assert "no access" in result["error"]
def test_numpy_tolist_conversion(self, monkeypatch):
mgr, _ = _import_manager(monkeypatch)
class FakeNdarray:
def __init__(self, data):
self._data = data
def tolist(self):
return self._data
_mock_embedder(
monkeypatch,
{"success": True, "embeddings": [FakeNdarray([0.7, 0.8])]},
)
mock_proc = MagicMock()
mock_proc.returncode = 0
mock_proc.stdout = json.dumps({"success": True})
with patch.object(subprocess, "run", return_value=mock_proc) as mock_run:
result = mgr._vectorize_completed_tasks("BR", ["task [2026-03-01]"])
assert result["success"] is True
call_kwargs = mock_run.call_args
sent_input = json.loads(call_kwargs.kwargs.get("input") or call_kwargs[1].get("input"))
assert sent_input["embeddings"] == [[0.7, 0.8]]
@@ -0,0 +1,752 @@
# ===================AIPASS====================
# Name: tests/test_orchestrator_exec.py
# Date: 2026-04-26
# Version: 1.0.0
# Category: memory/tests
# =============================================
"""Tests for orchestrator execute_rollover pipeline -- line coverage.
Covers: from aipass.memory.apps.handlers.rollover.orchestrator import execute_rollover
"""
import sys
from unittest.mock import MagicMock
# ---------------------------------------------------------------------------
# Helper: import orchestrator with mocked infrastructure
# ---------------------------------------------------------------------------
def _import_orchestrator(monkeypatch):
"""Import orchestrator with mocked infrastructure dependencies."""
mock_detector = MagicMock()
mock_detector._read_registry = MagicMock(return_value=[])
mock_detector.check_all_branches = MagicMock(return_value={"success": True, "triggers": []})
mock_extractor = MagicMock()
mock_line_counter = MagicMock()
monitor_pkg = MagicMock()
monitor_pkg.detector = mock_detector
rollover_pkg = MagicMock()
rollover_pkg.extractor = mock_extractor
tracking_pkg = MagicMock()
tracking_pkg.line_counter = mock_line_counter
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.monitor", monitor_pkg)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.monitor.detector", mock_detector)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.rollover.extractor", mock_extractor)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.tracking", tracking_pkg)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.tracking.line_counter", mock_line_counter)
sys.modules.pop("aipass.memory.apps.handlers.rollover.orchestrator", None)
parent = sys.modules.get("aipass.memory.apps.handlers.rollover")
if parent is not None and hasattr(parent, "orchestrator"):
delattr(parent, "orchestrator")
from aipass.memory.apps.handlers.rollover import orchestrator
monkeypatch.setattr(orchestrator, "detector", mock_detector)
monkeypatch.setattr(orchestrator, "extractor", mock_extractor)
monkeypatch.setattr(orchestrator, "line_counter", mock_line_counter)
return orchestrator, {
"detector": mock_detector,
"extractor": mock_extractor,
"line_counter": mock_line_counter,
}
def _make_trigger(tmp_path, branch="TEST", memory_type="sessions"):
"""Build a mock trigger object with required attributes."""
trigger = MagicMock()
trigger.file_path = tmp_path / f"{branch}.local.json"
trigger.branch = branch
trigger.memory_type = memory_type
trigger.__str__ = MagicMock(return_value=f"{branch}.local.json")
return trigger
# ===========================================================================
# execute_rollover
# ===========================================================================
class TestExecuteRolloverCheckBranches:
"""Tests for the trigger-detection phase of execute_rollover."""
def test_check_branches_fails(self, monkeypatch, tmp_path):
"""check_all_branches returns success=False."""
orch, mocks = _import_orchestrator(monkeypatch)
mocks["detector"].check_all_branches.return_value = {
"success": False,
"error": "registry missing",
}
result = orch.execute_rollover()
assert result["success"] is False
assert "registry missing" in result["error"]
def test_no_triggers(self, monkeypatch, tmp_path):
"""check_all_branches returns empty triggers list."""
orch, mocks = _import_orchestrator(monkeypatch)
mocks["detector"].check_all_branches.return_value = {
"success": True,
"triggers": [],
}
result = orch.execute_rollover()
assert result["success"] is True
assert result["triggers_count"] == 0
class TestExecuteRolloverBackup:
"""Tests for the backup phase."""
def test_backup_fails(self, monkeypatch, tmp_path):
"""Backup failure skips trigger and adds to failed list."""
orch, mocks = _import_orchestrator(monkeypatch)
trigger = _make_trigger(tmp_path)
mocks["detector"].check_all_branches.return_value = {
"success": True,
"triggers": [trigger],
}
mocks["extractor"].create_rollover_backup.return_value = {
"success": False,
"error": "disk full",
}
result = orch.execute_rollover()
assert result["success"] is False
assert len(result["failed"]) == 1
assert result["failed"][0]["stage"] == "backup"
class TestExecuteRolloverExtraction:
"""Tests for the extraction phase."""
def test_extract_fails_and_restores(self, monkeypatch, tmp_path):
"""Extraction failure triggers restore_from_backup."""
orch, mocks = _import_orchestrator(monkeypatch)
trigger = _make_trigger(tmp_path)
mocks["detector"].check_all_branches.return_value = {
"success": True,
"triggers": [trigger],
}
mocks["extractor"].create_rollover_backup.return_value = {
"success": True,
"message": "backup created",
}
mocks["extractor"].extract_with_metadata.return_value = {
"success": False,
"error": "parse error",
}
mocks["extractor"].restore_from_backup.return_value = {"success": True}
result = orch.execute_rollover()
assert len(result["failed"]) == 1
assert result["failed"][0]["stage"] == "extraction"
mocks["extractor"].restore_from_backup.assert_called_once()
def test_no_branch_in_result(self, monkeypatch, tmp_path):
"""Extraction succeeds but returns no branch field and trigger has empty branch."""
orch, mocks = _import_orchestrator(monkeypatch)
trigger = _make_trigger(tmp_path, branch="")
# Also clear the trigger.branch so the fallback is empty
trigger.branch = ""
mocks["detector"].check_all_branches.return_value = {
"success": True,
"triggers": [trigger],
}
mocks["extractor"].create_rollover_backup.return_value = {
"success": True,
"message": "ok",
}
mocks["extractor"].extract_with_metadata.return_value = {
"success": True,
"entries": [{"text": "data"}],
"branch": "",
"type": "sessions",
"old_lines": 100,
"new_lines": 50,
}
result = orch.execute_rollover()
assert len(result["failed"]) == 1
assert result["failed"][0]["error"] == "No branch in result"
class TestExecuteRolloverEmbedding:
"""Tests for the embedding phase."""
def _setup_to_embedding(self, monkeypatch, tmp_path, mocks, trigger=None):
"""Set up mocks so execution reaches the embedding step."""
if trigger is None:
trigger = _make_trigger(tmp_path)
mocks["detector"].check_all_branches.return_value = {
"success": True,
"triggers": [trigger],
}
mocks["extractor"].create_rollover_backup.return_value = {
"success": True,
"message": "ok",
}
mocks["extractor"].extract_with_metadata.return_value = {
"success": True,
"entries": [{"text": "memory item"}],
"branch": trigger.branch or "TEST",
"type": trigger.memory_type or "sessions",
"old_lines": 100,
"new_lines": 50,
}
mocks["extractor"].restore_from_backup.return_value = {"success": True}
return trigger
def test_embed_fails_and_restores(self, monkeypatch, tmp_path):
"""Embedding failure triggers restore."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_to_embedding(monkeypatch, tmp_path, mocks)
monkeypatch.setattr(
orch,
"encode_batch_subprocess",
lambda texts: {"success": False, "error": "model load fail"},
)
result = orch.execute_rollover()
assert len(result["failed"]) == 1
assert result["failed"][0]["stage"] == "embedding"
mocks["extractor"].restore_from_backup.assert_called_once()
def test_no_embeddings_returned(self, monkeypatch, tmp_path):
"""Embed succeeds but returns empty embeddings list."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_to_embedding(monkeypatch, tmp_path, mocks)
monkeypatch.setattr(
orch,
"encode_batch_subprocess",
lambda texts: {"success": True, "embeddings": []},
)
result = orch.execute_rollover()
assert len(result["failed"]) == 1
assert result["failed"][0]["stage"] == "embedding"
assert "No embeddings" in result["failed"][0]["error"]
class TestExecuteRolloverStorage:
"""Tests for the storage phases (local + global)."""
def _setup_to_storage(self, monkeypatch, tmp_path, orch, mocks, trigger=None):
"""Set up mocks so execution reaches the storage step."""
if trigger is None:
trigger = _make_trigger(tmp_path)
mocks["detector"].check_all_branches.return_value = {
"success": True,
"triggers": [trigger],
}
mocks["extractor"].create_rollover_backup.return_value = {
"success": True,
"message": "ok",
}
mocks["extractor"].extract_with_metadata.return_value = {
"success": True,
"entries": [{"text": "item", "_metadata": {"source": "test"}, "timestamp": "2026-01-01"}],
"branch": trigger.branch or "TEST",
"type": trigger.memory_type or "sessions",
"old_lines": 100,
"new_lines": 50,
}
mocks["extractor"].restore_from_backup.return_value = {"success": True}
monkeypatch.setattr(
orch,
"encode_batch_subprocess",
lambda texts: {"success": True, "embeddings": [[0.1, 0.2]]},
)
# Default: local chroma path not found (None)
monkeypatch.setattr(orch, "get_branch_local_chroma_path", lambda b: None)
return trigger
def test_local_storage_fails_continues(self, monkeypatch, tmp_path):
"""Local chroma failure still proceeds to global storage."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_to_storage(monkeypatch, tmp_path, orch, mocks)
# Enable local path so local storage is attempted
monkeypatch.setattr(orch, "get_branch_local_chroma_path", lambda b: tmp_path / ".chroma")
call_count = {"n": 0}
def fake_store(**kw):
"""Simulate local failure, global success."""
call_count["n"] += 1
if kw.get("db_path"):
# Local storage fails
return {"success": False, "error": "local disk error"}
# Global storage succeeds
return {"success": True, "collection": "col", "total_vectors": 1}
monkeypatch.setattr(orch, "store_vectors_subprocess", fake_store)
mocks["line_counter"].update_line_count.return_value = {"success": True}
result = orch.execute_rollover()
assert result["success_count"] == 1
assert call_count["n"] == 2 # both local and global called
def test_global_storage_fails_and_restores(self, monkeypatch, tmp_path):
"""Global storage failure triggers restore -- CRITICAL path."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_to_storage(monkeypatch, tmp_path, orch, mocks)
monkeypatch.setattr(
orch,
"store_vectors_subprocess",
lambda **kw: {"success": False, "error": "chroma crash"},
)
result = orch.execute_rollover()
assert len(result["failed"]) == 1
assert result["failed"][0]["stage"] == "global_storage"
mocks["extractor"].restore_from_backup.assert_called_once()
def test_global_storage_fails_restore_fails(self, monkeypatch, tmp_path):
"""Global storage fails AND restore fails -- CRITICAL logging path."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_to_storage(monkeypatch, tmp_path, orch, mocks)
monkeypatch.setattr(
orch,
"store_vectors_subprocess",
lambda **kw: {"success": False, "error": "chroma crash"},
)
mocks["extractor"].restore_from_backup.return_value = {
"success": False,
"error": "backup file missing",
}
result = orch.execute_rollover()
assert len(result["failed"]) == 1
assert result["failed"][0]["stage"] == "global_storage"
class TestExecuteRolloverFullPipeline:
"""Tests for the full success pipeline and post-rollover chain."""
def _setup_full_success(self, monkeypatch, tmp_path, orch, mocks, trigger=None):
"""Set up mocks for a complete successful rollover."""
if trigger is None:
trigger = _make_trigger(tmp_path)
mocks["detector"].check_all_branches.return_value = {
"success": True,
"triggers": [trigger],
}
mocks["extractor"].create_rollover_backup.return_value = {
"success": True,
"message": "ok",
}
mocks["extractor"].extract_with_metadata.return_value = {
"success": True,
"entries": [{"text": "mem", "_metadata": {"src": "t"}, "timestamp": "2026-01-01"}],
"branch": trigger.branch or "TEST",
"type": trigger.memory_type or "sessions",
"old_lines": 100,
"new_lines": 50,
}
monkeypatch.setattr(
orch,
"encode_batch_subprocess",
lambda texts: {"success": True, "embeddings": [[0.1]]},
)
monkeypatch.setattr(orch, "get_branch_local_chroma_path", lambda b: None)
monkeypatch.setattr(
orch,
"store_vectors_subprocess",
lambda **kw: {"success": True, "collection": "mem_TEST", "total_vectors": 5},
)
mocks["line_counter"].update_line_count.return_value = {"success": True}
return trigger
def test_full_success_pipeline(self, monkeypatch, tmp_path):
"""Complete success: backup, extract, embed, store global, line update."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_full_success(monkeypatch, tmp_path, orch, mocks)
# Mock post-rollover imports so they don't error
mock_trigger_core = MagicMock()
monkeypatch.setitem(sys.modules, "aipass.trigger.apps.modules.core", mock_trigger_core)
mock_central = MagicMock()
mock_central.update_central = MagicMock(return_value={"success": True})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.central_writer", mock_central)
mock_dash = MagicMock()
mock_dash.push_memory_bank_dashboard = MagicMock(return_value=True)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.dashboard_push", mock_dash)
mock_pool = MagicMock()
mock_pool.process_memory_pool = MagicMock(return_value={"files_processed": 0})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake.pool_processor", mock_pool)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake", MagicMock(pool_processor=mock_pool))
result = orch.execute_rollover()
assert result["success"] is True
assert result["success_count"] == 1
assert result["triggers_count"] == 1
assert len(result["results"]) == 1
assert result["results"][0]["memories_count"] == 1
def test_post_rollover_trigger_fires(self, monkeypatch, tmp_path):
"""After success, Trigger.fire is called."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_full_success(monkeypatch, tmp_path, orch, mocks)
mock_trigger_mod = MagicMock()
mock_trigger_cls = MagicMock()
mock_trigger_mod.Trigger = mock_trigger_cls
monkeypatch.setitem(sys.modules, "aipass.trigger.apps.modules.core", mock_trigger_mod)
mock_central = MagicMock()
mock_central.update_central = MagicMock(return_value={"success": True})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.central_writer", mock_central)
mock_dash = MagicMock()
mock_dash.push_memory_bank_dashboard = MagicMock(return_value=True)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.dashboard_push", mock_dash)
mock_pool = MagicMock()
mock_pool.process_memory_pool = MagicMock(return_value={"files_processed": 0})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake.pool_processor", mock_pool)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake", MagicMock(pool_processor=mock_pool))
orch.execute_rollover()
mock_trigger_cls.fire.assert_called_once()
def test_post_rollover_central_update(self, monkeypatch, tmp_path):
"""After success, central_writer.update_central is called."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_full_success(monkeypatch, tmp_path, orch, mocks)
mock_trigger_mod = MagicMock()
monkeypatch.setitem(sys.modules, "aipass.trigger.apps.modules.core", mock_trigger_mod)
mock_central = MagicMock()
mock_central.update_central = MagicMock(return_value={"success": True})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.central_writer", mock_central)
mock_dash = MagicMock()
mock_dash.push_memory_bank_dashboard = MagicMock(return_value=True)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.dashboard_push", mock_dash)
mock_pool = MagicMock()
mock_pool.process_memory_pool = MagicMock(return_value={"files_processed": 0})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake.pool_processor", mock_pool)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake", MagicMock(pool_processor=mock_pool))
orch.execute_rollover()
mock_central.update_central.assert_called_once()
def test_post_rollover_dashboard_push(self, monkeypatch, tmp_path):
"""After success, dashboard push is called."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_full_success(monkeypatch, tmp_path, orch, mocks)
mock_trigger_mod = MagicMock()
monkeypatch.setitem(sys.modules, "aipass.trigger.apps.modules.core", mock_trigger_mod)
mock_central = MagicMock()
mock_central.update_central = MagicMock(return_value={"success": True})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.central_writer", mock_central)
mock_dash = MagicMock()
mock_dash.push_memory_bank_dashboard = MagicMock(return_value=True)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.dashboard_push", mock_dash)
mock_pool = MagicMock()
mock_pool.process_memory_pool = MagicMock(return_value={"files_processed": 0})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake.pool_processor", mock_pool)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake", MagicMock(pool_processor=mock_pool))
orch.execute_rollover()
mock_dash.push_memory_bank_dashboard.assert_called_once()
def test_post_rollover_pool_processing(self, monkeypatch, tmp_path):
"""After success, pool_processor is called."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_full_success(monkeypatch, tmp_path, orch, mocks)
mock_trigger_mod = MagicMock()
monkeypatch.setitem(sys.modules, "aipass.trigger.apps.modules.core", mock_trigger_mod)
mock_central = MagicMock()
mock_central.update_central = MagicMock(return_value={"success": True})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.central_writer", mock_central)
mock_dash = MagicMock()
mock_dash.push_memory_bank_dashboard = MagicMock(return_value=True)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.dashboard_push", mock_dash)
mock_pool = MagicMock()
mock_pool.process_memory_pool = MagicMock(return_value={"files_processed": 2})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake.pool_processor", mock_pool)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake", MagicMock(pool_processor=mock_pool))
orch.execute_rollover()
mock_pool.process_memory_pool.assert_called_once()
def test_post_rollover_trigger_exception(self, monkeypatch, tmp_path):
"""Trigger.fire raises but does not crash the pipeline."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_full_success(monkeypatch, tmp_path, orch, mocks)
mock_trigger_mod = MagicMock()
mock_trigger_mod.Trigger.fire.side_effect = ImportError("trigger missing")
monkeypatch.setitem(sys.modules, "aipass.trigger.apps.modules.core", mock_trigger_mod)
mock_central = MagicMock()
mock_central.update_central = MagicMock(return_value={"success": True})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.central_writer", mock_central)
mock_dash = MagicMock()
mock_dash.push_memory_bank_dashboard = MagicMock(return_value=True)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.dashboard_push", mock_dash)
mock_pool = MagicMock()
mock_pool.process_memory_pool = MagicMock(return_value={"files_processed": 0})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake.pool_processor", mock_pool)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake", MagicMock(pool_processor=mock_pool))
result = orch.execute_rollover()
# Pipeline still succeeds despite trigger error
assert result["success"] is True
def test_post_rollover_central_returns_none(self, monkeypatch, tmp_path):
"""central_writer.update_central returns None (warning path)."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_full_success(monkeypatch, tmp_path, orch, mocks)
mock_trigger_mod = MagicMock()
monkeypatch.setitem(sys.modules, "aipass.trigger.apps.modules.core", mock_trigger_mod)
mock_central = MagicMock()
mock_central.update_central = MagicMock(return_value=None)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.central_writer", mock_central)
mock_dash = MagicMock()
mock_dash.push_memory_bank_dashboard = MagicMock(return_value=True)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.dashboard_push", mock_dash)
mock_pool = MagicMock()
mock_pool.process_memory_pool = MagicMock(return_value={"files_processed": 0})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake.pool_processor", mock_pool)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake", MagicMock(pool_processor=mock_pool))
result = orch.execute_rollover()
assert result["success"] is True
def test_post_rollover_dashboard_returns_false(self, monkeypatch, tmp_path):
"""Dashboard push returns False (warning path)."""
orch, mocks = _import_orchestrator(monkeypatch)
self._setup_full_success(monkeypatch, tmp_path, orch, mocks)
mock_trigger_mod = MagicMock()
monkeypatch.setitem(sys.modules, "aipass.trigger.apps.modules.core", mock_trigger_mod)
mock_central = MagicMock()
mock_central.update_central = MagicMock(return_value={"success": True})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.central_writer", mock_central)
mock_dash = MagicMock()
mock_dash.push_memory_bank_dashboard = MagicMock(return_value=False)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.dashboard_push", mock_dash)
mock_pool = MagicMock()
mock_pool.process_memory_pool = MagicMock(return_value={"files_processed": 0})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake.pool_processor", mock_pool)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake", MagicMock(pool_processor=mock_pool))
result = orch.execute_rollover()
assert result["success"] is True
class TestExecuteRolloverMultiple:
"""Tests with multiple triggers."""
def test_multiple_triggers_mixed_results(self, monkeypatch, tmp_path):
"""Two triggers: one succeeds, one fails at backup."""
orch, mocks = _import_orchestrator(monkeypatch)
trigger_ok = _make_trigger(tmp_path, branch="GOOD")
trigger_bad = _make_trigger(tmp_path, branch="BAD")
mocks["detector"].check_all_branches.return_value = {
"success": True,
"triggers": [trigger_ok, trigger_bad],
}
def fake_backup(fp):
"""Fail for BAD branch, succeed for others."""
if "BAD" in str(fp):
return {"success": False, "error": "disk full"}
return {"success": True, "message": "ok"}
mocks["extractor"].create_rollover_backup.side_effect = fake_backup
mocks["extractor"].extract_with_metadata.return_value = {
"success": True,
"entries": [{"text": "m", "_metadata": {}, "timestamp": "2026-01-01"}],
"branch": "GOOD",
"type": "sessions",
"old_lines": 80,
"new_lines": 40,
}
monkeypatch.setattr(
orch,
"encode_batch_subprocess",
lambda texts: {"success": True, "embeddings": [[0.1]]},
)
monkeypatch.setattr(orch, "get_branch_local_chroma_path", lambda b: None)
monkeypatch.setattr(
orch,
"store_vectors_subprocess",
lambda **kw: {"success": True, "collection": "c", "total_vectors": 1},
)
mocks["line_counter"].update_line_count.return_value = {"success": True}
# Mock post-rollover chain (success_count > 0 triggers it)
mock_trigger_mod = MagicMock()
monkeypatch.setitem(sys.modules, "aipass.trigger.apps.modules.core", mock_trigger_mod)
mock_central = MagicMock()
mock_central.update_central = MagicMock(return_value={"success": True})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.central_writer", mock_central)
mock_dash = MagicMock()
mock_dash.push_memory_bank_dashboard = MagicMock(return_value=True)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.dashboard_push", mock_dash)
mock_pool = MagicMock()
mock_pool.process_memory_pool = MagicMock(return_value={"files_processed": 0})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake.pool_processor", mock_pool)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake", MagicMock(pool_processor=mock_pool))
result = orch.execute_rollover()
assert result["success"] is True
assert result["success_count"] == 1
assert result["triggers_count"] == 2
assert len(result["failed"]) == 1
def test_line_count_update_fails(self, monkeypatch, tmp_path):
"""Line count update failure after successful storage is non-fatal."""
orch, mocks = _import_orchestrator(monkeypatch)
trigger = _make_trigger(tmp_path)
mocks["detector"].check_all_branches.return_value = {
"success": True,
"triggers": [trigger],
}
mocks["extractor"].create_rollover_backup.return_value = {
"success": True,
"message": "ok",
}
mocks["extractor"].extract_with_metadata.return_value = {
"success": True,
"entries": [{"text": "m", "_metadata": {}, "timestamp": "2026-01-01"}],
"branch": "TEST",
"type": "sessions",
"old_lines": 100,
"new_lines": 50,
}
monkeypatch.setattr(
orch,
"encode_batch_subprocess",
lambda texts: {"success": True, "embeddings": [[0.1]]},
)
monkeypatch.setattr(orch, "get_branch_local_chroma_path", lambda b: None)
monkeypatch.setattr(
orch,
"store_vectors_subprocess",
lambda **kw: {"success": True, "collection": "c", "total_vectors": 1},
)
mocks["line_counter"].update_line_count.return_value = {
"success": False,
"error": "permission denied",
}
# Mock post-rollover chain
mock_trigger_mod = MagicMock()
monkeypatch.setitem(sys.modules, "aipass.trigger.apps.modules.core", mock_trigger_mod)
mock_central = MagicMock()
mock_central.update_central = MagicMock(return_value={"success": True})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.central_writer", mock_central)
mock_dash = MagicMock()
mock_dash.push_memory_bank_dashboard = MagicMock(return_value=True)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.dashboard_push", mock_dash)
mock_pool = MagicMock()
mock_pool.process_memory_pool = MagicMock(return_value={"files_processed": 0})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake.pool_processor", mock_pool)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake", MagicMock(pool_processor=mock_pool))
result = orch.execute_rollover()
# Still succeeds -- line count is non-fatal
assert result["success"] is True
assert result["success_count"] == 1
def test_local_storage_with_path(self, monkeypatch, tmp_path):
"""Local chroma path exists and local storage succeeds."""
orch, mocks = _import_orchestrator(monkeypatch)
trigger = _make_trigger(tmp_path)
mocks["detector"].check_all_branches.return_value = {
"success": True,
"triggers": [trigger],
}
mocks["extractor"].create_rollover_backup.return_value = {
"success": True,
"message": "ok",
}
mocks["extractor"].extract_with_metadata.return_value = {
"success": True,
"entries": [{"text": "m", "_metadata": {}, "timestamp": "2026-01-01"}],
"branch": "TEST",
"type": "sessions",
"old_lines": 100,
"new_lines": 50,
}
monkeypatch.setattr(
orch,
"encode_batch_subprocess",
lambda texts: {"success": True, "embeddings": [[0.1]]},
)
# Return a real path for local chroma
local_chroma = tmp_path / ".chroma"
monkeypatch.setattr(orch, "get_branch_local_chroma_path", lambda b: local_chroma)
monkeypatch.setattr(
orch,
"store_vectors_subprocess",
lambda **kw: {"success": True, "collection": "c", "total_vectors": 1},
)
mocks["line_counter"].update_line_count.return_value = {"success": True}
# Mock post-rollover chain
mock_trigger_mod = MagicMock()
monkeypatch.setitem(sys.modules, "aipass.trigger.apps.modules.core", mock_trigger_mod)
mock_central = MagicMock()
mock_central.update_central = MagicMock(return_value={"success": True})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.central_writer", mock_central)
mock_dash = MagicMock()
mock_dash.push_memory_bank_dashboard = MagicMock(return_value=True)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.dashboard_push", mock_dash)
mock_pool = MagicMock()
mock_pool.process_memory_pool = MagicMock(return_value={"files_processed": 0})
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake.pool_processor", mock_pool)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.intake", MagicMock(pool_processor=mock_pool))
result = orch.execute_rollover()
assert result["success"] is True
assert result["results"][0]["local_stored"] is True
# ===========================================================================
# sync_line_counts
# ===========================================================================
class TestSyncLineCounts:
"""Tests for sync_line_counts."""
def test_sync_success(self, monkeypatch):
"""line_counter returns success."""
orch, mocks = _import_orchestrator(monkeypatch)
mocks["line_counter"].update_all_memory_files.return_value = {
"success": True,
"updated": 5,
"failed": 0,
}
result = orch.sync_line_counts()
assert result["success"] is True
assert result["updated"] == 5
def test_sync_failure(self, monkeypatch):
"""line_counter returns failure."""
orch, mocks = _import_orchestrator(monkeypatch)
mocks["line_counter"].update_all_memory_files.return_value = {
"success": False,
"updated": 0,
"failed": 3,
}
result = orch.sync_line_counts()
assert result["success"] is False
@@ -0,0 +1,689 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: tests/test_plans_processor.py
# Date: 2026-04-26
# Version: 1.0.0
# Category: memory/tests
# =============================================
"""Tests for plans_processor handler -- line coverage for all functions.
Covers: from aipass.memory.apps.handlers.intake.plans_processor import process_plans
"""
import json
import subprocess
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
# ---------------------------------------------------------------------------
# Import helper
# ---------------------------------------------------------------------------
def _import_plans_processor(monkeypatch):
"""Import plans_processor with mocked dependencies."""
mock_memory_files = MagicMock()
monkeypatch.setitem(
sys.modules,
"aipass.memory.apps.handlers.json.memory_files",
mock_memory_files,
)
sys.modules.pop("aipass.memory.apps.handlers.intake.plans_processor", None)
parent = sys.modules.get("aipass.memory.apps.handlers.intake")
if parent is not None and hasattr(parent, "plans_processor"):
delattr(parent, "plans_processor")
from aipass.memory.apps.handlers.intake import plans_processor
return plans_processor
# ===========================================================================
# Tests: _chunk_plan_text
# ===========================================================================
class TestChunkPlanText:
"""Test _chunk_plan_text function."""
def test_chunks_by_markdown_headers(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
text = (
"## Introduction\n"
"This is the introduction section with enough text to pass the 30-char threshold easily.\n"
"## Details\n"
"Here are the details of the plan with plenty of content to exceed thirty characters.\n"
)
result = mod._chunk_plan_text(text, "plan.md")
assert len(result) == 2
assert result[0]["section"] == "Introduction"
assert result[1]["section"] == "Details"
assert "Introduction" in result[0]["text"] or "introduction" in result[0]["text"]
def test_flushes_last_section(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
text = (
"## Header One\n"
"Content for header one, long enough to pass thirty characters.\n"
"Trailing content without a following header, also long enough to be a real section."
)
result = mod._chunk_plan_text(text, "plan.md")
assert len(result) == 1
# The last section should be flushed since there is only one header
assert result[0]["section"] == "Header One"
assert "Trailing content" in result[0]["text"]
def test_skips_short_sections(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
text = (
"## Short\n"
"Tiny.\n"
"## Long Section\n"
"This section has enough content to pass the thirty-character minimum requirement.\n"
)
result = mod._chunk_plan_text(text, "plan.md")
# The "Short" section has text "## Short\nTiny." stripped -> "## Short\nTiny."
# which is short, so it should be skipped
assert len(result) == 1
assert result[0]["section"] == "Long Section"
def test_fallback_to_size_chunking(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
# Headers present but every section body is tiny (< 30 chars), so the
# header-based pass produces zero chunks and the size-based fallback
# triggers on the full text which exceeds MAX_CHUNK_CHARS.
# Each pair "## Hxxx\nx\n" is ~12 chars; need > 1500 total.
num_sections = (mod.MAX_CHUNK_CHARS // 8) + 50
header_lines = []
for i in range(num_sections):
header_lines.append(f"## H{i:04d}")
header_lines.append("x")
text = "\n".join(header_lines)
# Confirm text is actually long enough for the size-based fallback
assert len(text) > mod.MAX_CHUNK_CHARS
result = mod._chunk_plan_text(text, "plan.md")
assert len(result) >= 2
assert result[0]["section"].startswith("plan.md_part")
def test_small_text_no_headers(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
text = "This is a plain text plan without any markdown headers at all."
result = mod._chunk_plan_text(text, "plan.md")
assert len(result) == 1
assert result[0]["section"] == "plan.md"
assert result[0]["text"] == text
def test_tiny_text_skipped(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
text = "Short."
result = mod._chunk_plan_text(text, "plan.md")
assert result == []
def test_splits_oversized_chunks(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
# Create a markdown section that is > MAX_CHUNK_CHARS * 2
big_body = "X" * (mod.MAX_CHUNK_CHARS * 3)
text = f"## Big Section\n{big_body}\n"
result = mod._chunk_plan_text(text, "plan.md")
# The single chunk was > MAX_CHUNK_CHARS * 2, so it gets split
assert len(result) >= 2
for chunk in result:
assert "_part" in chunk["section"] or chunk["section"] == "Big Section"
def test_empty_text(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
result = mod._chunk_plan_text("", "plan.md")
assert result == []
# ===========================================================================
# Tests: _load_manifest / _save_manifest
# ===========================================================================
class TestManifest:
"""Test _load_manifest and _save_manifest."""
def test_load_manifest_file_exists(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path.parent.mkdir(parents=True)
manifest_data = {"plan1.md": "2026-01-01T00:00:00", "plan2.md": "2026-01-02T00:00:00"}
manifest_path.write_text(json.dumps(manifest_data), encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
result = mod._load_manifest()
assert result == manifest_data
def test_load_manifest_file_missing(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
manifest_path = tmp_path / "config" / ".plans_processed.json"
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
result = mod._load_manifest()
assert result == {}
def test_load_manifest_bad_json(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path.parent.mkdir(parents=True)
manifest_path.write_text("not valid json {{{{", encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
result = mod._load_manifest()
assert result == {}
def test_save_manifest_creates_parent_dirs(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
manifest_path = tmp_path / "deep" / "nested" / "config" / ".plans_processed.json"
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
data = {"file.md": "2026-04-26T12:00:00"}
mod._save_manifest(data)
assert manifest_path.exists()
loaded = json.loads(manifest_path.read_text(encoding="utf-8"))
assert loaded == data
# ===========================================================================
# Tests: _embed_texts
# ===========================================================================
class TestEmbedTexts:
"""Test _embed_texts subprocess wrapper."""
def test_embed_texts_success(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
expected = {"success": True, "embeddings": [[0.1, 0.2], [0.3, 0.4]]}
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = json.dumps(expected)
with patch.object(subprocess, "run", return_value=mock_result) as mock_run:
result = mod._embed_texts(["hello", "world"])
assert result["success"] is True
assert result["embeddings"] == [[0.1, 0.2], [0.3, 0.4]]
mock_run.assert_called_once()
def test_embed_texts_nonzero_return(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stderr = "model not found"
with patch.object(subprocess, "run", return_value=mock_result):
result = mod._embed_texts(["hello"])
assert result["success"] is False
assert "model not found" in result["error"]
def test_embed_texts_exception(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
with patch.object(subprocess, "run", side_effect=OSError("no such binary")):
result = mod._embed_texts(["hello"])
assert result["success"] is False
assert "no such binary" in result["error"]
# ===========================================================================
# Tests: _store_vectors
# ===========================================================================
class TestStoreVectors:
"""Test _store_vectors subprocess wrapper."""
def test_store_vectors_success(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
expected = {"success": True, "stored": 5}
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = json.dumps(expected)
with patch.object(subprocess, "run", return_value=mock_result):
result = mod._store_vectors(
embeddings=[[0.1, 0.2]],
documents=["doc1"],
metadatas=[{"key": "val"}],
collection_name="test_col",
)
assert result["success"] is True
assert result["stored"] == 5
def test_store_vectors_nonzero_return(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stderr = "db locked"
with patch.object(subprocess, "run", return_value=mock_result):
result = mod._store_vectors(
embeddings=[[0.1]],
documents=["doc"],
metadatas=[{}],
)
assert result["success"] is False
assert "db locked" in result["error"]
def test_store_vectors_exception(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
with patch.object(subprocess, "run", side_effect=TimeoutError("timed out")):
result = mod._store_vectors(
embeddings=[[0.1]],
documents=["doc"],
metadatas=[{}],
)
assert result["success"] is False
assert "timed out" in result["error"]
# ===========================================================================
# Tests: _find_repo_root / _get_memory_python (module-level)
# ===========================================================================
class TestFindRepoRoot:
"""Test _find_repo_root function."""
def test_find_repo_root_with_registry(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
# Create a fake registry file
(tmp_path / "AIPASS_REGISTRY.json").write_text("{}", encoding="utf-8")
sub = tmp_path / "a" / "b" / "c"
sub.mkdir(parents=True)
fake_file = sub / "plans_processor.py"
fake_file.write_text("", encoding="utf-8")
# Patch __file__ to be inside tmp_path tree
monkeypatch.setattr(mod, "__file__", str(fake_file))
# Re-call _find_repo_root which reads __file__ at module level;
# but the function uses Path(__file__) inside, so we need to patch the
# function's reference to __file__. We do this by calling it after
# monkeypatching the module's __file__.
result = mod._find_repo_root()
assert result == tmp_path
def test_find_repo_root_falls_back_to_cwd(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
# Point __file__ to a location with no registry
nowhere = tmp_path / "nowhere" / "file.py"
nowhere.parent.mkdir(parents=True)
nowhere.write_text("", encoding="utf-8")
monkeypatch.setattr(mod, "__file__", str(nowhere))
monkeypatch.chdir(tmp_path)
result = mod._find_repo_root()
assert result == Path.cwd()
class TestGetMemoryPython:
"""Test _get_memory_python function."""
def test_env_override(self, monkeypatch):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setenv("AIPASS_MEMORY_PYTHON", "/custom/python")
result = mod._get_memory_python()
assert result == "/custom/python"
def test_venv_python_exists(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.delenv("AIPASS_MEMORY_PYTHON", raising=False)
venv_python = tmp_path / ".venv" / "bin" / "python"
venv_python.parent.mkdir(parents=True)
venv_python.write_text("#!/usr/bin/env python", encoding="utf-8")
monkeypatch.setattr(mod, "_MEMORY_VENV_PYTHON", venv_python)
result = mod._get_memory_python()
assert result == str(venv_python)
def test_falls_back_to_sys_executable(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.delenv("AIPASS_MEMORY_PYTHON", raising=False)
# Point to a non-existent venv
monkeypatch.setattr(mod, "_MEMORY_VENV_PYTHON", tmp_path / "nonexistent" / "python")
result = mod._get_memory_python()
assert result == sys.executable
# ===========================================================================
# Tests: process_plans
# ===========================================================================
class TestProcessPlans:
"""Test process_plans main entry point."""
def _setup_config(self, tmp_path, config_data):
"""Write a memory_bank.config.json and return its path."""
config_dir = tmp_path / "config"
config_dir.mkdir(parents=True, exist_ok=True)
config_path = config_dir / "memory_bank.config.json"
config_path.write_text(json.dumps(config_data), encoding="utf-8")
return config_path
def test_process_plans_config_load_fails(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
# Point _MEMORY_ROOT to tmp_path -- no config file exists
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
result = mod.process_plans()
assert result["success"] is False
assert "Config load failed" in result["error"]
def test_process_plans_disabled(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
self._setup_config(tmp_path, {"plans": {"enabled": False}})
result = mod.process_plans()
assert result["success"] is True
assert result["skipped"] is True
assert "disabled" in result["reason"]
def test_process_plans_dir_not_found(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": "nonexistent/plans"}},
)
# _find_repo_root will return tmp_path
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
result = mod.process_plans()
assert result["success"] is True
assert result["files_processed"] == 0
assert "not found" in result.get("reason", "")
def test_process_plans_no_files(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
result = mod.process_plans()
assert result["success"] is True
assert result["files_processed"] == 0
def test_process_plans_all_already_processed(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
plan_file = plans_dir / "done.md"
plan_file.write_text("Already processed plan content that is long enough.", encoding="utf-8")
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
# Pre-populate the manifest
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path.write_text(json.dumps({"done.md": "2026-01-01T00:00:00"}), encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
result = mod.process_plans()
assert result["success"] is True
assert result["files_processed"] == 0
assert "already processed" in result.get("reason", "")
def test_process_plans_success(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
# Create plans directory with a file
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
plan_file = plans_dir / "new_plan.md"
plan_file.write_text(
"## Objective\nThis is the objective section with enough content to exceed thirty characters.\n"
"## Steps\nThese are the steps of the plan with sufficient length for chunking.\n",
encoding="utf-8",
)
self._setup_config(
tmp_path,
{
"plans": {
"enabled": True,
"path": str(plans_dir),
"supported_extensions": [".md"],
"collection_name": "test_plans",
}
},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
# Empty manifest
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
# Mock _embed_texts to return success
monkeypatch.setattr(
mod,
"_embed_texts",
lambda texts: {"success": True, "embeddings": [[0.1, 0.2]] * len(texts)},
)
# Mock _store_vectors to return success
monkeypatch.setattr(
mod,
"_store_vectors",
lambda emb, docs, metas, collection_name="flow_plans": {"success": True, "stored": len(docs)},
)
mock_jh = MagicMock()
monkeypatch.setattr(mod, "json_handler", mock_jh)
result = mod.process_plans()
assert result["success"] is True
assert result["files_processed"] == 1
assert result["total_chunks"] >= 2
mock_jh.log_operation.assert_called_once()
# Manifest should be updated
updated_manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
assert "new_plan.md" in updated_manifest
def test_process_plans_embed_fails(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
plan_file = plans_dir / "plan.md"
plan_file.write_text(
"## Section\nThis section has enough content to pass the minimum threshold for chunking.\n",
encoding="utf-8",
)
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
# Mock _embed_texts to return failure
monkeypatch.setattr(
mod,
"_embed_texts",
lambda texts: {"success": False, "error": "GPU out of memory"},
)
mock_jh = MagicMock()
monkeypatch.setattr(mod, "json_handler", mock_jh)
result = mod.process_plans()
assert result["success"] is False
assert "errors" in result
assert any("embed" in e for e in result["errors"])
def test_process_plans_no_embeddings(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
plan_file = plans_dir / "plan.md"
plan_file.write_text(
"## Section\nThis section has enough content to pass the minimum threshold for chunking.\n",
encoding="utf-8",
)
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
# Embed succeeds but returns empty embeddings
monkeypatch.setattr(
mod,
"_embed_texts",
lambda texts: {"success": True, "embeddings": []},
)
mock_jh = MagicMock()
monkeypatch.setattr(mod, "json_handler", mock_jh)
result = mod.process_plans()
assert "errors" in result
assert any("no embeddings" in e for e in result["errors"])
def test_process_plans_store_fails(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
plan_file = plans_dir / "plan.md"
plan_file.write_text(
"## Section\nThis section has enough content to pass the minimum threshold for chunking.\n",
encoding="utf-8",
)
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
# Embed succeeds
monkeypatch.setattr(
mod,
"_embed_texts",
lambda texts: {"success": True, "embeddings": [[0.1, 0.2]] * len(texts)},
)
# Store fails
monkeypatch.setattr(
mod,
"_store_vectors",
lambda emb, docs, metas, collection_name="flow_plans": {"success": False, "error": "disk full"},
)
mock_jh = MagicMock()
monkeypatch.setattr(mod, "json_handler", mock_jh)
result = mod.process_plans()
assert result["success"] is False
assert "errors" in result
assert any("store" in e for e in result["errors"])
def test_process_plans_empty_chunks(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
plan_file = plans_dir / "tiny.md"
# Content that will produce zero chunks (under 30 chars, no headers)
plan_file.write_text("Hi.", encoding="utf-8")
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
mock_jh = MagicMock()
monkeypatch.setattr(mod, "json_handler", mock_jh)
result = mod.process_plans()
# No chunks produced, but no errors either -- files_without_chunks path
assert result["success"] is True
assert result["files_processed"] == 0
# File should still be marked in manifest (files_without_chunks)
updated_manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
assert "tiny.md" in updated_manifest
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,675 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: tests/test_templates_display.py
# Date: 2026-04-26
# Version: 1.0.0
# Category: memory/tests
# =============================================
"""Tests for templates module display functions -- line coverage.
Covers: from aipass.memory.apps.modules.templates import handle_command
"""
import sys
from pathlib import Path
from unittest.mock import MagicMock
# ---------------------------------------------------------------------------
# Helpers: build the full mock graph that templates.py needs at import time
# ---------------------------------------------------------------------------
def _prepare_templates_mocks(monkeypatch):
mock_panel = MagicMock()
rich_panel_mod = MagicMock()
rich_panel_mod.Panel = mock_panel
monkeypatch.setitem(sys.modules, "rich.panel", rich_panel_mod)
monkeypatch.setitem(sys.modules, "rich", MagicMock())
mock_console = MagicMock()
mock_error = MagicMock()
mock_warning = MagicMock()
cli_modules_mod = MagicMock()
cli_modules_mod.console = mock_console
cli_modules_mod.error = mock_error
cli_modules_mod.warning = mock_warning
monkeypatch.setitem(sys.modules, "aipass.cli", MagicMock())
monkeypatch.setitem(sys.modules, "aipass.cli.apps", MagicMock())
monkeypatch.setitem(sys.modules, "aipass.cli.apps.modules", cli_modules_mod)
mock_memory_files = MagicMock()
mock_memory_files.read_memory_file_data = MagicMock(return_value=None)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.json.memory_files", mock_memory_files)
mock_pusher = MagicMock()
mock_pusher.push_templates = MagicMock(
return_value={
"success": True,
"branches_scanned": 5,
"branches_updated": 2,
"files_modified": 3,
"changes": [],
"errors": [],
}
)
mock_pusher.get_template_status = MagicMock(
return_value={
"version": "2.0.0",
"last_push": "2026-03-20",
"local_template_exists": True,
"observations_template_exists": True,
"templates_dir": "/tmp/templates",
"last_push_branches": [],
}
)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.templates.pusher", mock_pusher)
mock_differ = MagicMock()
mock_differ.diff_template_vs_branch = MagicMock(
return_value={
"local": [],
"observations": [],
"errors": [],
}
)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.templates.differ", mock_differ)
mock_spawn_pusher = MagicMock()
mock_spawn_pusher.push_to_spawn_templates = MagicMock(
return_value={
"success": True,
"template_sets_found": [],
"template_sets_updated": 0,
"files_modified": 0,
"changes": [],
}
)
monkeypatch.setitem(sys.modules, "aipass.memory.apps.handlers.templates.spawn_pusher", mock_spawn_pusher)
monkeypatch.setitem(
sys.modules,
"aipass.memory.apps.handlers.templates",
MagicMock(pusher=mock_pusher, differ=mock_differ, spawn_pusher=mock_spawn_pusher),
)
return {
"console": mock_console,
"error": mock_error,
"warning": mock_warning,
"pusher": mock_pusher,
"differ": mock_differ,
"spawn_pusher": mock_spawn_pusher,
"memory_files": mock_memory_files,
}
def _import_templates(monkeypatch):
mocks = _prepare_templates_mocks(monkeypatch)
sys.modules.pop("aipass.memory.apps.modules.templates", None)
parent = sys.modules.get("aipass.memory.apps.modules")
if parent is not None and hasattr(parent, "templates"):
delattr(parent, "templates")
from aipass.memory.apps.modules import templates
return templates, mocks
# ===========================================================================
# Tests: print_help
# ===========================================================================
class TestPrintHelp:
"""Tests for print_help -- outputs usage panel and command list."""
def test_print_help_outputs_usage(self, monkeypatch) -> None:
"""print_help calls console.print multiple times for usage info."""
templates, mocks = _import_templates(monkeypatch)
templates.print_help()
# Lines 192-209: 7+ console.print calls (blank lines + panel + usage lines)
assert mocks["console"].print.call_count >= 7
# ===========================================================================
# Tests: print_introspection
# ===========================================================================
class TestPrintIntrospection:
"""Tests for print_introspection -- displays module identity, handlers, subcommands."""
def test_introspection_with_handlers(self, monkeypatch) -> None:
"""With discovered handlers, prints handler listing and subcommands."""
templates, mocks = _import_templates(monkeypatch)
fake_handlers = {
"json": ["json_handler.py", "memory_files.py"],
"templates": ["pusher.py", "differ.py"],
}
monkeypatch.setattr(templates, "_discover_handlers", lambda: fake_handlers)
templates.print_introspection()
# Should print handler dirs and subcommand list
assert mocks["console"].print.call_count >= 10
def test_introspection_no_handlers(self, monkeypatch) -> None:
"""With no handlers found, prints 'No handlers found' message."""
templates, mocks = _import_templates(monkeypatch)
monkeypatch.setattr(templates, "_discover_handlers", lambda: {})
templates.print_introspection()
# Should still print subcommands and hints even with no handlers
assert mocks["console"].print.call_count >= 8
# ===========================================================================
# Tests: _display_push_results
# ===========================================================================
class TestDisplayPushResults:
"""Tests for _display_push_results -- formats push_templates() handler result."""
def test_push_results_success_with_changes(self, monkeypatch) -> None:
"""Success with changes list, not dry_run -- displays change details."""
templates, mocks = _import_templates(monkeypatch)
result = {
"success": True,
"branches_scanned": 5,
"branches_updated": 2,
"files_modified": 3,
"changes": [
{
"branch": "CLI",
"file": "local.json",
"changes": ["added field_x", "added field_y"],
},
],
"errors": [],
}
templates._display_push_results(result, dry_run=False)
# Verify console.print was called (panel, summary, changes, final status)
assert mocks["console"].print.call_count >= 8
def test_push_results_success_no_changes(self, monkeypatch) -> None:
"""Success with no changes -- displays 'up to date' message."""
templates, mocks = _import_templates(monkeypatch)
result = {
"success": True,
"branches_scanned": 5,
"branches_updated": 0,
"files_modified": 0,
"changes": [],
"errors": [],
}
templates._display_push_results(result, dry_run=False)
# Should hit the 'All branches are up to date' and 'No updates needed' paths
call_args_list = [str(c) for c in mocks["console"].print.call_args_list]
joined = " ".join(call_args_list)
assert "up to date" in joined or "No updates needed" in joined
def test_push_results_dry_run_with_changes(self, monkeypatch) -> None:
"""dry_run=True with changes -- shows DRY RUN label and 'would be updated'."""
templates, mocks = _import_templates(monkeypatch)
result = {
"success": True,
"branches_scanned": 5,
"branches_updated": 2,
"files_modified": 3,
"changes": [
{"branch": "CLI", "file": "local.json", "changes": ["added field"]},
],
"errors": [],
}
templates._display_push_results(result, dry_run=True)
call_args_list = [str(c) for c in mocks["console"].print.call_args_list]
joined = " ".join(call_args_list)
assert "DRY RUN" in joined
def test_push_results_failure(self, monkeypatch) -> None:
"""success=False with errors -- displays failure message and errors."""
templates, mocks = _import_templates(monkeypatch)
result = {
"success": False,
"branches_scanned": 0,
"branches_updated": 0,
"files_modified": 0,
"changes": [],
"errors": ["Registry not found", "Permission denied"],
}
templates._display_push_results(result, dry_run=False)
# error() should be called for each error plus the failure header
assert mocks["error"].call_count >= 2
def test_push_results_with_errors(self, monkeypatch) -> None:
"""success=True but errors list non-empty -- shows both success summary and errors."""
templates, mocks = _import_templates(monkeypatch)
result = {
"success": True,
"branches_scanned": 5,
"branches_updated": 2,
"files_modified": 3,
"changes": [
{"branch": "CLI", "file": "local.json", "changes": ["added field"]},
],
"errors": ["Branch ALPHA: permission denied"],
}
templates._display_push_results(result, dry_run=False)
# error() called for the error entry
assert mocks["error"].call_count >= 1
# console.print still used for summary
assert mocks["console"].print.call_count >= 8
# ===========================================================================
# Tests: _display_spawn_push_results
# ===========================================================================
class TestDisplaySpawnPushResults:
"""Tests for _display_spawn_push_results -- formats spawn template push results."""
def test_spawn_push_failure(self, monkeypatch) -> None:
"""success=False with errors -- calls error() for each."""
templates, mocks = _import_templates(monkeypatch)
result = {
"success": False,
"errors": ["Spawn dir not found", "Template missing"],
}
templates._display_spawn_push_results(result, dry_run=False)
assert mocks["error"].call_count >= 2
def test_spawn_push_up_to_date(self, monkeypatch) -> None:
"""success=True, files_modified=0, no changes -- prints 'up to date'."""
templates, mocks = _import_templates(monkeypatch)
result = {
"success": True,
"template_sets_found": ["set_a"],
"template_sets_updated": 0,
"files_modified": 0,
"changes": [],
}
templates._display_spawn_push_results(result, dry_run=False)
call_args_list = [str(c) for c in mocks["console"].print.call_args_list]
joined = " ".join(call_args_list)
assert "up to date" in joined
def test_spawn_push_with_changes(self, monkeypatch) -> None:
"""success=True, files_modified>0, changes present -- prints update details."""
templates, mocks = _import_templates(monkeypatch)
result = {
"success": True,
"template_sets_found": ["set_a", "set_b"],
"template_sets_updated": 1,
"files_modified": 2,
"changes": [
{"template_set": "set_a", "file": "local.json", "action": "updated"},
{"template_set": "set_a", "file": "obs.json", "action": "created"},
],
}
templates._display_spawn_push_results(result, dry_run=False)
# Should print summary line + change detail lines
assert mocks["console"].print.call_count >= 3
def test_spawn_push_dry_run(self, monkeypatch) -> None:
"""dry_run=True with changes -- uses 'would update' phrasing."""
templates, mocks = _import_templates(monkeypatch)
result = {
"success": True,
"template_sets_found": ["set_a"],
"template_sets_updated": 1,
"files_modified": 1,
"changes": [
{"template_set": "set_a", "file": "local.json", "action": "updated"},
],
}
templates._display_spawn_push_results(result, dry_run=True)
call_args_list = [str(c) for c in mocks["console"].print.call_args_list]
joined = " ".join(call_args_list)
assert "would update" in joined
# ===========================================================================
# Tests: _display_diff_results
# ===========================================================================
class TestDisplayDiffResults:
"""Tests for _display_diff_results -- calls differ handler, displays per-branch results."""
def _make_branches(self, tmp_path: Path, names: list[str]) -> list[dict]:
"""Create branch dicts with real temp directories."""
branches = []
for name in names:
branch_dir = tmp_path / name.lower()
branch_dir.mkdir(parents=True, exist_ok=True)
branches.append(
{
"name": name,
"path": str(branch_dir),
"status": "active",
}
)
return branches
def test_diff_all_branches_no_diffs(self, tmp_path: Path, monkeypatch) -> None:
"""All branches up to date -- shows 'up to date' for each and summary."""
templates, mocks = _import_templates(monkeypatch)
branches = self._make_branches(tmp_path, ["CLI", "MEMORY"])
monkeypatch.setattr(templates, "_load_branches_from_registry", lambda: branches)
monkeypatch.setattr(
templates,
"diff_template_vs_branch",
lambda path: {"local": [], "observations": [], "errors": []},
)
templates._display_diff_results(None)
call_args_list = [str(c) for c in mocks["console"].print.call_args_list]
joined = " ".join(call_args_list)
assert "up to date" in joined.lower()
def test_diff_with_diffs(self, tmp_path: Path, monkeypatch) -> None:
"""Branches have local/obs diffs -- displays diff entries and warning."""
templates, mocks = _import_templates(monkeypatch)
branches = self._make_branches(tmp_path, ["CLI", "MEMORY"])
monkeypatch.setattr(templates, "_load_branches_from_registry", lambda: branches)
def fake_diff(path: str) -> dict:
"""Return fake diff result with local and observations diffs."""
return {
"local": [{"file": "local.json", "additions": ["field_x"]}],
"observations": [{"file": "observations.json", "removals": ["old_field"]}],
"errors": [],
}
monkeypatch.setattr(templates, "diff_template_vs_branch", fake_diff)
templates._display_diff_results(None)
# warning() called because branches have diffs
assert mocks["warning"].call_count >= 1
def test_diff_specific_branch(self, tmp_path: Path, monkeypatch) -> None:
"""branch_name filter applied -- only diffs the named branch."""
templates, mocks = _import_templates(monkeypatch)
branches = self._make_branches(tmp_path, ["CLI", "MEMORY", "DRONE"])
monkeypatch.setattr(templates, "_load_branches_from_registry", lambda: branches)
monkeypatch.setattr(
templates,
"diff_template_vs_branch",
lambda path: {"local": [], "observations": [], "errors": []},
)
templates._display_diff_results("CLI")
call_args_list = [str(c) for c in mocks["console"].print.call_args_list]
joined = " ".join(call_args_list)
assert "CLI" in joined
def test_diff_branch_not_found(self, tmp_path: Path, monkeypatch) -> None:
"""Filtered branch not in registry -- displays error."""
templates, mocks = _import_templates(monkeypatch)
branches = self._make_branches(tmp_path, ["CLI", "MEMORY"])
monkeypatch.setattr(templates, "_load_branches_from_registry", lambda: branches)
templates._display_diff_results("NONEXISTENT")
assert mocks["error"].call_count >= 1
def test_diff_registry_load_fails(self, monkeypatch) -> None:
"""_load_branches_from_registry returns None -- displays error."""
templates, mocks = _import_templates(monkeypatch)
monkeypatch.setattr(templates, "_load_branches_from_registry", lambda: None)
templates._display_diff_results(None)
assert mocks["error"].call_count >= 1
def test_diff_branch_path_missing(self, tmp_path: Path, monkeypatch) -> None:
"""Branch path doesn't exist on disk -- displays error and increments error count."""
templates, mocks = _import_templates(monkeypatch)
branches = [
{"name": "GHOST", "path": str(tmp_path / "nonexistent_dir"), "status": "active"},
]
monkeypatch.setattr(templates, "_load_branches_from_registry", lambda: branches)
templates._display_diff_results(None)
# error() called for missing path
assert mocks["error"].call_count >= 1
def test_diff_handler_exception(self, tmp_path: Path, monkeypatch) -> None:
"""diff_template_vs_branch raises exception -- caught and error displayed."""
templates, mocks = _import_templates(monkeypatch)
branches = self._make_branches(tmp_path, ["CLI"])
monkeypatch.setattr(templates, "_load_branches_from_registry", lambda: branches)
monkeypatch.setattr(
templates,
"diff_template_vs_branch",
MagicMock(side_effect=RuntimeError("handler exploded")),
)
templates._display_diff_results(None)
assert mocks["error"].call_count >= 1
def test_diff_handler_returns_errors(self, tmp_path: Path, monkeypatch) -> None:
"""Diff result has errors list -- errors displayed per branch."""
templates, mocks = _import_templates(monkeypatch)
branches = self._make_branches(tmp_path, ["CLI"])
monkeypatch.setattr(templates, "_load_branches_from_registry", lambda: branches)
monkeypatch.setattr(
templates,
"diff_template_vs_branch",
lambda path: {"local": [], "observations": [], "errors": ["file not readable"]},
)
templates._display_diff_results(None)
# error() called for the error entry
assert mocks["error"].call_count >= 1
# ===========================================================================
# Tests: _display_file_diffs
# ===========================================================================
class TestDisplayFileDiffs:
"""Tests for _display_file_diffs -- displays individual file diff entries."""
def test_file_diffs_additions(self, monkeypatch) -> None:
"""Entries with additions -- prints green + lines."""
templates, mocks = _import_templates(monkeypatch)
file_diffs = [
{"file": "local.json", "additions": ["field_a", "field_b"]},
]
templates._display_file_diffs(file_diffs)
# One file header line + two addition lines
assert mocks["console"].print.call_count >= 3
def test_file_diffs_removals(self, monkeypatch) -> None:
"""Entries with removals -- prints red - lines."""
templates, mocks = _import_templates(monkeypatch)
file_diffs = [
{"file": "local.json", "removals": ["old_field"]},
]
templates._display_file_diffs(file_diffs)
assert mocks["console"].print.call_count >= 2
def test_file_diffs_modifications(self, monkeypatch) -> None:
"""Entries with modifications -- prints yellow ~ lines."""
templates, mocks = _import_templates(monkeypatch)
file_diffs = [
{"file": "local.json", "modifications": ["changed_field"]},
]
templates._display_file_diffs(file_diffs)
assert mocks["console"].print.call_count >= 2
def test_file_diffs_empty(self, monkeypatch) -> None:
"""Empty list -- no console.print calls."""
templates, mocks = _import_templates(monkeypatch)
templates._display_file_diffs([])
assert mocks["console"].print.call_count == 0
# ===========================================================================
# Tests: _display_status
# ===========================================================================
class TestDisplayStatus:
"""Tests for _display_status -- displays template status info."""
def test_status_templates_exist(self, monkeypatch) -> None:
"""local + obs templates found -- shows 'found' labels."""
templates, mocks = _import_templates(monkeypatch)
status = {
"version": "2.0.0",
"last_push": "2026-03-20",
"local_template_exists": True,
"observations_template_exists": True,
"templates_dir": "/tmp/templates",
"last_push_branches": [],
}
templates._display_status(status)
call_args_list = [str(c) for c in mocks["console"].print.call_args_list]
joined = " ".join(call_args_list)
assert "found" in joined
assert "2.0.0" in joined
def test_status_templates_missing(self, monkeypatch) -> None:
"""Templates not found -- shows 'MISSING' labels."""
templates, mocks = _import_templates(monkeypatch)
status = {
"version": None,
"last_push": None,
"local_template_exists": False,
"observations_template_exists": False,
"templates_dir": "/tmp/templates",
"last_push_branches": [],
}
templates._display_status(status)
call_args_list = [str(c) for c in mocks["console"].print.call_args_list]
joined = " ".join(call_args_list)
assert "MISSING" in joined
def test_status_with_pushed_branches(self, monkeypatch) -> None:
"""last_push_branches has entries -- shows branch count and names."""
templates, mocks = _import_templates(monkeypatch)
status = {
"version": "2.0.0",
"last_push": "2026-03-20",
"local_template_exists": True,
"observations_template_exists": True,
"templates_dir": "/tmp/templates",
"last_push_branches": ["CLI", "MEMORY", "DRONE"],
}
templates._display_status(status)
call_args_list = [str(c) for c in mocks["console"].print.call_args_list]
joined = " ".join(call_args_list)
assert "3" in joined
assert "CLI" in joined
def test_status_many_branches(self, monkeypatch) -> None:
""">8 pushed branches -- display is truncated with '... (+N more)'."""
templates, mocks = _import_templates(monkeypatch)
branch_names = [f"BRANCH_{i}" for i in range(12)]
status = {
"version": "2.0.0",
"last_push": "2026-03-20",
"local_template_exists": True,
"observations_template_exists": True,
"templates_dir": "/tmp/templates",
"last_push_branches": branch_names,
}
templates._display_status(status)
call_args_list = [str(c) for c in mocks["console"].print.call_args_list]
joined = " ".join(call_args_list)
assert "+4 more" in joined
assert "12" in joined
def test_status_no_pushed_branches(self, monkeypatch) -> None:
"""Empty last_push_branches -- shows 'none'."""
templates, mocks = _import_templates(monkeypatch)
status = {
"version": "2.0.0",
"last_push": "2026-03-20",
"local_template_exists": True,
"observations_template_exists": True,
"templates_dir": "/tmp/templates",
"last_push_branches": [],
}
templates._display_status(status)
call_args_list = [str(c) for c in mocks["console"].print.call_args_list]
joined = " ".join(call_args_list)
assert "none" in joined