Files
DerrorsandClaude Opus 5.5 a931af3861 test: 测试按模块重组,共用替身移到 tests/fakes.py
- 按被测模块组织(test_pipeline、test_storage、test_web_auth 等),删除按开发阶段命名的文件与失效的 smoke_console.py
- FakeAI、FakeTelegram、样例消息与 PipelineCase / ServiceCase / WebCase 基类集中到 tests/fakes.py
- 补充测试:季集识别、访问守卫与跨站写、密钥随地址清除、批量操作计划、StorageBusy 延后、风控暂停、
  cookie 先验后存、密钥原子更新、配置快照、心跳限频、通知清理、Telegram 等待配置与补拉水位

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 23:15:04 +08:00

106 lines
5.8 KiB
Python

"""结构化过滤与一句话规则。"""
from __future__ import annotations
import unittest
import json
from reelay.core import rules
from reelay.core.config import normalize_config
from reelay.core.contracts import Decision, ParsedResource
from reelay.core.service import ConflictError
from reelay.providers.decision import DefaultDecision
from tests.fakes import FakeAI, ServiceCase, media, message
class TestStructuredFilters(ServiceCase):
async def decide(self, filters, **fields):
ctx = message()
ctx.parsed = ParsedResource(**{**media(), **fields}, share_code="abc123")
ctx.source_rule = {"approval_mode": "auto", "filters": filters}
return await DefaultDecision().decide(ctx), ctx
async def test_allow_deny_and_minimum_resolution(self):
allow = {"allow": [{"media_type": "tv", "origin_country": "CN"}, {"genre_ids": "16", "origin_country": "JP"}]}
self.assertEqual((await self.decide(allow))[0], Decision.SAVE)
self.assertEqual((await self.decide(allow, origin_country=["US"]))[0], Decision.SKIP)
self.assertEqual((await self.decide(allow, media_type="anime", genre_ids=[16], origin_country=["JP"]))[0],
Decision.SAVE)
self.assertEqual((await self.decide({"deny": [{"genre_ids": "10764,10767"}]}, genre_ids=[10764]))[0],
Decision.SKIP)
self.assertEqual((await self.decide({"min_resolution": "2160p"}))[0], Decision.SKIP)
decision, ctx = await self.decide({"min_resolution": "1080p"}, resolution=None)
self.assertEqual(decision, Decision.ASK)
self.assertTrue(any("清晰度未知" in line for line in ctx.trail))
def test_invalid_filters_are_rejected_by_config(self):
for filters in ({"allow": [{"genre_ids": "动画"}]}, {"deny": [{}]}, {"min_resolution": "4k"},
{"allow": [{"media_type": "music"}]}, {"unknown": []}):
with self.subTest(filters=filters), self.assertRaises(ValueError):
normalize_config({**self.CFG, "defaults": {"save_to": "默认", "filters": filters}})
rules.validate_filters({"allow": [{"origin_country": "cn,jp"}], "min_resolution": "720p"}, "ok")
class TestCompileRules(ServiceCase):
DRAFT = {
"filters": {"must_have": [], "block_keywords": ["广告"], "allow": [
{"media_type": "tv", "genre_ids": None, "origin_country": "CN"},
{"media_type": None, "genre_ids": "16", "origin_country": "JP"}],
"deny": [{"media_type": None, "genre_ids": "10764,10767", "origin_country": None}],
"min_resolution": "1080p"},
"release": {"prefer_subtitles": ["chs"], "prefer_audio": ["mandarin"], "hdr": "any", "prefer_sources": []},
"summary": "只收国产剧和日本动画,不要综艺,至少 1080p",
"unsupported": [],
}
async def test_draft_is_validated_and_returned_as_a_reviewable_diff(self):
self.parser._client = FakeAI(self.DRAFT)
before = self.config_path.read_text(encoding="utf-8")
result = await self.service.compile_rules("只要 1080p 以上的国产剧和日本动画,综艺不要,优先国语中字")
texts = [c["text"] for c in result["changes"]]
self.assertIn("屏蔽关键词:广告", texts)
self.assertIn("只收:类型 tv 且 地区 CN", texts)
self.assertIn("不收:类型 ID 真人秀(10764)、脱口秀(10767)", texts)
self.assertIn("最低清晰度:不限 → 1080p", texts)
self.assertTrue(any(c["section"] == "版本偏好" for c in result["changes"]))
self.assertEqual(set(result["sections"]), {"defaults", "release"})
self.assertEqual(self.config_path.read_text(encoding="utf-8"), before) # 只起草,不写入
saved = normalize_config({**self.pl.cfg, **result["sections"]})
self.assertEqual(saved["defaults"]["filters"]["min_resolution"], "1080p")
self.assertEqual(self.service.llm_usage()["purposes"][0]["purpose"], "compile")
async def test_invalid_draft_is_sent_back_once_with_the_error(self):
bad = json.loads(json.dumps(self.DRAFT))
bad["filters"]["allow"] = [{"media_type": None, "genre_ids": "动画", "origin_country": None}]
self.parser._client = FakeAI(bad, self.DRAFT)
result = await self.service.compile_rules("国产剧")
self.assertIn("未通过配置校验", self.parser._client.calls[1]["messages"][-1]["content"])
self.assertTrue(result["changes"])
self.parser._client = FakeAI(bad)
with self.assertRaises(ValueError):
await self.service.compile_rules("国产剧")
async def test_source_scope_writes_explicit_overrides(self):
cfg = {**self.CFG, "defaults": {"save_to": "默认", "filters": {"block_keywords": ["广告"]}},
"sources": [{"bot": "@bot"}]}
self.configure(cfg)
draft = json.loads(json.dumps(self.DRAFT))
draft["filters"].update(block_keywords=[], allow=[], deny=[], min_resolution=None)
draft["release"] = None
self.parser._client = FakeAI(draft)
result = await self.service.compile_rules("这个来源不屏蔽任何词", scope="@bot")
self.assertEqual(result["sections"]["sources"][0]["filters"]["block_keywords"], [])
self.assertEqual(result["changes"], [{"section": "来源 @bot", "kind": "remove", "text": "屏蔽关键词:广告"}])
with self.assertRaises(ValueError):
await self.service.compile_rules("x", scope="@missing")
async def test_without_ai_the_feature_is_unavailable(self):
self.parser._client = None
with self.assertRaises(ConflictError) as raised:
await self.service.compile_rules("国产剧")
self.assertIn("未配置 AI", str(raised.exception))
if __name__ == "__main__":
unittest.main()