Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

158 lines
8.1 KiB
Go

package agentruntime
import (
"encoding/json"
"errors"
"strings"
"testing"
"github.com/tutti-os/tutti/packages/agent/daemon/providerregistry"
)
func TestClaudePreAcceptanceFailureSanitizesReturnedError(t *testing.T) {
err := newClaudeSDKProviderRejectedError(Session{Provider: "claude-code"}, map[string]any{
"code": "authentication_failed", "apiErrorStatus": float64(401),
"error": "Authorization: Bearer must-not-escape",
})
var appErr *AppError
if !errors.As(err, &appErr) || appErr == nil {
t.Fatalf("error = %#v", err)
}
if containsAny(err.Error(), "must-not-escape") || containsAny(appErr.DebugMessage, "must-not-escape") {
t.Fatalf("unsanitized rejection error = %v debug=%q", err, appErr.DebugMessage)
}
}
func TestClaudeProviderFailureClassification(t *testing.T) {
tests := []struct {
name string
code string
status int64
message string
wantCode string
wantImpact string
}{
{name: "auth", code: "authentication_failed", status: 401, wantCode: "auth_required", wantImpact: providerFailureAuthRequired},
{name: "insufficient account balance", code: "authentication_failed", status: 403, message: "Failed to authenticate. API Error: 403 Insufficient account balance", wantCode: FailureCodeInsufficientCredits, wantImpact: providerFailureAuthNone},
{name: "forbidden authentication error", code: "authentication_failed", status: 403, message: "Failed to authenticate. API Error: 403 Forbidden", wantCode: "provider_error", wantImpact: providerFailureAuthNone},
{name: "org", code: "oauth_org_not_allowed", status: 403, wantCode: "account_not_allowed", wantImpact: providerFailureAuthNone},
{name: "specific org beats status", code: "oauth_org_not_allowed", status: 401, wantCode: "account_not_allowed", wantImpact: providerFailureAuthNone},
{name: "billing", code: "billing_error", status: 402, wantCode: "billing_error", wantImpact: providerFailureAuthNone},
{name: "rate", code: "rate_limit", status: 429, wantCode: FailureCodeQuotaOrRateLimit, wantImpact: providerFailureAuthNone},
{name: "overloaded", code: "overloaded", status: 503, wantCode: "provider_unavailable", wantImpact: providerFailureAuthNone},
{name: "timeout", code: "server_error", status: 524, wantCode: "request_timed_out", wantImpact: providerFailureAuthNone},
{name: "unknown forbidden", code: "unknown", status: 403, wantCode: "provider_error", wantImpact: providerFailureAuthNone},
{name: "transport", code: "unknown", wantCode: "network_error", wantImpact: providerFailureAuthNone},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
payload := map[string]any{"code": test.code, "error": firstNonEmptyString(test.message, "upstream detail")}
if test.status != 0 {
payload["apiErrorStatus"] = test.status
}
failure := claudeProviderFailure(payload)
if failure.Code != test.wantCode || failure.AuthImpact != test.wantImpact {
t.Fatalf("failure = %#v, want code=%q impact=%q", failure, test.wantCode, test.wantImpact)
}
})
}
}
func TestAppServerProviderFailureUsesCodexErrorInfo(t *testing.T) {
data := json.RawMessage(`{"message":"upstream rejected","additionalDetails":"details","codexErrorInfo":{"type":"unauthorized","httpStatusCode":401},"willRetry":false}`)
failure := failureFromACPCall(&acpCallError{Method: "turn/start", Err: acpError{Code: -32000, Message: "wrapper", Data: data}})
if failure.Code != "auth_required" || failure.ProviderCode != "unauthorized" || failure.HTTPStatus == nil || *failure.HTTPStatus != 401 || failure.AuthImpact != providerFailureAuthRequired {
t.Fatalf("failure = %#v", failure)
}
}
func TestCodexProviderFailureCoversProtocolVariants(t *testing.T) {
tests := []struct {
value any
wantCode string
status int
}{
{value: "unauthorized", wantCode: "auth_required"},
{value: "usageLimitExceeded", wantCode: FailureCodeQuotaOrRateLimit},
{value: "sessionBudgetExceeded", wantCode: FailureCodeQuotaOrRateLimit},
{value: "serverOverloaded", wantCode: "provider_unavailable"},
{value: "internalServerError", wantCode: "provider_unavailable"},
{value: "badRequest", wantCode: "invalid_request"},
{value: "contextWindowExceeded", wantCode: "invalid_request"},
{value: "cyberPolicy", wantCode: "provider_error"},
{value: map[string]any{"httpConnectionFailed": map[string]any{"httpStatusCode": float64(503)}}, wantCode: "provider_unavailable", status: 503},
{value: map[string]any{"responseStreamConnectionFailed": map[string]any{"httpStatusCode": float64(524)}}, wantCode: "request_timed_out", status: 524},
{value: map[string]any{"responseStreamDisconnected": map[string]any{"httpStatusCode": nil}}, wantCode: "provider_stream_disconnected"},
{value: map[string]any{"responseTooManyFailedAttempts": map[string]any{"httpStatusCode": float64(429)}}, wantCode: FailureCodeQuotaOrRateLimit, status: 429},
{value: map[string]any{"activeTurnNotSteerable": map[string]any{"turnKind": "review"}}, wantCode: "provider_error"},
}
for _, test := range tests {
providerCode, status := codexFailureIdentity(test.value)
code, authImpact, _ := codexFailureClassification(providerCode, status)
if code != test.wantCode {
t.Fatalf("codexErrorInfo=%#v providerCode=%q code=%q, want %q", test.value, providerCode, code, test.wantCode)
}
if code != "auth_required" && authImpact != providerFailureAuthNone {
t.Fatalf("codexErrorInfo=%#v authImpact=%q", test.value, authImpact)
}
if test.status == 0 && status != nil {
t.Fatalf("codexErrorInfo=%#v status=%v, want nil", test.value, status)
}
if test.status != 0 && (status == nil || *status != test.status) {
t.Fatalf("codexErrorInfo=%#v status=%v, want %d", test.value, status, test.status)
}
}
}
func TestStandardACPUnknownErrorNeverInfersAuthenticationFromText(t *testing.T) {
err := &acpCallError{Method: "session/prompt", Err: acpError{Code: -32000, Message: "auth token quota gateway error", Data: json.RawMessage(`{"message":"invalid api key"}`)}}
failure := failureFromACPCall(err)
if failure.Code != "provider_error" || failure.AuthImpact != providerFailureAuthNone || err.AuthRequired() {
t.Fatalf("failure = %#v AuthRequired=%v", failure, err.AuthRequired())
}
}
func TestStandardACPFailurePreservesSanitizedJSONRPCFacts(t *testing.T) {
err := &acpCallError{Method: "session/prompt", Err: acpError{
Code: -32042, Message: "relay rejected request",
Data: json.RawMessage(`{"message":"model unavailable","apiKey":"must-not-persist","requestId":"req-1"}`),
}}
metadata := acpFailureMetadata(err)
if metadata["code"] != "provider_error" || metadata["acpErrorCode"] != -32042 || metadata["acpErrorMessage"] != "relay rejected request" {
t.Fatalf("metadata = %#v", metadata)
}
rawData, _ := metadata["acpErrorData"].(string)
if !strings.Contains(rawData, `"requestId":"req-1"`) || strings.Contains(rawData, "must-not-persist") {
t.Fatalf("sanitized ACP data = %q", rawData)
}
}
func TestProviderFailureSanitizesSecretsBeforeMetadata(t *testing.T) {
metadata := (ProviderFailure{Code: "provider_error", Message: "Authorization: Bearer secret-token\nCookie: session=first-secret; refresh=second-secret\nupstream headers: Set-Cookie: inline-secret; HttpOnly\nANTHROPIC_AUTH_TOKEN=env-secret\n{\"apiKey\":\"json-secret\",\"Cookie\":\"json-cookie-secret; other=value\"} https://example.test/?token=query-secret", Origin: providerFailureOriginProvider}).metadata()
message, _ := metadata["error"].(string)
if message == "" || containsAny(message, "secret-token", "first-secret", "second-secret", "inline-secret", "env-secret", "json-secret", "json-cookie-secret", "query-secret") {
t.Fatalf("sanitized message = %q", message)
}
}
func TestEveryRegisteredRuntimeKindHasFailureMapper(t *testing.T) {
for _, descriptor := range providerregistry.Migrated() {
switch descriptor.Runtime.Kind {
case providerregistry.RuntimeKindClaudeSDK,
providerregistry.RuntimeKindCodexAppServer,
providerregistry.RuntimeKindStandardACP:
default:
t.Fatalf("provider %q has runtime kind %q without a failure mapper", descriptor.Identity.ID, descriptor.Runtime.Kind)
}
}
}
func containsAny(value string, candidates ...string) bool {
for _, candidate := range candidates {
if strings.Contains(value, candidate) {
return true
}
}
return false
}