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
18 changes: 15 additions & 3 deletions agents/services/base_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,28 @@ func (s *RuntimeWrapper) TestResponseWithResults(run, passed, failed, skipped in
func (s *RuntimeWrapper) TestError(err error) (*runtimev0.TestResponse, error) {
s.Lock()
defer s.Unlock()
s.TestStatus = &runtimev0.TestStatus{State: runtimev0.TestStatus_ERROR, Message: err.Error(), Failure: operationFailure("runtime.test", err, err.Error())}
return &runtimev0.TestResponse{Status: s.TestStatus}, nil
message := err.Error()
s.TestStatus = &runtimev0.TestStatus{State: runtimev0.TestStatus_ERROR, Message: message, Failure: operationFailure("runtime.test", err, message)}
return typedTestErrorResponse(s.TestStatus, message), nil
}

func (s *RuntimeWrapper) TestErrorf(err error, msg string, args ...any) (*runtimev0.TestResponse, error) {
s.Lock()
defer s.Unlock()
message := ErrorMessage(err, msg, args...)
s.TestStatus = &runtimev0.TestStatus{State: runtimev0.TestStatus_ERROR, Message: message, Failure: operationFailure("runtime.test", err, message)}
return &runtimev0.TestResponse{Status: s.TestStatus}, nil
return typedTestErrorResponse(s.TestStatus, message), nil
}

// typedTestErrorResponse makes pre-execution failures terminal and explicit.
// A status-only response decodes as TestRunResult_UNKNOWN and loses the
// distinction between a failed test and an environment that never ran tests.
func typedTestErrorResponse(status *runtimev0.TestStatus, message string) *runtimev0.TestResponse {
return &runtimev0.TestResponse{
Status: status,
Result: &runtimev0.TestRunResult{State: runtimev0.TestRunResult_ERRORED, Message: message},
Counts: &runtimev0.TestCounts{},
}
}

// ── Build ─────────────────────────────────────────────────
Expand Down
16 changes: 16 additions & 0 deletions agents/services/error_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ func TestRuntimeErrorHelperReturnsStructuredResponseWithoutTransportError(t *tes
}
}

func TestRuntimeTestErrorIsTypedTerminalResponse(t *testing.T) {
wrapper := &RuntimeWrapper{}
response, err := wrapper.TestError(errors.New("module preparation failed"))
if err != nil {
t.Fatalf("TestError returned transport error: %v", err)
}
if response.GetStatus().GetState() != runtimev0.TestStatus_ERROR ||
response.GetResult().GetState() != runtimev0.TestRunResult_ERRORED ||
response.GetResult().GetMessage() != "module preparation failed" || response.GetCounts() == nil {
t.Fatalf("TestError response = %+v, want ERROR/ERRORED with explicit zero counts", response)
}
if response.GetStatus().GetFailure().GetOperation() != "runtime.test" {
t.Fatalf("TestError failure = %+v", response.GetStatus().GetFailure())
}
}

func TestRuntimeLintErrorPreservesFailureOutput(t *testing.T) {
wrapper := &RuntimeWrapper{}
response, err := wrapper.LintErrorf(errors.New("main.go:4:2: undefined: value"), "lint failed")
Expand Down
8 changes: 6 additions & 2 deletions runners/golang/formula.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ func ClassifyEnvError(raw string, runErr error) (reason, detail string) {
return EnvErrorToolchainMissing, execErr.Error()
}
}
classificationText := raw
if runErr != nil {
classificationText = strings.TrimSpace(strings.Join([]string{classificationText, runErr.Error()}, "\n"))
}
for _, marker := range []string{
"errors parsing go.mod",
"unknown directive:",
Expand All @@ -116,8 +120,8 @@ func ClassifyEnvError(raw string, runErr error) (reason, detail string) {
"cannot load module",
"unsupported toolchain",
} {
if idx := strings.Index(raw, marker); idx >= 0 {
rest := raw[idx:]
if idx := strings.Index(classificationText, marker); idx >= 0 {
rest := classificationText[idx:]
line := rest
if nl := strings.IndexByte(rest, '\n'); nl >= 0 {
line = rest[:nl]
Expand Down
8 changes: 8 additions & 0 deletions runners/golang/formula_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package golang

import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -236,6 +237,13 @@ func TestRunFormula_BrokenGoModIsEnvBlocked(t *testing.T) {
}
}

func TestClassifyEnvErrorReadsPreExecutionError(t *testing.T) {
reason, detail := ClassifyEnvError("", errors.New("prepare Go dependencies: go: errors parsing go.mod:\ngo.mod:1: unknown directive: modle"))
if reason != EnvErrorModuleBroken || !strings.Contains(detail, "unknown directive") {
t.Fatalf("ClassifyEnvError = %q/%q, want module-broken with parser detail", reason, detail)
}
}

func TestRunFormula_CompileErrorIsNotEnvBlocked(t *testing.T) {
dir := writeModule(t, map[string]string{
"go.mod": "module example.com/stringsx\n\ngo 1.21\n",
Expand Down
10 changes: 9 additions & 1 deletion runners/golang/runner.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package golang

import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strings"

"github.com/codefly-dev/core/builders"
"github.com/codefly-dev/core/resources"
Expand Down Expand Up @@ -436,16 +438,22 @@ func (r *GoRunnerEnvironment) GoModuleHandling(ctx context.Context) error {
if err != nil {
return w.Wrapf(err, "cannot create go mod download process for %s", dir)
}
var output bytes.Buffer
writer := io.Writer(&output)
if r.out != nil {
proc.WithOutput(r.out)
writer = io.MultiWriter(r.out, &output)
}
proc.WithOutput(writer)
proc.WithDir(dir)
// Ambient parent workspaces are disabled; a workspace owned by the
// attached source root remains authoritative for all module work.
if !r.withGoWorkspace {
proc.WithEnvironmentVariables(ctx, resources.Env("GOWORK", "off"))
}
if err := proc.Run(ctx); err != nil {
if detail := strings.TrimSpace(output.String()); detail != "" {
return w.Wrapf(err, "cannot run go mod download in %s: %s", dir, detail)
}
return w.Wrapf(err, "cannot run go mod download in %s", dir)
}
}
Expand Down
15 changes: 15 additions & 0 deletions runners/golang/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,21 @@ func TestGoModuleHandlingCacheAdvancesOnlyOnDownloadSuccess(t *testing.T) {
})
}

func TestGoModuleHandlingPreservesParserDiagnostics(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "go.mod"), []byte("modle example.com/broken\n\ngo 1.21\n"), 0o644))
env, err := golang.NewNativeGoRunner(ctx, root, ".")
require.NoError(t, err)
env.WithLocalCacheDir(t.TempDir())
t.Cleanup(func() { require.NoError(t, env.Shutdown(context.Background())) })

err = env.GoModuleHandling(ctx)
require.Error(t, err)
require.Contains(t, err.Error(), "errors parsing go.mod")
require.Contains(t, err.Error(), "unknown directive: modle")
}

func TestGoRunnerInitDoesNotResolveOrMutateProjectDependencies(t *testing.T) {
ctx := context.Background()
t.Setenv("GOPROXY", "off")
Expand Down
Loading