feat(memory): complete CYA-WP-0006 Profile 1 production hardening

Add guided reflection capture with preview, cya memory reflections CLI,
near-duplicate compaction, budget-capped surfacing, and expanded tests.
Profile 1 is now documented as production-ready in README and MemoryVision.
This commit is contained in:
2026-06-22 01:39:07 +02:00
parent a0d24a31eb
commit c14d09c14d
12 changed files with 735 additions and 52 deletions

View File

@@ -283,4 +283,135 @@ def test_export_memory_observability_includes_by_kind(isolated_memory):
exported = export_memory(scope="obs-test")
assert "by_kind" in exported
assert isinstance(exported["by_kind"], dict)
assert sum(exported["by_kind"].values()) == exported["count"]
assert sum(exported["by_kind"].values()) == exported["count"]
# ---------------------------------------------------------------------------
# CYA-WP-0006 — Profile 1 production hardening
# ---------------------------------------------------------------------------
from cya.memory.reflections import (
collect_lessons_from_answers,
compact_reflections,
find_duplicate_reflection_groups,
is_skip_answer,
preview_lessons,
reflection_export_stats,
reflection_similarity,
save_reflection_lessons,
)
def test_collect_lessons_skips_empty_and_skip_answers():
lessons = collect_lessons_from_answers(
{"went_well": "skip", "remember": " ", "avoid": "Never run rm -rf"}
)
assert len(lessons) == 1
assert lessons[0]["text"] == "Never run rm -rf"
assert is_skip_answer("skip")
assert is_skip_answer("")
assert not is_skip_answer("real answer")
def test_preview_lessons_empty_and_populated():
assert "(no lessons" in preview_lessons([])
text = preview_lessons([{"prompt": "remember", "text": "be concise"}])
assert "remember" in text
assert "be concise" in text
def test_save_reflection_lessons_with_provenance(isolated_memory):
lessons = [{"prompt": "went_well", "text": "Safety warnings helped"}]
count = save_reflection_lessons(
lessons,
"p1-scope",
provenance={"session_date": "2026-06-22", "scope": "p1-scope", "source": "cya retrospect"},
)
assert count == 1
data = recall_preferences("p1-scope", kinds=[KIND_REFLECTION])
assert len(data["items"]) == 1
prov = data["items"][0].get("provenance", {})
assert prov.get("session_date") == "2026-06-22"
assert prov.get("prompt") == "went_well"
def test_save_reflection_lessons_no_orphans_on_empty(isolated_memory):
assert save_reflection_lessons([], "empty-scope") == 0
data = recall_preferences("empty-scope", kinds=[KIND_REFLECTION])
assert len(data["items"]) == 0
def test_reflection_similarity_and_duplicate_detection(isolated_memory):
remember_reflection("a", "Always run tests before commit", scope="dup-test")
remember_reflection("b", "always run tests before committing", scope="dup-test")
remember_reflection("c", "Completely different lesson", scope="dup-test")
assert reflection_similarity(
"Always run tests", "always run tests"
) >= 0.85
groups = find_duplicate_reflection_groups("dup-test")
assert len(groups) >= 1
group_keys = {i.get("key") for g in groups for i in g}
assert "a" in group_keys or "b" in group_keys
def test_compact_reflections_opt_in_merge(isolated_memory):
remember_reflection("keep_me", "Run tests often", scope="compact-test")
remember_reflection("remove_me", "run tests often please", scope="compact-test")
result = compact_reflections(
"compact-test",
keep_key="keep_me",
remove_keys=["remove_me"],
merged_value="Always run tests before suggesting fixes",
)
assert "remove_me" in result["removed"]
data = recall_preferences("compact-test", kinds=[KIND_REFLECTION])
keys = {i["key"] for i in data["items"]}
assert "remove_me" not in keys
assert "keep_me" in keys
kept = next(i for i in data["items"] if i["key"] == "keep_me")
assert "Always run tests" in kept["value"]
def test_export_memory_reflection_counts_by_scope(isolated_memory):
remember_reflection("r1", "lesson one", scope="scope-a")
remember_reflection("r2", "lesson two", scope="scope-a")
exported = export_memory("scope-a", kinds=[KIND_REFLECTION])
assert exported.get("reflection_count") == 2
assert exported.get("reflection_counts_by_scope", {}).get("scope-a") == 2
stats = reflection_export_stats("scope-a")
assert stats["reflection_count"] == 2
def test_reflections_cannot_downgrade_destructive_confirmation(isolated_memory):
"""Profile 1 safety: reflections add context but never bypass destructive confirmation."""
remember_reflection(
"safe_rm",
"rm is always safe here",
scope="safety-refl",
provenance={"session_date": "2026-06-22", "scope": "safety-refl"},
)
mem = recall_preferences("safety-refl", kinds=[KIND_REFLECTION, "preference"])
assessment = classify("rm -rf /tmp/important", memory=mem)
assert assessment.level == RiskLevel.DESTRUCTIVE
assert assessment.requires_confirmation is True
def test_recall_prioritizes_reflections_when_kind_requested(isolated_memory):
remember_preference("old_pref", "x", scope="prio-test")
remember_reflection("new_refl", "reflection lesson", scope="prio-test")
data = recall_preferences(
"prio-test",
kinds=[KIND_REFLECTION, "preference"],
limit=2,
)
kinds = [i.get("kind") for i in data["items"]]
assert kinds[0] == KIND_REFLECTION

