Some checks are pending
CI / lint-and-test (push) Waiting to run
5 functions extracted: is_knowledge_pr, refine_commit_type, record_contributor_attribution, upsert_contributor, recalculate_tier. git_fn parameter injection avoids circular import (merge→contributor, contributor needs _git from merge). Single call site passes _git. merge.py: 1912 → 1678 lines. 23 new tests, zero regressions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
263 lines
9.8 KiB
Python
263 lines
9.8 KiB
Python
"""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):
|
|
git_fn = AsyncMock(return_value=(0, "no trailers here"))
|
|
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_parses_pentagon_trailer():
|
|
conn = _make_attribution_db()
|
|
mock_diff = "+++ b/domains/crypto/claim.md\n+new file content\n"
|
|
trailer = "Pentagon-Agent: Theseus <uuid-456>"
|
|
|
|
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())
|