Pipeline reliability (8 fixes, reviewed by Ganymede+Rhea+Leo+Rio):
1. Merge API recovery — pre-flight approval check, transient/permanent distinction, jitter
2. Ghost PR detection — ls-remote branch check in reconciliation, network guard
3. Source status contract — directory IS status, no code change needed
4. Batch-state markers eliminated — two-gate skip (archive-check + batched branch-check)
5. Branch SHA tracking — batched ls-remote, auto-reset verdicts, dismiss stale reviews
6. Mirror pre-flight permissions — chown check in sync-mirror.sh
7. Telegram archive commit-after-write — git add/commit/push with rebase --abort fallback
8. Post-merge source archiving — queue/ → archive/{domain}/ after merge
Pipeline fixes:
- merge_cycled flag — eval attempts preserved during merge-failure cycling (Ganymede+Rhea)
- merge_failures diagnostic counter
- Startup recovery preserves eval_attempts (was incorrectly resetting to 0)
- No-diff PRs auto-closed by eval (root cause of 17 zombie PRs)
- GC threshold aligned with substantive fixer budget (was 2, now 4)
- Conflict retry with 3-attempt budget + permanent conflict handler
- Local ff-merge fallback for Forgejo 405 errors
Telegram bot:
- KB retrieval: 3-layer (entity resolution → claim search → agent context)
- Reply-to-bot handler (context.bot.id check)
- Tag regex: @teleo|@futairdbot
- Prompt rewrite for natural analyst voice
- Market data API integration (Ben's token price endpoint)
- Conversation windows (5-message unanswered counter, per-user-per-chat)
- Conversation history in prompt (last 5 exchanges)
- Worktree file lock for archive writes
Infrastructure:
- worktree_lock.py — file-based lock (flock) for main worktree coordination
- backfill-sources.py — source DB registration for Argus funnel
- batch-extract-50.sh v3 — two-gate skip, batched ls-remote, network guard
- sync-mirror.sh — auto-PR creation for mirrored GitHub branches, permission pre-flight
- Argus dashboard — conflicts + reviewing in backlog, queue count in funnel
- Enrichment-inside-frontmatter bug fix (regex anchor, not --- split)
Pentagon-Agent: Epimetheus <3D35839A-7722-4740-B93D-51157F7D5E70>
121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
"""Tests for attribution module."""
|
|
|
|
import pytest
|
|
|
|
from lib.attribution import (
|
|
build_attribution_block,
|
|
parse_attribution,
|
|
role_counts_from_attribution,
|
|
validate_attribution,
|
|
)
|
|
|
|
|
|
class TestParseAttribution:
|
|
def test_nested_format(self):
|
|
fm = {
|
|
"type": "claim",
|
|
"attribution": {
|
|
"extractor": [{"handle": "rio", "agent_id": "760F7FE7"}],
|
|
"sourcer": [{"handle": "@theiaresearch", "context": "annual letter"}],
|
|
},
|
|
}
|
|
result = parse_attribution(fm)
|
|
assert len(result["extractor"]) == 1
|
|
assert result["extractor"][0]["handle"] == "rio"
|
|
assert result["sourcer"][0]["handle"] == "theiaresearch" # @ stripped
|
|
|
|
def test_flat_format(self):
|
|
fm = {
|
|
"type": "claim",
|
|
"attribution_extractor": "rio",
|
|
"attribution_sourcer": "@theiaresearch",
|
|
}
|
|
result = parse_attribution(fm)
|
|
assert result["extractor"][0]["handle"] == "rio"
|
|
assert result["sourcer"][0]["handle"] == "theiaresearch"
|
|
|
|
def test_legacy_source_fallback(self):
|
|
fm = {
|
|
"type": "claim",
|
|
"source": "@pineanalytics, Q4 2025 report",
|
|
}
|
|
result = parse_attribution(fm)
|
|
assert result["sourcer"][0]["handle"] == "pineanalytics"
|
|
|
|
def test_empty_attribution(self):
|
|
fm = {"type": "claim"}
|
|
result = parse_attribution(fm)
|
|
assert all(len(v) == 0 for v in result.values())
|
|
|
|
def test_string_entries(self):
|
|
fm = {
|
|
"attribution": {
|
|
"extractor": ["rio"],
|
|
"sourcer": "theiaresearch",
|
|
},
|
|
}
|
|
result = parse_attribution(fm)
|
|
assert result["extractor"][0]["handle"] == "rio"
|
|
assert result["sourcer"][0]["handle"] == "theiaresearch"
|
|
|
|
|
|
class TestValidateAttribution:
|
|
def test_valid_attribution(self):
|
|
fm = {
|
|
"attribution": {
|
|
"extractor": [{"handle": "rio"}],
|
|
},
|
|
}
|
|
issues = validate_attribution(fm)
|
|
assert len(issues) == 0
|
|
|
|
def test_missing_extractor(self):
|
|
fm = {"attribution": {"sourcer": [{"handle": "someone"}]}}
|
|
issues = validate_attribution(fm)
|
|
assert "missing_attribution_extractor" in issues
|
|
|
|
def test_no_attribution_block_passes(self):
|
|
"""Legacy claims without attribution block should NOT be blocked."""
|
|
fm = {"type": "claim", "source": "some source"}
|
|
issues = validate_attribution(fm)
|
|
assert len(issues) == 0 # No attribution block = legacy, not an error
|
|
|
|
def test_attribution_block_missing_extractor(self):
|
|
"""Claims WITH attribution block but missing extractor SHOULD be blocked."""
|
|
fm = {"type": "claim", "attribution": {"sourcer": [{"handle": "someone"}]}}
|
|
issues = validate_attribution(fm)
|
|
assert "missing_attribution_extractor" in issues
|
|
|
|
|
|
class TestBuildAttributionBlock:
|
|
def test_basic_build(self):
|
|
attr = build_attribution_block("rio", agent_id="760F7FE7")
|
|
assert attr["extractor"][0]["handle"] == "rio"
|
|
assert attr["extractor"][0]["agent_id"] == "760F7FE7"
|
|
|
|
def test_with_sourcer(self):
|
|
attr = build_attribution_block("rio", source_handle="@PineAnalytics", source_context="Q4 report")
|
|
assert attr["sourcer"][0]["handle"] == "pineanalytics"
|
|
assert attr["sourcer"][0]["context"] == "Q4 report"
|
|
|
|
def test_empty_roles(self):
|
|
attr = build_attribution_block("rio")
|
|
assert attr["challenger"] == []
|
|
assert attr["synthesizer"] == []
|
|
assert attr["reviewer"] == []
|
|
|
|
|
|
class TestRoleCounts:
|
|
def test_basic_counts(self):
|
|
attribution = {
|
|
"extractor": [{"handle": "rio"}],
|
|
"sourcer": [{"handle": "theia"}, {"handle": "pine"}],
|
|
"challenger": [],
|
|
"synthesizer": [],
|
|
"reviewer": [{"handle": "leo"}],
|
|
}
|
|
counts = role_counts_from_attribution(attribution)
|
|
assert counts["extractor"] == ["rio"]
|
|
assert counts["sourcer"] == ["theia", "pine"]
|
|
assert "challenger" not in counts
|
|
assert counts["reviewer"] == ["leo"]
|