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
9 changes: 5 additions & 4 deletions pkg/tools/builtin/shell/helpers_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ func pwdCmd() string {
}

// envDumpCmd returns a command printing the full environment of the
// spawned process as "key=value" lines. cmd's `set` builtin produces that
// format; it is addressed via SystemRoot (always injected by os/exec on
// Windows) because the test env may not contain PATH.
// spawned process as "key=value" lines, matching Linux `env` output.
// Uses Invoke-Expression with string formatting to avoid writing literal
// $ signs, which the script tool's arg validator would misinterpret as
// an undefined arg reference.
func envDumpCmd() string {
return `[Console]::Out.Write([Environment]::GetEnvironmentVariable('name'))`
return `Invoke-Expression ('Get-ChildItem env: | ForEach-Object {{ [string]::Concat({0}_.Name, ''='', {0}_.Value) }}' -f [char]36)`
}

func envDumpContainsName(output, name string) bool {
Expand Down
14 changes: 7 additions & 7 deletions 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 All @@ -316,12 +316,12 @@ func TestCreateScriptToolSet_EnvPrecedence(t *testing.T) {
}, tools.NopRuntime{})
require.NoError(t, err)
require.False(t, result.IsError, "unexpected error: %s", result.Output)
// `env` prints the spawned process's effective environment, i.e. what
// exec.Cmd kept after last-wins dedup.
assert.Contains(t, result.Output, "SCRIPT_PREC_OS=from-os\n")
assert.Contains(t, result.Output, "SCRIPT_PREC_TOOLSET=from-toolset\n")
assert.Contains(t, result.Output, "SCRIPT_PREC_TOOL=from-tool\n")
assert.Contains(t, result.Output, "SCRIPT_PREC_ARG=from-arg\n")
// Normalize CRLF (Windows) to LF for portable assertions.
output := strings.ReplaceAll(result.Output, "\r\n", "\n")
assert.Contains(t, output, "SCRIPT_PREC_OS=from-os\n")
assert.Contains(t, output, "SCRIPT_PREC_TOOLSET=from-toolset\n")
assert.Contains(t, output, "SCRIPT_PREC_TOOL=from-tool\n")
assert.Contains(t, output, "SCRIPT_PREC_ARG=from-arg\n")
}

func TestScriptShellTool_PerToolEnvOverridesToolsetEnv(t *testing.T) {
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
Loading