"""Recurring-error signal + clustering (WP-0006 T02).""" import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from session_memory.detect.cluster import cluster # noqa: E402 from session_memory.detect.signals import ( # noqa: E402 extract_signals, sig_recurring_error, ) def _digest(uid, repo, flavor="claude", snippets=None): return { "session_uid": uid, "flavor": flavor, "repo": repo, "outcome": "success", "cost": {"input_tokens": 1, "output_tokens": 1}, "markers": {"errors": 0, "retries": 0, "test_runs": 0}, "tool_histogram": {}, "error_snippets": snippets or [], } _FP = "modulenotfounderror: no module named 'foo' at :" def test_signal_per_distinct_fingerprint(): d = _digest("claude:a", "r1", snippets=[ {"fingerprint": _FP, "sample": "ModuleNotFoundError ...", "count": 3, "tool": "Bash"}, {"fingerprint": "keyerror: ", "sample": "KeyError", "count": 1, "tool": None}, ]) sigs = sig_recurring_error(d, {}) assert len(sigs) == 2 top = [s for s in sigs if s.locus == _FP][0] assert top.type == "recurring_error" assert top.magnitude == 3.0 assert top.detail["sample"].startswith("ModuleNotFound") def test_clusters_across_sessions_and_flavors(): # same fingerprint in a claude and a grok session -> cross-flavor candidate digs = [ _digest("claude:a", "r1", "claude", [{"fingerprint": _FP, "sample": "ModuleNotFoundError", "count": 2, "tool": "Bash"}]), _digest("grok:b", "r2", "grok", [{"fingerprint": _FP, "sample": "ModuleNotFoundError", "count": 1, "tool": None}]), ] signals = extract_signals(digs) pats = cluster([s for s in signals if s.type == "recurring_error"], min_frequency=2) assert len(pats) == 1 p = pats[0] assert p.signal_type == "recurring_error" assert p.cross_flavor is True assert sorted(p.flavors) == ["claude", "grok"] assert p.frequency == 2 def test_no_snippets_no_signal(): assert sig_recurring_error(_digest("claude:a", "r1"), {}) == []