generated from coulomb/repo-seed
session-memory Phase 2: review workflow (T03)
UI-free discuss/approve/reject engine driving detect candidates into the catalog via a decide callback. candidate_to_pattern builds a provisional SolutionPattern with per-flavor rendering-hint stubs. ReviewLog makes re-review idempotent: prior rejects remembered, re-surfaced only when the evidence fingerprint changes. 6 new tests; suite 58/58 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
93
tests/test_curate_review.py
Normal file
93
tests/test_curate_review.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Review workflow tests (T03): promote/reject/discuss + idempotent re-review."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from session_memory.curate.catalog import Catalog # noqa: E402
|
||||
from session_memory.curate.review import ( # noqa: E402
|
||||
APPROVE,
|
||||
DISCUSS,
|
||||
REJECT,
|
||||
ReviewLog,
|
||||
candidate_to_pattern,
|
||||
review,
|
||||
)
|
||||
from session_memory.curate.schema import SolutionPattern # noqa: E402
|
||||
|
||||
|
||||
def _candidate(key="success:clean_pass:outcome", freq=18, flavors=("claude", "grok")):
|
||||
return {
|
||||
"key": key,
|
||||
"polarity": key.split(":")[0],
|
||||
"signal_type": key.split(":")[1],
|
||||
"locus": key.split(":")[2],
|
||||
"title": "cross-flavor success: clean pass",
|
||||
"frequency": freq,
|
||||
"flavors": list(flavors),
|
||||
"repos": ["agentic-resources"],
|
||||
"sessions": [f"s{i}" for i in range(freq)],
|
||||
"cross_flavor": len(flavors) > 1,
|
||||
"cost_impact": 12.5,
|
||||
}
|
||||
|
||||
|
||||
def _decider(action, rationale="because"):
|
||||
return lambda cand: (action, rationale)
|
||||
|
||||
|
||||
def test_approve_promotes_to_catalog(tmp_path):
|
||||
cat = Catalog(str(tmp_path / "catalog"))
|
||||
log = ReviewLog(str(tmp_path / "reviews.jsonl"))
|
||||
res = review([_candidate()], _decider(APPROVE), cat, log)
|
||||
assert len(res.approved) == 1
|
||||
p = cat.load(SolutionPattern.make_id("success:clean_pass:outcome"))
|
||||
assert p is not None
|
||||
assert p.scope.flavors == ["claude", "grok"]
|
||||
assert set(p.rendering_hints) == {"claude", "grok"}
|
||||
assert p.provenance.evidence["frequency"] == 18
|
||||
|
||||
|
||||
def test_reject_records_no_catalog_write(tmp_path):
|
||||
cat = Catalog(str(tmp_path / "catalog"))
|
||||
log = ReviewLog(str(tmp_path / "reviews.jsonl"))
|
||||
res = review([_candidate()], _decider(REJECT), cat, log)
|
||||
assert res.rejected == ["success:clean_pass:outcome"]
|
||||
assert cat.list() == []
|
||||
|
||||
|
||||
def test_discuss_defers_and_is_not_final(tmp_path):
|
||||
cat = Catalog(str(tmp_path / "catalog"))
|
||||
log = ReviewLog(str(tmp_path / "reviews.jsonl"))
|
||||
res = review([_candidate()], _decider(DISCUSS), cat, log)
|
||||
assert res.deferred == ["success:clean_pass:outcome"]
|
||||
# not recorded as final -> a later pass re-surfaces it
|
||||
res2 = review([_candidate()], _decider(APPROVE), cat, log)
|
||||
assert len(res2.approved) == 1
|
||||
|
||||
|
||||
def test_prior_reject_remembered_same_evidence(tmp_path):
|
||||
cat = Catalog(str(tmp_path / "catalog"))
|
||||
log_path = str(tmp_path / "reviews.jsonl")
|
||||
review([_candidate()], _decider(REJECT), cat, ReviewLog(log_path))
|
||||
# fresh log instance (reloads from disk) + same evidence -> skipped
|
||||
res = review([_candidate()], _decider(APPROVE), cat, ReviewLog(log_path))
|
||||
assert res.skipped == ["success:clean_pass:outcome"]
|
||||
assert cat.list() == []
|
||||
|
||||
|
||||
def test_changed_evidence_resurfaces(tmp_path):
|
||||
cat = Catalog(str(tmp_path / "catalog"))
|
||||
log_path = str(tmp_path / "reviews.jsonl")
|
||||
review([_candidate(freq=18)], _decider(REJECT), cat, ReviewLog(log_path))
|
||||
# more evidence now -> not skipped, gets re-reviewed
|
||||
res = review([_candidate(freq=40)], _decider(APPROVE), cat, ReviewLog(log_path))
|
||||
assert len(res.approved) == 1
|
||||
|
||||
|
||||
def test_candidate_to_pattern_defaults():
|
||||
p = candidate_to_pattern(_candidate(flavors=("claude",)))
|
||||
assert p.status == "provisional"
|
||||
assert p.rendering_hints["claude"]["target"] == "CLAUDE.md"
|
||||
assert p.polarity == "success"
|
||||
Reference in New Issue
Block a user