Files
violation-detector/tests/test_config.py
T
3218485270 b413104587 feat: 结构化输出按官方规范 + 两段式重试 + DeepSeek 复检开关
- 豆包: response_format.json_schema(strict);DeepSeek: 切 Responses API text.format.json_schema
  (官方 chat/completions 通道不支持 json_schema),不支持时自动降级 json_object([output] schema)
- 重试两段式:传输错误 cfg.retries 次;解析失败/空正文有独立 3 次专用重试,仍失败落 parse_fail
- 移除三票复核(decide_final/_verify_clean/verify_clean),DeepSeek 单次判定即终稿
- 新增 [run] deepseek_recheck 开关(默认 no=仅豆包初筛;yes=追加 DeepSeek 复检)
- GUI 不再覆盖 config 提示词;prompt 相对/前导斜杠路径按 exe 目录解析
- 报表去「票型/复核」列并同步说明;README/config.example.ini 同步
- 测试新增输出格式、重试、开关与提示词路径用例(50 passed)
2026-09-03 11:44:10 +08:00

166 lines
6.6 KiB
Python
Raw 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.
# -*- coding: utf-8 -*-
"""config 模块测试:模板生成、缺项检测、读写回环、提示词路径。"""
import violation_detector.config as config_mod
from violation_detector.config import (
AppConfig, effective_mode, ensure_config, load_config, missing_fields,
save_config,
)
def test_ensure_config_creates_template(tmp_path):
ini = tmp_path / "config.ini"
path = ensure_config(str(ini))
assert path == ini
text = ini.read_text(encoding="utf-8")
assert "[ark]" in text and "[deepseek]" in text
def test_ensure_config_keeps_existing(tmp_path):
ini = tmp_path / "config.ini"
ini.write_text("[ark]\napi_key = abc\n", encoding="utf-8")
ensure_config(str(ini))
assert "api_key = abc" in ini.read_text(encoding="utf-8")
def test_missing_fields_all_empty():
# deepseek_model 有默认值;只有用户在 ini 里清空才视为缺失
missing = missing_fields(AppConfig(deepseek_model=""))
assert len(missing) == 4
assert any("豆包 API Key" in m for m in missing)
assert any("DeepSeek API Key" in m for m in missing)
def test_missing_fields_default_model_ok():
# 默认仅缺:豆包 Key、豆包接入点、DeepSeek KeyDeepSeek 模型有默认值)
missing = missing_fields(AppConfig())
assert len(missing) == 3
assert not any("DeepSeek 模型" in m for m in missing)
def test_missing_fields_none_when_filled():
cfg = AppConfig(ark_api_key="k1", ark_model="ep-1",
deepseek_api_key="sk-1", deepseek_model="m1")
assert missing_fields(cfg) == []
def test_missing_fields_mode_aware():
cfg = AppConfig(ark_api_key="k", ark_model="ep", deepseek_api_key="", deepseek_model="m")
# cascade 需要 DeepSeek Key
assert len(missing_fields(cfg, "cascade")) == 1
# doubao 模式不需要 DeepSeek 配置
assert missing_fields(cfg, "doubao") == []
# deepseek 模式不需要豆包配置
assert missing_fields(AppConfig(deepseek_api_key="sk"), "deepseek") == []
def test_mode_from_ini(tmp_path):
ini = tmp_path / "config.ini"
ini.write_text("[run]\nmode = deepseek\n", encoding="utf-8")
assert load_config(str(ini)).mode == "deepseek"
# 非法值回退 cascade
ini.write_text("[run]\nmode = turbo\n", encoding="utf-8")
assert load_config(str(ini)).mode == "cascade"
# 未配置默认 cascade
ini.write_text("[ark]\n", encoding="utf-8")
assert load_config(str(ini)).mode == "cascade"
def test_output_schema_from_ini(tmp_path):
ini = tmp_path / "config.ini"
ini.write_text("[output]\nschema = on\n", encoding="utf-8")
assert load_config(str(ini)).output_schema_mode == "on"
ini.write_text("[output]\nschema = off\n", encoding="utf-8")
assert load_config(str(ini)).output_schema_mode == "off"
ini.write_text("[output]\nschema = auto\n", encoding="utf-8")
assert load_config(str(ini)).output_schema_mode == "auto"
# 非法值回退 auto;未配置默认 auto
ini.write_text("[output]\nschema = 乱来\n", encoding="utf-8")
assert load_config(str(ini)).output_schema_mode == "auto"
ini.write_text("[ark]\n", encoding="utf-8")
assert load_config(str(ini)).output_schema_mode == "auto"
def test_output_schema_roundtrip(tmp_path):
ini = tmp_path / "config.ini"
save_config(AppConfig(output_schema_mode="off"), str(ini))
assert load_config(str(ini)).output_schema_mode == "off"
assert AppConfig().output_schema_mode == "auto"
def test_deepseek_recheck_default_and_ini(tmp_path):
# 默认关
assert AppConfig().deepseek_recheck is False
ini = tmp_path / "config.ini"
ini.write_text("[run]\ndeepseek_recheck = yes\n", encoding="utf-8")
assert load_config(str(ini)).deepseek_recheck is True
ini.write_text("[run]\ndeepseek_recheck = no\n", encoding="utf-8")
assert load_config(str(ini)).deepseek_recheck is False
# 非法值/未配置保持默认关
ini.write_text("[run]\ndeepseek_recheck = 也许\n", encoding="utf-8")
assert load_config(str(ini)).deepseek_recheck is False
ini.write_text("[ark]\n", encoding="utf-8")
assert load_config(str(ini)).deepseek_recheck is False
def test_effective_mode_respects_recheck_switch():
on = AppConfig(mode="cascade", deepseek_recheck=True)
off = AppConfig(mode="cascade", deepseek_recheck=False)
assert effective_mode(on, "cascade") == "cascade"
assert effective_mode(off, "cascade") == "doubao"
# 开关只影响 cascadedoubao/deepseek 不受影响
assert effective_mode(off, "doubao") == "doubao"
assert effective_mode(off, "deepseek") == "deepseek"
def test_deepseek_recheck_roundtrip(tmp_path):
ini = tmp_path / "config.ini"
save_config(AppConfig(deepseek_recheck=True), str(ini))
assert load_config(str(ini)).deepseek_recheck is True
def test_save_and_load_roundtrip(tmp_path):
ini = tmp_path / "config.ini"
cfg = AppConfig(ark_api_key="ark-key", ark_model="ep-xyz",
deepseek_api_key="sk-key", deepseek_model="ds-model",
ark_workers=8, max_tokens=4000, prompt_file="my.txt")
save_config(cfg, str(ini))
loaded = load_config(str(ini))
assert loaded.ark_api_key == "ark-key"
assert loaded.ark_model == "ep-xyz"
assert loaded.deepseek_api_key == "sk-key"
assert loaded.deepseek_model == "ds-model"
assert loaded.ark_workers == 8
assert loaded.max_tokens == 4000
assert loaded.prompt_file == "my.txt"
assert loaded.recheck_categories == ["无违规", "违规不明", "检测异常"]
def test_prompt_path_defaults_to_builtin():
cfg = AppConfig()
assert cfg.prompt_path.name == "prompts.txt"
def test_prompt_path_custom(tmp_path):
p = tmp_path / "p.txt"
p.write_text("x", encoding="utf-8")
cfg = AppConfig(prompt_file=str(p))
assert cfg.prompt_path == p
def test_prompt_path_relative_prefers_app_dir(tmp_path, monkeypatch):
(tmp_path / "prompts.txt").write_text("x", encoding="utf-8")
monkeypatch.setattr(config_mod, "app_dir", lambda: tmp_path)
monkeypatch.setattr(config_mod, "resource_dir", lambda: tmp_path)
cfg = AppConfig(prompt_file="prompts.txt")
assert cfg.prompt_path == tmp_path / "prompts.txt"
def test_prompt_path_leading_slash_treated_relative(tmp_path, monkeypatch):
# 无盘符的 /prompts.txt 应视为 exe 同目录的相对文件(Windows 容错)
(tmp_path / "prompts.txt").write_text("x", encoding="utf-8")
monkeypatch.setattr(config_mod, "app_dir", lambda: tmp_path)
monkeypatch.setattr(config_mod, "resource_dir", lambda: tmp_path)
for raw in ("/prompts.txt", "\\prompts.txt"):
cfg = AppConfig(prompt_file=raw)
assert cfg.prompt_path == tmp_path / "prompts.txt", raw