From 90655811dd670a73049eeac93ae5f86c0f03dcd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 22:08:40 +0000 Subject: [PATCH 1/6] Initial plan From f6ff539db82956a3a078bc8da8bedbde2494a1b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 22:17:10 +0000 Subject: [PATCH 2/6] fix: make fix suggestion storage thread-safe Agent-Logs-Url: https://github.com/microsoft/azure-linux-dev-tools/sessions/5c16317f-6885-4df7-8091-874f9b19917c Co-authored-by: dmcilvaney <23200982+dmcilvaney@users.noreply.github.com> --- internal/app/azldev/env.go | 40 +++++++++++++--- internal/app/azldev/env_test.go | 83 +++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 6 deletions(-) diff --git a/internal/app/azldev/env.go b/internal/app/azldev/env.go index 493a2a62..af66440e 100644 --- a/internal/app/azldev/env.go +++ b/internal/app/azldev/env.go @@ -14,6 +14,7 @@ import ( "path/filepath" "runtime" "strings" + "sync" "time" "github.com/charmbracelet/gum/confirm" @@ -92,13 +93,38 @@ type Env struct { // Fix suggestion: a list of human readable hints that will be printed after an error to help the user // resolve the issue. Printed in FIFO order. - fixSuggestions []string + fixSuggestions *fixSuggestionState // lockStore provides cached access to per-component lock files. // Nil when no project directory is configured. lockStore *lockfile.Store } +type fixSuggestionState struct { + mu sync.Mutex + suggestions []string +} + +func newFixSuggestionState() *fixSuggestionState { + return &fixSuggestionState{ + suggestions: []string{}, + } +} + +func (state *fixSuggestionState) Add(suggestion string) { + state.mu.Lock() + defer state.mu.Unlock() + + state.suggestions = append(state.suggestions, suggestion) +} + +func (state *fixSuggestionState) Snapshot() []string { + state.mu.Lock() + defer state.mu.Unlock() + + return append([]string(nil), state.suggestions...) +} + // Constructs a new [Env] using specified options. func NewEnv(ctx context.Context, options EnvOptions) *Env { var workDir, logDir, outputDir string @@ -150,7 +176,7 @@ func NewEnv(ctx context.Context, options EnvOptions) *Env { constructionTime: time.Now(), // No fix suggestions to start. - fixSuggestions: []string{}, + fixSuggestions: newFixSuggestionState(), // Lock store: created when we have a project directory. lockStore: newLockStore(options.ProjectDir, options.Config, options.Interfaces.FileSystemFactory), @@ -300,12 +326,14 @@ func (env *Env) OutputDir() string { // AddFixSuggestion records a human-readable hint that will be printed after an // error to help the user resolve the issue. Suggestions are printed in FIFO order. func (env *Env) AddFixSuggestion(suggestion string) { - env.fixSuggestions = append(env.fixSuggestions, suggestion) + env.fixSuggestions.Add(suggestion) } // PrintFixSuggestions prints the current fix suggestions, if any. func (env *Env) PrintFixSuggestions() { - if len(env.fixSuggestions) == 0 { + suggestions := env.fixSuggestions.Snapshot() + + if len(suggestions) == 0 { return } @@ -324,7 +352,7 @@ func (env *Env) PrintFixSuggestions() { paddingSize := len(padding) maxMsgLength := 0 - for _, suggestion := range env.fixSuggestions { + for _, suggestion := range suggestions { if len(suggestion) > maxMsgLength { maxMsgLength = len(suggestion) } @@ -335,7 +363,7 @@ func (env *Env) PrintFixSuggestions() { slog.Warn(boxEdgeString) - for _, suggestion := range env.fixSuggestions { + for _, suggestion := range suggestions { slog.Warn(padding + suggestion) } diff --git a/internal/app/azldev/env_test.go b/internal/app/azldev/env_test.go index 456624ef..7ac19476 100644 --- a/internal/app/azldev/env_test.go +++ b/internal/app/azldev/env_test.go @@ -4,8 +4,13 @@ package azldev_test import ( + "bytes" "context" + "fmt" + "log/slog" "os" + "strings" + "sync" "testing" "time" @@ -16,6 +21,17 @@ import ( "github.com/stretchr/testify/require" ) +func setDummyLogger(logBuffer *bytes.Buffer) *slog.Logger { + logger := slog.New(slog.NewTextHandler(logBuffer, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + + oldDefault := slog.Default() + slog.SetDefault(logger) + + return oldDefault +} + func TestNewEnv(t *testing.T) { const ( testProjectRoot = "/non/existent/dir" @@ -168,4 +184,71 @@ func TestFixSuggestions(t *testing.T) { testEnv.Env.PrintFixSuggestions() }) }) + + t.Run("suggestions added from child envs are visible on the parent env", func(t *testing.T) { + testEnv := testutils.NewTestEnv(t) + + const suggestionCount = 32 + + var waitGroup sync.WaitGroup + for suggestionIndex := range suggestionCount { + waitGroup.Add(1) + + go func(index int) { + defer waitGroup.Done() + + childEnv, cancel := testEnv.Env.WithCancel() + defer cancel() + + childEnv.AddFixSuggestion(fmt.Sprintf("child suggestion %d", index)) + }(suggestionIndex) + } + + waitGroup.Wait() + + var logBuffer bytes.Buffer + + oldDefault := setDummyLogger(&logBuffer) + defer slog.SetDefault(oldDefault) + + testEnv.Env.PrintFixSuggestions() + + output := logBuffer.String() + for suggestionIndex := range suggestionCount { + assert.Contains(t, output, fmt.Sprintf("child suggestion %d", suggestionIndex)) + } + }) + + t.Run("concurrent suggestions on the same env are all preserved", func(t *testing.T) { + testEnv := testutils.NewTestEnv(t) + + const suggestionCount = 128 + + var waitGroup sync.WaitGroup + for suggestionIndex := range suggestionCount { + waitGroup.Add(1) + + go func(index int) { + defer waitGroup.Done() + + testEnv.Env.AddFixSuggestion(fmt.Sprintf("shared suggestion %d", index)) + }(suggestionIndex) + } + + waitGroup.Wait() + + var logBuffer bytes.Buffer + + oldDefault := setDummyLogger(&logBuffer) + defer slog.SetDefault(oldDefault) + + testEnv.Env.PrintFixSuggestions() + + output := logBuffer.String() + assert.Equal(t, suggestionCount, strings.Count(output, "shared suggestion ")) + + for suggestionIndex := range suggestionCount { + assert.Contains(t, output, fmt.Sprintf("shared suggestion %d", suggestionIndex)) + } + }) } From 6737093f49943b90997067e4bc25c9f2586375bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 22:21:02 +0000 Subject: [PATCH 3/6] test: polish concurrent fix suggestion coverage Agent-Logs-Url: https://github.com/microsoft/azure-linux-dev-tools/sessions/5c16317f-6885-4df7-8091-874f9b19917c Co-authored-by: dmcilvaney <23200982+dmcilvaney@users.noreply.github.com> --- internal/app/azldev/env.go | 4 +--- internal/app/azldev/env_test.go | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/app/azldev/env.go b/internal/app/azldev/env.go index af66440e..6fffa056 100644 --- a/internal/app/azldev/env.go +++ b/internal/app/azldev/env.go @@ -106,9 +106,7 @@ type fixSuggestionState struct { } func newFixSuggestionState() *fixSuggestionState { - return &fixSuggestionState{ - suggestions: []string{}, - } + return &fixSuggestionState{} } func (state *fixSuggestionState) Add(suggestion string) { diff --git a/internal/app/azldev/env_test.go b/internal/app/azldev/env_test.go index 7ac19476..54daa6dd 100644 --- a/internal/app/azldev/env_test.go +++ b/internal/app/azldev/env_test.go @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/require" ) -func setDummyLogger(logBuffer *bytes.Buffer) *slog.Logger { +func setUpTestLogger(logBuffer *bytes.Buffer) *slog.Logger { logger := slog.New(slog.NewTextHandler(logBuffer, &slog.HandlerOptions{ Level: slog.LevelDebug, })) @@ -208,7 +208,7 @@ func TestFixSuggestions(t *testing.T) { var logBuffer bytes.Buffer - oldDefault := setDummyLogger(&logBuffer) + oldDefault := setUpTestLogger(&logBuffer) defer slog.SetDefault(oldDefault) testEnv.Env.PrintFixSuggestions() @@ -239,7 +239,7 @@ func TestFixSuggestions(t *testing.T) { var logBuffer bytes.Buffer - oldDefault := setDummyLogger(&logBuffer) + oldDefault := setUpTestLogger(&logBuffer) defer slog.SetDefault(oldDefault) testEnv.Env.PrintFixSuggestions() From 4b570a48686b09ae70dcc1e2f1d40bdbc7d42716 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 22:23:28 +0000 Subject: [PATCH 4/6] refactor: simplify fix suggestion state helper Agent-Logs-Url: https://github.com/microsoft/azure-linux-dev-tools/sessions/5c16317f-6885-4df7-8091-874f9b19917c Co-authored-by: dmcilvaney <23200982+dmcilvaney@users.noreply.github.com> --- internal/app/azldev/env.go | 6 +----- internal/app/azldev/env_test.go | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/internal/app/azldev/env.go b/internal/app/azldev/env.go index 6fffa056..6411c9bd 100644 --- a/internal/app/azldev/env.go +++ b/internal/app/azldev/env.go @@ -105,10 +105,6 @@ type fixSuggestionState struct { suggestions []string } -func newFixSuggestionState() *fixSuggestionState { - return &fixSuggestionState{} -} - func (state *fixSuggestionState) Add(suggestion string) { state.mu.Lock() defer state.mu.Unlock() @@ -174,7 +170,7 @@ func NewEnv(ctx context.Context, options EnvOptions) *Env { constructionTime: time.Now(), // No fix suggestions to start. - fixSuggestions: newFixSuggestionState(), + fixSuggestions: &fixSuggestionState{}, // Lock store: created when we have a project directory. lockStore: newLockStore(options.ProjectDir, options.Config, options.Interfaces.FileSystemFactory), diff --git a/internal/app/azldev/env_test.go b/internal/app/azldev/env_test.go index 54daa6dd..be6e24df 100644 --- a/internal/app/azldev/env_test.go +++ b/internal/app/azldev/env_test.go @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/require" ) -func setUpTestLogger(logBuffer *bytes.Buffer) *slog.Logger { +func setupTestLogger(logBuffer *bytes.Buffer) *slog.Logger { logger := slog.New(slog.NewTextHandler(logBuffer, &slog.HandlerOptions{ Level: slog.LevelDebug, })) @@ -208,7 +208,7 @@ func TestFixSuggestions(t *testing.T) { var logBuffer bytes.Buffer - oldDefault := setUpTestLogger(&logBuffer) + oldDefault := setupTestLogger(&logBuffer) defer slog.SetDefault(oldDefault) testEnv.Env.PrintFixSuggestions() @@ -239,7 +239,7 @@ func TestFixSuggestions(t *testing.T) { var logBuffer bytes.Buffer - oldDefault := setUpTestLogger(&logBuffer) + oldDefault := setupTestLogger(&logBuffer) defer slog.SetDefault(oldDefault) testEnv.Env.PrintFixSuggestions() From 63b8c609002ce0bead2329004d57e8013a14ed46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 22:25:19 +0000 Subject: [PATCH 5/6] test: simplify logger restoration helper Agent-Logs-Url: https://github.com/microsoft/azure-linux-dev-tools/sessions/5c16317f-6885-4df7-8091-874f9b19917c Co-authored-by: dmcilvaney <23200982+dmcilvaney@users.noreply.github.com> --- internal/app/azldev/env_test.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/internal/app/azldev/env_test.go b/internal/app/azldev/env_test.go index be6e24df..007e5174 100644 --- a/internal/app/azldev/env_test.go +++ b/internal/app/azldev/env_test.go @@ -9,7 +9,6 @@ import ( "fmt" "log/slog" "os" - "strings" "sync" "testing" "time" @@ -21,15 +20,17 @@ import ( "github.com/stretchr/testify/require" ) -func setupTestLogger(logBuffer *bytes.Buffer) *slog.Logger { +func setupTestLogger(logBuffer *bytes.Buffer) func() { logger := slog.New(slog.NewTextHandler(logBuffer, &slog.HandlerOptions{ Level: slog.LevelDebug, })) - oldDefault := slog.Default() + previousDefaultLogger := slog.Default() slog.SetDefault(logger) - return oldDefault + return func() { + slog.SetDefault(previousDefaultLogger) + } } func TestNewEnv(t *testing.T) { @@ -208,8 +209,8 @@ func TestFixSuggestions(t *testing.T) { var logBuffer bytes.Buffer - oldDefault := setupTestLogger(&logBuffer) - defer slog.SetDefault(oldDefault) + restoreLogger := setupTestLogger(&logBuffer) + defer restoreLogger() testEnv.Env.PrintFixSuggestions() @@ -239,14 +240,12 @@ func TestFixSuggestions(t *testing.T) { var logBuffer bytes.Buffer - oldDefault := setupTestLogger(&logBuffer) - defer slog.SetDefault(oldDefault) + restoreLogger := setupTestLogger(&logBuffer) + defer restoreLogger() testEnv.Env.PrintFixSuggestions() output := logBuffer.String() - assert.Equal(t, suggestionCount, strings.Count(output, "shared suggestion ")) - for suggestionIndex := range suggestionCount { assert.Contains(t, output, fmt.Sprintf("shared suggestion %d", suggestionIndex)) } From 38f5a58a870dfa0a28177c2a8eaa514c6b87b0c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 23:35:17 +0000 Subject: [PATCH 6/6] refactor: simplify fix suggestion helper naming Agent-Logs-Url: https://github.com/microsoft/azure-linux-dev-tools/sessions/74c9103d-8308-4e14-a8f4-abbb5d53d3fc Co-authored-by: dmcilvaney <23200982+dmcilvaney@users.noreply.github.com> --- internal/app/azldev/env.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/app/azldev/env.go b/internal/app/azldev/env.go index 6411c9bd..b1d79acb 100644 --- a/internal/app/azldev/env.go +++ b/internal/app/azldev/env.go @@ -105,18 +105,20 @@ type fixSuggestionState struct { suggestions []string } -func (state *fixSuggestionState) Add(suggestion string) { - state.mu.Lock() - defer state.mu.Unlock() +// Add appends a fix suggestion in FIFO order. +func (suggestions *fixSuggestionState) Add(suggestion string) { + suggestions.mu.Lock() + defer suggestions.mu.Unlock() - state.suggestions = append(state.suggestions, suggestion) + suggestions.suggestions = append(suggestions.suggestions, suggestion) } -func (state *fixSuggestionState) Snapshot() []string { - state.mu.Lock() - defer state.mu.Unlock() +// All returns a copy of all collected fix suggestions. +func (suggestions *fixSuggestionState) All() []string { + suggestions.mu.Lock() + defer suggestions.mu.Unlock() - return append([]string(nil), state.suggestions...) + return append([]string(nil), suggestions.suggestions...) } // Constructs a new [Env] using specified options. @@ -325,7 +327,7 @@ func (env *Env) AddFixSuggestion(suggestion string) { // PrintFixSuggestions prints the current fix suggestions, if any. func (env *Env) PrintFixSuggestions() { - suggestions := env.fixSuggestions.Snapshot() + suggestions := env.fixSuggestions.All() if len(suggestions) == 0 { return