Skip to content

Commit 4f0bcd3

Browse files
authored
refactor: semantic function clustering — absorb dockerutil, reduce duplication, move outlier free functions (#1933)
## Summary Addresses the actionable improvements identified in issue #1827 (semantic function clustering analysis). ## Changes ### 1. Absorb `internal/dockerutil` into `internal/mcp` The `internal/dockerutil` package contained a single function (`ExpandEnvArgs`) used exclusively by `internal/mcp/connection.go`. Moving it eliminates the single-function package: - Created `internal/mcp/dockerenv.go` with `ExpandEnvArgs` - Updated `connection.go` to use the local function (no more import of `dockerutil`) - Consolidated `TestExpandDockerEnvArgs` in `connection_test.go` with the 4 additional test cases from the old `dockerutil/env_test.go` - Deleted `internal/dockerutil/` ### 2. Reduce duplication in `auth.TruncateSessionID` `TruncateSessionID` in `internal/auth/header.go` duplicated the truncation logic already in `internal/strutil/truncate.go`. It now delegates to `strutil.Truncate(sessionID, 8)`, keeping only the `"(none)"` special-case for empty strings. ### 3. Move `toDIFCTags`/`tagsToStrings` from `unified.go` to `internal/difc` These free functions only deal with `difc.Tag` types and have no dependency on server logic. Exported as `difc.StringsToTags` / `difc.TagsToStrings` in a new `internal/difc/tags.go` file with unit tests. ### 4. Move `convertToCallToolResult`/`parseToolArguments` from `unified.go` to `internal/mcp` These MCP SDK utility functions are generic enough to belong in the `mcp` package alongside other protocol helpers. Exported as `mcp.ConvertToCallToolResult` / `mcp.ParseToolArguments` in `internal/mcp/tool_result.go`, using `logger.New("mcp:tool_result")` for debug logging per project conventions. ## Testing - All existing unit and integration tests pass (`make agent-finished`) - New unit tests added for `difc.StringsToTags` / `difc.TagsToStrings` - CodeQL: no alerts Closes #1827
2 parents 0dc86c6 + 7e0153a commit 4f0bcd3

File tree

10 files changed

+283
-274
lines changed

10 files changed

+283
-274
lines changed

internal/auth/header.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040

4141
"github.com/github/gh-aw-mcpg/internal/logger"
4242
"github.com/github/gh-aw-mcpg/internal/logger/sanitize"
43+
"github.com/github/gh-aw-mcpg/internal/strutil"
4344
)
4445

4546
var log = logger.New("auth:header")
@@ -173,8 +174,5 @@ func TruncateSessionID(sessionID string) string {
173174
if sessionID == "" {
174175
return "(none)"
175176
}
176-
if len(sessionID) <= 8 {
177-
return sessionID
178-
}
179-
return sessionID[:8] + "..."
177+
return strutil.Truncate(sessionID, 8)
180178
}

internal/difc/tags.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package difc
2+
3+
import "strings"
4+
5+
// StringsToTags converts a slice of strings to a slice of Tags,
6+
// trimming whitespace and skipping empty values.
7+
func StringsToTags(values []string) []Tag {
8+
tags := make([]Tag, 0, len(values))
9+
for _, value := range values {
10+
trimmed := strings.TrimSpace(value)
11+
if trimmed != "" {
12+
tags = append(tags, Tag(trimmed))
13+
}
14+
}
15+
return tags
16+
}
17+
18+
// TagsToStrings converts a slice of Tags to a slice of strings.
19+
func TagsToStrings(tags []Tag) []string {
20+
values := make([]string, 0, len(tags))
21+
for _, tag := range tags {
22+
values = append(values, string(tag))
23+
}
24+
return values
25+
}

