Classify recovery setup failures by their actual cause
This commit is contained in:
@@ -224,7 +224,7 @@ func recoveryGuidance(err error) string {
|
||||
case "endpoint_changed":
|
||||
return "Review Git URL rewrite settings and restore the captured endpoint mapping before retrying."
|
||||
case "transport_configuration":
|
||||
return "Review the current supported transport settings for the recorded remote, then retry observation."
|
||||
return "Review Git configuration and the supported transport settings, then retry observation."
|
||||
case "canceled":
|
||||
return "Retry observation with a context that allows it to complete."
|
||||
case "remote_observation", "inconsistent_observation":
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -15,6 +17,98 @@ import (
|
||||
"go.maximumdirect.net/releasekit/internal/publish"
|
||||
)
|
||||
|
||||
func TestRecoveryEndpointSetupFailureDiagnostics(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fixture injects Git failures through a POSIX launcher")
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name, remote, kind, guidance string
|
||||
exitCode int
|
||||
}{
|
||||
{"unnamed configuration", "", "transport_configuration", "Git configuration", 128},
|
||||
{"named configuration", "origin", "transport_configuration", "Git configuration", 128},
|
||||
{"rewrite", "", "endpoint_changed", "URL rewrite", 0},
|
||||
{"tooling", "", "remote_observation", "Git diagnostic", 71},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
repo := newCLIRepository(t)
|
||||
evidence := publish.Evidence{RepositoryDir: repo, Endpoint: repo, Tag: "v1.2.3", Remote: test.remote}
|
||||
path, err := publish.SaveEvidence(t.Context(), repo, t.TempDir(), evidence)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if test.kind == "transport_configuration" {
|
||||
file, err := os.OpenFile(filepath.Join(repo, ".git", "config"), os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := file.WriteString("\n[broken\n"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else if test.kind == "endpoint_changed" {
|
||||
cliGitRun(t, repo, "config", "url."+t.TempDir()+".insteadOf", repo)
|
||||
}
|
||||
realGit, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
marker := filepath.Join(t.TempDir(), "queried")
|
||||
bin := t.TempDir()
|
||||
failValidation := ""
|
||||
if test.name == "tooling" {
|
||||
failValidation = "[ \"$arg\" = check-ref-format ] && exit 71\n"
|
||||
}
|
||||
script := "#!/bin/sh\nquery=0\nget_url=0\nfor arg in \"$@\"; do\n" + failValidation + "[ \"$arg\" = ls-remote ] && query=1\n[ \"$arg\" = --get-url ] && get_url=1\ndone\nif [ \"$query\" = 1 ] && [ \"$get_url\" = 0 ]; then : > " + shellQuote(marker) + "; exit 72; fi\nexec " + shellQuote(realGit) + " \"$@\"\n"
|
||||
if err := os.WriteFile(filepath.Join(bin, "git"), []byte(script), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
loaded, err := releasekit.LoadPublicationEvidence(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := releasekit.ReconcilePublication(t.Context(), loaded)
|
||||
var typed *releasekit.PublicationRecoveryError
|
||||
if !errors.As(err, &typed) || typed.Kind != test.kind || result.State != releasekit.PublicationUnknown {
|
||||
t.Fatalf("setup classification = %#v, %v", result, err)
|
||||
}
|
||||
if test.exitCode != 0 {
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) || exitErr.ExitCode() != test.exitCode {
|
||||
t.Fatalf("lost Git cause: %v", err)
|
||||
}
|
||||
}
|
||||
for _, structured := range []bool{false, true} {
|
||||
args := []string{"reconcile", "--record", path}
|
||||
if structured {
|
||||
args = append(args, "--json")
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
if status := run(t.Context(), args, &stdout, &stderr); status != 3 {
|
||||
t.Fatalf("setup status=%d: %s%s", status, &stdout, &stderr)
|
||||
}
|
||||
if structured {
|
||||
var document reconciliationDocument
|
||||
if err := json.Unmarshal(stdout.Bytes(), &document); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if document.State != "unknown" || document.Diagnostic == nil || document.Diagnostic.Kind != test.kind {
|
||||
t.Fatalf("setup JSON: %s", &stdout)
|
||||
}
|
||||
} else if !strings.Contains(stderr.String(), test.kind) || !strings.Contains(stderr.String(), test.guidance) {
|
||||
t.Fatalf("setup guidance: %s", &stderr)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(marker); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("setup failure reached remote query: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type failingRecoveryWriter struct{ err error }
|
||||
|
||||
func (writer failingRecoveryWriter) Write([]byte) (int, error) { return 0, writer.err }
|
||||
|
||||
Reference in New Issue
Block a user