init: llm-wiki-haness 하네스 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:21:35 +09:00
parent 42bf3db4fd
commit 6c53ded9cb
2436 changed files with 194486 additions and 1 deletions
+153
View File
@@ -0,0 +1,153 @@
import pytest
from pydantic import ValidationError
from deep_research.schemas import Scope, Angle, Search, SearchResult, Extract, Claim, Verdict, Report
from deep_research.core import norm_url, Deduper, rank_claims, tally, build_synth_blocks, build_stats
from deep_research.config import Config
def test_scope_requires_min_3_angles():
with pytest.raises(ValidationError):
Scope(question="q", summary="s", angles=[Angle(label="a", query="x")])
def test_scope_accepts_5_angles():
angles = [Angle(label=f"a{i}", query="x") for i in range(5)]
s = Scope(question="q", summary="s", angles=angles)
assert len(s.angles) == 5
def test_search_relevance_enum_enforced():
with pytest.raises(ValidationError):
SearchResult(url="http://a", title="t", relevance="bogus")
def test_json_schema_export_works():
schema = Scope.model_json_schema()
assert "angles" in schema["properties"]
assert schema["properties"]["angles"]["minItems"] == 3
assert schema["properties"]["angles"]["maxItems"] == 6
def test_norm_url_strips_www_and_trailing_slash():
assert norm_url("http://www.Example.com/Path/") == "example.com/path"
def test_norm_url_bare_host():
assert norm_url("https://example.com") == "example.com"
def test_norm_url_fallback_on_garbage():
assert norm_url("not a url") == "not a url"
def test_deduper_filters_exact_dupes():
d = Deduper(Config(MAX_FETCH=15))
r = [{"url": "http://a.com/x", "title": "t", "relevance": "high"}]
assert len(d.filter_novel("angle1", r)) == 1
# same normalized url from another angle -> dup
r2 = [{"url": "http://www.a.com/x/", "title": "t2", "relevance": "high"}]
assert d.filter_novel("angle2", r2) == []
assert len(d.dupes) == 1
def test_deduper_budget_drops_medium_low_when_slots_exhausted():
d = Deduper(Config(MAX_FETCH=1))
first = [{"url": "http://a.com/1", "title": "t", "relevance": "high"}]
d.filter_novel("a1", first) # consumes the only slot
more = [
{"url": "http://b.com/2", "title": "t", "relevance": "medium"},
{"url": "http://c.com/3", "title": "t", "relevance": "low"},
]
assert d.filter_novel("a2", more) == []
assert len(d.budget_dropped) == 2
def test_deduper_high_passes_even_when_slots_exhausted():
# 원본: high(rank 0)는 budget 조건(rank>=1)에 안 걸려 slot<=0 이어도 통과
d = Deduper(Config(MAX_FETCH=1))
d.filter_novel("a1", [{"url": "http://a.com/1", "title": "t", "relevance": "high"}])
high = [{"url": "http://d.com/4", "title": "t", "relevance": "high"}]
assert len(d.filter_novel("a2", high)) == 1
assert d.fetch_slots == -1
def _claim(imp, qual, name="c"):
return {"claim": name, "importance": imp, "sourceQuality": qual}
def test_rank_claims_orders_by_importance_then_quality():
claims = [
_claim("tangential", "primary", "t-prim"),
_claim("central", "blog", "c-blog"),
_claim("central", "primary", "c-prim"),
]
ranked = rank_claims(claims, max_verify=25)
assert [c["claim"] for c in ranked] == ["c-prim", "c-blog", "t-prim"]
def test_rank_claims_truncates_to_max():
claims = [_claim("central", "primary", f"c{i}") for i in range(30)]
assert len(rank_claims(claims, max_verify=25)) == 25
def test_tally_survives_2_valid_0_refute():
v = [{"refuted": False}, {"refuted": False}, {"refuted": False}]
t = tally(v, votes_per_claim=3, refutations_required=2)
assert t["survives"] is True and t["refutedVotes"] == 0
def test_tally_killed_2_refute():
v = [{"refuted": True}, {"refuted": True}, {"refuted": False}]
t = tally(v, votes_per_claim=3, refutations_required=2)
assert t["survives"] is False and t["refutedVotes"] == 2
def test_tally_all_abstain_does_not_survive():
# ⚠️ 거짓 생존 차단: all-None -> refuted=0 이지만 valid<2 라 미생존
t = tally([None, None, None], votes_per_claim=3, refutations_required=2)
assert t["survives"] is False and t["abstained"] == 3
def test_tally_one_valid_two_abstain_does_not_survive():
t = tally([{"refuted": False}, None, None], votes_per_claim=3, refutations_required=2)
assert t["survives"] is False # valid(1) < 2
def test_tally_boundary_one_refute_two_valid_survives():
t = tally([{"refuted": True}, {"refuted": False}], votes_per_claim=3, refutations_required=2)
assert t["survives"] is True # 1 refute < 2 required, quorum met
def test_tally_boundary_two_refute_two_valid_killed():
t = tally([{"refuted": True}, {"refuted": True}], votes_per_claim=3, refutations_required=2)
assert t["survives"] is False # 2 refute == 2 required, strict < fails
def test_build_synth_blocks_includes_vote_and_source():
confirmed = [{
"claim": "X causes Y", "quote": "q", "sourceUrl": "http://a", "sourceQuality": "primary",
"valid": [{"refuted": False, "confidence": "high", "evidence": "e"}],
"refutedVotes": 0,
}]
killed = [{
"claim": "Z", "sourceUrl": "http://b", "valid": [{"refuted": True}], "refutedVotes": 1,
}]
block, killed_block = build_synth_blocks(confirmed, killed)
assert "X causes Y" in block and "1-0" in block
assert "Refuted claims" in killed_block and "Z" in killed_block
def test_build_synth_blocks_no_killed():
block, killed_block = build_synth_blocks([{
"claim": "X", "quote": "q", "sourceUrl": "http://a", "sourceQuality": "primary",
"valid": [{"refuted": False, "confidence": "high", "evidence": "e"}], "refutedVotes": 0,
}], [])
assert killed_block == ""
def test_build_stats_agent_calls_formula():
s = build_stats(angles=5, sources=12, claims=20, voted=18, confirmed=10, killed=8,
after_synth=6, dupes=3, budget_dropped=2, votes_per_claim=3)
# 1 + angles + sources + voted*votes_per_claim + 1
assert s["agentCalls"] == 1 + 5 + 12 + 18 * 3 + 1
assert s["confirmed"] == 10 and s["afterSynthesis"] == 6