From c985d2c6054567f2bbcfddd98809e1d7c84ed043 Mon Sep 17 00:00:00 2001 From: gahusb Date: Wed, 20 May 2026 02:32:10 +0900 Subject: [PATCH] =?UTF-8?q?test(lotto-signals):=20=EB=A9=94=ED=8A=B8?= =?UTF-8?q?=EB=A6=AD=20=ED=95=A8=EC=88=98=C2=B7adaptive=20baseline=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- agent-office/tests/test_lotto_signals.py | 126 +++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 agent-office/tests/test_lotto_signals.py diff --git a/agent-office/tests/test_lotto_signals.py b/agent-office/tests/test_lotto_signals.py new file mode 100644 index 0000000..989de13 --- /dev/null +++ b/agent-office/tests/test_lotto_signals.py @@ -0,0 +1,126 @@ +# agent-office/tests/test_lotto_signals.py +import math +import pytest + +from app.curator import signals + + +def test_sim_consensus_top10_geomean(): + """top-10 consensus 평균이 기하평균 기반인지.""" + best_picks = [ + {"scores": [10, 10, 10, 10, 10]}, # high & uniform + {"scores": [9, 9, 9, 9, 9]}, + {"scores": [8, 8, 8, 8, 8]}, + {"scores": [7, 7, 7, 7, 7]}, + {"scores": [6, 6, 6, 6, 6]}, + {"scores": [5, 5, 5, 5, 5]}, + {"scores": [4, 4, 4, 4, 4]}, + {"scores": [3, 3, 3, 3, 3]}, + {"scores": [2, 2, 2, 2, 2]}, + {"scores": [1, 1, 1, 1, 1]}, # top 10 + {"scores": [0, 0, 0, 0, 0]}, # bottom 10 + ] * 1 + [{"scores": [0, 0, 0, 0, 0]}] * 10 + result = signals.sim_consensus_score(best_picks) + assert 0.0 <= result <= 1.0 + assert result > 0.4 + + +def test_sim_consensus_geomean_penalizes_imbalance(): + """5종 중 한 종만 폭주하는 outlier 후보는 균형 후보보다 작아야 한다.""" + balanced = [{"scores": [5, 5, 5, 5, 5]}] * 20 + imbalanced = [{"scores": [25, 0, 0, 0, 0]}] * 20 + s_balanced = signals.sim_consensus_score(balanced) + s_imbalanced = signals.sim_consensus_score(imbalanced) + assert s_imbalanced < s_balanced + + +def test_strategy_drift_score(): + """drift = 전략별 가중치 변화 절댓값 합.""" + w_prev = {"gap_focus": 0.30, "hot_focus": 0.25, "pair_bias": 0.45} + w_curr = {"gap_focus": 0.40, "hot_focus": 0.20, "pair_bias": 0.40} + result = signals.strategy_drift_score(w_prev, w_curr) + assert abs(result - 0.20) < 1e-9 + + +def test_strategy_drift_new_strategy_appears(): + """이전에 없던 전략이 등장하면 그 가중치 전체가 drift에 가산.""" + w_prev = {"gap_focus": 0.5, "hot_focus": 0.5} + w_curr = {"gap_focus": 0.4, "hot_focus": 0.4, "newbie": 0.2} + result = signals.strategy_drift_score(w_prev, w_curr) + assert abs(result - 0.4) < 1e-9 + + +def test_confidence_score_passthrough(): + """confidence는 큐레이션 결과의 값 그대로 (0~1 clamp 확인).""" + assert signals.confidence_score({"confidence": 0.85}) == 0.85 + assert signals.confidence_score({"confidence": 1.2}) == 1.0 + assert signals.confidence_score({"confidence": -0.1}) == 0.0 + assert signals.confidence_score({}) is None + + +def test_adaptive_baseline_cold_start(): + """window 크기 < 4 → warmup, z=None.""" + bl = signals.AdaptiveBaseline(window=[1.0, 1.1, 0.9], window_max=8) + z, fire = bl.evaluate(value=1.5, z_normal=1.5, z_urgent=2.5) + assert fire == "warmup" + assert z is None + + +def test_adaptive_baseline_preparing(): + """window 4~7 → 보수적 임계치 z=2.0.""" + bl = signals.AdaptiveBaseline(window=[1.0, 1.0, 1.0, 1.0], window_max=8) + z, fire = bl.evaluate(value=3.0, z_normal=1.5, z_urgent=2.5) + assert fire in ("normal", "urgent") + + +def test_adaptive_baseline_normal_window_full(): + """window 8 풀, value가 평균보다 1.5σ 이상이면 normal.""" + bl = signals.AdaptiveBaseline( + window=[1.0, 1.1, 0.9, 1.0, 1.0, 1.1, 0.9, 1.0], + window_max=8, + ) + z, fire = bl.evaluate(value=1.20, z_normal=1.5, z_urgent=2.5) + assert fire == "normal" + assert z is not None and z >= 1.5 + + +def test_adaptive_baseline_urgent(): + """z >= 2.5 → urgent.""" + bl = signals.AdaptiveBaseline( + window=[1.0, 1.1, 0.9, 1.0, 1.0, 1.1, 0.9, 1.0], + window_max=8, + ) + z, fire = bl.evaluate(value=2.0, z_normal=1.5, z_urgent=2.5) + assert fire == "urgent" + + +def test_adaptive_baseline_push_updates_window(): + """push 시 FIFO 동작.""" + bl = signals.AdaptiveBaseline(window=[1, 2, 3, 4, 5, 6, 7, 8], window_max=8) + bl.push(9.0) + assert bl.window == [2, 3, 4, 5, 6, 7, 8, 9.0] + + +def test_decide_fire_level_combination(): + """2개 이상 normal 발화 → urgent.""" + sigs = [ + {"metric": "sim", "z": 1.6, "fire": "normal"}, + {"metric": "drift", "z": 1.7, "fire": "normal"}, + {"metric": "conf", "z": 0.5, "fire": "noop"}, + ] + assert signals.decide_overall_fire(sigs) == "urgent" + + sigs2 = [ + {"metric": "sim", "z": 1.6, "fire": "normal"}, + {"metric": "drift", "z": 0.3, "fire": "noop"}, + ] + assert signals.decide_overall_fire(sigs2) == "normal" + + sigs3 = [ + {"metric": "sim", "z": 3.0, "fire": "urgent"}, + {"metric": "drift", "z": 0.2, "fire": "noop"}, + ] + assert signals.decide_overall_fire(sigs3) == "urgent" + + sigs4 = [{"metric": "sim", "z": 0.5, "fire": "noop"}] + assert signals.decide_overall_fire(sigs4) == "noop"