From fff5c2d4e13ba954803e423e010da6f6072e9f6d Mon Sep 17 00:00:00 2001 From: David Gageot Date: Sat, 14 Feb 2026 09:46:24 +0100 Subject: [PATCH 1/4] Remove duplication from config.Resolv Signed-off-by: David Gageot --- .dockerignore | 3 +- .../coder.yaml} | 0 .../default.yaml} | 0 pkg/config/resolve.go | 115 +++++++++--------- pkg/teamloader/teamloader_test.go | 2 +- 5 files changed, 58 insertions(+), 62 deletions(-) rename pkg/config/{coder-agent.yaml => builtin-agents/coder.yaml} (100%) rename pkg/config/{default-agent.yaml => builtin-agents/default.yaml} (100%) diff --git a/.dockerignore b/.dockerignore index d800123fc..40b2fd660 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,6 +7,5 @@ !./**/*.css !./**/*.go !./**/*.txt -!/pkg/config/default-agent.yaml -!/pkg/config/coder-agent.yaml +!/pkg/config/builtin-agents/*.yaml !/pkg/tui/styles/themes/*.yaml \ No newline at end of file diff --git a/pkg/config/coder-agent.yaml b/pkg/config/builtin-agents/coder.yaml similarity index 100% rename from pkg/config/coder-agent.yaml rename to pkg/config/builtin-agents/coder.yaml diff --git a/pkg/config/default-agent.yaml b/pkg/config/builtin-agents/default.yaml similarity index 100% rename from pkg/config/default-agent.yaml rename to pkg/config/builtin-agents/default.yaml diff --git a/pkg/config/resolve.go b/pkg/config/resolve.go index e675b47ec..27cf1f543 100644 --- a/pkg/config/resolve.go +++ b/pkg/config/resolve.go @@ -5,8 +5,10 @@ import ( _ "embed" "fmt" "log/slog" + "maps" "os" "path/filepath" + "slices" "strings" "github.com/google/go-containerregistry/pkg/name" @@ -16,10 +18,10 @@ import ( "github.com/docker/cagent/pkg/userconfig" ) -//go:embed default-agent.yaml +//go:embed builtin-agents/default.yaml var defaultAgent []byte -//go:embed coder-agent.yaml +//go:embed builtin-agents/coder.yaml var coderAgent []byte // builtinAgents maps built-in agent names to their embedded YAML configurations. @@ -30,11 +32,7 @@ var builtinAgents = map[string][]byte{ // BuiltinAgentNames returns the names of all built-in agents. func BuiltinAgentNames() []string { - names := make([]string, 0, len(builtinAgents)) - for name := range builtinAgents { - names = append(names, name) - } - return names + return slices.Sorted(maps.Keys(builtinAgents)) } // ResolveAlias resolves an agent reference and returns the alias if it exists and has options. @@ -70,61 +68,24 @@ func GetUserSettings() *userconfig.Settings { // when fetching from GitHub URLs. // For OCI references, always checks remote for updates but falls back to local cache if offline. func ResolveSources(agentsPath string, envProvider environment.Provider) (Sources, error) { - // Handle URL references first (before resolve() which converts to absolute path) - if IsURLReference(agentsPath) { - return map[string]Source{ - agentsPath: NewURLSource(agentsPath, envProvider), - }, nil - } - resolvedPath, err := resolve(agentsPath) if err != nil { + // resolve() only fails for non-OCI, non-URL, non-builtin references + // that can't be made absolute. Try OCI as last resort. if IsOCIReference(agentsPath) { - return map[string]Source{ - reference.OciRefToFilename(agentsPath): NewOCISource(agentsPath), - }, nil + return singleSource(reference.OciRefToFilename(agentsPath), NewOCISource(agentsPath)), nil } return nil, err } - if data, ok := builtinAgents[resolvedPath]; ok { - return map[string]Source{ - resolvedPath: NewBytesSource(resolvedPath, data), - }, nil - } - - if isLocalFile(resolvedPath) { - return map[string]Source{ - fileNameWithoutExt(resolvedPath): NewFileSource(resolvedPath), - }, nil - } - + // Only directories need special handling to enumerate YAML files. if dirExists(resolvedPath) { - sources := make(Sources) - entries, err := os.ReadDir(resolvedPath) - if err != nil { - return nil, fmt.Errorf("reading agents directory %s: %w", resolvedPath, err) - } - for _, entry := range entries { - if entry.IsDir() { - continue - } - ext := strings.ToLower(filepath.Ext(entry.Name())) - if ext != ".yaml" && ext != ".yml" { - continue - } - a := filepath.Join(resolvedPath, entry.Name()) - sources[fileNameWithoutExt(a)], err = Resolve(a, envProvider) - if err != nil { - return nil, err - } - } - return sources, nil + return resolveDirectory(resolvedPath, envProvider) } - return map[string]Source{ - reference.OciRefToFilename(resolvedPath): NewOCISource(resolvedPath), - }, nil + // For all other reference types, delegate to resolveOne. + key, source := resolveOne(resolvedPath, envProvider) + return singleSource(key, source), nil } // Resolve resolves an agent file reference (local file, URL, or OCI image) to a source. @@ -140,19 +101,55 @@ func Resolve(agentFilename string, envProvider environment.Provider) (Source, er return nil, err } - if data, ok := builtinAgents[resolvedPath]; ok { - return NewBytesSource(resolvedPath, data), nil + _, source := resolveOne(resolvedPath, envProvider) + return source, nil +} + +// resolveOne maps a resolved path to the appropriate Source and a key for use +// in Sources maps. The path must already be resolved via resolve(). +// This is the single place that decides which source type a reference maps to. +// To add a new source type, add a case here. +func resolveOne(resolvedPath string, envProvider environment.Provider) (string, Source) { + switch { + case builtinAgents[resolvedPath] != nil: + return resolvedPath, NewBytesSource(resolvedPath, builtinAgents[resolvedPath]) + case IsURLReference(resolvedPath): + return resolvedPath, NewURLSource(resolvedPath, envProvider) + case isLocalFile(resolvedPath): + return fileNameWithoutExt(resolvedPath), NewFileSource(resolvedPath) + default: + return reference.OciRefToFilename(resolvedPath), NewOCISource(resolvedPath) } +} - if IsURLReference(resolvedPath) { - return NewURLSource(resolvedPath, envProvider), nil +// resolveDirectory enumerates YAML files in a directory and resolves each one. +func resolveDirectory(dirPath string, envProvider environment.Provider) (Sources, error) { + entries, err := os.ReadDir(dirPath) + if err != nil { + return nil, fmt.Errorf("reading agents directory %s: %w", dirPath, err) } - if isLocalFile(resolvedPath) { - return NewFileSource(resolvedPath), nil + sources := make(Sources) + for _, entry := range entries { + if entry.IsDir() { + continue + } + ext := strings.ToLower(filepath.Ext(entry.Name())) + if ext != ".yaml" && ext != ".yml" { + continue + } + a := filepath.Join(dirPath, entry.Name()) + sources[fileNameWithoutExt(a)], err = Resolve(a, envProvider) + if err != nil { + return nil, err + } } + return sources, nil +} - return NewOCISource(resolvedPath), nil +// singleSource wraps a single source in a Sources map. +func singleSource(key string, source Source) Sources { + return Sources{key: source} } // resolve resolves an agent reference, handling aliases and defaults diff --git a/pkg/teamloader/teamloader_test.go b/pkg/teamloader/teamloader_test.go index cf30b65e7..f8570b44f 100644 --- a/pkg/teamloader/teamloader_test.go +++ b/pkg/teamloader/teamloader_test.go @@ -139,7 +139,7 @@ func TestLoadExamples(t *testing.T) { func TestLoadDefaultAgent(t *testing.T) { t.Parallel() - agentSource, err := config.Resolve("../../pkg/config/default-agent.yaml", nil) + agentSource, err := config.Resolve("default", nil) require.NoError(t, err) runConfig := &config.RuntimeConfig{ From 8c93dfc2d66faa80f7c5dfbc4a15faa3f2eb552d Mon Sep 17 00:00:00 2001 From: David Gageot Date: Sat, 14 Feb 2026 09:57:31 +0100 Subject: [PATCH 2/4] Move GetUserSettings() from pkg/config to pkg/userconfig as Get() The function only wraps userconfig.Load() + cfg.GetSettings(), so it belongs in pkg/userconfig rather than pkg/config which is about agent configuration resolution. Renamed to Get() since the package name already provides context. Assisted-By: cagent --- cmd/root/run.go | 5 +++-- pkg/config/resolve.go | 10 ---------- pkg/config/resolve_test.go | 28 ---------------------------- pkg/userconfig/userconfig.go | 10 ++++++++++ pkg/userconfig/userconfig_test.go | 28 ++++++++++++++++++++++++++++ 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/cmd/root/run.go b/cmd/root/run.go index 48e045e04..01dfcbe63 100644 --- a/cmd/root/run.go +++ b/cmd/root/run.go @@ -24,6 +24,7 @@ import ( "github.com/docker/cagent/pkg/teamloader" "github.com/docker/cagent/pkg/telemetry" "github.com/docker/cagent/pkg/tui/styles" + "github.com/docker/cagent/pkg/userconfig" ) type runExecFlags struct { @@ -160,7 +161,7 @@ func (f *runExecFlags) runOrExec(ctx context.Context, out *cli.Printer, args []s // Apply global user settings first (lowest priority) // User settings only apply if the flag wasn't explicitly set by the user - userSettings := config.GetUserSettings() + userSettings := userconfig.Get() if userSettings.HideToolResults && !f.hideToolResults { f.hideToolResults = true slog.Debug("Applying user settings", "hide_tool_results", true) @@ -493,7 +494,7 @@ func (f *runExecFlags) handleRunMode(ctx context.Context, rt runtime.Runtime, se func applyTheme() { // Resolve theme from user config > built-in default themeRef := styles.DefaultThemeRef - if userSettings := config.GetUserSettings(); userSettings.Theme != "" { + if userSettings := userconfig.Get(); userSettings.Theme != "" { themeRef = userSettings.Theme } diff --git a/pkg/config/resolve.go b/pkg/config/resolve.go index 27cf1f543..e909492cb 100644 --- a/pkg/config/resolve.go +++ b/pkg/config/resolve.go @@ -53,16 +53,6 @@ func ResolveAlias(agentFilename string) *userconfig.Alias { return alias } -// GetUserSettings returns the global user settings from the config file. -// Returns an empty Settings if the config file doesn't exist or has no settings. -func GetUserSettings() *userconfig.Settings { - cfg, err := userconfig.Load() - if err != nil { - return &userconfig.Settings{} - } - return cfg.GetSettings() -} - // ResolveSources resolves an agent file reference (local file, URL, or OCI image) to sources. // If envProvider is non-nil, it will be used to look up GITHUB_TOKEN for authentication // when fetching from GitHub URLs. diff --git a/pkg/config/resolve_test.go b/pkg/config/resolve_test.go index 390bb33a1..60ecdc4eb 100644 --- a/pkg/config/resolve_test.go +++ b/pkg/config/resolve_test.go @@ -619,31 +619,3 @@ func TestResolveAlias_WithAllOptions(t *testing.T) { assert.Equal(t, "anthropic/claude-sonnet-4-0", alias.Model) assert.True(t, alias.HideToolResults) } - -func TestGetUserSettings_Empty(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - - // No config file exists - settings := GetUserSettings() - require.NotNil(t, settings) - assert.False(t, settings.HideToolResults) -} - -func TestGetUserSettings_WithHideToolResults(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - - // Set up config with settings - cfg, err := userconfig.Load() - require.NoError(t, err) - cfg.Settings = &userconfig.Settings{ - HideToolResults: true, - } - require.NoError(t, cfg.Save()) - - // Get settings - settings := GetUserSettings() - require.NotNil(t, settings) - assert.True(t, settings.HideToolResults) -} diff --git a/pkg/userconfig/userconfig.go b/pkg/userconfig/userconfig.go index 00f9b9f2f..972820ecf 100644 --- a/pkg/userconfig/userconfig.go +++ b/pkg/userconfig/userconfig.go @@ -278,3 +278,13 @@ func (c *Config) GetSettings() *Settings { } return c.Settings } + +// Get returns the global user settings from the config file. +// Returns an empty Settings if the config file doesn't exist or has no settings. +func Get() *Settings { + cfg, err := Load() + if err != nil { + return &Settings{} + } + return cfg.GetSettings() +} diff --git a/pkg/userconfig/userconfig_test.go b/pkg/userconfig/userconfig_test.go index 5ea2d777f..f0adc3366 100644 --- a/pkg/userconfig/userconfig_test.go +++ b/pkg/userconfig/userconfig_test.go @@ -782,3 +782,31 @@ func TestConfig_DefaultModel_SaveAndLoad(t *testing.T) { require.NotNil(t, loaded.DefaultModel.ThinkingBudget) assert.Equal(t, 10000, loaded.DefaultModel.ThinkingBudget.Tokens) } + +func TestGet_Empty(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + + // No config file exists + settings := Get() + require.NotNil(t, settings) + assert.False(t, settings.HideToolResults) +} + +func TestGet_WithHideToolResults(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + + // Set up config with settings + cfg, err := Load() + require.NoError(t, err) + cfg.Settings = &Settings{ + HideToolResults: true, + } + require.NoError(t, cfg.Save()) + + // Get settings + settings := Get() + require.NotNil(t, settings) + assert.True(t, settings.HideToolResults) +} From a1366796809829f14961dd33c2eed0f2091d4ebc Mon Sep 17 00:00:00 2001 From: David Gageot Date: Sat, 14 Feb 2026 10:01:51 +0100 Subject: [PATCH 3/4] Remove redundant Reader interface from pkg/config Load() now accepts Source directly since every caller already passes a Source. The test helper testfileSource is replaced with the existing NewFileSource(). Assisted-By: cagent --- pkg/config/commands_test.go | 16 ++++------------ pkg/config/config.go | 6 +----- pkg/config/config_test.go | 34 +++++++++++++++++----------------- pkg/config/examples_test.go | 2 +- pkg/config/validation_test.go | 8 ++++---- 5 files changed, 27 insertions(+), 39 deletions(-) diff --git a/pkg/config/commands_test.go b/pkg/config/commands_test.go index ffd2c1c5a..9969bc4c7 100644 --- a/pkg/config/commands_test.go +++ b/pkg/config/commands_test.go @@ -1,15 +1,13 @@ package config import ( - "context" - "os" "testing" "github.com/stretchr/testify/require" ) func TestV2Commands_AllForms(t *testing.T) { - cfg, err := Load(t.Context(), testfileSource("testdata/commands_v2.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/commands_v2.yaml")) require.NoError(t, err) // Test simple map format @@ -40,7 +38,7 @@ func TestV2Commands_AllForms(t *testing.T) { } func TestV2Commands_DisplayText(t *testing.T) { - cfg, err := Load(t.Context(), testfileSource("testdata/commands_v2.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/commands_v2.yaml")) require.NoError(t, err) // Simple format: DisplayText returns the instruction @@ -53,7 +51,7 @@ func TestV2Commands_DisplayText(t *testing.T) { } func TestMigrate_v1_Commands_AllForms(t *testing.T) { - cfg, err := Load(t.Context(), testfileSource("testdata/commands_v1.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/commands_v1.yaml")) require.NoError(t, err) require.Equal(t, "root", cfg.Agents[0].Name) @@ -71,7 +69,7 @@ func TestMigrate_v1_Commands_AllForms(t *testing.T) { } func TestMigrate_v0_Commands_AllForms(t *testing.T) { - cfg, err := Load(t.Context(), testfileSource("testdata/commands_v0.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/commands_v0.yaml")) require.NoError(t, err) require.Equal(t, "root", cfg.Agents[0].Name) @@ -87,9 +85,3 @@ func TestMigrate_v0_Commands_AllForms(t *testing.T) { require.Equal(t, "yet_another_agent", cfg.Agents[2].Name) require.Empty(t, cfg.Agents[2].Commands) } - -type testfileSource string - -func (s testfileSource) Read(context.Context) ([]byte, error) { - return os.ReadFile(string(s)) -} diff --git a/pkg/config/config.go b/pkg/config/config.go index 67ca82067..fe4cf247f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,11 +16,7 @@ import ( "github.com/docker/cagent/pkg/environment" ) -type Reader interface { - Read(ctx context.Context) ([]byte, error) -} - -func Load(ctx context.Context, source Reader) (*latest.Config, error) { +func Load(ctx context.Context, source Source) (*latest.Config, error) { data, err := source.Read(ctx) if err != nil { return nil, err diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 44ccbd2c6..f3d76a1a1 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -15,7 +15,7 @@ import ( func TestAutoRegisterModels(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/autoregister.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/autoregister.yaml")) require.NoError(t, err) assert.Len(t, cfg.Models, 2) @@ -28,7 +28,7 @@ func TestAutoRegisterModels(t *testing.T) { func TestAutoRegisterAlloy(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/autoregister_alloy.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/autoregister_alloy.yaml")) require.NoError(t, err) assert.Len(t, cfg.Models, 2) @@ -41,7 +41,7 @@ func TestAutoRegisterAlloy(t *testing.T) { func TestAlloyModelComposition(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/alloy_model_composition.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/alloy_model_composition.yaml")) require.NoError(t, err) // The alloy model should be expanded to its constituent models @@ -57,7 +57,7 @@ func TestAlloyModelComposition(t *testing.T) { func TestAlloyModelNestedComposition(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/alloy_model_nested.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/alloy_model_nested.yaml")) require.NoError(t, err) // The nested alloy should be fully expanded to all constituent models @@ -72,7 +72,7 @@ func TestAlloyModelNestedComposition(t *testing.T) { func TestMigrate_v0_v1_provider(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/provider_v0.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/provider_v0.yaml")) require.NoError(t, err) assert.Equal(t, "openai", cfg.Models["gpt"].Provider) @@ -81,7 +81,7 @@ func TestMigrate_v0_v1_provider(t *testing.T) { func TestMigrate_v1_provider(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/provider_v1.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/provider_v1.yaml")) require.NoError(t, err) assert.Equal(t, "openai", cfg.Models["gpt"].Provider) @@ -90,7 +90,7 @@ func TestMigrate_v1_provider(t *testing.T) { func TestMigrate_v0_v1_todo(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/todo_v0.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/todo_v0.yaml")) require.NoError(t, err) assert.Len(t, cfg.Agents.First().Toolsets, 2) @@ -102,7 +102,7 @@ func TestMigrate_v0_v1_todo(t *testing.T) { func TestMigrate_v1_todo(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/todo_v1.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/todo_v1.yaml")) require.NoError(t, err) assert.Len(t, cfg.Agents.First().Toolsets, 2) @@ -114,7 +114,7 @@ func TestMigrate_v1_todo(t *testing.T) { func TestMigrate_v0_v1_shared_todo(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/shared_todo_v0.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/shared_todo_v0.yaml")) require.NoError(t, err) assert.Len(t, cfg.Agents.First().Toolsets, 2) @@ -126,7 +126,7 @@ func TestMigrate_v0_v1_shared_todo(t *testing.T) { func TestMigrate_v1_shared_todo(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/shared_todo_v1.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/shared_todo_v1.yaml")) require.NoError(t, err) assert.Len(t, cfg.Agents.First().Toolsets, 2) @@ -138,7 +138,7 @@ func TestMigrate_v1_shared_todo(t *testing.T) { func TestMigrate_v0_v1_think(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/think_v0.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/think_v0.yaml")) require.NoError(t, err) assert.Len(t, cfg.Agents.First().Toolsets, 2) @@ -149,7 +149,7 @@ func TestMigrate_v0_v1_think(t *testing.T) { func TestMigrate_v1_think(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/think_v1.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/think_v1.yaml")) require.NoError(t, err) assert.Len(t, cfg.Agents.First().Toolsets, 2) @@ -160,7 +160,7 @@ func TestMigrate_v1_think(t *testing.T) { func TestMigrate_v0_v1_memory(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/memory_v0.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/memory_v0.yaml")) require.NoError(t, err) assert.Len(t, cfg.Agents.First().Toolsets, 2) @@ -172,7 +172,7 @@ func TestMigrate_v0_v1_memory(t *testing.T) { func TestMigrate_v1_memory(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/memory_v1.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/memory_v1.yaml")) require.NoError(t, err) assert.Len(t, cfg.Agents.First().Toolsets, 2) @@ -184,7 +184,7 @@ func TestMigrate_v1_memory(t *testing.T) { func TestMigrate_v1(t *testing.T) { t.Parallel() - _, err := Load(t.Context(), testfileSource("testdata/v1.yaml")) + _, err := Load(t.Context(), NewFileSource("testdata/v1.yaml")) require.NoError(t, err) } @@ -262,7 +262,7 @@ func TestCheckRequiredEnvVars(t *testing.T) { t.Run(test.yaml, func(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/env/"+test.yaml)) + cfg, err := Load(t.Context(), NewFileSource("testdata/env/"+test.yaml)) require.NoError(t, err) err = CheckRequiredEnvVars(t.Context(), cfg, "", &noEnvProvider{}) @@ -282,7 +282,7 @@ func TestCheckRequiredEnvVars(t *testing.T) { func TestCheckRequiredEnvVarsWithModelGateway(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource("testdata/env/all.yaml")) + cfg, err := Load(t.Context(), NewFileSource("testdata/env/all.yaml")) require.NoError(t, err) err = CheckRequiredEnvVars(t.Context(), cfg, "gateway:8080", &noEnvProvider{}) diff --git a/pkg/config/examples_test.go b/pkg/config/examples_test.go index eebc4a9fb..070b9f515 100644 --- a/pkg/config/examples_test.go +++ b/pkg/config/examples_test.go @@ -42,7 +42,7 @@ func TestParseExamples(t *testing.T) { t.Run(file, func(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource(file)) + cfg, err := Load(t.Context(), NewFileSource(file)) require.NoError(t, err) require.Equal(t, latest.Version, cfg.Version, "Version should be %d in %s", latest.Version, file) diff --git a/pkg/config/validation_test.go b/pkg/config/validation_test.go index fe5e409e1..f2d901438 100644 --- a/pkg/config/validation_test.go +++ b/pkg/config/validation_test.go @@ -21,11 +21,11 @@ agents: err := tmpRoot.WriteFile("valid.yaml", []byte(validConfig), 0o644) require.NoError(t, err) - cfg, err := Load(t.Context(), testfileSource(filepath.Join(tmp, "valid.yaml"))) + cfg, err := Load(t.Context(), NewFileSource(filepath.Join(tmp, "valid.yaml"))) require.NoError(t, err) require.NotNil(t, cfg) - _, err = Load(t.Context(), testfileSource(filepath.Join(tmp, "../../../etc/passwd"))) //nolint: gocritic // testing invalid path + _, err = Load(t.Context(), NewFileSource(filepath.Join(tmp, "../../../etc/passwd"))) //nolint: gocritic // testing invalid path require.Error(t, err) } @@ -66,7 +66,7 @@ func TestValidationErrors(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - _, err := Load(t.Context(), testfileSource(filepath.Join("testdata", tt.path))) + _, err := Load(t.Context(), NewFileSource(filepath.Join("testdata", tt.path))) require.Error(t, err) }) } @@ -114,7 +114,7 @@ func TestValidSkillsConfiguration(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - cfg, err := Load(t.Context(), testfileSource(filepath.Join("testdata", tt.path))) + cfg, err := Load(t.Context(), NewFileSource(filepath.Join("testdata", tt.path))) require.NoError(t, err) require.NotNil(t, cfg) }) From a6a5fc9560862ae9665f1b4178acc12eb2643dcb Mon Sep 17 00:00:00 2001 From: David Gageot Date: Sat, 14 Feb 2026 10:03:14 +0100 Subject: [PATCH 4/4] Fix leaked os.Root handle in fileSource.Read Assisted-By: cagent --- pkg/config/sources.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/config/sources.go b/pkg/config/sources.go index 9325121bf..89a6f0ef6 100644 --- a/pkg/config/sources.go +++ b/pkg/config/sources.go @@ -55,6 +55,7 @@ func (a fileSource) Read(context.Context) ([]byte, error) { if err != nil { return nil, fmt.Errorf("opening filesystem %s: %w", parentDir, err) } + defer fs.Close() fileName := filepath.Base(a.path) data, err := fs.ReadFile(fileName)