Files
pack/core/mise/install_test.go
Ed GaniukovandGitHub 01ecfada39 fix: exit 75 for transient failures (#690)
Every failure exited 1, so callers could only retry blindly:
deterministic failures (no provider detected, missing start command)
were run and logged three times by builderv3 before giving up.

Transient failures are now typed as mise.TemporaryError and map to exit
75 (EX_TEMPFAIL): transport errors, 429, 408, 5xx, and truncated
bodies. A 404 for a pinned mise version and a malformed archive stay
deterministic, since retrying them cannot succeed. The mise download
also had no response status check at all, so a 5xx body was written to
disk and later failed as "binary not found in archive".

Carrying that across the process boundary meant giving
GenerateBuildPlan an error return: a mise install failure was
previously swallowed into BuildResult{Success: false} and exited 1
along with every app-level failure. The error is non-nil only for
transient failures, so !Success stays on exit 1.

The download gets an http.Client timeout, since http.DefaultClient has
none and a stalled connection would hang the build forever. It
deliberately does not retry in-process: the caller already retries the
whole command, and retrying here would multiply that and delay the
signal.

Also writes --info-out even when the build result is unsuccessful, so
callers can read {"success": false, "logs": [...]} instead of scraping
stdout.
2026-08-12 14:46:23 +02:00

101 lines
3.0 KiB
Go

package mise
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync/atomic"
"testing"
"github.com/stretchr/testify/require"
)
func TestDownloadArchiveClassifiesStatusCodes(t *testing.T) {
tests := []struct {
status int
isTemporary bool
}{
{status: http.StatusOK},
{status: http.StatusNotFound},
{status: http.StatusForbidden},
{status: http.StatusRequestTimeout, isTemporary: true},
{status: http.StatusTooManyRequests, isTemporary: true},
{status: http.StatusInternalServerError, isTemporary: true},
{status: http.StatusServiceUnavailable, isTemporary: true},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("status-%d", tt.status), func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tt.status)
}))
defer server.Close()
err := downloadArchive(server.URL, filepath.Join(t.TempDir(), "mise.tar.gz"))
if tt.status == http.StatusOK {
require.NoError(t, err)
return
}
require.Error(t, err)
require.Equal(t, tt.isTemporary, IsTemporary(err))
})
}
}
func TestDownloadArchiveWritesTheResponseBody(t *testing.T) {
var requests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests.Add(1)
_, _ = w.Write([]byte("archive contents"))
}))
defer server.Close()
archivePath := filepath.Join(t.TempDir(), "mise.tar.gz")
require.NoError(t, downloadArchive(server.URL, archivePath))
// the download is attempted once; retries are the caller's decision
require.EqualValues(t, 1, requests.Load())
contents, err := os.ReadFile(archivePath)
require.NoError(t, err)
require.Equal(t, "archive contents", string(contents))
}
func TestDownloadArchiveTransportFailureIsTemporary(t *testing.T) {
// closing the server up front guarantees a connection error rather than a response
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
url := server.URL
server.Close()
err := downloadArchive(url, filepath.Join(t.TempDir(), "mise.tar.gz"))
require.True(t, IsTemporary(err))
}
// the install path wraps the download error several times before a caller sees it
func TestEnsureInstalledKeepsTemporaryFailuresDetectable(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer server.Close()
originalBase := githubReleaseBase
githubReleaseBase = server.URL
t.Cleanup(func() { githubReleaseBase = originalBase })
_, err := ensureInstalled(filepath.Join(t.TempDir(), "mise"))
require.Error(t, err)
require.True(t, IsTemporary(err))
}
func TestIsTemporaryUnwrapsWrappedErrors(t *testing.T) {
temporary := &TemporaryError{URL: "https://example.com", Err: fmt.Errorf("i/o timeout")}
require.True(t, IsTemporary(fmt.Errorf("failed to ensure mise is installed: %w", temporary)))
require.False(t, IsTemporary(fmt.Errorf("binary not found in archive")))
require.False(t, IsTemporary(nil))
}