Files
searxng-use-cli/tests/test_output_format.py
T
thzxx f983a9377e feat(v1.7.0): AI 友好度增强 + 测试补全 (155→309)
核心新增(面向 AI Agent 程序化使用):
- 结构化错误码体系:E_CONFIG/E_AUTH/E_NETWORK/E_RATE_LIMIT/E_PARSE/E_EMPTY/E_INPUT/E_INTERNAL
  classify_error() 自动分类异常,JSON 错误输出含 error_code 字段
- JSON Lines 流式输出 (--stream):每条结果独立一行,AI 可增量处理
- 进度事件 (--progress):JSON Lines 事件流到 stderr(start/cache_hit/fetch_ok/done 等)

测试补全(+154 例,覆盖全部高风险盲区):
- HTML 回退搜索路径 (19)
- --fetch 自动抓取 (21)
- --verify 健康检查 (15)
- 输出格式化 (15)
- 实例解析链 (20)
- 并行多实例搜索 (10)
- CLI 入口与端到端 (17)
- 错误码分类 (27)
- 流式输出与进度事件 (10)

源码改进:
- search.py: h3 内 a 标签 href 作为 url fallback,提升 SearXNG 主题兼容性
- common.py: 新增 classify_error/emit_progress/set_progress_enabled

文档同步:SKILL.md 新增 AI Agent Integration Guide 章节,README.md 更新参数与错误码表
2026-08-01 17:40:14 +08:00

188 lines
6.2 KiB
Python

"""Tests for scripts/search.py — output formatting functions.
Pure-function tests (no network, no mocking) covering:
* format_brief — title/url/content rendering, snippet truncation,
suggestions, answers, empty input
* format_urls — basic list, skip-empty-url, empty input
* _format_results — json/urls/brief/csv dispatch + fetched-pages section
"""
import json
import types
from search import format_brief, format_urls, _format_results
# ----- format_brief -----
def test_format_brief_basic():
"""Basic brief output includes title, URL, and content lines."""
results = {"results": [
{"title": "Hello World", "url": "https://example.com/1",
"content": "A short snippet."},
]}
out = format_brief(results)
assert "1. Hello World" in out
assert "https://example.com/1" in out
assert "A short snippet." in out
def test_format_brief_no_content():
"""When content is empty/missing, no content line is emitted."""
results = {"results": [
{"title": "No Snippet", "url": "https://example.com/2"},
]}
out = format_brief(results)
assert "1. No Snippet" in out
assert "https://example.com/2" in out
# Only title + url lines plus a trailing blank line — no content line.
lines = out.split("\n")
assert len(lines) == 3
assert lines[2] == ""
def test_format_brief_snippet_truncation():
"""snippet_len > 0 truncates content to that many characters."""
long_text = "abcdefghijklmnopqrstuvwxyz"
results = {"results": [
{"title": "T", "url": "https://example.com", "content": long_text},
]}
out = format_brief(results, snippet_len=5)
assert "abcde" in out
assert "abcdef" not in out # truncated beyond 5 chars
def test_format_brief_with_suggestions():
"""Suggestions list is rendered as a comma-separated line."""
results = {
"results": [{"title": "T", "url": "https://example.com", "content": "c"}],
"suggestions": ["python asyncio", "python requests"],
}
out = format_brief(results)
assert "Suggestions: python asyncio, python requests" in out
def test_format_brief_with_answers():
"""Each answer is emitted on its own 'Answer:' line."""
results = {
"results": [{"title": "T", "url": "https://example.com", "content": "c"}],
"answers": ["42", "the answer"],
}
out = format_brief(results)
assert "Answer: 42" in out
assert "Answer: the answer" in out
def test_format_brief_empty_results():
"""Empty results list (or missing key) yields an empty string."""
assert format_brief({"results": []}) == ""
assert format_brief({}) == ""
# ----- format_urls -----
def test_format_urls_basic():
"""Each result URL appears on its own line."""
results = {"results": [
{"url": "https://a.com/1"},
{"url": "https://b.com/2"},
]}
out = format_urls(results)
assert out == "https://a.com/1\nhttps://b.com/2"
def test_format_urls_skips_empty():
"""Results with empty/missing URLs are skipped."""
results = {"results": [
{"url": "https://a.com/1"},
{"url": ""},
{"title": "no url here"},
{"url": "https://b.com/2"},
]}
out = format_urls(results)
assert out == "https://a.com/1\nhttps://b.com/2"
def test_format_urls_empty_results():
"""Empty results yield an empty string."""
assert format_urls({"results": []}) == ""
assert format_urls({}) == ""
# ----- _format_results -----
def test_format_results_json():
"""json format emits valid, parseable JSON mirroring the input."""
results = {"results": [
{"title": "T", "url": "https://example.com", "content": "c"},
], "suggestions": ["x"]}
args = types.SimpleNamespace(format="json")
out = _format_results(results, args)
parsed = json.loads(out)
assert parsed == results
def test_format_results_brief():
"""brief format dispatches to format_brief."""
results = {"results": [
{"title": "T", "url": "https://example.com", "content": "snippet"},
]}
args = types.SimpleNamespace(format="brief", snippet_len=0, fetch=0)
out = _format_results(results, args)
assert "1. T" in out
assert "https://example.com" in out
assert "snippet" in out
def test_format_results_urls():
"""urls format dispatches to format_urls."""
results = {"results": [
{"url": "https://a.com/1"},
{"url": "https://b.com/2"},
]}
args = types.SimpleNamespace(format="urls")
out = _format_results(results, args)
assert out == "https://a.com/1\nhttps://b.com/2"
def test_format_results_csv_smoke():
"""csv format emits the header row + one row per result (smoke test)."""
results = {"results": [
{"title": "T1", "url": "https://a.com", "engine": "google",
"score": 1.0, "published_date": "2024-01-01", "content": "snip"},
]}
args = types.SimpleNamespace(format="csv", snippet_len=0, fetch=0)
out = _format_results(results, args)
assert "title,url,engine,score,published_date,content" in out
assert "T1" in out
assert "https://a.com" in out
def test_format_results_brief_with_fetched():
"""brief + args.fetch>0 + results['fetched'] appends a fetched section."""
results = {
"results": [{"title": "T", "url": "https://example.com", "content": "c"}],
"fetched": [
{"url": "https://example.com", "status": "ok",
"text": "Full page content here."},
],
}
args = types.SimpleNamespace(format="brief", snippet_len=0, fetch=3)
out = _format_results(results, args)
assert "FETCHED PAGES (1 pages)" in out
assert "--- https://example.com ---" in out
assert "Full page content here." in out
def test_format_results_brief_with_fetched_error():
"""Fetched entries with status != 'ok' render an [ERROR: ...] line."""
results = {
"results": [{"title": "T", "url": "https://example.com", "content": "c"}],
"fetched": [
{"url": "https://broken.example", "status": "error", "error": "timeout"},
],
}
args = types.SimpleNamespace(format="brief", snippet_len=0, fetch=3)
out = _format_results(results, args)
assert "FETCHED PAGES (1 pages)" in out
assert "[ERROR: timeout]" in out