"""Tests for the real (T02) memory ports + T04/T05 safety integration. These tests exercise the user-controlled json-backed implementation and verify: - Actual persistence across calls (within test scope) - Proper scoping - Memory + risk safety interaction (never prefs still force confirmation) - Graceful degradation - Observability data in the return values """ import json import tempfile from pathlib import Path from unittest.mock import patch import pytest from cya.memory import ( remember_preference, recall_preferences, forget, export_memory, ) from cya.safety.risk import classify, RiskLevel @pytest.fixture def isolated_memory(monkeypatch, tmp_path): """Make memory use a completely isolated temp directory for the test.""" mem_dir = tmp_path / "memory" mem_dir.mkdir() def _fake_mem_path(scope: str = "cwd") -> Path: return mem_dir / f"{scope}.json" monkeypatch.setattr("cya.memory._mem_path", _fake_mem_path) return mem_dir def test_remember_and_recall_roundtrip(isolated_memory): remember_preference("project_name", "can-you-assist", scope="test-scope") remember_preference("favorite_cmd", "git status", scope="test-scope") data = recall_preferences(scope="test-scope") assert data["phase"] == "fluid" assert len(data["items"]) == 2 keys = {item["key"] for item in data["items"]} assert "project_name" in keys assert "favorite_cmd" in keys # Observability / provenance is present prov = data.get("provenance", [{}])[0] assert "source" in prov assert "cya-local-memory" in prov.get("source", "") def test_forget_specific_keys(isolated_memory): remember_preference("a", 1, scope="forget-test") remember_preference("b", 2, scope="forget-test") forget(scope="forget-test", keys=["a"]) data = recall_preferences(scope="forget-test") keys = {item["key"] for item in data["items"]} assert "a" not in keys assert "b" in keys def test_forget_all(isolated_memory): remember_preference("x", "y", scope="clear-test") forget(scope="clear-test") # no keys = clear all data = recall_preferences(scope="clear-test") assert len(data["items"]) == 0 def test_memory_signals_still_force_confirmation_on_dangerous(isolated_memory): """Core T04 + T05 invariant: memory can never bypass safety.""" # User has a standing "never" preference remember_preference( "never_auto_run", "rm -rf", scope="safety-test" ) mem = recall_preferences(scope="safety-test") assessment = classify("rm -rf /tmp/important", memory=mem) assert assessment.level == RiskLevel.DESTRUCTIVE assert assessment.requires_confirmation is True assert any("Memory" in r or "never" in r.lower() for r in assessment.rules_triggered) def test_graceful_degradation_when_storage_fails(monkeypatch, tmp_path): """Memory should not crash the assistant if the backing store is broken.""" def _broken_mem_path(scope="cwd"): p = tmp_path / "broken" / f"{scope}.json" p.parent.mkdir(parents=True) # Make the parent read-only after creation so writes will fail p.parent.chmod(0o400) return p monkeypatch.setattr("cya.memory._mem_path", _broken_mem_path) # Should not raise remember_preference("will_fail", "value", scope="broken") data = recall_preferences(scope="broken") assert isinstance(data, dict) # still returns something usable def test_export_memory_observability(isolated_memory): remember_preference("theme", "dark", scope="export-test") exported = export_memory(scope="export-test") assert exported["status"].startswith("real") assert exported["count"] >= 1 assert "provenance_summary" in exported assert "phase" in exported