package release import ( "bytes" "os" "path/filepath" "testing" "time" ) const ( nsHost = "held.example.com" nsOp = "mv_abcdefghijklmnopqrstuvwxyz" ) func adoptionRefusalFor(host string) Refusal { return Refusal{Host: host, App: "console", Version: "v0", SHA256: "aa", Reason: "did not come up", PointerPromotedAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC), RefusedAt: time.Date(2026, 10, 1, 0, 1, 0, 0, time.UTC)} } func heldRefusalFor(host string) Refusal { return Refusal{Host: host, App: "console", Version: "v1", SHA256: "bb", Reason: "not proven: staged sha256 bb", RefusedAt: time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC), Kind: RefusalNotProven, Recovered: true} } // Final review finding 3: held cleanup reads a record and then removes a // path. While held and adoption refusals shared refusals/.json, an // adoption refusal written between the two (adoption does not take the // root adopt lock) was removed with it. Held refusals now live in their own // namespace, so the adoption refusal survives. The hook makes the // interleaving deterministic. func TestHeldCleanupLeavesAConcurrentAdoptionRefusal(t *testing.T) { bake := t.TempDir() if err := heldRefusalWriter(bake, nsOp)(heldRefusalFor(nsHost)); err != nil { t.Fatal(err) } prev := heldRetireBeforeClear t.Cleanup(func() { heldRetireBeforeClear = prev }) raced := false heldRetireBeforeClear = func(host string) { raced = true if err := WriteRefusal(bake, adoptionRefusalFor(host)); err != nil { t.Fatal(err) } } retired, err := RetireHeldRefusals(bake, func(Refusal) bool { return false }) if err != nil || len(retired) != 1 || !raced { t.Fatalf("retired %v, %v (raced %v)", retired, err, raced) } r, err := ReadRefusal(bake, nsHost) if err != nil || r == nil || r.MoveOp != "" || r.Reason != "did not come up" { t.Fatalf("the adoption refusal written during held cleanup: %+v, %v; want it kept", r, err) } if held, _ := ListHeldRefusals(bake); len(held) != 0 { t.Fatalf("held refusals left: %+v", held) } } // The held writer never touches adoption's record: with an adoption // refusal standing for the host, the held one is still recorded, beside // it, and the adoption file is byte-identical. Adoption's readers // (ReadRefusal, ListRefusals) never see a held record. func TestHeldRefusalsHaveTheirOwnNamespace(t *testing.T) { bake := t.TempDir() if err := WriteRefusal(bake, adoptionRefusalFor(nsHost)); err != nil { t.Fatal(err) } path := filepath.Join(RefusalDir(bake), nsHost+".json") before, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if err := heldRefusalWriter(bake, nsOp)(heldRefusalFor(nsHost)); err != nil { t.Fatalf("the held refusal was not recorded beside an adoption one: %v", err) } if after, _ := os.ReadFile(path); !bytes.Equal(before, after) { t.Fatalf("the adoption refusal changed:\n%s\nwant\n%s", after, before) } held, err := ListHeldRefusals(bake) if err != nil || len(held) != 1 || held[0].MoveOp != nsOp || held[0].Host != nsHost { t.Fatalf("held refusals %+v, %v", held, err) } // Adoption's readers see only adoption's record, on this host and on // one with only a held refusal. other := "only-held.example.com" if err := heldRefusalWriter(bake, nsOp)(heldRefusalFor(other)); err != nil { t.Fatal(err) } if r, _ := ReadRefusal(bake, other); r != nil { t.Fatalf("adoption reads a held refusal: %+v", r) } recs, _ := ListRefusals(bake) if len(recs) != 1 || recs[0].MoveOp != "" { t.Fatalf("adoption's list holds %+v", recs) } // Retiring the held one leaves adoption's. if err := RetireHeldRefusal(bake, nsHost, nsOp); err != nil { t.Fatal(err) } if after, _ := os.ReadFile(path); !bytes.Equal(before, after) { t.Fatal("retiring the held refusal touched the adoption one") } }