| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "os" |
| 6 | "os/exec" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | "time" |
| 10 | ) |
| 11 | |
| 12 | type recoveryCrashStore struct { |
| 13 | *healthyRetryStore |
| 14 | boundary string |
| 15 | } |
| 16 | |
| 17 | func (s *recoveryCrashStore) Write(name string, raw []byte) error { |
| 18 | if !strings.HasSuffix(name, ".outcome") { |
| 19 | return s.healthyRetryStore.Write(name, raw) |
| 20 | } |
| 21 | if s.boundary == "visible" { |
| 22 | s.fail = 1 |
| 23 | } |
| 24 | err := s.healthyRetryStore.Write(name, raw) |
| 25 | if err != nil && s.boundary != "visible" { |
| 26 | return err |
| 27 | } |
| 28 | os.Exit(74) |
| 29 | return nil |
| 30 | } |
| 31 | |
| 32 | func TestPlan14HistoryRecoveryProcessCrash(t *testing.T) { |
| 33 | b := HistoryBinding{Host: proofHost, Account: "bab", App: "console"} |
| 34 | now := time.Date(2026, 10, 13, 0, 0, 0, 0, time.UTC) |
| 35 | r := HistoryRecovery{Binding: b, Attempt: HistoryAttempt{ID: "crashed-recovery", Source: "canonical", Version: "v2", SHA256: "failed-digest", Platform: AdoptPlatform(), Started: now, PromotedAt: now}, Settled: true, Failed: true} |
| 36 | if boundary := os.Getenv("PLAN14_OUTCOME_CRASH"); boundary != "" { |
| 37 | h := &RootHistory{Store: &recoveryCrashStore{healthyRetryStore: &healthyRetryStore{dir: os.Getenv("PLAN14_OUTCOME_DIR"), fault: errors.New("directory sync failed")}, boundary: boundary}} |
| 38 | if err := h.RetainRecovery(r); err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | t.Fatal("crash boundary not reached") |
| 42 | } |
| 43 | for _, boundary := range []string{"durable", "visible"} { |
| 44 | t.Run(boundary, func(t *testing.T) { |
| 45 | _, s, b := rootHistoryFixture(t) |
| 46 | cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14HistoryRecoveryProcessCrash$", "-test.count=1") |
| 47 | cmd.Dir = t.TempDir() |
| 48 | cmd.Env = append(os.Environ(), "PLAN14_OUTCOME_CRASH="+boundary, "PLAN14_OUTCOME_DIR="+s.dir) |
| 49 | out, err := cmd.CombinedOutput() |
| 50 | var exit *exec.ExitError |
| 51 | if !errors.As(err, &exit) || exit.ExitCode() != 74 { |
| 52 | t.Fatalf("child: %v\n%s", err, out) |
| 53 | } |
| 54 | h := &RootHistory{Store: s} |
| 55 | if refs, err := h.Refusals(b); err != nil || len(refs) != 1 || refs[0].SHA256 != "failed-digest" { |
| 56 | t.Fatalf("crash erased refusal: %+v %v", refs, err) |
| 57 | } |
| 58 | }) |
| 59 | } |
| 60 | } |
| 61 | |