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
158 lines
8.1 KiB
Go
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
|
|
}
|