Skip to content

Commit 18b19a6

Browse files
authored
Refactor utility placement: move shared helpers out of specialized files and remove micro-packages/files (#4117)
Semantic clustering flagged utility outliers and micro-file/package sprawl across `internal/`. This PR consolidates shared helpers into appropriate common packages and removes single-purpose file/package fragmentation while preserving existing behavior. - **Version composition moved to `internal/version`** - Extracted `buildVersionString` from `main.go` into `version.BuildVersionString(mainVersion, gitCommit, buildDate)`. - `main` now delegates version-string assembly to `internal/version`, making logic package-local and testable outside `main`. - **Transient HTTP classification centralized in `httputil`** - Promoted config-local `isTransientHTTPError` to shared `httputil.IsTransientHTTPError`. - Updated config schema fetch retry path to call the shared helper. - Moved associated test coverage from `internal/config` to `internal/httputil`. - **Removed `timeutil` micro-package** - Moved `FormatDuration` from `internal/timeutil` to `internal/strutil`. - Updated logger time-diff formatting call sites to `strutil.FormatDuration`. - Deleted `internal/timeutil` files. - **Collapsed micro-files in `auth` and `oidc`** - Merged `GenerateRandomAPIKey` (and logger) into `internal/auth/header.go`; removed `internal/auth/apikey.go`. - Merged `ErrMissingOIDCEnvVar` into `internal/oidc/provider.go`; removed `internal/oidc/errors.go`. - **Inlined single-use logger helper** - Removed `getMapKeys` from `internal/logger/rpc_helpers.go`. - Inlined key extraction at the call site and dropped now-obsolete unit test for that private helper. - **Docs alignment** - Updated package-structure docs to reflect removal of `internal/timeutil` and expanded `internal/strutil` role. ```go // before (main.go) versionStr := buildVersionString() // after versionStr := version.BuildVersionString(Version, GitCommit, BuildDate) ``` > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `example.com` > - Triggering command: `/tmp/go-build3787159813/b514/launcher.test /tmp/go-build3787159813/b514/launcher.test -test.testlogfile=/tmp/go-build3787159813/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build3787159813/b423/vet.cfg rotocol/go-sdk@v1.5.0/jsonrpc/jsonrpc.go =0 x_amd64/vet --gdwarf-5 esource/v1 -o x_amd64/vet --de�� _.a --debug-prefix-m-ifaceassert x_amd64/vet -I go-sdk/mcp -I x_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build2056270914/b513/launcher.test /tmp/go-build2056270914/b513/launcher.test -test.testlogfile=/tmp/go-build2056270914/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� known-linux-gnu/lib/rustlib/x86_/home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/guards/github-guard/rustgit known-linux-gnu/lib/rustlib/x86_/home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/guards/github-guard/rustpush x_amd64/compile known-linux-gnu/git known-linux-gnu/ls-files known-linux-gnu/--exclude-standard x_amd64/compile know�� known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libminiz_oxide-2b6a8d2f6e1dc71b.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libadler2-39ffdbc27c978ccc.rlib /opt/containerd/bin/bash /run/containerd/git 7213e0dede2210feconfig json 7213e0dede2210fed31/init.pid` (dns block) > - `invalid-host-that-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build3787159813/b496/config.test /tmp/go-build3787159813/b496/config.test -test.testlogfile=/tmp/go-build3787159813/b496/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build3787159813/b317/vet.cfg @v1.1.3/cpu/cpu.-errorsas 89155655/b151//_-ifaceassert x_amd64/vet --gdwarf-5 --64 155655/b151/ x_amd64/vet -w ache/go/1.25.8/x-errorsas dHAhgBmen x_amd64/vet OUTPUT -d ut-3746032072.c x_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build1806864329/b491/config.test /tmp/go-build1806864329/b491/config.test -test.testlogfile=/tmp/go-build1806864329/b491/testlog.txt -test.paniconexit0 -test.timeout=10m0s bug/deps/serde_derive-bdc7cd22a58a5141.serde_derive.12123747d8da05ed-cgu.04.rcgu-importcfg bug/deps/serde_derive-bdc7cd22a58a5141.serde_derive.12123747d8da05ed-cgu.05.rcgu/tmp/go-build1806864329/b237/importcfg bug/deps/serde_derive-bdc7cd22a58a5141.serde_derive.12123747d8da05ed-cgu.06.rcgu-pack bug/deps/serde_derive-bdc7cd22a58a5141.serde_derive.12123747d8da05ed-cgu.07.rcgu/home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/internal/difc/agent.go bug/deps/serde_derive-bdc7cd22a58a5141.serde_derive.12123747d8da05ed-cgu.08.rcgu/home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/internal/difc/capabilities.go bug/deps/serde_derive-bdc7cd22a58a5141.serde_derive.12123747d8da05ed-cgu.09.rcgu.o bug/deps/serde_derive-bdc7cd22a58a5141.serde_derive.12123747d8da05ed-cgu.10.rcgu.o bug/deps/serde_derive-bdc7cd22a58a5141.serde_derive.12123747d8da05ed-cgu.11.rcgu.o bug/�� bug/deps/serde_derive-bdc7cd22a58a5141.serde_derive.12123747d8da05ed-cgu.13.rcgu.o bug/deps/serde_derive-bdc7cd22a58a5141.serde_derive.12123747d8da05ed-cgu.14.rcgu.o -guard/target/de--build-id ntime.v2.task/mo/opt/copilot-runtime/copilot-developer-action-main/dist/ripgrep/bin/linux-x64/rg-trimpath /tmp/go-build389--files lib/rustlib/x86_--hidden lib/rustlib/x86_--glob` (dns block) > - `nonexistent.local` > - Triggering command: `/tmp/go-build3787159813/b514/launcher.test /tmp/go-build3787159813/b514/launcher.test -test.testlogfile=/tmp/go-build3787159813/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build3787159813/b423/vet.cfg rotocol/go-sdk@v1.5.0/jsonrpc/jsonrpc.go =0 x_amd64/vet --gdwarf-5 esource/v1 -o x_amd64/vet --de�� _.a --debug-prefix-m-ifaceassert x_amd64/vet -I go-sdk/mcp -I x_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build2056270914/b513/launcher.test /tmp/go-build2056270914/b513/launcher.test -test.testlogfile=/tmp/go-build2056270914/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� known-linux-gnu/lib/rustlib/x86_/home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/guards/github-guard/rustgit known-linux-gnu/lib/rustlib/x86_/home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/guards/github-guard/rustpush x_amd64/compile known-linux-gnu/git known-linux-gnu/ls-files known-linux-gnu/--exclude-standard x_amd64/compile know�� known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libminiz_oxide-2b6a8d2f6e1dc71b.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libadler2-39ffdbc27c978ccc.rlib /opt/containerd/bin/bash /run/containerd/git 7213e0dede2210feconfig json 7213e0dede2210fed31/init.pid` (dns block) > - `slow.example.com` > - Triggering command: `/tmp/go-build3787159813/b514/launcher.test /tmp/go-build3787159813/b514/launcher.test -test.testlogfile=/tmp/go-build3787159813/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build3787159813/b423/vet.cfg rotocol/go-sdk@v1.5.0/jsonrpc/jsonrpc.go =0 x_amd64/vet --gdwarf-5 esource/v1 -o x_amd64/vet --de�� _.a --debug-prefix-m-ifaceassert x_amd64/vet -I go-sdk/mcp -I x_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build2056270914/b513/launcher.test /tmp/go-build2056270914/b513/launcher.test -test.testlogfile=/tmp/go-build2056270914/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� known-linux-gnu/lib/rustlib/x86_/home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/guards/github-guard/rustgit known-linux-gnu/lib/rustlib/x86_/home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/guards/github-guard/rustpush x_amd64/compile known-linux-gnu/git known-linux-gnu/ls-files known-linux-gnu/--exclude-standard x_amd64/compile know�� known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libminiz_oxide-2b6a8d2f6e1dc71b.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libadler2-39ffdbc27c978ccc.rlib /opt/containerd/bin/bash /run/containerd/git 7213e0dede2210feconfig json 7213e0dede2210fed31/init.pid` (dns block) > - `this-host-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build3787159813/b523/mcp.test /tmp/go-build3787159813/b523/mcp.test -test.testlogfile=/tmp/go-build3787159813/b523/testlog.txt -test.paniconexit0 -test.timeout=10m0s -W cfg /tmp/go-build389-ifaceassert x_amd64/vet _amd64.s g/protobuf/proto--version --64 x_amd64/vet cfg 155655/b281/_pkg_.a -fPIC x_amd64/vet -pthread .io/otel/exporte/usr/bin/runc -fmessage-length--version x_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build2056270914/b522/mcp.test /tmp/go-build2056270914/b522/mcp.test -test.testlogfile=/tmp/go-build2056270914/b522/testlog.txt -test.paniconexit0 -test.timeout=10m0s go1.25.8 -c=4 -nolocalimports -importcfg /tmp/go-build3333051407/b001/importcfg -pack /home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/main.go /home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/version.go ive.�� ive.12123747d8da05ed-cgu.02.rcgugo1.25.8 ive.12123747d8da05ed-cgu.03.rcgu-c=4 ive.12123747d8da05ed-cgu.04.rcgu-nolocalimports ive.12123747d8da/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/link ive.12123747d8da-V=full ive.12123747d8da05ed-cgu.07.rcgu-bool ive.12123747d8da05ed-cgu.08.rcgu-buildtags` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/github/gh-aw-mcpg/settings/copilot/coding_agent) (admins only) > > </details>
2 parents a4700bd + d6c0c9a commit 18b19a6

