Skip to content
Merged
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
11 changes: 9 additions & 2 deletions src/cmd/cli/command/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,16 @@ func handleComposeUpErr(ctx context.Context, debugger *debug.Debugger, project *
}

term.Error("Error:", client.PrettyError(originalErr))
return debugger.DebugDeploymentError(ctx, debug.DebugConfig{
// The debugger runs for its side effect only. It returns nil once it has
// explained the failure, so returning its error here would turn a fatal
// deployment error into exit code 0 — a false-green CI deploy for every
// account that auto-approves the debugger.
if debugErr := debugger.DebugDeploymentError(ctx, debug.DebugConfig{
Project: project,
}, originalErr)
}, originalErr); debugErr != nil {
term.Debug("debugger failed:", debugErr)
}
return originalErr
}

func handleTooManyProjectsError(ctx context.Context, provider client.Provider, originalErr error) error {
Expand Down
70 changes: 70 additions & 0 deletions src/cmd/cli/command/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package command
import (
"bytes"
"context"
"errors"
"os"
"strings"
"testing"
"time"

"connectrpc.com/connect"
"github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/DefangLabs/defang/src/pkg/cli/compose"
"github.com/DefangLabs/defang/src/pkg/debug"
"github.com/DefangLabs/defang/src/pkg/term"
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
)
Expand Down Expand Up @@ -129,3 +132,70 @@ func TestResolveTTL(t *testing.T) {
})
}
}

// stubDebugAgent stands in for the AI agent: it records that it ran and returns whatever the
// test wants the debug session to end with.
type stubDebugAgent struct {
called bool
err error
}

func (s *stubDebugAgent) StartWithMessage(context.Context, string) error {
s.called = true
return s.err
}

// Regression for issue 2227: handleComposeUpErr used to return the DEBUGGER's error. The
// debugger returns nil once it has explained the failure, so a fatal compose up error exited 0
// and CI went green on a deploy that never happened.
func TestHandleComposeUpErrKeepsTheDeploymentError(t *testing.T) {
prevNonInteractive := global.NonInteractive
global.NonInteractive = true
t.Cleanup(func() { global.NonInteractive = prevNonInteractive })

originalErr := errors.New(`service "fabric": port 50051: 'target' must be an integer between 1 and 32767`)

for _, tt := range []struct {
name string
debugErr error
}{
{name: "debugger succeeds", debugErr: nil},
{name: "debugger fails", debugErr: errors.New("agent unavailable")},
} {
t.Run(tt.name, func(t *testing.T) {
agent := &stubDebugAgent{err: tt.debugErr}
// defaultPermission=true is the paid/auto-approve account: the only one that
// reaches the debugger in CI, and so the only one that hit this bug.
debugger := debug.NewDebuggerForTest(agent, true, false)

err := handleComposeUpErr(context.Background(), debugger, &compose.Project{}, nil, originalErr)

if !agent.called {
t.Error("expected the debugger to run")
}
if !errors.Is(err, originalErr) {
t.Errorf("expected the original deployment error, got %v", err)
}
})
}
}

// A free-tier CI account never reaches the debugger; it must still get the error.
func TestHandleComposeUpErrWithoutAutoApprove(t *testing.T) {
prevNonInteractive := global.NonInteractive
global.NonInteractive = true
t.Cleanup(func() { global.NonInteractive = prevNonInteractive })

originalErr := errors.New("boom")
agent := &stubDebugAgent{}
debugger := debug.NewDebuggerForTest(agent, false, false)

err := handleComposeUpErr(context.Background(), debugger, &compose.Project{}, nil, originalErr)

if agent.called {
t.Error("the debugger must not auto-run for a free-tier account")
}
if !errors.Is(err, originalErr) {
t.Errorf("expected the original deployment error, got %v", err)
}
}
12 changes: 12 additions & 0 deletions src/pkg/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ func NewDebugger(ctx context.Context, fabricAddr string, stack *stacks.Parameter
}, nil
}

// NewDebuggerForTest builds a Debugger around a stub agent. NewDebugger needs a live Fabric
// connection, so tests in OTHER packages — the ones exercising how a caller handles what the
// debugger returns — have no other way to get one.
func NewDebuggerForTest(agent DebugAgent, defaultPermission, interactive bool) *Debugger {
return &Debugger{
agent: agent,
surveyor: &surveyor{},
defaultPermission: defaultPermission,
interactive: interactive,
}
}

// AutoApprove reports whether the debugger will run without an interactive prompt. This is true
// for paid accounts and lets callers decide whether to invoke the debugger in non-interactive
// environments (CI) or just print a hint.
Expand Down
Loading