package store import ( "bytes" "testing" "time" ) // R-1255 (review round 2, HUNT-3 H3/H4): two escrow edges at the store. // H3: the R-1176 carry took the deleted host's NEWEST retained row and checked only its identity blob. A newer // row with an identity but NO repository-password hash (a package that seals no off-site key) was carried, and // the household's real package stayed behind under the deleted host. The carry takes the newest row that has // BOTH. // RED-PROOF: drop `AND restic_pw_sha256 != ”` from the carry's retained lookup → the hash-less row is carried → FAILS. func TestR1255_TheCarryTakesTheNewestPackageThatHasAHash(t *testing.T) { s := newTestStore(t) const cust = "T" identity := []byte("sealed-identity-opening-the-repo") carrySetupOldBox(t, s, cust, "T-old", "SHA_REAL", identity) if err := s.DeleteHost("T-old", true); err != nil { t.Fatal(err) } // A newer retained row of the same deleted host: an identity, no repository-password hash. if _, err := s.db.Exec(`INSERT INTO host_escrow_superseded (host_id, blob, key_fingerprint, posture, created_at, restic_pw_sha256, identity_blob, repo_tag) VALUES ('T-old', x'0101', 'fp-x', 'zero_knowledge', '2026-10-19T00:00:00Z', '', x'0202', 'live')`); err != nil { t.Fatal(err) } if err := s.UpsertHost(&Host{HostID: "T-new", CustomerID: cust, APIKey: "k2"}); err != nil { t.Fatal(err) } res, err := s.ReattachRetainedEscrow(cust) if err != nil { t.Fatal(err) } if res.Outcome != CarryDone || res.ResticPwSHA256 != "SHA_REAL" { t.Fatalf("carry = %+v — want the package with the household's hash (SHA_REAL)", res) } b, err := s.GetHostDRBundle("T-new") if err != nil || b == nil || !bytes.Equal(b.IdentityBlob, identity) { t.Fatalf("the new host holds %+v (err %v) — want the household's identity package", b, err) } // The hash-less row stays retained under the deleted host (nothing is thrown away). ret, _ := s.ListSupersededEscrow("T-old") if len(ret) != 1 || ret[0].ResticPwSHA256 != "" { t.Fatalf("retained under the deleted host: %+v — want only the hash-less row", ret) } } // Only hash-less rows (a legacy package: its identity may still seal the password, the hash was never recorded): the // newest with an identity is still carried, as before — never fewer ways back than before the fix. func TestR1255_WithNoHashedPackageTheNewestIdentityIsStillCarried(t *testing.T) { s := newTestStore(t) const cust = "U" carrySetupOldBox(t, s, cust, "U-old", "", []byte("id")) if err := s.DeleteHost("U-old", true); err != nil { t.Fatal(err) } if err := s.UpsertHost(&Host{HostID: "U-new", CustomerID: cust, APIKey: "k2"}); err != nil { t.Fatal(err) } if res, err := s.ReattachRetainedEscrow(cust); err != nil || res.Outcome != CarryDone || res.FromHost != "U-old" { t.Fatalf("carry = %+v err=%v — want the legacy package carried, as before", res, err) } } // H4: two set-asides of one customer can carry the SAME path (the first was abandoned and deleted, then a second // copy was set aside under the free name). The purge deleted every row tagged with a deleted path — so the SECOND // copy's package went too, while that copy is still stored. A row is purged only when it was tagged no later than // the deletion of its path. // RED-PROOF: drop the tagged-before-the-deletion condition from PurgeAbandonedEscrowForCustomer → the second copy's // package is purged → FAILS. func TestR1255_APurgeNeverDeletesAPackageAReusedPathStillNeeds(t *testing.T) { s := newTestStore(t) const cust = "R" const repo = "/home/felhom/R/repo" path := repo + ".orphaned-20261028" if err := s.UpsertHost(&Host{HostID: "R-h", CustomerID: cust, APIKey: "k"}); err != nil { t.Fatal(err) } save := func(sha string) { t.Helper() if _, _, err := s.SaveHostEscrow("R-h", []byte("k-"+sha), "fp", "zk", "2026-10-01T00:00:00Z", sha); err != nil { t.Fatal(err) } if err := s.SaveHostDRBundle("R-h", []byte("id-"+sha)); err != nil { t.Fatal(err) } } save("SHA_1") save("SHA_2") // key 1 retained; its copy set aside: if n, err := s.TagRetainedEscrowSetAside(cust, path); err != nil || n != 1 { t.Fatalf("first set-aside tagged %d err=%v", n, err) } id, err := s.CreateOffsiteAbandon(cust, path, time.Now()) if err != nil { t.Fatal(err) } if err := s.MarkOffsiteAbandonDeleted(id); err != nil { t.Fatal(err) } time.Sleep(1100 * time.Millisecond) // the hub's clock has second precision; a real reuse is hours later save("SHA_3") // key 2 retained; a second copy is set aside under the same (now free) name: if n, err := s.TagRetainedEscrowSetAside(cust, path); err != nil || n != 1 { t.Fatalf("second set-aside tagged %d err=%v", n, err) } n, err := s.PurgeAbandonedEscrowForCustomer(cust) if err != nil || n != 1 { t.Fatalf("purge removed %d err=%v — want exactly the deleted copy's package (key 1)", n, err) } kept := map[string]bool{} rows, _ := s.ListSupersededEscrow("R-h") for _, r := range rows { kept[r.ResticPwSHA256] = true } if !kept["SHA_2"] { t.Fatal("the package of the copy STILL STORED under the reused name was purged — R-1255 H4") } if kept["SHA_1"] { t.Fatal("the deleted copy's package survived the purge") } if left, _ := s.AbandonedEscrowLeft(cust); left != 0 { t.Fatalf("after the purge, packages of deleted copies = %d, want 0 (the reused path's package is not one)", left) } }