"""Tests for the feedback.db -> Langfuse score bridge.""" import os import sys import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "pilot")) import feedback # noqa: E402 import feedback_scores as fs # noqa: E402 @pytest.fixture def db(tmp_path): conn = feedback.init(str(tmp_path / "fb.db")) yield conn conn.close() def _seed_finding(conn, repo="o/r", pr=1, comment_id=100, path="a.py", line=1): cur = conn.execute( "INSERT INTO review (repo, pr, head_sha, posted_at) VALUES (?,?,?,?)", (repo, pr, "deadbeef", 1000), ) review_id = cur.lastrowid cur = conn.execute( """INSERT INTO inline_finding (review_id, repo, pr, path, line, severity, problem, comment_id, posthash, posted_at) VALUES (?,?,?,?,?,?,?,?,?,?)""", (review_id, repo, pr, path, line, "HIGH", "problem", comment_id, f"h{comment_id}", 1000), ) conn.commit() return cur.lastrowid # --- score_pr maths ------------------------------------------------------- def test_engagement_zero_when_nobody_responded(): v = fs.score_pr({"total": 4, "engaged": 0, "positive": 0, "negative": 0}) assert v[fs.REVIEW_ENGAGEMENT] == 0.0 def test_acceptance_absent_when_nobody_engaged(): # Not 0.0 — zero would claim humans judged it neutral. v = fs.score_pr({"total": 4, "engaged": 0, "positive": 0, "negative": 0}) assert v[fs.REVIEW_ACCEPTANCE] is None def test_engagement_is_a_share_of_findings(): v = fs.score_pr({"total": 4, "engaged": 1, "positive": 1, "negative": 0}) assert v[fs.REVIEW_ENGAGEMENT] == 0.25 def test_acceptance_all_positive(): v = fs.score_pr({"total": 2, "engaged": 2, "positive": 3, "negative": 0}) assert v[fs.REVIEW_ACCEPTANCE] == 1.0 def test_acceptance_all_negative(): v = fs.score_pr({"total": 2, "engaged": 2, "positive": 0, "negative": 2}) assert v[fs.REVIEW_ACCEPTANCE] == -1.0 def test_acceptance_mixed_is_normalised(): v = fs.score_pr({"total": 4, "engaged": 4, "positive": 3, "negative": 1}) assert v[fs.REVIEW_ACCEPTANCE] == 0.5 def test_engagement_absent_when_no_findings_at_all(): v = fs.score_pr({"total": 0, "engaged": 0, "positive": 0, "negative": 0}) assert v[fs.REVIEW_ENGAGEMENT] is None # --- collect_pr_feedback over a real sqlite ------------------------------ def test_collect_counts_nothing_on_untouched_findings(db): _seed_finding(db) tally = fs.collect_pr_feedback(db, "o/r", 1) assert tally == {"total": 1, "engaged": 0, "positive": 0, "negative": 0} def test_collect_counts_positive_reaction(db): _seed_finding(db, comment_id=101) db.execute( "INSERT INTO reaction (comment_id, user, content, created_at) VALUES (?,?,?,?)", (101, "alice", "+1", 1), ) db.commit() tally = fs.collect_pr_feedback(db, "o/r", 1) assert tally["positive"] == 1 and tally["engaged"] == 1 def test_collect_counts_negative_reaction(db): _seed_finding(db, comment_id=102) db.execute( "INSERT INTO reaction (comment_id, user, content, created_at) VALUES (?,?,?,?)", (102, "bob", "-1", 1), ) db.commit() tally = fs.collect_pr_feedback(db, "o/r", 1) assert tally["negative"] == 1 and tally["engaged"] == 1 def test_resolved_thread_counts_positive(db): fid = _seed_finding(db, comment_id=103) db.execute( "INSERT INTO thread_state (finding_id, resolved, checked_at) VALUES (?,?,?)", (fid, 1, 1), ) db.commit() tally = fs.collect_pr_feedback(db, "o/r", 1) assert tally["positive"] == 1 and tally["engaged"] == 1 def test_unresolved_thread_is_not_a_vote(db): fid = _seed_finding(db, comment_id=104) db.execute( "INSERT INTO thread_state (finding_id, resolved, checked_at) VALUES (?,?,?)", (fid, 0, 1), ) db.commit() tally = fs.collect_pr_feedback(db, "o/r", 1) assert tally == {"total": 1, "engaged": 0, "positive": 0, "negative": 0} def test_negation_reply_counts_negative(db): fid = _seed_finding(db, comment_id=105) db.execute( "INSERT INTO reply (finding_id, author, body, created_at) VALUES (?,?,?,?)", (fid, "carol", "this is a false positive", 1), ) db.commit() tally = fs.collect_pr_feedback(db, "o/r", 1) assert tally["negative"] == 1 and tally["engaged"] == 1 def test_neutral_reply_is_engagement_but_not_a_vote(db): fid = _seed_finding(db, comment_id=106) db.execute( "INSERT INTO reply (finding_id, author, body, created_at) VALUES (?,?,?,?)", (fid, "dave", "done", 1), ) db.commit() tally = fs.collect_pr_feedback(db, "o/r", 1) assert tally["engaged"] == 1 assert tally["positive"] == 0 and tally["negative"] == 0 # --- event shape ---------------------------------------------------------- def test_build_score_events_shape(): events = fs.build_score_events("o/r", 7, {fs.REVIEW_ENGAGEMENT: 0.5}, "claude") assert len(events) == 1 body = events[0]["body"] assert events[0]["type"] == "score-create" assert body["sessionId"] == "o/r#7" assert body["value"] == 0.5 assert body["environment"] == "claude" def test_build_score_events_skips_none(): events = fs.build_score_events("o/r", 7, {fs.REVIEW_ACCEPTANCE: None}) assert events == [] def test_score_ids_are_stable_across_runs(): # A backfill re-run must update, not duplicate. a = fs.build_score_events("o/r", 7, {fs.REVIEW_ENGAGEMENT: 0.5})[0]["body"]["id"] b = fs.build_score_events("o/r", 7, {fs.REVIEW_ENGAGEMENT: 0.9})[0]["body"]["id"] assert a == b def test_score_ids_differ_per_pr_and_name(): e1 = fs.build_score_events("o/r", 7, {fs.REVIEW_ENGAGEMENT: 1})[0]["body"]["id"] e2 = fs.build_score_events("o/r", 8, {fs.REVIEW_ENGAGEMENT: 1})[0]["body"]["id"] e3 = fs.build_score_events("o/r", 7, {fs.REVIEW_ACCEPTANCE: 1})[0]["body"]["id"] assert len({e1, e2, e3}) == 3 def test_backfill_dry_run_reports_without_posting(db, tmp_path): _seed_finding(db, comment_id=107) db.commit() path = db.execute("PRAGMA database_list").fetchone()[2] summary = fs.backfill(path, dry_run=True) assert summary["prs_scanned"] == 1 assert summary["prs_with_engagement"] == 0 assert summary["posted"] is False def test_every_emitted_score_has_a_config(): configured = {c["name"] for c in fs.SCORE_CONFIGS} assert {fs.REVIEW_ENGAGEMENT, fs.REVIEW_ACCEPTANCE} == configured def test_every_event_carries_a_timestamp(): # Without one the ingestion endpoint 400s the event inside a 207 that the # caller reads as success. events = fs.build_score_events("o/r", 1, {fs.REVIEW_ENGAGEMENT: 0.0}) assert events assert all(e.get("timestamp") for e in events) def test_explicit_timestamp_is_used(): events = fs.build_score_events( "o/r", 1, {fs.REVIEW_ENGAGEMENT: 0.0}, timestamp="2026-01-02T03:04:05Z" ) assert events[0]["timestamp"] == "2026-01-02T03:04:05Z"