Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions pkg/desktop/transport/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,20 @@ func (f *fallbackTransport) RoundTrip(req *http.Request) (*http.Response, error)
}

// isProxySocketError checks if the error indicates the proxy socket is unavailable.
// This includes:
// - "no such file or directory" - socket file was deleted
// - "connection refused" - socket exists but nothing is listening
// - "dial unix" errors - general Unix socket connection failures
// Direct target TCP dial errors (e.g. dial tcp) return false to avoid disabling the proxy.
func isProxySocketError(err error) bool {
if err == nil {
return false
}

errStr := strings.ToLower(err.Error())

// Check for common proxy socket failure patterns
// A bare "dial tcp" error is a target host failure, not a proxy socket failure:
// the proxy is only reached via Unix socket or named pipe, never plain TCP.
if strings.Contains(errStr, "dial tcp") && !strings.Contains(errStr, "proxyconnect tcp") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavioral note worth confirming: previously, a target refusal surfaced through the proxy triggered a direct-transport retry, which could succeed when the proxy resolves loopback differently from the agent process. With the guard, the error is now returned as-is with no direct fallback. This appears to be the intended trade-off, but it deserves an explicit mention in the PR description.

return false
}

proxyErrorPatterns := []string{
"no such file or directory", // Socket file deleted
"connect: connection refused", // Socket exists but no listener
Expand Down
21 changes: 21 additions & 0 deletions pkg/desktop/transport/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,37 @@ func TestIsProxySocketError(t *testing.T) {
errStr: "Post https://api.anthropic.com/v1/messages: proxyconnect tcp: some error",
expected: true,
},
{
name: "proxyconnect tcp with dial tcp error",
errStr: "proxyconnect tcp: dial tcp 10.0.0.1:443: connect: connection refused",
expected: true,
},
{
name: "dial unix error",
errStr: "dial unix /var/run/docker.sock: operation timed out",
expected: true,
},
{
name: "bare connection refused (unix socket missing listener)",
errStr: "connect: connection refused",
expected: true,
},
{
name: "bare-TCP proxy failure (unsupported, should be rejected by guard)",
errStr: "dial tcp 127.0.0.1:8080: connect: connection refused",
expected: false, // hit guard, returns false
},
{
name: "regular network error",
errStr: "dial tcp 192.168.1.1:443: i/o timeout",
expected: false,
},

{
name: "target HTTP request dial refusal",
errStr: "Get \"http://127.0.0.1:8080\": dial tcp 127.0.0.1:8080: connect: connection refused",
expected: false,
},
{
name: "HTTP error",
errStr: "HTTP 500: internal server error",
Expand Down
24 changes: 19 additions & 5 deletions pkg/environment/credential_helper_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package environment

import (
"runtime"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -17,6 +18,19 @@ func TestNewCredentialHelperProvider(t *testing.T) {
func TestCredentialHelperProvider_Get(t *testing.T) {
t.Parallel()

echoCmd := "echo"
echoArgs := func(v string) []string { return []string{v} }
falseCmd := "false"

if runtime.GOOS == "windows" {
echoCmd = "powershell"
echoArgs = func(v string) []string {
return []string{"-NoProfile", "-Command", "Write-Output '" + v + "'"}
}
falseCmd = "powershell"
// simulate 'false' by exiting with 1
}

tests := []struct {
name string
command string
Expand All @@ -25,11 +39,11 @@ func TestCredentialHelperProvider_Get(t *testing.T) {
wantValue string
wantFound bool
}{
{"ignores non-DOCKER_TOKEN vars", "echo", []string{"test-token"}, "OTHER_VAR", "", false},
{"success", "echo", []string{"my-secret-token"}, DockerDesktopTokenEnv, "my-secret-token", true},
{"trims whitespace", "echo", []string{" token-with-spaces "}, DockerDesktopTokenEnv, "token-with-spaces", true},
{"empty output", "echo", []string{""}, DockerDesktopTokenEnv, "", false},
{"command fails", "false", nil, DockerDesktopTokenEnv, "", false},
{"ignores non-DOCKER_TOKEN vars", echoCmd, echoArgs("test-token"), "OTHER_VAR", "", false},
{"success", echoCmd, echoArgs("my-secret-token"), DockerDesktopTokenEnv, "my-secret-token", true},
{"trims whitespace", echoCmd, echoArgs(" token-with-spaces "), DockerDesktopTokenEnv, "token-with-spaces", true},
{"empty output", echoCmd, echoArgs(""), DockerDesktopTokenEnv, "", false},
{"command fails", falseCmd, []string{"-NoProfile", "-Command", "exit 1"}, DockerDesktopTokenEnv, "", false},
{"command not found", "nonexistent-command-12345", nil, DockerDesktopTokenEnv, "", false},
}

Expand Down
10 changes: 4 additions & 6 deletions pkg/model/provider/gemini/schema_boolean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package gemini

import (
"testing"

"github.com/stretchr/testify/require"
)

// A tool input schema containing a boolean sub-schema — the shape a JSON Schema
Expand Down Expand Up @@ -30,12 +32,8 @@ func TestConvertParametersToSchema_BooleanSubSchema(t *testing.T) {
}

schema, err := ConvertParametersToSchema(params)
if err != nil {
t.Fatalf("ConvertParametersToSchema: %v", err)
}
if schema == nil {
t.Fatal("nil schema")
}
require.NoError(t, err)
require.NotNil(t, schema)
if _, ok := schema.Properties["count"]; !ok {
t.Errorf("count property dropped; got %v", schema.Properties)
}
Expand Down
7 changes: 7 additions & 0 deletions pkg/skills/skills_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,13 @@ func TestSkill_IsFork(t *testing.T) {
}

func TestProjectSearchDirs(t *testing.T) {
// On Windows, t.TempDir() is often inside %USERPROFILE% (AppData\Local\Temp).
// To ensure the first three tests behave exactly as they do on Unix (where /tmp
// is outside $HOME), we mock HOME to a sibling temp directory.
fakeHome := t.TempDir()
t.Setenv("HOME", fakeHome)
t.Setenv("USERPROFILE", fakeHome)

t.Run("in git repo", func(t *testing.T) {
tmpRepo := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(tmpRepo, ".git"), 0o755))
Expand Down
2 changes: 1 addition & 1 deletion pkg/tools/builtin/shell/script_shell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func TestCreateScriptToolSet_EnvPrecedence(t *testing.T) {
},
Shell: map[string]latest.ScriptShellToolConfig{
"show_env": {
Cmd: "env",
Cmd: envDumpCmd(),
Env: map[string]string{
"SCRIPT_PREC_TOOL": "from-tool",
},
Expand Down
3 changes: 2 additions & 1 deletion pkg/tui/components/reasoningblock/reasoningblock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/paths"
"github.com/docker/docker-agent/pkg/session"
"github.com/docker/docker-agent/pkg/tools"
Expand Down Expand Up @@ -190,7 +191,7 @@ func TestReasoningBlockExpandedShowsFullToolRenderer(t *testing.T) {

stripped := ansi.Strip(block.View())
assert.Contains(t, stripped, "Edit")
assert.Contains(t, stripped, path)
assert.Contains(t, stripped, pathx.ShortenHome(path))
assert.Contains(t, stripped, "old line")
assert.Contains(t, stripped, "new line")
}
Expand Down