Files
searxng-use-cli/tests/test_batch.py
T
thzxx fb9b2af45f feat(v1.8.0): 稳定性修复 + AI Agent 体验增强
稳定性修复:

- 修复 cache.py SQLite 连接泄漏(contextlib.closing 包装)

- 修复 fetch.py requests stream=True 连接泄漏(try/finally resp.close())

- RETRYABLE_STATUS 新增 403,激活 UA fallback 切换逻辑

- --cache-stats 移至实例解析前,无需实例即可查询

- classify_error 从错误消息提取 HTTP 状态码,正确分类 E_AUTH/E_RATE_LIMIT

- --stream 与 --queries-file 互斥检查,违规报 E_INPUT

- batch 退出码语义统一(0=有结果 / 1=全部错误 / 2=全部空结果)

AI Agent 体验增强:

- 错误码体系完善:E_CONFIG/E_AUTH/E_NETWORK/E_RATE_LIMIT/E_PARSE/E_EMPTY/E_INPUT/E_INTERNAL

- recovery_hint 恢复提示字段,AI Agent 可程序化决策恢复策略

- stream 模式新增 error 事件类型(含 error_code + recovery_hint)

- 进度事件扩展:instance_try/instance_ok/instance_fail

- batch 模式统一 schema(status 字段区分 success/failed)

- JSON 输出含 schema_version 字段确保版本兼容

测试与文档:

- 测试覆盖:330 -> 352

- SKILL.md / README.md 同步更新
2026-08-01 19:02:44 +08:00

135 lines
5.3 KiB
Python

"""Tests for --queries-file batch mode: exit code semantics and JSON schema.
Covers:
* Exit codes: all-empty (no error) -> 2, all-error -> 1, partial -> 0
* Batch JSON shape: {"schema_version": "1.0", "queries": [...]}
* Per-entry "status" field ("ok"/"error") and result/error payload shape
Uses in-process main() with search_multi mocked so no network is touched.
``--retry 0`` is passed to avoid backoff sleeps when search_multi raises.
"""
import json
import sys
import urllib.error
from unittest.mock import patch
import pytest
import search as search_mod
from search import main
def _batch_argv(queries_file_path, fmt="json"):
"""Build the sys.argv for a batch main() invocation.
``--retry 0`` keeps failing tests fast (no backoff sleeps).
"""
return ["search.py", "-i", "https://x.example.com",
"--queries-file", str(queries_file_path),
"--format", fmt, "--retry", "0"]
# ===== batch exit code semantics =====
def test_batch_all_empty_results_exits_2(tmp_path, capsys):
"""All queries return empty results (no error) -> exit 2."""
qf = tmp_path / "queries.txt"
qf.write_text("q1\nq2\n", encoding="utf-8")
empty = {"results": []}
with patch.object(search_mod, "search_multi", return_value=empty):
with pytest.raises(SystemExit) as exc_info:
with patch.object(sys, "argv", _batch_argv(qf)):
with patch.object(search_mod, "setup_logging"):
main()
assert exc_info.value.code == 2
def test_batch_all_error_exits_1(tmp_path, capsys):
"""All queries error -> exit 1."""
qf = tmp_path / "queries.txt"
qf.write_text("q1\nq2\n", encoding="utf-8")
err = urllib.error.URLError("connection refused")
with patch.object(search_mod, "search_multi", side_effect=err):
with pytest.raises(SystemExit) as exc_info:
with patch.object(sys, "argv", _batch_argv(qf)):
with patch.object(search_mod, "setup_logging"):
main()
assert exc_info.value.code == 1
def test_batch_partial_results_exits_0(tmp_path, capsys):
"""At least one query returns results -> exit 0."""
qf = tmp_path / "queries.txt"
qf.write_text("q1\nq2\n", encoding="utf-8")
with_data = {"results": [{"url": "https://a.com/1", "title": "A"}]}
empty = {"results": []}
with patch.object(search_mod, "search_multi",
side_effect=[with_data, empty]):
with pytest.raises(SystemExit) as exc_info:
with patch.object(sys, "argv", _batch_argv(qf)):
with patch.object(search_mod, "setup_logging"):
main()
assert exc_info.value.code == 0
# ===== batch unified JSON schema =====
def test_batch_json_has_schema_version_and_queries(tmp_path, capsys):
"""Batch JSON output wraps entries in {schema_version, queries}."""
qf = tmp_path / "queries.txt"
qf.write_text("q1\n", encoding="utf-8")
mock_results = {"results": [{"url": "https://a.com/1", "title": "A"}]}
with patch.object(search_mod, "search_multi", return_value=mock_results):
with pytest.raises(SystemExit):
with patch.object(sys, "argv", _batch_argv(qf)):
with patch.object(search_mod, "setup_logging"):
main()
out, _ = capsys.readouterr()
data = json.loads(out)
assert data["schema_version"] == "1.0"
assert "queries" in data
assert isinstance(data["queries"], list)
assert len(data["queries"]) == 1
def test_batch_entry_has_status_field(tmp_path, capsys):
"""Each batch entry has a 'status' field of 'ok' or 'error'."""
qf = tmp_path / "queries.txt"
qf.write_text("q1\nq2\n", encoding="utf-8")
ok_results = {"results": [{"url": "https://a.com/1", "title": "A"}]}
err = urllib.error.URLError("down")
with patch.object(search_mod, "search_multi",
side_effect=[ok_results, err]):
with pytest.raises(SystemExit):
with patch.object(sys, "argv", _batch_argv(qf)):
with patch.object(search_mod, "setup_logging"):
main()
out, _ = capsys.readouterr()
data = json.loads(out)
statuses = [e["status"] for e in data["queries"]]
assert "ok" in statuses
assert "error" in statuses
def test_batch_success_entry_has_results_error_entry_has_error_and_code(tmp_path, capsys):
"""ok entry has 'results'; error entry has 'error' and 'error_code'."""
qf = tmp_path / "queries.txt"
qf.write_text("q1\nq2\n", encoding="utf-8")
ok_results = {"results": [{"url": "https://a.com/1", "title": "A"}]}
err = urllib.error.URLError("connection refused")
with patch.object(search_mod, "search_multi",
side_effect=[ok_results, err]):
with pytest.raises(SystemExit):
with patch.object(sys, "argv", _batch_argv(qf)):
with patch.object(search_mod, "setup_logging"):
main()
out, _ = capsys.readouterr()
data = json.loads(out)
ok_entry = next(e for e in data["queries"] if e["status"] == "ok")
assert "results" in ok_entry
err_entry = next(e for e in data["queries"] if e["status"] == "error")
assert "error" in err_entry
assert "error_code" in err_entry
# URLError -> E_NETWORK per classify_error
assert err_entry["error_code"] == "E_NETWORK"