Files
vpn-rules/tests/test_module_spec.py
ao gong 112374e2a1
CI / lite (push) Successful in 3s
test(gen-script): lock in to_rule() inline-# handling with regression tests
新增 2 个 test method 覆盖 b4a4769 修复的边界 case:
- test_to_rule_strips_inline_hash_comment_before_classifying_no_resolve
  锁定带 inline # 注释的条目(本次修复的真实场景)
  + 验证注释里出现 'no-resolve' / ',DIRECT' 字符串不被误处理
- test_to_rule_preserves_canonical_forms
  锁定 happy path(纯规则、带 no-resolve、带行尾空白)

为让 test_checked_in_router_projections_match_the_rule_source 通过,
同步重生成 router/direct-plain.txt 和 router/direct-rules.yaml。
这俩投影文件自 907fe9d 起就 stale 14+ 条规则(国内 LLM、UGREEN、
Steam 段全没同步),本次连同 b4a4769 的修复一起补齐。

测试结果:python3 -m unittest discover -s tests
  Ran 9 tests in 0.028s — OK
2026-09-05 13:40:50 +08:00

253 lines
8.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path, PurePosixPath
ROOT = Path(__file__).resolve().parents[1]
SPEC_PATH = ROOT / "MODULE_SPEC.yaml"
sys.path.insert(0, str(ROOT / "scripts"))
from gen_clash_verge_script import ( # noqa: E402
default_output_path,
load_payload,
render_script,
to_rule,
)
class ModuleSpecTests(unittest.TestCase):
def test_clash_verge_default_output_path_is_platform_aware(self) -> None:
home = Path("C:/Users/example")
appdata = Path("C:/Users/example/AppData/Roaming")
self.assertEqual(
default_output_path("win32", home=home, appdata=appdata),
appdata
/ "io.github.clash-verge-rev.clash-verge-rev"
/ "profiles"
/ "Script.js",
)
self.assertEqual(
default_output_path("darwin", home=home),
home
/ "Library/Application Support"
/ "io.github.clash-verge-rev.clash-verge-rev"
/ "profiles"
/ "Script.js",
)
def test_clash_verge_generator_supports_windows_gbk_console(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
output = Path(temp_dir) / "Script.js"
env = os.environ.copy()
env["PYTHONIOENCODING"] = "gbk"
result = subprocess.run(
[
sys.executable,
str(ROOT / "scripts/gen_clash_verge_script.py"),
"--out",
str(output),
],
cwd=ROOT,
env=env,
capture_output=True,
check=False,
)
self.assertEqual(
result.returncode,
0,
result.stderr.decode("gbk", errors="replace"),
)
self.assertTrue(output.is_file())
def test_module_spec_declares_rule_authority_and_safe_context(self) -> None:
spec = json.loads(SPEC_PATH.read_text(encoding="utf-8"))
self.assertEqual(
set(spec),
{
"schema_version",
"module_id",
"authority",
"repository",
"bounded_context",
"capabilities",
"data",
"contracts",
"dependencies",
"agent_context",
"verification",
},
)
self.assertEqual(spec["schema_version"], 1)
self.assertEqual(spec["module_id"], "vpn-rules")
self.assertEqual(
spec["authority"],
{
"scope": "module_metadata",
"subject": "vpn-rules",
"owner": "vpn-rules-owner",
"source": "MODULE_SPEC.yaml",
"revision": 1,
"effective_from": "2026-08-20T00:00:00+08:00",
},
)
self.assertEqual(
spec["repository"],
{
"name": "vpn-rules",
"workspace_id": None,
"type": "infrastructure",
"maturity": "operational",
},
)
boundary = spec["bounded_context"]
self.assertEqual(boundary["domain"], "network-routing-policy")
self.assertIn("single source of truth", boundary["responsibility"].lower())
prohibited = " ".join(boundary["prohibited_responsibilities"]).lower()
self.assertIn("production", prohibited)
self.assertIn("credential", prohibited)
self.assertIn("proxy subscription", prohibited)
self.assertEqual(
{capability["id"] for capability in spec["capabilities"]},
{
"direct-routing-rule-authoring",
"clash-verge-script-generation",
"router-rule-projection",
},
)
self.assertEqual(spec["contracts"]["consumes"], [])
self.assertEqual(spec["dependencies"], [])
provided = {
contract["contract_id"]: contract
for contract in spec["contracts"]["provides"]
}
self.assertEqual(
set(provided),
{
"mihomo-direct-rule-provider",
"router-direct-rules",
"router-direct-plain-list",
},
)
self.assertEqual(
{contract["version"] for contract in provided.values()}, {"1.0.0"}
)
for contract in provided.values():
self.assertTrue((ROOT / contract["path"]).is_file())
context = spec["agent_context"]
self.assertLessEqual(context["max_default_tokens"], 6000)
self.assertEqual(
{entry["path"] for entry in context["default_entrypoints"]},
{"README.md", "direct.yaml"},
)
for entry in context["default_entrypoints"]:
self.assertTrue((ROOT / entry["path"]).is_file())
self.assertIn("router", context["excluded_paths"])
for value in context["excluded_paths"]:
path = PurePosixPath(value)
self.assertFalse(path.is_absolute())
self.assertNotIn("..", path.parts)
self.assertEqual(
spec["verification"],
{
"commands": [
{
"id": "rule-contract-tests",
"argv": [
"python3",
"-m",
"unittest",
"discover",
"-s",
"tests",
"-v",
],
"cwd": ".",
"network": False,
"required": True,
}
]
},
)
def test_checked_in_router_projections_match_the_rule_source(self) -> None:
payload = load_payload(ROOT / "direct.yaml")
plain = (ROOT / "router/direct-plain.txt").read_text(encoding="utf-8")
router = (ROOT / "router/direct-rules.yaml").read_text(encoding="utf-8")
self.assertEqual(plain, "\n".join(payload) + "\n")
self.assertEqual(
router.split("rules:\n", 1)[1],
"\n".join(f" - {to_rule(entry)}" for entry in payload) + "\n",
)
self.assertTrue(all(",DIRECT" not in entry for entry in payload))
def test_onedrive_process_is_forced_to_use_direct_routing(self) -> None:
payload = load_payload(ROOT / "direct.yaml")
self.assertIn("PROCESS-NAME,OneDrive.exe", payload)
self.assertEqual(
to_rule("PROCESS-NAME,OneDrive.exe"),
"PROCESS-NAME,OneDrive.exe,DIRECT",
)
def test_to_rule_strips_inline_hash_comment_before_classifying_no_resolve(self) -> None:
# 回归测试 (b4a4769):to_rule() 在判断 entry.endswith(',no-resolve') 之前
# 没有剥掉行尾 # 注释,导致带 inline 注释的 Akamai Steam CDN 规则
# 走错分支,把注释当成 entry 的一部分拼进去,生成 mihomo 解析失败的规则。
self.assertEqual(
to_rule("IP-CIDR,162.254.192.0/18,no-resolve # Akamai Steam CDN 主段"),
"IP-CIDR,162.254.192.0/18,DIRECT,no-resolve",
)
# 注释里出现 "no-resolve" 不能误判为 suffix
self.assertEqual(
to_rule("DOMAIN-SUFFIX,example.com # 走 no-resolve 路径"),
"DOMAIN-SUFFIX,example.com,DIRECT",
)
# 注释里出现 ",DIRECT" 不能误拼到 proxy 字段
self.assertEqual(
to_rule("IP-CIDR,10.0.0.0/8 # 不是 DIRECT, 是 REJECT"),
"IP-CIDR,10.0.0.0/8,DIRECT",
)
def test_to_rule_preserves_canonical_forms(self) -> None:
# 不带注释、不带 no-resolve
self.assertEqual(
to_rule("DOMAIN-SUFFIX,bilibili.com"),
"DOMAIN-SUFFIX,bilibili.com,DIRECT",
)
# 带 no-resolve、无注释
self.assertEqual(
to_rule("IP-CIDR,127.0.0.0/8,no-resolve"),
"IP-CIDR,127.0.0.0/8,DIRECT,no-resolve",
)
# 带 no-resolve + 行尾空白
self.assertEqual(
to_rule("IP-CIDR,127.0.0.0/8,no-resolve "),
"IP-CIDR,127.0.0.0/8,DIRECT,no-resolve",
)
def test_clash_verge_script_adds_reachable_direct_dns_fallbacks(self) -> None:
script = render_script(["PROCESS-NAME,OneDrive.exe"])
self.assertIn("const directDnsFallbacks", script)
self.assertIn("https://dns.alidns.com/dns-query", script)
self.assertIn("https://doh.pub/dns-query", script)
self.assertIn("config.dns = dns", script)
if __name__ == "__main__":
unittest.main()