internal/difc/tags_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package difc
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestStringsToTags(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
values []string
13+
expected []Tag
14+
}{
15+
{
16+
name: "empty slice",
17+
values: []string{},
18+
expected: []Tag{},
19+
},
20+
{
21+
name: "nil slice",
22+
values: nil,
23+
expected: []Tag{},
24+
},
25+
{
26+
name: "single value",
27+
values: []string{"private:owner"},
28+
expected: []Tag{"private:owner"},
29+
},
30+
{
31+
name: "multiple values",
32+
values: []string{"private:owner", "private:owner/repo"},
33+
expected: []Tag{"private:owner", "private:owner/repo"},
34+
},
35+
{
36+
name: "trims whitespace",
37+
values: []string{" private:owner ", "\tprivate:repo\t"},
38+
expected: []Tag{"private:owner", "private:repo"},
39+
},
40+
{
41+
name: "skips empty strings",
42+
values: []string{"private:owner", "", " ", "private:repo"},
43+
expected: []Tag{"private:owner", "private:repo"},
44+
},
45+
}
46+
47+
for _, tt := range tests {
48+
t.Run(tt.name, func(t *testing.T) {
49+
result := StringsToTags(tt.values)
50+
assert.Equal(t, tt.expected, result)
51+
})
52+
}
53+
}
54+
55+
func TestTagsToStrings(t *testing.T) {
56+
tests := []struct {
57+
name string
58+
tags []Tag
59+
expected []string
60+
}{
61+
{
62+
name: "empty slice",
63+
tags: []Tag{},
64+
expected: []string{},
65+
},
66+
{
67+
name: "nil slice",
68+
tags: nil,
69+
expected: []string{},
70+
},
71+
{
72+
name: "single tag",
73+
tags: []Tag{"private:owner"},
74+
expected: []string{"private:owner"},
75+
},
76+
{
77+
name: "multiple tags",
78+
tags: []Tag{"private:owner", "private:owner/repo"},
79+
expected: []string{"private:owner", "private:owner/repo"},
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
result := TagsToStrings(tt.tags)
86+
assert.Equal(t, tt.expected, result)
87+
})
88+
}
89+
}

internal/dockerutil/env_test.go

Lines changed: 0 additions & 107 deletions
This file was deleted.

internal/mcp/connection.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"sync"
1616
"time"
1717

