mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
147 lines
6.1 KiB
Python
147 lines
6.1 KiB
Python
"""redact_for_telemetry is the wall between a model_error diagnostic and a key
|
|
leak: in own_key mode the subprocess stderr we now attach can echo the user's
|
|
provider key, so these tests pin that no secret shape survives while the actual
|
|
error text (the whole point of capturing stderr) does.
|
|
|
|
The secret-shaped inputs are built by concatenation on purpose: no contiguous
|
|
key-shaped literal lands in this source file (so it never trips gitleaks or
|
|
alarms a reader), yet the runtime values are still key-shaped enough to exercise
|
|
the scrub. None of these are real keys; they unlock nothing."""
|
|
import asyncio
|
|
|
|
from backend.apps.agents.core.error_classify import (
|
|
capacity_retry_wait,
|
|
is_auth_error,
|
|
is_cli_binary_missing,
|
|
is_context_overflow_error,
|
|
is_free_trial_exhausted,
|
|
is_transient_capacity_error,
|
|
is_unknown_model_error,
|
|
)
|
|
from backend.apps.agents.core.redact_for_telemetry import redact_for_telemetry
|
|
from backend.apps.agents.core.first_real_exception import first_real_exception
|
|
|
|
# Verbatim field strings from prod analytics (2026-07): the exact shapes users hit.
|
|
P_FIELD_POOL_BUSY = (
|
|
"Error code: 429 - {'type': 'error', 'error': {'type': 'free_pool_busy', "
|
|
"'message': \"OpenSwarm's free pool is busy right now. Sign in for more, or try again shortly.\"}}"
|
|
)
|
|
P_FIELD_CLI_MISSING = (
|
|
"Claude Code not found at: C:\\Users\\Rishi\\AppData\\Local\\openswarm\\app-1.5.6\\resources"
|
|
"\\python-env\\Lib\\site-packages\\claude_agent_sdk\\_bundled\\claude.exe"
|
|
)
|
|
|
|
|
|
def test_redacts_provider_key_shapes_keeps_context():
|
|
anthropic = "sk-" + "ant-" + "A" * 28
|
|
openai = "sk-" + "B" * 24
|
|
google = "AIza" + "C" * 30
|
|
github = "ghp" + "_" + "D" * 24
|
|
s = f"9router: invalid x-api-key {anthropic} {openai} {google} {github}"
|
|
out = redact_for_telemetry(s)
|
|
for secret in (anthropic, openai, google, github):
|
|
assert secret not in out
|
|
assert "[redacted]" in out
|
|
# The diagnostic signal survives, that's the reason we capture stderr at all.
|
|
assert "9router: invalid x-api-key" in out
|
|
|
|
|
|
def test_redacts_bearer_and_key_value():
|
|
bearer_token = "E" * 24
|
|
kv_value = "F" * 16
|
|
s = "Authorization: " + "Bearer " + bearer_token + "\n" + "api_key=" + kv_value
|
|
out = redact_for_telemetry(s)
|
|
assert bearer_token not in out
|
|
assert kv_value not in out
|
|
|
|
|
|
def test_keeps_tail_and_bounds_length():
|
|
# The real error lands at the end of the stderr stream, so we keep the tail.
|
|
s = "old noise\n" * 500 + "Command failed: ENOENT spawn 9router"
|
|
out = redact_for_telemetry(s, limit=120)
|
|
assert len(out) <= 120
|
|
assert "Command failed: ENOENT spawn 9router" in out
|
|
|
|
|
|
def test_empty_is_safe():
|
|
assert redact_for_telemetry("") == ""
|
|
|
|
|
|
def test_field_pool_busy_is_transient_and_retried():
|
|
e = Exception(P_FIELD_POOL_BUSY)
|
|
assert is_transient_capacity_error(e)
|
|
assert capacity_retry_wait(e, 0) == 5
|
|
# Must not be claimed by the branches that would surface a card instead of retrying.
|
|
assert not is_free_trial_exhausted(e)
|
|
assert not is_auth_error(e)
|
|
|
|
|
|
def test_cli_missing_matches_field_string_and_nothing_else_claims_it():
|
|
e = Exception(P_FIELD_CLI_MISSING)
|
|
assert is_cli_binary_missing(e)
|
|
assert not is_transient_capacity_error(e)
|
|
assert not is_auth_error(e)
|
|
assert not is_unknown_model_error(e)
|
|
|
|
|
|
def test_cli_missing_matches_sdk_exception_type():
|
|
class CLINotFoundError(Exception):
|
|
pass
|
|
assert is_cli_binary_missing(CLINotFoundError("whatever text"))
|
|
|
|
|
|
# The overflow family across providers; each of these shapes used to kill the run with either a raw error card or (worse) a fake "completed".
|
|
P_OVERFLOW_SHAPES = (
|
|
"API Error: 400 {\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",\"message\":\"prompt is too long: 214384 tokens > 200000 maximum\"}}",
|
|
"Error code: 429 - extra usage is required for long context",
|
|
"This model's maximum context length is 128000 tokens. However, your messages resulted in 131074 tokens.",
|
|
"Error code: 400 - {'error': {'code': 'context_length_exceeded'}}",
|
|
"The input token count (1048577) exceeds the maximum number of tokens allowed (1048576).",
|
|
"Error code: 429 - Request too large for gpt-4o on tokens per min (TPM)",
|
|
)
|
|
|
|
|
|
def test_overflow_family_is_claimed_and_never_retried_verbatim():
|
|
for s in P_OVERFLOW_SHAPES:
|
|
e = Exception(s)
|
|
assert is_context_overflow_error(e), s
|
|
# Retrying the identical oversized request is guaranteed futile; the valve owns it.
|
|
assert not is_transient_capacity_error(e), s
|
|
assert capacity_retry_wait(e, 0) is None, s
|
|
|
|
|
|
def test_overflow_does_not_claim_ordinary_errors():
|
|
for s in (P_FIELD_POOL_BUSY, P_FIELD_CLI_MISSING, "529 overloaded, try again shortly", "401 invalid x-api-key"):
|
|
assert not is_context_overflow_error(Exception(s)), s
|
|
|
|
|
|
def test_first_real_exception_unwraps_nested_groups():
|
|
boom = ValueError("boom")
|
|
group = BaseExceptionGroup(
|
|
"outer", [asyncio.CancelledError(), ExceptionGroup("inner", [boom])]
|
|
)
|
|
assert first_real_exception(group) is boom
|
|
|
|
|
|
def test_first_real_exception_all_cancelled_is_none():
|
|
group = BaseExceptionGroup("outer", [asyncio.CancelledError()])
|
|
assert first_real_exception(group) is None
|
|
|
|
|
|
def test_first_real_exception_plain_passthrough():
|
|
boom = RuntimeError("x")
|
|
assert first_real_exception(boom) is boom
|
|
|
|
|
|
def test_cert_failure_is_never_transient():
|
|
import httpx
|
|
from backend.apps.agents.core.error_classify import is_cert_failure, is_transient_capacity_error
|
|
exc = httpx.ConnectError("[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate")
|
|
assert is_cert_failure(exc) is True
|
|
# httpx.ConnectError subclasses the transient TransportError; the cert check must win (ENG-218).
|
|
assert is_transient_capacity_error(exc) is False
|
|
for msg in ("unable to get local issuer certificate", "certificate has expired", "Hostname mismatch"):
|
|
assert is_transient_capacity_error(httpx.ConnectError(msg)) is False
|
|
# A cert-free transport hiccup keeps its transient classification.
|
|
assert is_transient_capacity_error(httpx.ConnectError("Connection refused")) is True
|