R-1252 (P2): a rejected or absent backup block never deletes second-drive copies — reconcile only on a valid classification; a rejected block is said on the app's status and once a day to the operator (backup_run_failures); kept copies stay kept (H2); Remove drops the kept records it deleted (H9). Commit only.
gates / gates (push) Successful in 1m18s

This commit is contained in:
2026-10-24 20:07:16 +02:00
parent 062f9b41d9
commit 518a13592d
16 changed files with 400 additions and 6 deletions
@@ -215,3 +215,26 @@ func TestCatalogBlocks_Fidelity(t *testing.T) {
})
}
}
// R-1252: the second-drive copy must tell a REJECTED block from an absent one (a rejected one keeps every copy and
// alarms; an absent one is quiet). Through the REAL LoadMetadata path. RED-PROOF: drop the BackupRejected assignment
// in LoadMetadata → the rejected fixture reads "" and this fails.
func TestR1252_BackupBlockRejectedNamesTheReason(t *testing.T) {
compose := `services:
app:
volumes:
- ${USERDATA_PATH}/media/audiobooks:/audiobooks
`
bad := newClassifyManager(t, "display_name: App\nbackup:\n userdata:\n - path: media/nope\n class: mandatory\n", compose)
if why := bad.BackupBlockRejected("app"); why == "" {
t.Fatal("a block whose path matches no compose bind must be reported as rejected, with its reason")
}
good := newClassifyManager(t, "display_name: App\nbackup:\n userdata:\n - path: media/audiobooks\n class: mandatory\n", compose)
if why := good.BackupBlockRejected("app"); why != "" {
t.Fatalf("a valid block is not rejected, got %q", why)
}
none := newClassifyManager(t, "display_name: App\n", compose)
if why := none.BackupBlockRejected("app"); why != "" {
t.Fatalf("an absent block is not rejected, got %q", why)
}
}