|
@@ -52,6 +52,7 @@ def _expire_on_commit_session_factory() -> sessionmaker[Session]:
|
|
|
engine = create_engine("sqlite+pysqlite:///:memory:")
|
|
engine = create_engine("sqlite+pysqlite:///:memory:")
|
|
|
# SQLite 对 BigInteger PK 不会自增,测试里显式写入 id。
|
|
# SQLite 对 BigInteger PK 不会自增,测试里显式写入 id。
|
|
|
VideoDiscoveryRun.__table__.create(engine)
|
|
VideoDiscoveryRun.__table__.create(engine)
|
|
|
|
|
+ VideoDiscoverySearch.__table__.create(engine)
|
|
|
VideoDiscoveryCandidate.__table__.create(engine)
|
|
VideoDiscoveryCandidate.__table__.create(engine)
|
|
|
return sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
|
return sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
|
|
|
|
|
|
@@ -167,8 +168,20 @@ def test_prepare_then_reuse_run_id_scheduled_flow(
|
|
|
assert run_id == "scheduled-run"
|
|
assert run_id == "scheduled-run"
|
|
|
|
|
|
|
|
reuse_id, reuse_skip = prepare_video_discovery_run(ctx)
|
|
reuse_id, reuse_skip = prepare_video_discovery_run(ctx)
|
|
|
- assert reuse_id == "scheduled-run"
|
|
|
|
|
- assert reuse_skip is None
|
|
|
|
|
|
|
+ assert reuse_id is None
|
|
|
|
|
+ assert reuse_skip is not None
|
|
|
|
|
+ assert "attempt_count=1" in reuse_skip
|
|
|
|
|
+
|
|
|
|
|
+ forced_id, forced_skip = prepare_video_discovery_run(ctx, force=True)
|
|
|
|
|
+ assert forced_id == "scheduled-run"
|
|
|
|
|
+ assert forced_skip is None
|
|
|
|
|
+ from supply_infra.services.video_discovery_service import (
|
|
|
|
|
+ get_video_discovery_service,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ forced_run = get_video_discovery_service().lookup_run("scheduled-run")
|
|
|
|
|
+ assert forced_run is not None
|
|
|
|
|
+ assert forced_run["attempt_count"] == 2
|
|
|
|
|
|
|
|
payload = json.loads(
|
|
payload = json.loads(
|
|
|
video_discovery_store.create_video_discovery_run(
|
|
video_discovery_store.create_video_discovery_run(
|
|
@@ -380,12 +393,23 @@ def test_p0_gate_keeps_content_and_account_portraits_separate() -> None:
|
|
|
content_only = evaluate_candidate_gate(
|
|
content_only = evaluate_candidate_gate(
|
|
|
_p0_candidate(
|
|
_p0_candidate(
|
|
|
content_50_plus_ratio=0.28,
|
|
content_50_plus_ratio=0.28,
|
|
|
- account_50_plus_ratio=0.08,
|
|
|
|
|
|
|
+ account_50_plus_ratio=None,
|
|
|
),
|
|
),
|
|
|
_P0_RULES,
|
|
_P0_RULES,
|
|
|
)
|
|
)
|
|
|
assert content_only["primary_eligible"] is True
|
|
assert content_only["primary_eligible"] is True
|
|
|
|
|
|
|
|
|
|
+ one_side_low = evaluate_candidate_gate(
|
|
|
|
|
+ _p0_candidate(
|
|
|
|
|
+ content_50_plus_ratio=None,
|
|
|
|
|
+ account_50_plus_ratio=0.08,
|
|
|
|
|
+ ),
|
|
|
|
|
+ _P0_RULES,
|
|
|
|
|
+ )
|
|
|
|
|
+ assert one_side_low["primary_eligible"] is False
|
|
|
|
|
+ assert "PORTRAIT_50_PLUS_TOO_LOW" in one_side_low["failed_reason_codes"]
|
|
|
|
|
+ assert "CONTENT_PORTRAIT_MISSING" not in one_side_low["failed_reason_codes"]
|
|
|
|
|
+
|
|
|
both_low = evaluate_candidate_gate(
|
|
both_low = evaluate_candidate_gate(
|
|
|
_p0_candidate(
|
|
_p0_candidate(
|
|
|
content_50_plus_ratio=0.10,
|
|
content_50_plus_ratio=0.10,
|
|
@@ -453,7 +477,7 @@ def test_all_search_sources_default_to_thirty_seconds() -> None:
|
|
|
assert AUTHOR_SEARCH_MIN_DURATION == 30
|
|
assert AUTHOR_SEARCH_MIN_DURATION == 30
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_repository_rejects_primary_that_fails_p0_gate() -> None:
|
|
|
|
|
|
|
+def test_repository_reclassifies_primary_that_fails_p0_gate() -> None:
|
|
|
factory = _expire_on_commit_session_factory()
|
|
factory = _expire_on_commit_session_factory()
|
|
|
with factory() as session:
|
|
with factory() as session:
|
|
|
session.add(
|
|
session.add(
|
|
@@ -485,11 +509,233 @@ def test_repository_rejects_primary_that_fails_p0_gate() -> None:
|
|
|
|
|
|
|
|
with factory() as session:
|
|
with factory() as session:
|
|
|
repo = VideoDiscoveryRepository(session)
|
|
repo = VideoDiscoveryRepository(session)
|
|
|
- with pytest.raises(ValueError, match="SHARE_COUNT_TOO_LOW"):
|
|
|
|
|
- repo.update_candidates(
|
|
|
|
|
- "p0-gate-run",
|
|
|
|
|
- [{"candidate_id": 2, "decision_bucket": "primary"}],
|
|
|
|
|
|
|
+ batch = repo.update_candidates(
|
|
|
|
|
+ "p0-gate-run",
|
|
|
|
|
+ [{"candidate_id": 2, "decision_bucket": "primary"}],
|
|
|
|
|
+ )
|
|
|
|
|
+ session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ assert batch.candidates[0].decision_bucket == "rejected"
|
|
|
|
|
+ assert batch.candidates[0].reject_reason_code == "SHARE_COUNT_TOO_LOW"
|
|
|
|
|
+ assert batch.reclassified == [
|
|
|
|
|
+ {
|
|
|
|
|
+ "candidate_id": 2,
|
|
|
|
|
+ "requested_bucket": "primary",
|
|
|
|
|
+ "saved_bucket": "rejected",
|
|
|
|
|
+ "failed_reason_codes": ["SHARE_COUNT_TOO_LOW"],
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_repository_keeps_valid_items_when_same_batch_contains_gate_failure() -> None:
|
|
|
|
|
+ factory = _expire_on_commit_session_factory()
|
|
|
|
|
+ with factory() as session:
|
|
|
|
|
+ session.add(
|
|
|
|
|
+ VideoDiscoveryRun(
|
|
|
|
|
+ id=1,
|
|
|
|
|
+ run_id="mixed-gate-run",
|
|
|
|
|
+ demand_word="生活技巧",
|
|
|
|
|
+ relevant_points_json="[]",
|
|
|
|
|
+ status="running",
|
|
|
|
|
+ rule_version="test-p0",
|
|
|
|
|
+ rule_config_json=json.dumps(_P0_RULES, ensure_ascii=False),
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ session.add_all(
|
|
|
|
|
+ [
|
|
|
|
|
+ VideoDiscoveryCandidate(
|
|
|
|
|
+ id=2,
|
|
|
|
|
+ run_id="mixed-gate-run",
|
|
|
|
|
+ aweme_id="valid-video",
|
|
|
|
|
+ decision_bucket="pending_evaluation",
|
|
|
|
|
+ ),
|
|
|
|
|
+ VideoDiscoveryCandidate(
|
|
|
|
|
+ id=3,
|
|
|
|
|
+ run_id="mixed-gate-run",
|
|
|
|
|
+ aweme_id="low-share-video",
|
|
|
|
|
+ decision_bucket="pending_evaluation",
|
|
|
|
|
+ ),
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+ session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ base = {
|
|
|
|
|
+ "publish_at": datetime(2026, 7, 31, 9, 0),
|
|
|
|
|
+ "duration_seconds": Decimal("30.000"),
|
|
|
|
|
+ "content_50_plus_ratio": Decimal("0.280000"),
|
|
|
|
|
+ "account_50_plus_ratio": Decimal("0.080000"),
|
|
|
|
|
+ "decision_bucket": "primary",
|
|
|
|
|
+ }
|
|
|
|
|
+ with factory() as session:
|
|
|
|
|
+ batch = VideoDiscoveryRepository(session).update_candidates(
|
|
|
|
|
+ "mixed-gate-run",
|
|
|
|
|
+ [
|
|
|
|
|
+ {"candidate_id": 2, "share_count": 1000, **base},
|
|
|
|
|
+ {"candidate_id": 3, "share_count": 999, **base},
|
|
|
|
|
+ ],
|
|
|
|
|
+ )
|
|
|
|
|
+ session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ assert [item.decision_bucket for item in batch.candidates] == [
|
|
|
|
|
+ "primary",
|
|
|
|
|
+ "rejected",
|
|
|
|
|
+ ]
|
|
|
|
|
+ assert [item["candidate_id"] for item in batch.reclassified] == [3]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_finish_run_computes_partial_from_distinct_valid_primary_and_archives_pending() -> None:
|
|
|
|
|
+ factory = _expire_on_commit_session_factory()
|
|
|
|
|
+ with factory() as session:
|
|
|
|
|
+ session.add(
|
|
|
|
|
+ VideoDiscoveryRun(
|
|
|
|
|
+ id=1,
|
|
|
|
|
+ run_id="finish-partial",
|
|
|
|
|
+ demand_word="生活技巧",
|
|
|
|
|
+ relevant_points_json="[]",
|
|
|
|
|
+ status="running",
|
|
|
)
|
|
)
|
|
|
|
|
+ )
|
|
|
|
|
+ session.add_all(
|
|
|
|
|
+ [
|
|
|
|
|
+ VideoDiscoveryCandidate(
|
|
|
|
|
+ id=2,
|
|
|
|
|
+ run_id="finish-partial",
|
|
|
|
|
+ aweme_id="same-video",
|
|
|
|
|
+ decision_bucket="primary",
|
|
|
|
|
+ gate_status="pass",
|
|
|
|
|
+ ),
|
|
|
|
|
+ VideoDiscoveryCandidate(
|
|
|
|
|
+ id=3,
|
|
|
|
|
+ run_id="finish-partial",
|
|
|
|
|
+ aweme_id="same-video",
|
|
|
|
|
+ decision_bucket="primary",
|
|
|
|
|
+ gate_status="pass",
|
|
|
|
|
+ ),
|
|
|
|
|
+ VideoDiscoveryCandidate(
|
|
|
|
|
+ id=4,
|
|
|
|
|
+ run_id="finish-partial",
|
|
|
|
|
+ aweme_id="not-selected",
|
|
|
|
|
+ decision_bucket="pending_evaluation",
|
|
|
|
|
+ ),
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+ session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ with factory() as session:
|
|
|
|
|
+ repo = VideoDiscoveryRepository(session)
|
|
|
|
|
+ run = repo.finish_run("finish-partial", status="finished")
|
|
|
|
|
+ session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ assert run.primary_count == 2
|
|
|
|
|
+ assert run.valid_primary_count == 1
|
|
|
|
|
+ assert run.outcome_status == "partial"
|
|
|
|
|
+ pending = session.scalar(
|
|
|
|
|
+ select(VideoDiscoveryCandidate).where(VideoDiscoveryCandidate.id == 4)
|
|
|
|
|
+ )
|
|
|
|
|
+ assert pending is not None
|
|
|
|
|
+ assert pending.decision_bucket == "rejected"
|
|
|
|
|
+ assert pending.reject_reason_code == "NOT_SELECTED_AFTER_EVALUATION"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_new_run_lifecycle_accepts_one_portrait_side_and_finishes_partial(
|
|
|
|
|
+ monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
+) -> None:
|
|
|
|
|
+ """贯穿工具、Service、P0、结束归档和运行结果判定的完整新流程。"""
|
|
|
|
|
+ factory = _expire_on_commit_session_factory()
|
|
|
|
|
+ _patch_service_session(monkeypatch, factory)
|
|
|
|
|
+ with factory() as session:
|
|
|
|
|
+ session.add(
|
|
|
|
|
+ VideoDiscoveryRun(
|
|
|
|
|
+ id=1,
|
|
|
|
|
+ run_id="lifecycle-one-side",
|
|
|
|
|
+ demand_word="生活技巧",
|
|
|
|
|
+ relevant_points_json="[]",
|
|
|
|
|
+ status="running",
|
|
|
|
|
+ attempt_count=1,
|
|
|
|
|
+ rule_version="test-p0",
|
|
|
|
|
+ rule_config_json=json.dumps(_P0_RULES, ensure_ascii=False),
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ session.add_all(
|
|
|
|
|
+ [
|
|
|
|
|
+ VideoDiscoveryCandidate(
|
|
|
|
|
+ id=2,
|
|
|
|
|
+ run_id="lifecycle-one-side",
|
|
|
|
|
+ aweme_id="account-portrait-only",
|
|
|
|
|
+ title="适合家庭分享的生活技巧",
|
|
|
|
|
+ decision_bucket="pending_evaluation",
|
|
|
|
|
+ ),
|
|
|
|
|
+ VideoDiscoveryCandidate(
|
|
|
|
|
+ id=3,
|
|
|
|
|
+ run_id="lifecycle-one-side",
|
|
|
|
|
+ aweme_id="not-evaluated",
|
|
|
|
|
+ decision_bucket="pending_evaluation",
|
|
|
|
|
+ ),
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+ session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ updated = json.loads(
|
|
|
|
|
+ video_discovery_store.batch_update_video_discovery_candidates(
|
|
|
|
|
+ "lifecycle-one-side",
|
|
|
|
|
+ [
|
|
|
|
|
+ {
|
|
|
|
|
+ "candidate_id": 2,
|
|
|
|
|
+ "publish_at": "2026-07-31T09:00:00+08:00",
|
|
|
|
|
+ "duration_seconds": 30,
|
|
|
|
|
+ "share_count": 1000,
|
|
|
|
|
+ "content_50_plus_ratio": None,
|
|
|
|
|
+ "account_50_plus_ratio": None,
|
|
|
|
|
+ "age_normalization": {
|
|
|
|
|
+ "content": {
|
|
|
|
|
+ "has_age_portrait": False,
|
|
|
|
|
+ "older_ratio": 0,
|
|
|
|
|
+ "strength": "missing",
|
|
|
|
|
+ },
|
|
|
|
|
+ "account": {
|
|
|
|
|
+ "has_age_portrait": True,
|
|
|
|
|
+ "older_ratio": 0.55,
|
|
|
|
|
+ "strength": "strong",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "decision_bucket": "primary",
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ assert "error" not in updated
|
|
|
|
|
+ assert updated["reclassified_count"] == 0
|
|
|
|
|
+ assert updated["candidates"][0]["decision_bucket"] == "primary"
|
|
|
|
|
+ assert updated["candidates"][0]["gate_status"] == "pass"
|
|
|
|
|
+ assert updated["candidates"][0]["content_50_plus_ratio"] is None
|
|
|
|
|
+ assert updated["candidates"][0]["account_50_plus_ratio"] == 0.55
|
|
|
|
|
+
|
|
|
|
|
+ finished = json.loads(
|
|
|
|
|
+ video_discovery_store.update_video_discovery_run_status(
|
|
|
|
|
+ "lifecycle-one-side",
|
|
|
|
|
+ "finished",
|
|
|
|
|
+ stop_reason="合理搜索前沿已耗尽",
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ assert "error" not in finished
|
|
|
|
|
+ assert finished["run"]["valid_primary_count"] == 1
|
|
|
|
|
+ assert finished["run"]["outcome_status"] == "partial"
|
|
|
|
|
+
|
|
|
|
|
+ from agents.find_agent.run_outcome import evaluate_find_agent_run
|
|
|
|
|
+
|
|
|
|
|
+ outcome = evaluate_find_agent_run("lifecycle-one-side")
|
|
|
|
|
+ assert outcome.succeeded is True
|
|
|
|
|
+ assert outcome.business_outcome == "partial"
|
|
|
|
|
+ assert outcome.goal_met is False
|
|
|
|
|
+ assert outcome.valid_primary_count == 1
|
|
|
|
|
+
|
|
|
|
|
+ with factory() as session:
|
|
|
|
|
+ pending = session.scalar(
|
|
|
|
|
+ select(VideoDiscoveryCandidate).where(VideoDiscoveryCandidate.id == 3)
|
|
|
|
|
+ )
|
|
|
|
|
+ assert pending is not None
|
|
|
|
|
+ assert pending.decision_bucket == "rejected"
|
|
|
|
|
+ assert pending.reject_reason_code == "NOT_SELECTED_AFTER_EVALUATION"
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
@@ -646,6 +892,12 @@ def test_batch_update_candidates_uses_database_candidate_id(
|
|
|
"relevance_score": 0.8,
|
|
"relevance_score": 0.8,
|
|
|
"elder_score": 0.7,
|
|
"elder_score": 0.7,
|
|
|
"share_score": 0.6,
|
|
"share_score": 0.6,
|
|
|
|
|
+ "age_normalization": {
|
|
|
|
|
+ "age_normalization_result": {
|
|
|
|
|
+ "content": {"older_ratio": 0.62},
|
|
|
|
|
+ "account": {"older_ratio": 0.60},
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
}
|
|
}
|
|
|
],
|
|
],
|
|
|
)
|
|
)
|
|
@@ -654,9 +906,69 @@ def test_batch_update_candidates_uses_database_candidate_id(
|
|
|
assert result["updated_count"] == 1
|
|
assert result["updated_count"] == 1
|
|
|
assert captured["run_id"] == "run-update"
|
|
assert captured["run_id"] == "run-update"
|
|
|
assert captured["rows"][0]["candidate_id"] == 901
|
|
assert captured["rows"][0]["candidate_id"] == 901
|
|
|
|
|
+ assert captured["rows"][0]["content_50_plus_ratio"] == Decimal("0.620000")
|
|
|
|
|
+ assert captured["rows"][0]["account_50_plus_ratio"] == Decimal("0.600000")
|
|
|
assert "aweme_id" not in captured["rows"][0]
|
|
assert "aweme_id" not in captured["rows"][0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+@pytest.mark.parametrize(
|
|
|
|
|
+ ("age_normalization", "expected_content", "expected_account"),
|
|
|
|
|
+ [
|
|
|
|
|
+ (
|
|
|
|
|
+ {
|
|
|
|
|
+ "content": {
|
|
|
|
|
+ "has_age_portrait": False,
|
|
|
|
|
+ "older_ratio": 0.0,
|
|
|
|
|
+ "strength": "missing",
|
|
|
|
|
+ },
|
|
|
|
|
+ "account": {
|
|
|
|
|
+ "has_age_portrait": True,
|
|
|
|
|
+ "older_ratio": 0.55,
|
|
|
|
|
+ "strength": "strong",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ None,
|
|
|
|
|
+ Decimal("0.550000"),
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ {
|
|
|
|
|
+ "content": {
|
|
|
|
|
+ "has_age_portrait": True,
|
|
|
|
|
+ "older_ratio": 0.42,
|
|
|
|
|
+ "strength": "strong",
|
|
|
|
|
+ },
|
|
|
|
|
+ "account": {
|
|
|
|
|
+ "has_age_portrait": False,
|
|
|
|
|
+ "older_ratio": 0.0,
|
|
|
|
|
+ "strength": "missing",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ Decimal("0.420000"),
|
|
|
|
|
+ None,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+)
|
|
|
|
|
+def test_candidate_update_keeps_unavailable_portrait_side_missing(
|
|
|
|
|
+ age_normalization,
|
|
|
|
|
+ expected_content,
|
|
|
|
|
+ expected_account,
|
|
|
|
|
+) -> None:
|
|
|
|
|
+ from agents.find_agent.support.video_discovery import (
|
|
|
|
|
+ _normalize_candidate_update,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ row = _normalize_candidate_update(
|
|
|
|
|
+ {
|
|
|
|
|
+ "candidate_id": 1,
|
|
|
|
|
+ "decision_bucket": "primary",
|
|
|
|
|
+ "age_normalization": age_normalization,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ assert row["content_50_plus_ratio"] == expected_content
|
|
|
|
|
+ assert row["account_50_plus_ratio"] == expected_account
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def test_each_search_inserts_new_candidate_occurrences() -> None:
|
|
def test_each_search_inserts_new_candidate_occurrences() -> None:
|
|
|
engine = create_engine("sqlite+pysqlite:///:memory:")
|
|
engine = create_engine("sqlite+pysqlite:///:memory:")
|
|
|
VideoDiscoveryRun.__table__.create(engine)
|
|
VideoDiscoveryRun.__table__.create(engine)
|
|
@@ -756,28 +1068,34 @@ def _seed_candidate(
|
|
|
session.commit()
|
|
session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_list_skip_grade_ids_when_candidates_exist(
|
|
|
|
|
|
|
+def test_list_skip_grade_ids_uses_finished_or_attempt_limit(
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
) -> None:
|
|
) -> None:
|
|
|
factory = _expire_on_commit_session_factory()
|
|
factory = _expire_on_commit_session_factory()
|
|
|
_patch_service_session(monkeypatch, factory)
|
|
_patch_service_session(monkeypatch, factory)
|
|
|
- _seed_run(factory, run_id="running-empty", demand_grade_id=301, status="running")
|
|
|
|
|
|
|
+ _seed_run(factory, run_id="finished", demand_grade_id=301, status="finished")
|
|
|
_seed_run(
|
|
_seed_run(
|
|
|
factory,
|
|
factory,
|
|
|
- run_id="running-with-candidates",
|
|
|
|
|
|
|
+ run_id="attempted",
|
|
|
demand_grade_id=302,
|
|
demand_grade_id=302,
|
|
|
status="running",
|
|
status="running",
|
|
|
row_id=2,
|
|
row_id=2,
|
|
|
)
|
|
)
|
|
|
- _seed_candidate(factory, run_id="running-with-candidates")
|
|
|
|
|
|
|
+ with factory() as session:
|
|
|
|
|
+ attempted = session.scalar(
|
|
|
|
|
+ select(VideoDiscoveryRun).where(VideoDiscoveryRun.run_id == "attempted")
|
|
|
|
|
+ )
|
|
|
|
|
+ assert attempted is not None
|
|
|
|
|
+ attempted.attempt_count = 1
|
|
|
|
|
+ session.commit()
|
|
|
|
|
|
|
|
from supply_infra.services.video_discovery_service import get_video_discovery_service
|
|
from supply_infra.services.video_discovery_service import get_video_discovery_service
|
|
|
|
|
|
|
|
skip_ids = get_video_discovery_service().list_skip_grade_ids("20260728")
|
|
skip_ids = get_video_discovery_service().list_skip_grade_ids("20260728")
|
|
|
- assert skip_ids == {302}
|
|
|
|
|
|
|
+ assert skip_ids == {301, 302}
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_evaluate_find_agent_run_succeeds_when_candidates_exist(
|
|
|
|
|
|
|
+def test_evaluate_find_agent_run_accepts_partial_as_completed(
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
) -> None:
|
|
) -> None:
|
|
|
from agents.find_agent.run_outcome import evaluate_find_agent_run
|
|
from agents.find_agent.run_outcome import evaluate_find_agent_run
|
|
@@ -788,7 +1106,15 @@ def test_evaluate_find_agent_run_succeeds_when_candidates_exist(
|
|
|
lambda: type(
|
|
lambda: type(
|
|
|
"Svc",
|
|
"Svc",
|
|
|
(),
|
|
(),
|
|
|
- {"has_candidates": staticmethod(lambda _run_id: True)},
|
|
|
|
|
|
|
+ {
|
|
|
|
|
+ "lookup_run": staticmethod(
|
|
|
|
|
+ lambda _run_id: {
|
|
|
|
|
+ "status": "finished",
|
|
|
|
|
+ "outcome_status": "partial",
|
|
|
|
|
+ "valid_primary_count": 3,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
)(),
|
|
)(),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -797,10 +1123,13 @@ def test_evaluate_find_agent_run_succeeds_when_candidates_exist(
|
|
|
AgentResult(content="任意文案", messages=[], iterations=3, tool_calls_made=0),
|
|
AgentResult(content="任意文案", messages=[], iterations=3, tool_calls_made=0),
|
|
|
)
|
|
)
|
|
|
assert outcome.succeeded is True
|
|
assert outcome.succeeded is True
|
|
|
|
|
+ assert outcome.business_outcome == "partial"
|
|
|
|
|
+ assert outcome.goal_met is False
|
|
|
|
|
+ assert outcome.valid_primary_count == 3
|
|
|
assert outcome.failure_reason is None
|
|
assert outcome.failure_reason is None
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_evaluate_find_agent_run_fails_without_candidates(
|
|
|
|
|
|
|
+def test_evaluate_find_agent_run_fails_for_technical_failure(
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
) -> None:
|
|
) -> None:
|
|
|
from agents.find_agent.run_outcome import evaluate_find_agent_run
|
|
from agents.find_agent.run_outcome import evaluate_find_agent_run
|
|
@@ -811,7 +1140,15 @@ def test_evaluate_find_agent_run_fails_without_candidates(
|
|
|
lambda: type(
|
|
lambda: type(
|
|
|
"Svc",
|
|
"Svc",
|
|
|
(),
|
|
(),
|
|
|
- {"has_candidates": staticmethod(lambda _run_id: False)},
|
|
|
|
|
|
|
+ {
|
|
|
|
|
+ "lookup_run": staticmethod(
|
|
|
|
|
+ lambda _run_id: {
|
|
|
|
|
+ "status": "failed",
|
|
|
|
|
+ "outcome_status": "failed",
|
|
|
|
|
+ "valid_primary_count": 0,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
)(),
|
|
)(),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -825,7 +1162,8 @@ def test_evaluate_find_agent_run_fails_without_candidates(
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
|
assert outcome.succeeded is False
|
|
assert outcome.succeeded is False
|
|
|
- assert outcome.failure_reason == "no_candidates"
|
|
|
|
|
|
|
+ assert outcome.business_outcome == "failed"
|
|
|
|
|
+ assert outcome.failure_reason == "run_failed"
|
|
|
|
|
|
|
|
|
|
|
|
|
@patch(
|
|
@patch(
|