Files
graphify/tests/test_ingest.py
T
safishamsi 89dd00f140 feat: self-improving work-memory — save-result outcomes + graphify reflect (#1441)
Adds the deterministic work-memory loop: `save-result --outcome
useful|dead_end|corrected [--correction]` records how a saved Q&A turned out, and
`graphify reflect` aggregates graphify-out/memory/ into a deterministic
reflections/LESSONS.md an agent loads next session.

Source nodes are scored, not counted: signed, recency-decayed (useful +,
dead_end/corrected -, configurable --half-life-days, default 30), so a fresh dead
end outweighs a stale useful. A node is "preferred" only once corroborated by
>=--min-corroboration distinct results (default 2); others are "tentative", and
mixed-signal nodes render once as "contested" (recency-wins). Source nodes are
matched to the graph by label OR id, and citations whose node no longer exists are
dropped, so a plain `graphify update` after deleting code clears stale lessons.
Deterministic, no LLM; bare save-result and existing behavior unchanged.

Rigorously verified end-to-end on real data: corroboration boundary, recency flip,
contested verdict, foreign/malformed memory docs, cold start, 300-doc scale +
byte-stable output, and the node-gate dropping deleted-code lessons after update.
Full suite 2383 passed; skillgen --check clean; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:43:13 +01:00

101 lines
3.4 KiB
Python

"""Tests for graphify.ingest.save_query_result"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
from graphify.ingest import save_query_result
def test_file_created(tmp_path):
out = save_query_result("what is attention?", "Attention is...", tmp_path / "memory")
assert out.exists()
def test_filename_format(tmp_path):
mem = tmp_path / "memory"
out = save_query_result("what connects A to B?", "They share...", mem)
assert out.name.startswith("query_")
assert out.suffix == ".md"
def test_frontmatter_question(tmp_path):
mem = tmp_path / "memory"
question = "what is attention?"
out = save_query_result(question, "Attention is softmax.", mem)
content = out.read_text()
assert "question:" in content
assert "attention" in content.lower()
def test_frontmatter_type(tmp_path):
mem = tmp_path / "memory"
out = save_query_result("q", "a", mem, query_type="path_query")
content = out.read_text()
assert 'type: "path_query"' in content
def test_source_nodes_included(tmp_path):
mem = tmp_path / "memory"
nodes = ["AttentionLayer", "SoftmaxFunc"]
out = save_query_result("q", "a", mem, source_nodes=nodes)
content = out.read_text()
assert "AttentionLayer" in content
assert "SoftmaxFunc" in content
def test_source_nodes_capped_at_10(tmp_path):
mem = tmp_path / "memory"
nodes = [f"Node{i}" for i in range(20)]
out = save_query_result("q", "a", mem, source_nodes=nodes)
content = out.read_text()
# Only first 10 should appear in frontmatter source_nodes line
fm_line = [l for l in content.splitlines() if l.startswith("source_nodes:")][0]
assert fm_line.count('"Node') == 10
def test_memory_dir_created(tmp_path):
mem = tmp_path / "deep" / "memory"
assert not mem.exists()
save_query_result("q", "a", mem)
assert mem.exists()
def test_answer_in_body(tmp_path):
mem = tmp_path / "memory"
answer = "The answer is forty-two."
out = save_query_result("what is the answer?", answer, mem)
content = out.read_text()
assert answer in content
def test_outcome_in_frontmatter_and_body(tmp_path):
"""An outcome signal is written to both frontmatter (for `reflect`) and an
## Outcome body section (so it round-trips into the graph on re-extraction)."""
out = save_query_result("q", "a", tmp_path / "memory", outcome="useful")
content = out.read_text()
assert 'outcome: "useful"' in content
assert "## Outcome" in content
assert "- Signal: useful" in content
def test_correction_in_frontmatter_and_body(tmp_path):
out = save_query_result(
"what hashes passwords?", "MD5", tmp_path / "memory",
outcome="corrected", correction="It's bcrypt, see PasswordHasher",
)
content = out.read_text()
assert 'correction: "It\'s bcrypt, see PasswordHasher"' in content
assert "- Correction: It's bcrypt, see PasswordHasher" in content
def test_no_outcome_means_no_outcome_section(tmp_path):
"""Backward compatible: a result without an outcome looks exactly as before."""
out = save_query_result("q", "a", tmp_path / "memory")
content = out.read_text()
assert "outcome:" not in content
assert "## Outcome" not in content
def test_invalid_outcome_rejected(tmp_path):
with pytest.raises(ValueError):
save_query_result("q", "a", tmp_path / "memory", outcome="great")