File tree

17 files changed

+108
-182
lines changed

17 files changed

+108
-182
lines changed

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ Quick reference for AI agents working with MCP Gateway (Go-based MCP proxy serve
3535
- `internal/mcp/` - MCP protocol types with enhanced error logging
3636
- `internal/middleware/` - HTTP middleware (jq schema processing)
3737
- `internal/server/` - HTTP server (routed/unified modes)
38+
- `internal/strutil/` - String and formatting utilities
3839
- `internal/sys/` - System utilities
3940
- `internal/testutil/` - Test utilities and helpers
40-
- `internal/timeutil/` - Time formatting utilities
4141
- `internal/tty/` - Terminal detection utilities
4242
- `internal/version/` - Version management
4343

CONTRIBUTING.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,11 +267,10 @@ awmg/
267267
├── oidc/ # OIDC authentication for HTTP MCP backends
268268
├── proxy/ # HTTP forward proxy for DIFC filtering
269269
├── server/ # HTTP server (routed/unified modes)
270-
├── strutil/ # String utility helpers
270+
├── strutil/ # String and formatting utility helpers
271271
├── syncutil/ # Concurrency utility helpers
272272
├── sys/ # System utilities
273273
├── testutil/ # Test utilities and helpers
274-
├── timeutil/ # Time formatting utilities
275274
├── tracing/ # OpenTelemetry OTLP tracing helpers
276275
├── tty/ # Terminal detection utilities
277276
└── version/ # Version management
@@ -293,11 +292,10 @@ awmg/
293292
- **`internal/oidc/`** - OIDC authentication for HTTP MCP backends
294293
- **`internal/proxy/`** - HTTP forward proxy applying DIFC filtering to `gh` CLI and REST/GraphQL requests
295294
- **`internal/server/`** - HTTP server with routed and unified modes
296-
- **`internal/strutil/`** - String utility helpers (deduplication, trimming)
295+
- **`internal/strutil/`** - String and formatting utility helpers (deduplication, trimming, duration formatting)
297296
- **`internal/syncutil/`** - Concurrency utility helpers (get-or-create pattern)
298297
- **`internal/sys/`** - System utilities
299298
- **`internal/testutil/`** - Test utilities and helpers
300-
- **`internal/timeutil/`** - Time formatting utilities
301299
- **`internal/tracing/`** - OpenTelemetry OTLP trace export helpers (HTTP handler wrapping, provider management)
302300
- **`internal/tty/`** - Terminal detection utilities
303301
- **`internal/version/`** - Version management

internal/auth/apikey.go

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

internal/auth/header.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ package auth
3636

3737
import (
3838
"errors"
39+
"fmt"
3940
"strings"
4041

4142
"github.com/github/gh-aw-mcpg/internal/logger"
@@ -44,6 +45,7 @@ import (
4445
)
4546

4647
var log = logger.New("auth:header")
48+
var logAPIKey = logger.New("auth:apikey")
4749

4850
var (
4951
// ErrMissingAuthHeader is returned when the Authorization header is missing
@@ -180,3 +182,17 @@ func TruncateSessionID(sessionID string) string {
180182
}
181183
return strutil.Truncate(sessionID, 8)
182184
}
185+
186+
// GenerateRandomAPIKey generates a cryptographically random API key.
187+
// Per spec §7.3, the gateway SHOULD generate a random API key on startup
188+
// if none is provided. Returns a 32-byte hex-encoded string (64 chars).
189+
func GenerateRandomAPIKey() (string, error) {
190+
logAPIKey.Print("Generating random API key")
191+
key, err := strutil.RandomHex(32)
192+
if err != nil {
193+
logAPIKey.Printf("Random API key generation failed: %v", err)
194+
return "", fmt.Errorf("failed to generate random API key: %w", err)
195+
}
196+
logAPIKey.Print("Random API key generated successfully")
197+
return key, nil
198+
}

internal/config/validation_schema.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
"github.com/github/gh-aw-mcpg/internal/config/rules"
15+
"github.com/github/gh-aw-mcpg/internal/httputil"
1516
"github.com/github/gh-aw-mcpg/internal/logger"
1617
"github.com/github/gh-aw-mcpg/internal/version"
1718
"github.com/santhosh-tekuri/jsonschema/v5"
@@ -49,14 +50,6 @@ var schemaFetchRetryDelay = time.Second
4950
// so tests can shorten it to avoid long waits when testing timeout behaviour.
5051
var schemaHTTPClientTimeout = 10 * time.Second
5152

52-
// isTransientHTTPError returns true for status codes that indicate a temporary
53-
// server-side condition (rate-limiting or transient failure) worth retrying.
54-
func isTransientHTTPError(statusCode int) bool {
55-
return statusCode == http.StatusTooManyRequests ||
56-
statusCode == http.StatusServiceUnavailable ||
57-
(statusCode >= 500 && statusCode < 600)
58-
}
59-
6053
var (
6154
// Compile regex patterns from schema for additional validation
6255
containerPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9./_-]*(:([a-zA-Z0-9._-]+|latest))?(@sha256:[a-fA-F0-9]{64})?$`)
@@ -260,7 +253,7 @@ func fetchAndFixSchema(url string) ([]byte, error) {
260253
break
261254
}
262255

263-
if isTransientHTTPError(resp.StatusCode) {
256+
if httputil.IsTransientHTTPError(resp.StatusCode) {
264257
lastErr = fmt.Errorf("failed to fetch schema: HTTP %d", resp.StatusCode)
265258
logSchema.Printf("Schema fetch attempt %d returned transient error: HTTP %d, will retry", attempt, resp.StatusCode)
266259
resp.Body.Close()

internal/httputil/httputil.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,11 @@ func ParseRateLimitResetHeader(value string) time.Time {
3535
}
3636
return time.Unix(unix, 0)
3737
}
38+
39+
// IsTransientHTTPError returns true for status codes that indicate a temporary
40+
// server-side condition (rate-limiting or transient failure) worth retrying.
41+
func IsTransientHTTPError(statusCode int) bool {
42+
return statusCode == http.StatusTooManyRequests ||
43+
statusCode == http.StatusServiceUnavailable ||
44+
(statusCode >= 500 && statusCode < 600)
45+
}

internal/config/transient_http_error_test.go renamed to internal/httputil/transient_http_error_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package config
1+
package httputil
22

33
import (
44
"net/http"
@@ -7,7 +7,7 @@ import (
77
"github.com/stretchr/testify/assert"
88
)
99

10-
// TestIsTransientHTTPError verifies every status-code branch in isTransientHTTPError.
10+
// TestIsTransientHTTPError verifies every status-code branch in IsTransientHTTPError.
1111
// The function returns true for HTTP 429 (TooManyRequests), 503 (ServiceUnavailable),
1212
// and any 5xx status code, and false for all other codes.
1313
func TestIsTransientHTTPError(t *testing.T) {
@@ -121,8 +121,8 @@ func TestIsTransientHTTPError(t *testing.T) {
121121
for _, tt := range tests {
122122
t.Run(tt.name, func(t *testing.T) {
123123
t.Parallel()
124-
got := isTransientHTTPError(tt.statusCode)
125-
assert.Equal(t, tt.want, got, "isTransientHTTPError(%d)", tt.statusCode)
124+
got := IsTransientHTTPError(tt.statusCode)
125+
assert.Equal(t, tt.want, got, "IsTransientHTTPError(%d)", tt.statusCode)
126126
})
127127
}
128128
}

internal/logger/logger.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"sync"
99
"time"
1010

11-
"github.com/github/gh-aw-mcpg/internal/timeutil"
11+
"github.com/github/gh-aw-mcpg/internal/strutil"
1212
"github.com/github/gh-aw-mcpg/internal/tty"
1313
)
1414

@@ -122,9 +122,9 @@ func (l *Logger) Printf(format string, args ...any) {
122122

123123
// Write to stderr with colors and time diff
124124
if l.color != "" {
125-
fmt.Fprintf(os.Stderr, "%s%s%s %s +%s\n", l.color, l.namespace, colorReset, message, timeutil.FormatDuration(diff))
125+
fmt.Fprintf(os.Stderr, "%s%s%s %s +%s\n", l.color, l.namespace, colorReset, message, strutil.FormatDuration(diff))
126126
} else {
127-
fmt.Fprintf(os.Stderr, "%s %s +%s\n", l.namespace, message, timeutil.FormatDuration(diff))
127+
fmt.Fprintf(os.Stderr, "%s %s +%s\n", l.namespace, message, strutil.FormatDuration(diff))
128128
}
129129

130130
// Also write to file logger in text-only format (no colors, no time diff)
@@ -149,9 +149,9 @@ func (l *Logger) Print(args ...any) {
149149

150150
// Write to stderr with colors and time diff
151151
if l.color != "" {
152-
fmt.Fprintf(os.Stderr, "%s%s%s %s +%s\n", l.color, l.namespace, colorReset, message, timeutil.FormatDuration(diff))
152+
fmt.Fprintf(os.Stderr, "%s%s%s %s +%s\n", l.color, l.namespace, colorReset, message, strutil.FormatDuration(diff))
153153
} else {
154-
fmt.Fprintf(os.Stderr, "%s %s +%s\n", l.namespace, message, timeutil.FormatDuration(diff))
154+
fmt.Fprintf(os.Stderr, "%s %s +%s\n", l.namespace, message, strutil.FormatDuration(diff))
155155
}
156156

157157
// Also write to file logger in text-only format (no colors, no time diff)

internal/logger/rpc_helpers.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
//
77
// - truncateAndSanitize: Combines secret sanitization with length truncation
88
// - extractEssentialFields: Extracts key JSON-RPC fields for compact logging
9-
// - getMapKeys: Utility for extracting map keys without values
109
// - isEffectivelyEmpty: Checks if data is effectively empty (e.g., only params: null)
1110
// - ExtractErrorMessage: Extracts clean error messages from log lines
1211
//
@@ -78,22 +77,17 @@ func extractEssentialFields(payload []byte) map[string]interface{} {
7877
if params, ok := data["params"]; ok {
7978
if paramsMap, ok := params.(map[string]interface{}); ok {
8079
// Include param count and keys, but not values
81-
essential["params_keys"] = getMapKeys(paramsMap)
80+
keys := make([]string, 0, len(paramsMap))
81+
for k := range paramsMap {
82+
keys = append(keys, k)
83+
}
84+
essential["params_keys"] = keys
8285
}
8386
}
8487

8588
return essential
8689
}
8790

88-
// getMapKeys returns the keys of a map
89-
func getMapKeys(m map[string]interface{}) []string {
90-
keys := make([]string, 0, len(m))
91-
for k := range m {
92-
keys = append(keys, k)
93-
}
94-
return keys
95-
}
96-
9791
// isEffectivelyEmpty checks if the data is effectively empty (only contains params: null)
9892
func isEffectivelyEmpty(data map[string]interface{}) bool {
9993
// If empty, it's empty

internal/logger/rpc_helpers_test.go

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -330,53 +330,6 @@ func TestExtractEssentialFields(t *testing.T) {
330330
}
331331
}
332332

333-
// TestGetMapKeys tests the getMapKeys function
334-
func TestGetMapKeys(t *testing.T) {
335-
tests := []struct {
336-
name string
337-
m map[string]interface{}
338-
want []string
339-
}{
340-
{
341-
name: "normal map",
342-
m: map[string]interface{}{
343-
"key1": "value1",
344-
"key2": "value2",
345-
"key3": "value3",
346-
},
347-
want: []string{"key1", "key2", "key3"},
348-
},
349-
{
350-
name: "empty map",
351-
m: map[string]interface{}{},
352-
want: []string{},
353-
},
354-
{
355-
name: "single key",
356-
m: map[string]interface{}{
357-
"only": "value",
358-
},
359-
want: []string{"only"},
360-
},
361-
{
362-
name: "nil values",
363-
m: map[string]interface{}{
364-
"null1": nil,
365-
"null2": nil,
366-
},
367-
want: []string{"null1", "null2"},
368-
},
369-
}
370-
371-
for _, tt := range tests {
372-
t.Run(tt.name, func(t *testing.T) {
373-
result := getMapKeys(tt.m)
374-
assert.ElementsMatch(t, tt.want, result, "keys should match regardless of order")
375-
assert.Len(t, result, len(tt.want), "should have correct number of keys")
376-
})
377-
}
378-
}
379-
380333
// TestIsEffectivelyEmpty tests the isEffectivelyEmpty function
381334
func TestIsEffectivelyEmpty(t *testing.T) {
382335
tests := []struct {

0 commit comments

Comments
 (0)