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
135 lines
4.5 KiB
Go
135 lines
4.5 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewClientUsesStartupFriendlyTimeout(t *testing.T) {
|
|
client, err := NewClient(Endpoint{Addr: "127.0.0.1:1", Token: "token-1"})
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
|
|
if client.httpClient.Timeout < 30*time.Second {
|
|
t.Fatalf("timeout = %s, want at least 30s", client.httpClient.Timeout)
|
|
}
|
|
}
|
|
|
|
func TestInvokeWithTimeoutOverridesDefaultClientBudget(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
time.Sleep(40 * time.Millisecond)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"ok":true,"output":{"kind":"json","value":{"ok":true}}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := &Client{
|
|
baseURL: server.URL,
|
|
token: "token-1",
|
|
httpClient: &http.Client{
|
|
Timeout: 10 * time.Millisecond,
|
|
},
|
|
}
|
|
response, err := client.InvokeWithTimeout(context.Background(), "wait.command", InvokeRequest{}, 100*time.Millisecond)
|
|
if err != nil {
|
|
t.Fatalf("InvokeWithTimeout() error = %v", err)
|
|
}
|
|
if response.Output == nil || response.Output.Value["ok"] != true {
|
|
t.Fatalf("response = %#v", response)
|
|
}
|
|
if client.httpClient.Timeout != 10*time.Millisecond {
|
|
t.Fatalf("shared client timeout mutated to %s", client.httpClient.Timeout)
|
|
}
|
|
}
|
|
|
|
func TestDoJSONReportsTimeoutSeparatelyFromUnreachable(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
time.Sleep(50 * time.Millisecond)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"service":"tuttid","status":"ok"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := &Client{
|
|
baseURL: server.URL,
|
|
token: "token-1",
|
|
httpClient: &http.Client{
|
|
Timeout: 10 * time.Millisecond,
|
|
},
|
|
}
|
|
|
|
var result HealthStatus
|
|
err := client.DoJSON(context.Background(), http.MethodGet, healthPath, nil, &result)
|
|
if err == nil {
|
|
t.Fatal("expected timeout error")
|
|
}
|
|
if !strings.Contains(err.Error(), "daemon request timed out") {
|
|
t.Fatalf("error = %q, want timeout message", err.Error())
|
|
}
|
|
if strings.Contains(err.Error(), "daemon is not reachable") {
|
|
t.Fatalf("error = %q, should not report daemon as unreachable", err.Error())
|
|
}
|
|
details, ok := RequestErrorDetails(err)
|
|
if !ok || details.ReasonCode != "daemon_request_timed_out" || !details.Retryable {
|
|
t.Fatalf("details = %#v ok=%t", details, ok)
|
|
}
|
|
}
|
|
|
|
func TestDoJSONPreservesStructuredDaemonError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusNotFound)
|
|
_, _ = w.Write([]byte(`{"error":{"code":"workspace_not_found","reason":"workspace_agent_session_not_found","retryable":true,"developerMessage":"agent session was not found","correlationId":"corr-1"}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := &Client{baseURL: server.URL, token: "token-1", httpClient: server.Client()}
|
|
err := client.DoJSON(context.Background(), http.MethodGet, healthPath, nil, &HealthStatus{})
|
|
details, ok := RequestErrorDetails(err)
|
|
if !ok {
|
|
t.Fatalf("RequestErrorDetails(%v) did not match", err)
|
|
}
|
|
if details.ReasonCode != "workspace_agent_session_not_found" || details.Message != "agent session was not found" ||
|
|
!details.Retryable || details.CorrelationID != "corr-1" || details.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("details = %#v", details)
|
|
}
|
|
}
|
|
|
|
func TestDaemonRequestErrorKeepsPlainUnreachableOutsideAgent(t *testing.T) {
|
|
t.Setenv("TUTTI_AGENT_SESSION_ID", "")
|
|
t.Setenv("TUTTI_AGENT_ROUTING", "")
|
|
|
|
err := daemonRequestError(errors.New("dial tcp failed"))
|
|
|
|
if got, want := err.Error(), "daemon is not reachable"; got != want {
|
|
t.Fatalf("error = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDaemonRequestErrorHintsLocalDaemonAccessInsideAgent(t *testing.T) {
|
|
t.Setenv("TUTTI_AGENT_SESSION_ID", "session-1")
|
|
t.Setenv("TUTTI_AGENT_ROUTING", "")
|
|
|
|
err := daemonRequestError(errors.New("dial tcp failed"))
|
|
message := err.Error()
|
|
|
|
for _, expected := range []string{
|
|
"daemon is not reachable from this agent execution environment",
|
|
"execution environment with localhost/IPC access",
|
|
"localhost/IPC",
|
|
} {
|
|
if !strings.Contains(message, expected) {
|
|
t.Fatalf("error = %q, want to contain %q", message, expected)
|
|
}
|
|
}
|
|
if strings.Contains(message, "sandbox_permissions=require_escalated") {
|
|
t.Fatalf("error = %q, should not contain provider-specific Codex escalation syntax", message)
|
|
}
|
|
}
|