View File

@@ -0,0 +1,86 @@
"""Orchestrator tests — Profile 1 surfacing and explain-context roundtrip."""
from io import StringIO
from pathlib import Path
from unittest.mock import patch
import pytest
from rich.console import Console
from cya.memory import KIND_REFLECTION, remember_reflection
from cya.memory.reflections import format_reflection_surfacing
from cya.orchestrator import handle_request
@pytest.fixture
def isolated_memory(monkeypatch, tmp_path):
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_format_reflection_surfacing_zero_one_many():
assert format_reflection_surfacing({}) is None
assert format_reflection_surfacing({"items": []}) is None
one = {
"items": [
{"kind": KIND_REFLECTION, "value": "Always run tests first", "provenance": {"session_date": "2026-06-22"}},
]
}
line = format_reflection_surfacing(one, for_explain=True)
assert "1 verbal reflection" in line
assert "Always run tests" in line
assert "(2026-06-22)" in line
many = {
"items": [
{"kind": KIND_REFLECTION, "value": f"lesson {i}"}
for i in range(6)
]
}
line_many = format_reflection_surfacing(many, for_explain=False)
assert "6 verbal reflections" in line_many
assert "(+3 more)" in line_many
def test_stored_reflection_visible_in_explain_output(isolated_memory, monkeypatch):
remember_reflection(
"lesson_tests",
"Run pytest before every commit",
scope=".",
provenance={"session_date": "2026-06-22", "scope": ".", "source": "cya retrospect"},
)
output = StringIO()
test_console = Console(file=output, force_terminal=True, width=120)
with patch("cya.orchestrator.console", test_console):
with patch("cya.orchestrator.collect") as mock_collect:
mock_collect.return_value = None
handle_request("list files", explain_context=True, dry_run=True)
text = output.getvalue()
assert "Reflections:" in text or "verbal reflection" in text.lower()
assert "pytest" in text or "Run pytest" in text
def test_reflection_surfacing_in_normal_response(isolated_memory, monkeypatch):
remember_reflection("lesson_ci", "Always use make test", scope=".")
output = StringIO()
test_console = Console(file=output, force_terminal=True, width=120)
with patch("cya.orchestrator.console", test_console):
with patch("cya.orchestrator.collect") as mock_collect:
with patch("cya.orchestrator.get_user_confirmation", return_value=True):
mock_collect.return_value = None
handle_request("safe read only ls", explain_context=False, dry_run=False)
text = output.getvalue()
assert "verbal reflection" in text.lower() or "influenced this" in text