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 同步更新
This commit is contained in:
@@ -231,3 +231,70 @@ def test_run_single_query_propagates_rate_limit():
|
||||
_, _, err_code = _run_single_query(
|
||||
"test", args, ["https://x.example.com"], {}, 0)
|
||||
assert err_code == E_RATE_LIMIT
|
||||
|
||||
|
||||
# ----- classify_error: HTTP status extraction from RuntimeError messages -----
|
||||
#
|
||||
# search_multi wraps the last error into its RuntimeError message, e.g.
|
||||
# "All 3 instances failed. Last error: HTTP Error 403: Forbidden"
|
||||
# classify_error must extract the status code from that message rather than
|
||||
# always falling back to E_NETWORK.
|
||||
|
||||
def test_classify_runtime_all_instances_failed_with_http_403_is_auth():
|
||||
"""'...Last error: HTTP Error 403' -> E_AUTH (not E_NETWORK)."""
|
||||
err = RuntimeError("All 3 instances failed. Last error: HTTP Error 403: Forbidden")
|
||||
assert classify_error(err) == E_AUTH
|
||||
|
||||
|
||||
def test_classify_runtime_all_instances_failed_with_http_500_is_network():
|
||||
"""'...Last error: HTTP Error 500' -> E_NETWORK (5xx server error)."""
|
||||
err = RuntimeError("All 3 instances failed. Last error: HTTP Error 500: Internal Server Error")
|
||||
assert classify_error(err) == E_NETWORK
|
||||
|
||||
|
||||
def test_classify_runtime_parallel_failed_with_http_429_is_rate_limit():
|
||||
"""'...(parallel). Last error: HTTP Error 429' -> E_RATE_LIMIT."""
|
||||
err = RuntimeError("All 3 instances failed (parallel). Last error: HTTP Error 429: Too Many Requests")
|
||||
assert classify_error(err) == E_RATE_LIMIT
|
||||
|
||||
|
||||
# ----- _emit_error: recovery_hint in JSON output -----
|
||||
#
|
||||
# recovery_hint gives AI agents an actionable suggestion per error_code.
|
||||
# Only present when error_code is known; omitted otherwise (backwards compat).
|
||||
|
||||
def test_emit_error_json_includes_recovery_hint_for_config(capsys):
|
||||
"""JSON error with E_CONFIG includes the config recovery_hint."""
|
||||
args = SimpleNamespace(format="json")
|
||||
with pytest.raises(SystemExit):
|
||||
_emit_error("no instance resolved", args, error_code=E_CONFIG)
|
||||
out, _ = capsys.readouterr()
|
||||
data = json.loads(out)
|
||||
assert data["error_code"] == E_CONFIG
|
||||
assert "recovery_hint" in data
|
||||
hint = data["recovery_hint"].lower()
|
||||
assert "instance" in hint or "config" in hint
|
||||
|
||||
|
||||
def test_emit_error_json_includes_recovery_hint_for_auth(capsys):
|
||||
"""JSON error with E_AUTH includes the auth recovery_hint."""
|
||||
args = SimpleNamespace(format="json")
|
||||
with pytest.raises(SystemExit):
|
||||
_emit_error("forbidden", args, error_code=E_AUTH)
|
||||
out, _ = capsys.readouterr()
|
||||
data = json.loads(out)
|
||||
assert data["error_code"] == E_AUTH
|
||||
assert "recovery_hint" in data
|
||||
hint = data["recovery_hint"].lower()
|
||||
assert "credential" in hint or "token" in hint
|
||||
|
||||
|
||||
def test_emit_error_json_no_recovery_hint_without_error_code(capsys):
|
||||
"""No error_code -> no recovery_hint field (backwards compat)."""
|
||||
args = SimpleNamespace(format="json")
|
||||
with pytest.raises(SystemExit):
|
||||
_emit_error("something broke", args)
|
||||
out, _ = capsys.readouterr()
|
||||
data = json.loads(out)
|
||||
assert "error_code" not in data
|
||||
assert "recovery_hint" not in data
|
||||
|
||||
Reference in New Issue
Block a user