package host import ( "crypto/ed25519" "os" "testing" "time" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/release" ) func TestPlan14RecoveryKnownRefusal(t *testing.T) { for _, mode := range []string{"active", "failed", "inactive"} { for _, kind := range []string{release.RefusalNotUp, release.RefusalWrongProgram, release.RefusalNotProven} { for _, change := range []string{"same", "retry", "unsigned", "late-refusal"} { t.Run(mode+"/"+kind+"/"+change, func(t *testing.T) { e, u, c, source, pub := signedRecoveryFixture(t) rt, err := e.root.reg.Get(observedHost) if err != nil { t.Fatal(err) } h := seedHostRootHistory(t, *rt) b := release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App} p, err := release.Channel(t.Context(), e.store, pub, rt.Account, rt.App, rt.Channel) if err != nil { t.Fatal(err) } at := p.PromotedAt if change == "retry" { at = at.Add(-time.Second) } ref := release.Refusal{Host: rt.Host, App: rt.App, Version: p.Version, Kind: kind, PointerPromotedAt: at, RefusedAt: at} add := func() { if err := h.AddRefusal(b, ref); err != nil { t.Fatal(err) } } if change != "late-refusal" { add() } c.history = func() (*release.RootHistoryPass, error) { return &release.RootHistoryPass{History: h}, nil } n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink} if change == "unsigned" { n.pub = make(ed25519.PublicKey, ed25519.PublicKeySize) } if change == "late-refusal" { n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{before: add}} } if mode != "active" { u.props.ActiveState, u.props.SubState, u.props.MainPID = mode, mode, 0 } handled, err := n.recover(t.Context(), rt.Host, c, func(string) string { return "5s" }, e.root.reg) accepted := change == "retry" && kind == release.RefusalNotUp live, liveErr := release.LiveVersion(u.opt, rt.Host) want := "v1" effects := 0 if accepted { want = "v2" if mode == "active" { effects = 1 } } if !handled || (err == nil) != accepted || liveErr != nil || live != want || len(u.effects) != effects { t.Fatalf("handled=%v error=%v live=%s effects=%v", handled, err, live, u.effects) } refs, err := h.Refusals(b) if err != nil { t.Fatal(err) } if accepted && len(refs) != 0 { t.Fatalf("verified retry retained D1: %+v", refs) } if !accepted && len(refs) != 1 { t.Fatalf("refused target cleared evidence: %+v", refs) } }) } } } } func TestPlan14RecoveryOriginalRefusalWithUnknownHistory(t *testing.T) { e, u, c, source, pub := signedRecoveryFixture(t) old, err := c.host(t.Context(), observedHost) if err != nil { t.Fatal(err) } p, err := release.Channel(t.Context(), e.store, pub, old.Route.Account, old.Route.App, old.Route.Channel) if err != nil { t.Fatal(err) } disk, err := c.attempts() if err != nil { t.Fatal(err) } b := release.HistoryBinding{Host: old.Route.Host, Account: old.Route.Account, App: old.Route.App} a, err := captureRollback(disk, old, c.now(), unitSettleTimeout(func(string) string { return "5s" }), release.VersionPath(u.opt, b.Account, p.Version, b.App), c.identity, historyRollbackLink{Binding: b, Attempt: release.HistoryAttempt{ID: "failed", Source: old.Source, Version: p.Version, SHA256: "digest", Platform: release.AdoptPlatform(), Started: c.now(), PromotedAt: p.PromotedAt}, Independent: true}) if err != nil { t.Fatal(err) } a.Failed, a.Settled = true, true if err := writeRollbackState(disk, recoveryResultKey(a), a); err != nil { t.Fatal(err) } delete(disk.(*rollbackMemory).files, rollbackJournalKey(b.Host)) c.history = func() (*release.RootHistoryPass, error) { return nil, os.ErrPermission } n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink} handled, err := n.recover(t.Context(), b.Host, c, func(string) string { return "5s" }, e.root.reg) live, _ := release.LiveVersion(u.opt, b.Host) if !handled || err == nil || live != "v1" || len(u.effects) != 0 { t.Fatalf("retained failure ignored: handled=%v err=%v live=%s effects=%v", handled, err, live, u.effects) } }