Files
releasekit/cmd/releasekit/recovery_test.go

332 lines
14 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
releasekit "go.maximumdirect.net/releasekit"
"go.maximumdirect.net/releasekit/internal/publish"
)
func TestRecoveryDisplaysRedactCredentialPathsAndMetadata(t *testing.T) {
secret := "audit-secret"
raw := "https://audit-user:" + secret + "@example.invalid/attempt.json"
for _, jsonOutput := range []bool{false, true} {
args := []string{"reconcile", "--record", raw}
if jsonOutput {
args = append(args, "--json")
}
var out, diagnostics bytes.Buffer
if status := run(t.Context(), args, &out, &diagnostics); status != 2 {
t.Fatalf("load status=%d", status)
}
if strings.Contains(out.String()+diagnostics.String(), secret) {
t.Fatalf("credential leaked: %s%s", &out, &diagnostics)
}
}
repo := newCLIRepository(t)
store := t.TempDir()
evidence := publish.Evidence{RepositoryDir: raw, ModuleDir: raw, ModulePath: raw, Version: raw, Tag: raw, CandidateCommit: raw, TagObject: raw, Remote: raw, RecordedAt: raw}
path, err := publish.SaveEvidence(t.Context(), repo, store, evidence)
if err != nil {
t.Fatal(err)
}
for _, command := range [][]string{{"reconcile", "--record", path}, {"publication-attempts", "--record-dir", store}} {
for _, jsonOutput := range []bool{false, true} {
args := append([]string{}, command...)
if jsonOutput {
args = append(args, "--json")
}
var out, diagnostics bytes.Buffer
run(t.Context(), args, &out, &diagnostics)
if strings.Contains(out.String()+diagnostics.String(), secret) {
t.Fatalf("metadata leaked: %s%s", &out, &diagnostics)
}
}
}
loaded, err := publish.LoadEvidence(path)
if err != nil || loaded != evidence {
t.Fatalf("private metadata changed: %#v, %v", loaded, err)
}
var out bytes.Buffer
reportPublicationRecord(releasekit.PublicationResult{RecordPath: raw, Phase: releasekit.PublicationPush}, fmt.Errorf("push failed"), &out)
if strings.Contains(out.String(), secret) {
t.Fatalf("handoff leaked credentials: %s", &out)
}
}
func TestReconcileCommandClassifiesRecordsFromAnotherWorkingDirectory(t *testing.T) {
repository := newCLIRepository(t)
remote := filepath.Join(t.TempDir(), "bare remote.git")
cliGitRun(t, repository, "init", "--bare", "--initial-branch=main", remote)
cliGitRun(t, repository, "remote", "add", "origin", remote)
cliGitRun(t, repository, "tag", "-a", "v1.2.3", "-m", "first tag object")
cliGitRun(t, repository, "push", "origin", "refs/tags/v1.2.3")
commit := cliGitOutput(t, repository, "rev-parse", "HEAD")
tagObject := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3")
store := filepath.Join(t.TempDir(), "records with ' punctuation")
evidence := publish.Evidence{
RepositoryDir: repository,
ModuleDir: repository,
ModulePath: "example.com/cli-candidate",
Version: "v1.2.3",
Tag: "v1.2.3",
CandidateCommit: commit,
TagObject: tagObject,
Endpoint: remote,
Remote: "origin",
RecordedAt: "2026-11-02T00:00:00Z",
}
record, err := publish.SaveEvidence(t.Context(), repository, store, evidence)
if err != nil {
t.Fatal(err)
}
cliGitRun(t, repository, "switch", "-c", "changed-head")
cliGitRun(t, repository, "commit", "--allow-empty", "-m", "change current HEAD")
cliGitRun(t, repository, "tag", "-d", "v1.2.3")
if err := os.WriteFile(filepath.Join(repository, "uncommitted recovery check.txt"), []byte("dirty"), 0o600); err != nil {
t.Fatal(err)
}
otherDirectory := t.TempDir()
relativeRecord, err := filepath.Rel(otherDirectory, record)
if err != nil {
t.Fatal(err)
}
previousDirectory, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(otherDirectory); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := os.Chdir(previousDirectory); err != nil {
t.Errorf("restore working directory: %v", err)
}
})
var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"reconcile", "--record", relativeRecord, "--json"}, &stdout, &stderr)
if status != 0 || stderr.Len() != 0 {
t.Fatalf("published status = %d, stdout = %q, stderr = %q", status, stdout.String(), stderr.String())
}
var published map[string]any
if err := json.Unmarshal(stdout.Bytes(), &published); err != nil {
t.Fatalf("decode published JSON: %v; output = %q", err, stdout.String())
}
if published["state"] != string("published") || published["record_path"] != relativeRecord {
t.Fatalf("published JSON = %#v", published)
}
if _, ok := published["diagnostic"]; ok {
t.Fatalf("successful reconciliation includes a diagnostic: %#v", published)
}
evidenceJSON, ok := published["evidence"].(map[string]any)
if !ok || evidenceJSON["candidate_commit"] != commit || evidenceJSON["tag_object"] != tagObject {
t.Fatalf("safe evidence = %#v", published["evidence"])
}
if evidenceJSON["module_path"] != evidence.ModulePath || evidenceJSON["candidateCommit"] != nil {
t.Fatalf("evidence does not use the versioned snake-case shape: %#v", evidenceJSON)
}
cliGitRun(t, repository, "push", "origin", ":refs/tags/v1.2.3")
cliGitRun(t, repository, "tag", "-f", "-a", "v1.2.3", "-m", "replacement tag object")
cliGitRun(t, repository, "push", "origin", "refs/tags/v1.2.3")
var conflictOut, conflictErr bytes.Buffer
status = run(context.Background(), []string{"reconcile", "--record", record, "--json"}, &conflictOut, &conflictErr)
if status != 1 || conflictErr.Len() != 0 {
t.Fatalf("conflict status = %d, stdout = %q, stderr = %q", status, conflictOut.String(), conflictErr.String())
}
var conflict map[string]any
if err := json.Unmarshal(conflictOut.Bytes(), &conflict); err != nil {
t.Fatal(err)
}
if conflict["state"] != string("conflict") || conflict["observed_tag_object"] == tagObject {
t.Fatalf("conflict JSON = %#v", conflict)
}
var humanOut, humanErr bytes.Buffer
status = run(context.Background(), []string{"reconcile", "--record", record}, &humanOut, &humanErr)
if status != 1 || !strings.Contains(humanOut.String(), "expected tag-object="+tagObject) || !strings.Contains(humanOut.String(), "observed tag-object=") || !strings.Contains(humanOut.String(), "Do not replace a conflicting remote tag") {
t.Fatalf("human conflict status = %d, stdout = %q, stderr = %q", status, humanOut.String(), humanErr.String())
}
partialEvidence := evidence
partialEvidence.TagObject = cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3")
partialEvidence.CandidateCommit = ""
partialRecord, err := publish.SaveEvidence(t.Context(), repository, store, partialEvidence)
if err != nil {
t.Fatal(err)
}
var unknownOut, unknownErr bytes.Buffer
status = run(context.Background(), []string{"reconcile", "--record", partialRecord, "--json"}, &unknownOut, &unknownErr)
if status != 3 || unknownErr.Len() != 0 {
t.Fatalf("unknown status = %d, stdout = %q, stderr = %q", status, unknownOut.String(), unknownErr.String())
}
var unknown map[string]any
if err := json.Unmarshal(unknownOut.Bytes(), &unknown); err != nil {
t.Fatal(err)
}
unknownDiagnostic, ok := unknown["diagnostic"].(map[string]any)
if unknown["state"] != "unknown" || !ok || unknownDiagnostic["kind"] != "missing_evidence" {
t.Fatalf("unknown JSON = %#v", unknown)
}
var unknownHumanOut, unknownHumanErr bytes.Buffer
status = run(context.Background(), []string{"reconcile", "--record", partialRecord}, &unknownHumanOut, &unknownHumanErr)
if status != 3 || !strings.Contains(unknownHumanOut.String(), "state=unknown") || !strings.Contains(unknownHumanErr.String(), "candidate_commit") {
t.Fatalf("human unknown status = %d, stdout = %q, stderr = %q", status, unknownHumanOut.String(), unknownHumanErr.String())
}
cliGitRun(t, repository, "push", "origin", ":refs/tags/v1.2.3")
var absentOut, absentErr bytes.Buffer
status = run(context.Background(), []string{"reconcile", "--record", record, "--json"}, &absentOut, &absentErr)
if status != 1 || absentErr.Len() != 0 {
t.Fatalf("absent status = %d, stdout = %q, stderr = %q", status, absentOut.String(), absentErr.String())
}
var absent map[string]any
if err := json.Unmarshal(absentOut.Bytes(), &absent); err != nil {
t.Fatal(err)
}
if absent["state"] != "not_published" {
t.Fatalf("absence JSON = %#v", absent)
}
if _, ok := absent["diagnostic"]; ok {
t.Fatalf("known absence includes an unresolved diagnostic: %#v", absent)
}
var absentHumanOut, absentHumanErr bytes.Buffer
status = run(context.Background(), []string{"reconcile", "--record", record}, &absentHumanOut, &absentHumanErr)
if status != 1 || !strings.Contains(absentHumanOut.String(), "state=not_published") || !strings.Contains(absentHumanOut.String(), "current absence only") {
t.Fatalf("human absence status = %d, stdout = %q, stderr = %q", status, absentHumanOut.String(), absentHumanErr.String())
}
brokenRecord := filepath.Join(store, "broken record's.json")
if err := os.WriteFile(brokenRecord, []byte("{"), 0o600); err != nil {
t.Fatal(err)
}
var brokenOut, brokenErr bytes.Buffer
status = run(context.Background(), []string{"reconcile", "--record", brokenRecord, "--json"}, &brokenOut, &brokenErr)
if status != 2 || brokenErr.Len() != 0 {
t.Fatalf("record-format status = %d, stdout = %q, stderr = %q", status, brokenOut.String(), brokenErr.String())
}
var broken map[string]any
if err := json.Unmarshal(brokenOut.Bytes(), &broken); err != nil {
t.Fatal(err)
}
brokenDiagnostic, ok := broken["diagnostic"].(map[string]any)
if !ok || brokenDiagnostic["kind"] != "record_format" || broken["record_path"] != brokenRecord {
t.Fatalf("record-format JSON = %#v", broken)
}
for _, field := range []string{"evidence", "state", "observed_tag_object", "observed_commit"} {
if _, ok := broken[field]; ok {
t.Errorf("record-format JSON includes %q: %#v", field, broken)
}
}
}
func TestPublicationAttemptsCommandRedactsCredentialsAndReportsPartialDiscovery(t *testing.T) {
repository := newCLIRepository(t)
store := filepath.Join(t.TempDir(), "external records")
evidence := publish.Evidence{
RepositoryDir: repository,
ModulePath: "example.com/cli-candidate",
Version: "v1.2.3",
Tag: "v1.2.3",
Endpoint: "https://cli-user:cli-secret@example.test/repository.git",
Remote: "origin",
RecordedAt: "2026-11-02T00:00:00Z",
}
if _, err := publish.SaveEvidence(t.Context(), repository, store, evidence); err != nil {
t.Fatal(err)
}
brokenPath := filepath.Join(store, "damaged.json")
if err := os.WriteFile(brokenPath, []byte(`{"format_version":99,"evidence":{}}`), 0o600); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"publication-attempts", "--record-dir", store, "--json"}, &stdout, &stderr)
if status != 1 || stderr.Len() != 0 {
t.Fatalf("partial discovery status = %d, stdout = %q, stderr = %q", status, stdout.String(), stderr.String())
}
output := stdout.String()
if strings.Contains(output, "cli-user") || strings.Contains(output, "cli-secret") || !strings.Contains(output, "https://redacted@example.test/repository.git") {
t.Fatalf("structured discovery exposed or failed to redact endpoint credentials: %q", output)
}
var listed map[string]any
if err := json.Unmarshal(stdout.Bytes(), &listed); err != nil {
t.Fatal(err)
}
attempts, ok := listed["attempts"].([]any)
if !ok || len(attempts) != 1 {
t.Fatalf("attempts = %#v", listed["attempts"])
}
diagnostics, ok := listed["diagnostics"].([]any)
if !ok || len(diagnostics) != 1 {
t.Fatalf("diagnostics = %#v", listed["diagnostics"])
}
if listed["schema_version"] != float64(1) || listed["command"] != "publication-attempts" || listed["diagnostic"] != nil {
t.Fatalf("listing document = %#v", listed)
}
entry := attempts[0].(map[string]any)
if entry["record_path"] == "" {
t.Fatalf("attempt is missing record_path: %#v", entry)
}
entryEvidence := entry["evidence"].(map[string]any)
if entryEvidence["module_path"] != evidence.ModulePath || entryEvidence["recorded_at"] != evidence.RecordedAt {
t.Fatalf("attempt evidence = %#v", entryEvidence)
}
diagnostic := diagnostics[0].(map[string]any)
if diagnostic["kind"] != "record_format" || diagnostic["record_path"] != brokenPath {
t.Fatalf("record diagnostic = %#v", diagnostic)
}
emptyStore := filepath.Join(t.TempDir(), "empty store")
if err := os.Mkdir(emptyStore, 0o700); err != nil {
t.Fatal(err)
}
var emptyOut, emptyErr bytes.Buffer
status = run(context.Background(), []string{"publication-attempts", "--record-dir", emptyStore, "--json"}, &emptyOut, &emptyErr)
if status != 0 || emptyErr.Len() != 0 {
t.Fatalf("empty discovery status = %d, stdout = %q, stderr = %q", status, emptyOut.String(), emptyErr.String())
}
var empty map[string]any
if err := json.Unmarshal(emptyOut.Bytes(), &empty); err != nil {
t.Fatal(err)
}
if len(empty["attempts"].([]any)) != 0 || len(empty["diagnostics"].([]any)) != 0 || empty["diagnostic"] != nil {
t.Fatalf("empty listing document = %#v", empty)
}
var failedOut, failedErr bytes.Buffer
status = run(context.Background(), []string{"publication-attempts", "--record-dir", filepath.Join(emptyStore, "missing"), "--json"}, &failedOut, &failedErr)
if status != 1 || failedErr.Len() != 0 {
t.Fatalf("enumeration failure status = %d, stdout = %q, stderr = %q", status, failedOut.String(), failedErr.String())
}
var failed map[string]any
if err := json.Unmarshal(failedOut.Bytes(), &failed); err != nil {
t.Fatal(err)
}
failedDiagnostic, ok := failed["diagnostic"].(map[string]any)
if !ok || failedDiagnostic["kind"] != "storage_enumeration" {
t.Fatalf("enumeration failure JSON = %#v", failed)
}
}
func TestRecoveryArgumentErrorsUseExistingUsagePath(t *testing.T) {
var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"reconcile", "--json"}, &stdout, &stderr)
if status != 2 || stdout.Len() != 0 || !strings.Contains(stderr.String(), "reconcile requires --record FILE") {
t.Fatalf("argument failure status = %d, stdout = %q, stderr = %q", status, stdout.String(), stderr.String())
}
if strings.Contains(stdout.String(), "schema_version") || strings.Contains(stderr.String(), fmt.Sprint("\"command\"")) {
t.Fatalf("argument failure unexpectedly used structured output: stdout=%q stderr=%q", stdout.String(), stderr.String())
}
}