| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "errors" |
| 6 | "os" |
| 7 | "os/exec" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | type historyCrashStore struct { |
| 14 | *healthyRetryStore |
| 15 | boundary string |
| 16 | } |
| 17 | |
| 18 | func (s *historyCrashStore) Write(name string, raw []byte) error { |
| 19 | var r historyRecord |
| 20 | if err := json.Unmarshal(raw, &r); err != nil { |
| 21 | return err |
| 22 | } |
| 23 | reached := r.Attempt != nil && r.Attempt.ID == "crash-attempt" |
| 24 | if s.boundary == "settled" { |
| 25 | reached = r.Attempt == nil && len(r.Refusals.Values) == 1 |
| 26 | } |
| 27 | if reached && s.boundary == "intent-visible" { |
| 28 | s.fail = 1 |
| 29 | } |
| 30 | err := s.healthyRetryStore.Write(name, raw) |
| 31 | if reached { |
| 32 | if err != nil && !(s.boundary == "intent-visible" && errors.Is(err, s.fault)) { |
| 33 | return err |
| 34 | } |
| 35 | os.Exit(73) |
| 36 | } |
| 37 | return err |
| 38 | } |
| 39 | |
| 40 | func TestPlan14RootHistoryProcessCrash(t *testing.T) { |
| 41 | b := HistoryBinding{Host: proofHost, Account: "bab", App: "console"} |
| 42 | a := HistoryAttempt{ID: "crash-attempt", Version: "new", Source: "record-etag", Started: time.Date(2026, 10, 13, 0, 0, 0, 0, time.UTC)} |
| 43 | if boundary := os.Getenv("PLAN14_HISTORY_CRASH"); boundary != "" { |
| 44 | dir := os.Getenv("PLAN14_HISTORY_DIR") |
| 45 | disk := &healthyRetryStore{dir: dir, fault: errors.New("injected directory sync failure")} |
| 46 | h := &RootHistory{Store: &historyCrashStore{healthyRetryStore: disk, boundary: boundary}} |
| 47 | if boundary == "settled" { |
| 48 | if err := h.Settle(b, a.ID, &Refusal{Host: b.Host, App: b.App, Version: a.Version, RefusedAt: a.Started, Reason: "failed"}); err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | } else if err := h.Begin(b, a); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | t.Fatal("crash boundary not reached") |
| 55 | } |
| 56 | for _, boundary := range []string{"intent-durable", "intent-visible", "settled"} { |
| 57 | t.Run(boundary, func(t *testing.T) { |
| 58 | h, s, b := rootHistoryFixture(t) |
| 59 | if boundary == "settled" { |
| 60 | if err := h.Begin(b, a); err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | } |
| 64 | cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14RootHistoryProcessCrash$") |
| 65 | cmd.Dir = t.TempDir() |
| 66 | cmd.Env = append(os.Environ(), "PLAN14_HISTORY_CRASH="+boundary, "PLAN14_HISTORY_DIR="+s.dir) |
| 67 | out, err := cmd.CombinedOutput() |
| 68 | var exit *exec.ExitError |
| 69 | if !errors.As(err, &exit) || exit.ExitCode() != 73 { |
| 70 | t.Fatalf("child: %v\n%s", err, out) |
| 71 | } |
| 72 | h = &RootHistory{Store: &healthyRetryStore{dir: s.dir}} |
| 73 | r, err := h.read(b) |
| 74 | if err != nil { |
| 75 | t.Fatal(err) |
| 76 | } |
| 77 | if boundary == "settled" { |
| 78 | if r.Attempt != nil || len(r.Refusals.Values) != 1 { |
| 79 | t.Fatalf("lost completed refusal: %+v", r) |
| 80 | } |
| 81 | } else { |
| 82 | if r.Attempt == nil || *r.Attempt != a { |
| 83 | t.Fatalf("lost pending attempt: %+v", r) |
| 84 | } |
| 85 | blocked, err := h.Blocks(b, &ChannelPointer{Version: "other"}) |
| 86 | if err != nil || blocked { |
| 87 | t.Fatal("unknown outcome blocked a different signed recovery target") |
| 88 | } |
| 89 | if err := h.Begin(b, HistoryAttempt{ID: "another", Version: "other", Source: a.Source, Started: a.Started}); err == nil { |
| 90 | t.Fatal("crashed intent allowed an unjournalled forward attempt") |
| 91 | } |
| 92 | if _, err := h.Refusals(b); err == nil { |
| 93 | t.Fatal("crashed intent granted mint completeness") |
| 94 | } |
| 95 | } |
| 96 | if _, err := os.Stat(filepath.Join(s.dir, historyKey(b))); err != nil { |
| 97 | t.Fatal(err) |
| 98 | } |
| 99 | }) |
| 100 | } |
| 101 | } |
| 102 | |