18-
"github.com/github/gh-aw-mcpg/internal/dockerutil"
1918
"github.com/github/gh-aw-mcpg/internal/logger"
2019
"github.com/github/gh-aw-mcpg/internal/logger/sanitize"
2120
sdk "github.com/modelcontextprotocol/go-sdk/mcp"
@@ -131,7 +130,7 @@ func NewConnection(ctx context.Context, serverID, command string, args []string,
131130

132131
// Expand Docker -e flags that reference environment variables
133132
// Docker's `-e VAR_NAME` expects VAR_NAME to be in the environment
134-
expandedArgs := dockerutil.ExpandEnvArgs(args)
133+
expandedArgs := ExpandEnvArgs(args)
135134
logConn.Printf("Expanded args for Docker env: %v", sanitize.SanitizeArgs(expandedArgs))
136135

137136
// Create command transport

internal/mcp/connection_test.go

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"strings"
1111
"testing"
1212

13-
"github.com/github/gh-aw-mcpg/internal/dockerutil"
1413
"github.com/github/gh-aw-mcpg/internal/logger"
1514
"github.com/stretchr/testify/assert"
1615
"github.com/stretchr/testify/require"
@@ -202,13 +201,13 @@ func TestExpandDockerEnvArgs(t *testing.T) {
202201
expected: []string{"run", "-e", "VAR1=value1", "-e", "VAR2=fixed", "image"},
203202
},
204203
{
205-
name: "undefined env variable",
204+
name: "undefined env variable leaves arg unchanged",
206205
args: []string{"run", "-e", "UNDEFINED_VAR", "image"},
207206
envVars: map[string]string{},
208207
expected: []string{"run", "-e", "UNDEFINED_VAR", "image"},
209208
},
210209
{
211-
name: "empty env variable value",
210+
name: "empty env variable value expands to key=",
212211
args: []string{"run", "-e", "EMPTY_VAR", "image"},
213212
envVars: map[string]string{"EMPTY_VAR": ""},
214213
expected: []string{"run", "-e", "EMPTY_VAR=", "image"},
@@ -219,32 +218,45 @@ func TestExpandDockerEnvArgs(t *testing.T) {
219218
envVars: map[string]string{},
220219
expected: []string{"run", "image", "-e"},
221220
},
221+
{
222+
name: "nil args returns empty slice",
223+
args: nil,
224+
envVars: map[string]string{},
225+
expected: []string{},
226+
},
227+
{
228+
name: "empty args returns empty slice",
229+
args: []string{},
230+
envVars: map[string]string{},
231+
expected: []string{},
232+
},
233+
{
234+
name: "-e followed by empty string arg is not expanded",
235+
args: []string{"run", "-e", "", "image"},
236+
envVars: map[string]string{},
237+
expected: []string{"run", "-e", "", "image"},
238+
},
239+
{
240+
name: "value with equals sign in env var value",
241+
args: []string{"run", "-e", "KEY_WITH_EQUALS", "image"},
242+
envVars: map[string]string{"KEY_WITH_EQUALS": "a=b=c"},
243+
expected: []string{"run", "-e", "KEY_WITH_EQUALS=a=b=c", "image"},
244+
},
222245
}
223246

224247
for _, tt := range tests {
225248
t.Run(tt.name, func(t *testing.T) {
226-
// Set up environment variables for test
227249
for k, v := range tt.envVars {
228-
os.Setenv(k, v)
250+
require.NoError(t, os.Setenv(k, v))
229251
}
230-
// Clean up after test
231252
t.Cleanup(func() {
232253
for k := range tt.envVars {
233254
os.Unsetenv(k)
234255
}
235256
})
236257

237-
result := dockerutil.ExpandEnvArgs(tt.args)
238-
239-
if len(result) != len(tt.expected) {
240-
t.Fatalf("Expected %d args, got %d: %v", len(tt.expected), len(result), result)
241-
}
242-
243-
for i := range result {
244-
if result[i] != tt.expected[i] {
245-
t.Errorf("Arg %d: expected '%s', got '%s'", i, tt.expected[i], result[i])
246-
}
247-
}
258+
result := ExpandEnvArgs(tt.args)
259+
assert.Equal(t, tt.expected, result)
248260
})
249261
}
250262
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package dockerutil
1+
package mcp
22

33
import (
44
"fmt"
@@ -8,12 +8,12 @@ import (
88
"github.com/github/gh-aw-mcpg/internal/logger"
99
)
1010

11-
var logDockerutil = logger.New("dockerutil:env")
11+
var logDockerEnv = logger.New("mcp:dockerenv")
1212

13-
// ExpandEnvArgs expands Docker -e flags that reference environment variables
14-
// Converts "-e VAR_NAME" to "-e VAR_NAME=value" by reading from the process environment
13+
// ExpandEnvArgs expands Docker -e flags that reference environment variables.
14+
// Converts "-e VAR_NAME" to "-e VAR_NAME=value" by reading from the process environment.
1515
func ExpandEnvArgs(args []string) []string {
16-
logDockerutil.Printf("Expanding env args: input_count=%d", len(args))
16+
logDockerEnv.Printf("Expanding env args: input_count=%d", len(args))
1717
result := make([]string, 0, len(args))
1818
for i := 0; i < len(args); i++ {
1919
arg := args[i]
@@ -25,17 +25,17 @@ func ExpandEnvArgs(args []string) []string {
2525
if len(nextArg) > 0 && !strings.Contains(nextArg, "=") {
2626
// Look up the variable in the environment
2727
if value, exists := os.LookupEnv(nextArg); exists {
28-
logDockerutil.Printf("Expanding env var: name=%s", nextArg)
28+
logDockerEnv.Printf("Expanding env var: name=%s", nextArg)
2929
result = append(result, "-e")
3030
result = append(result, fmt.Sprintf("%s=%s", nextArg, value))
3131
i++ // Skip the next arg since we processed it
3232
continue
3333
}
34-
logDockerutil.Printf("Env var not found in process environment: name=%s", nextArg)
34+
logDockerEnv.Printf("Env var not found in process environment: name=%s", nextArg)
3535
}
3636
}
3737
result = append(result, arg)
3838
}
39-
logDockerutil.Printf("Env args expansion complete: output_count=%d", len(result))
39+
logDockerEnv.Printf("Env args expansion complete: output_count=%d", len(result))
4040
return result
4141
}

0 commit comments

Comments
 (0)