"""Tests for lib/contributor.py — contributor attribution functions.""" import sqlite3 import asyncio import sys import os from unittest.mock import AsyncMock, MagicMock, patch sys.modules.setdefault("aiohttp", MagicMock()) sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from lib.contributor import ( is_knowledge_pr, refine_commit_type, record_contributor_attribution, upsert_contributor, recalculate_tier, ) # --- is_knowledge_pr --- def test_knowledge_pr_domains(): diff = "+++ b/domains/crypto/some-claim.md\n" assert is_knowledge_pr(diff) is True def test_knowledge_pr_core(): diff = "+++ b/core/epistemology.md\n" assert is_knowledge_pr(diff) is True def test_knowledge_pr_foundations(): diff = "--- a/foundations/overview.md\n" assert is_knowledge_pr(diff) is True def test_knowledge_pr_decisions(): diff = "+++ b/decisions/some-decision.md\n" assert is_knowledge_pr(diff) is True def test_pipeline_only_pr(): diff = "+++ b/inbox/source.md\n+++ b/entities/metadao.md\n" assert is_knowledge_pr(diff) is False def test_mixed_pr_counts_as_knowledge(): diff = "+++ b/inbox/source.md\n+++ b/domains/crypto/claim.md\n" assert is_knowledge_pr(diff) is True def test_empty_diff(): assert is_knowledge_pr("") is False # --- refine_commit_type --- def test_refine_non_extract_unchanged(): assert refine_commit_type("anything", "research") == "research" assert refine_commit_type("anything", "entity") == "entity" def test_refine_extract_new_files(): diff = "diff --git a/x b/y\nnew file\n+++ b/domains/crypto/claim.md\n" assert refine_commit_type(diff, "extract") == "extract" def test_refine_extract_challenge(): diff = "diff --git a/x b/y\n+++ b/domains/crypto/claim.md\n+challenged_by: other\n" assert refine_commit_type(diff, "extract") == "challenge" def test_refine_extract_enrich(): diff = "diff --git a/x b/y\n+++ b/domains/crypto/claim.md\n+confidence: 0.8\n" assert refine_commit_type(diff, "extract") == "enrich" def test_refine_extract_mixed_new_and_modified(): diff = ( "diff --git a/x b/y\nnew file\n+++ b/domains/crypto/new.md\n" "diff --git a/x b/z\n+++ b/domains/crypto/existing.md\n+foo\n" ) assert refine_commit_type(diff, "extract") == "extract" # --- upsert_contributor + recalculate_tier --- def _make_db(): conn = sqlite3.connect(":memory:") conn.row_factory = sqlite3.Row conn.execute("""CREATE TABLE contributors ( handle TEXT PRIMARY KEY, agent_id TEXT, tier TEXT DEFAULT 'new', first_contribution TEXT, last_contribution TEXT, claims_merged INTEGER DEFAULT 0, challenges_survived INTEGER DEFAULT 0, sourcer_count INTEGER DEFAULT 0, extractor_count INTEGER DEFAULT 0, challenger_count INTEGER DEFAULT 0, synthesizer_count INTEGER DEFAULT 0, reviewer_count INTEGER DEFAULT 0, updated_at TEXT )""") conn.execute("""CREATE TABLE audit_log ( id INTEGER PRIMARY KEY, ts TEXT DEFAULT (datetime('now')), stage TEXT, event TEXT, detail TEXT )""") return conn def test_upsert_new_contributor(): conn = _make_db() with patch("lib.contributor.config") as mock_config: mock_config.CONTRIBUTOR_TIER_RULES = { "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5}, "contributor": {"claims_merged": 10}, } upsert_contributor(conn, "rio", "uuid-123", "extractor", "2026-04-16") row = conn.execute("SELECT * FROM contributors WHERE handle = 'rio'").fetchone() assert row["extractor_count"] == 1 assert row["claims_merged"] == 1 assert row["tier"] == "new" def test_upsert_increment(): conn = _make_db() upsert_contributor(conn, "rio", "uuid-123", "extractor", "2026-04-16") upsert_contributor(conn, "rio", "uuid-123", "extractor", "2026-04-17") row = conn.execute("SELECT * FROM contributors WHERE handle = 'rio'").fetchone() assert row["extractor_count"] == 2 assert row["claims_merged"] == 2 def test_upsert_reviewer_no_claim_increment(): conn = _make_db() upsert_contributor(conn, "leo", None, "reviewer", "2026-04-16") row = conn.execute("SELECT * FROM contributors WHERE handle = 'leo'").fetchone() assert row["reviewer_count"] == 1 assert row["claims_merged"] == 0 def test_upsert_unknown_role(): conn = _make_db() upsert_contributor(conn, "rio", None, "wizard", "2026-04-16") row = conn.execute("SELECT * FROM contributors WHERE handle = 'rio'").fetchone() assert row is None # Should not insert def test_recalculate_tier_contributor(): conn = _make_db() conn.execute( """INSERT INTO contributors (handle, claims_merged, challenges_survived, first_contribution, tier) VALUES ('rio', 15, 0, '2026-01-01', 'new')""" ) with patch("lib.contributor.config") as mock_config: mock_config.CONTRIBUTOR_TIER_RULES = { "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5}, "contributor": {"claims_merged": 10}, } recalculate_tier(conn, "rio") row = conn.execute("SELECT tier FROM contributors WHERE handle = 'rio'").fetchone() assert row["tier"] == "contributor" def test_recalculate_tier_veteran(): conn = _make_db() conn.execute( """INSERT INTO contributors (handle, claims_merged, challenges_survived, first_contribution, tier) VALUES ('rio', 60, 10, '2025-01-01', 'contributor')""" ) with patch("lib.contributor.config") as mock_config: mock_config.CONTRIBUTOR_TIER_RULES = { "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5}, "contributor": {"claims_merged": 10}, } recalculate_tier(conn, "rio") row = conn.execute("SELECT tier FROM contributors WHERE handle = 'rio'").fetchone() assert row["tier"] == "veteran" # --- record_contributor_attribution --- def _make_attribution_db(): conn = _make_db() conn.execute("""CREATE TABLE prs ( number INTEGER PRIMARY KEY, commit_type TEXT, agent TEXT )""") conn.execute("INSERT INTO prs VALUES (100, 'extract', 'rio')") return conn def test_record_skips_pipeline_only(): conn = _make_attribution_db() mock_diff = "+++ b/inbox/source.md\n" async def run(): with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=mock_diff): git_fn = AsyncMock(return_value=(0, "")) await record_contributor_attribution(conn, 100, "extract/test", git_fn) asyncio.run(run()) row = conn.execute("SELECT * FROM contributors").fetchone() assert row is None # No attribution for pipeline-only def test_record_fallback_to_pr_agent(): conn = _make_attribution_db() mock_diff = "+++ b/domains/crypto/claim.md\n+some content\n" async def run(): with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=mock_diff): # First call: trailer log (no trailers), Second call: author log (bot name → skipped) git_fn = AsyncMock(side_effect=[(0, "no trailers here"), (0, "m3taversal")]) with patch("lib.contributor.config") as mock_config: mock_config.CONTRIBUTOR_TIER_RULES = { "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5}, "contributor": {"claims_merged": 10}, } await record_contributor_attribution(conn, 100, "extract/test", git_fn) asyncio.run(run()) row = conn.execute("SELECT * FROM contributors WHERE handle = 'rio'").fetchone() assert row is not None assert row["extractor_count"] == 1 def test_record_fallback_to_git_author(): """External contributors get credited via git commit author.""" conn = _make_attribution_db() conn.execute("INSERT INTO prs VALUES (200, 'contrib', 'external')") mock_diff = "+++ b/domains/ai-alignment/claim.md\n+new content\n" async def run(): with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=mock_diff): # First call: trailer log (no trailers), Second call: author log (external name) git_fn = AsyncMock(side_effect=[(0, "no trailers"), (0, "Cameron-S1")]) with patch("lib.contributor.config") as mock_config: mock_config.CONTRIBUTOR_TIER_RULES = { "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5}, "contributor": {"claims_merged": 10}, } await record_contributor_attribution(conn, 200, "contrib/cameron/challenge", git_fn) asyncio.run(run()) row = conn.execute("SELECT * FROM contributors WHERE handle = 'cameron-s1'").fetchone() assert row is not None assert row["extractor_count"] == 1 def test_record_parses_pentagon_trailer(): conn = _make_attribution_db() mock_diff = "+++ b/domains/crypto/claim.md\n+new file content\n" trailer = "Pentagon-Agent: Theseus " async def run(): with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=mock_diff): git_fn = AsyncMock(return_value=(0, trailer)) with patch("lib.contributor.config") as mock_config: mock_config.CONTRIBUTOR_TIER_RULES = { "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5}, "contributor": {"claims_merged": 10}, } await record_contributor_attribution(conn, 100, "extract/test", git_fn) asyncio.run(run()) row = conn.execute("SELECT * FROM contributors WHERE handle = 'theseus'").fetchone() assert row is not None assert row["agent_id"] == "uuid-456" def test_record_refines_commit_type(): conn = _make_attribution_db() mock_diff = "diff --git a/x b/y\n+++ b/domains/crypto/claim.md\n+challenged_by: foo\n" async def run(): with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=mock_diff): git_fn = AsyncMock(return_value=(0, "")) with patch("lib.contributor.config") as mock_config: mock_config.CONTRIBUTOR_TIER_RULES = { "veteran": {"claims_merged": 50, "min_days_since_first": 90, "challenges_survived": 5}, "contributor": {"claims_merged": 10}, } await record_contributor_attribution(conn, 100, "extract/test", git_fn) asyncio.run(run()) row = conn.execute("SELECT commit_type FROM prs WHERE number = 100").fetchone() assert row["commit_type"] == "challenge" def test_record_no_diff_returns_early(): conn = _make_attribution_db() async def run(): with patch("lib.contributor.get_pr_diff", new_callable=AsyncMock, return_value=None): git_fn = AsyncMock() await record_contributor_attribution(conn, 100, "extract/test", git_fn) git_fn.assert_not_called() asyncio.run(run())