auto-sync: 2026-06-05 13:27:20
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import pytest
|
||||
from typing import Optional
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
from src.daemon.spawner import Spawner
|
||||
|
||||
|
||||
class TestClassifyCompleted:
|
||||
"""A1: 正常完成"""
|
||||
|
||||
def test_ok_completed(self):
|
||||
result = Spawner._classify_outcome(
|
||||
0, {"status": "ok", "summary": "completed"}, "", None
|
||||
)
|
||||
assert result == {"outcome": "completed", "should_retry": False}
|
||||
|
||||
def test_ok_completed_with_fallback(self):
|
||||
"""A5/A6: status=ok + fallback_used"""
|
||||
result = Spawner._classify_outcome(
|
||||
0,
|
||||
{"status": "ok", "summary": "completed", "fallback_used": True},
|
||||
"",
|
||||
None,
|
||||
)
|
||||
assert result["outcome"] == "fallback_timeout"
|
||||
assert result["should_retry"] is False
|
||||
|
||||
|
||||
class TestClassifyAgentFailed:
|
||||
"""A4: Agent 自标 failed"""
|
||||
|
||||
def test_task_status_failed(self):
|
||||
result = Spawner._classify_outcome(
|
||||
0, {"status": "ok"}, "", "failed"
|
||||
)
|
||||
assert result["outcome"] == "agent_failed"
|
||||
assert result["should_retry"] is False
|
||||
|
||||
|
||||
class TestClassifyTimeout:
|
||||
"""A2/A3: gateway timeout"""
|
||||
|
||||
def test_timeout_should_retry(self):
|
||||
result = Spawner._classify_outcome(0, {"status": "timeout"}, "", None)
|
||||
assert result["outcome"] == "gateway_timeout"
|
||||
assert result["should_retry"] is True
|
||||
assert result["retry_field"] == "retry_count"
|
||||
|
||||
|
||||
class TestClassifyInterrupted:
|
||||
"""A14: SIGINT / SIGTERM 外部中断"""
|
||||
|
||||
def test_sigint_exit_130(self):
|
||||
result = Spawner._classify_outcome(130, {}, "", None, "")
|
||||
assert result["outcome"] == "interrupted"
|
||||
assert result["should_retry"] is True
|
||||
assert result["retry_field"] == "retry_count"
|
||||
assert result["cooldown_seconds"] == 60
|
||||
|
||||
def test_sigterm_exit_143(self):
|
||||
result = Spawner._classify_outcome(143, {}, "", None, "")
|
||||
assert result["outcome"] == "interrupted"
|
||||
assert result["should_retry"] is True
|
||||
assert result["retry_field"] == "retry_count"
|
||||
assert result["cooldown_seconds"] == 60
|
||||
|
||||
|
||||
class TestClassifyGatewayUnreachableNoJson:
|
||||
"""A15: 无 JSON + exit≠0 + stderr 含 network 关键字"""
|
||||
|
||||
def test_stderr_econnrefused(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {}, "Error: ECONNREFUSED connection refused", None, ""
|
||||
)
|
||||
assert result["outcome"] == "gateway_unreachable"
|
||||
assert result["should_retry"] is True
|
||||
assert result["cooldown_seconds"] == 60
|
||||
|
||||
def test_stderr_gateway_closed(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {}, "gateway closed unexpectedly", None, ""
|
||||
)
|
||||
assert result["outcome"] == "gateway_unreachable"
|
||||
assert result["should_retry"] is True
|
||||
assert result["cooldown_seconds"] == 60
|
||||
|
||||
|
||||
class TestClassifyCompactInterruptedNoJson:
|
||||
"""A16: 无 JSON + exit≠0 + stderr 含 compact 关键字"""
|
||||
|
||||
def test_stderr_compaction_diag(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {}, "compaction-diag: overflow detected", None, ""
|
||||
)
|
||||
assert result["outcome"] == "compact_interrupted"
|
||||
assert result["should_retry"] is True
|
||||
assert result["cooldown_seconds"] == 60
|
||||
|
||||
|
||||
class TestClassifyCrashed:
|
||||
"""A17: 无 JSON + exit≠0 + 无匹配 stderr → crash"""
|
||||
|
||||
def test_crash_no_stderr(self):
|
||||
result = Spawner._classify_outcome(1, {}, "", None, "")
|
||||
assert result["outcome"] == "crashed"
|
||||
assert result["should_retry"] is False
|
||||
assert result["original"] == "process_crash"
|
||||
|
||||
|
||||
class TestClassifyNoJsonExit0:
|
||||
"""exit=0 + 空 stdout → 根据 task_status 判定"""
|
||||
|
||||
def test_task_status_done(self):
|
||||
result = Spawner._classify_outcome(0, {}, "", "done", "")
|
||||
assert result["outcome"] == "completed"
|
||||
assert result["should_retry"] is False
|
||||
|
||||
def test_task_status_review(self):
|
||||
result = Spawner._classify_outcome(0, {}, "", "review", "")
|
||||
assert result["outcome"] == "completed"
|
||||
assert result["should_retry"] is False
|
||||
|
||||
def test_task_status_pending(self):
|
||||
result = Spawner._classify_outcome(0, {}, "", "pending", "")
|
||||
assert result["outcome"] == "agent_error"
|
||||
assert result["should_retry"] is False
|
||||
|
||||
|
||||
class TestClassifyErrorAuth:
|
||||
"""A7: status=error + stderr 含 auth 关键字"""
|
||||
|
||||
def test_stderr_401(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {"status": "error"}, "HTTP 401 Unauthorized", None
|
||||
)
|
||||
assert result["outcome"] == "auth_failed"
|
||||
assert result["should_retry"] is False
|
||||
|
||||
def test_stderr_unauthorized(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {"status": "error"}, "unauthorized access", None
|
||||
)
|
||||
assert result["outcome"] == "auth_failed"
|
||||
assert result["should_retry"] is False
|
||||
|
||||
|
||||
class TestClassifyErrorGateway:
|
||||
"""A8: status=error + stderr 含 network 关键字"""
|
||||
|
||||
def test_stderr_econnrefused(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {"status": "error"}, "ECONNREFUSED 127.0.0.1:8083", None
|
||||
)
|
||||
assert result["outcome"] == "gateway_unreachable"
|
||||
assert result["should_retry"] is True
|
||||
assert result["cooldown_seconds"] == 60
|
||||
|
||||
|
||||
class TestClassifyErrorApi:
|
||||
"""A9: status=error + stderr 含 API 错误关键字"""
|
||||
|
||||
def test_stderr_rate_limit(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {"status": "error"}, "rate_limit exceeded", None
|
||||
)
|
||||
assert result["outcome"] == "api_error"
|
||||
assert result["should_retry"] is False
|
||||
|
||||
def test_stderr_500(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {"status": "error"}, "HTTP 500 Internal Server Error", None
|
||||
)
|
||||
assert result["outcome"] == "api_error"
|
||||
assert result["should_retry"] is False
|
||||
|
||||
|
||||
class TestClassifyErrorCompact:
|
||||
"""A10: status=error + stderr 含 compact 关键字"""
|
||||
|
||||
def test_stderr_compaction_diag(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {"status": "error"}, "compaction-diag report", None
|
||||
)
|
||||
assert result["outcome"] == "compact_failed"
|
||||
assert result["should_retry"] is False
|
||||
|
||||
|
||||
class TestClassifyErrorLock:
|
||||
"""A11: status=error + stderr 含 lock 关键字"""
|
||||
|
||||
def test_stderr_lock(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {"status": "error"}, "lock acquisition failed", None
|
||||
)
|
||||
assert result["outcome"] == "lock_conflict"
|
||||
assert result["should_retry"] is True
|
||||
assert result["cooldown_seconds"] == 60
|
||||
|
||||
def test_stderr_busy(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {"status": "error"}, "resource is busy", None
|
||||
)
|
||||
assert result["outcome"] == "lock_conflict"
|
||||
assert result["should_retry"] is True
|
||||
|
||||
|
||||
class TestClassifyErrorFallback:
|
||||
"""A12: status=error + stderr 无匹配"""
|
||||
|
||||
def test_no_matching_stderr(self):
|
||||
result = Spawner._classify_outcome(
|
||||
1, {"status": "error"}, "some unknown error", None
|
||||
)
|
||||
assert result["outcome"] == "agent_error"
|
||||
assert result["should_retry"] is False
|
||||
|
||||
|
||||
class TestClassifyUnknownStatus:
|
||||
"""兜底: status 为未知值"""
|
||||
|
||||
def test_unknown_status_value(self):
|
||||
result = Spawner._classify_outcome(
|
||||
0, {"status": "weird_value"}, "", None
|
||||
)
|
||||
assert result["outcome"] == "agent_error"
|
||||
assert result["should_retry"] is False
|
||||
assert result["original"] == "unknown_status"
|
||||
Reference in New Issue
Block a user