- 豆包: 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)
166 lines
6.6 KiB
Python
166 lines
6.6 KiB
Python
# -*- 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 Key(DeepSeek 模型有默认值)
|
||
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"
|
||
# 开关只影响 cascade;doubao/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
|