Stichprobenbasierter Scrub-Job: nimmt BAK-05s existing_in_storage, priorisiert nach eigenem scrub_state.last_scrubbed_at (nicht file_revisions.created_at, sonst kein echtes Rotationsverhalten), prueft Inhalt per SHA-256 gegen file_revisions.checksum_sha256. Meldung ueber echten dauerhaften /metrics-Endpunkt (Pull-Modell, OPS-03 scrapt, kein Push), Counter monoton steigend. Real registriert in Core metrics_sources, End-zu-Ende ueber OPS-03-Aggregator bestaetigt, realer Befund-Durchlauf mit absichtlich falscher Pruefsumme durchgefuehrt.
96 lines
3.3 KiB
Go
96 lines
3.3 KiB
Go
package scrub
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/archive/internal/reconcile"
|
|
)
|
|
|
|
var now = time.Date(2026, 8, 29, 12, 0, 0, 0, time.UTC)
|
|
|
|
// TestSample_PrioritizesNeverScrubbedAndOldest ist der Nachweis fuer das
|
|
// GoBD-Akzeptanzkriterium: nie geprueft ODER am laengsten nicht geprueft
|
|
// kommt zuerst, nicht bloss alphabetisch nach StorageKey.
|
|
func TestSample_PrioritizesNeverScrubbedAndOldest(t *testing.T) {
|
|
existing := []reconcile.Finding{
|
|
{StorageKey: "documents/a/revisions/r1"}, // vor 1 tag geprueft
|
|
{StorageKey: "documents/b/revisions/r1"}, // nie geprueft
|
|
{StorageKey: "documents/c/revisions/r1"}, // vor 30 tagen geprueft (aeltest)
|
|
}
|
|
lastScrubbed := map[string]time.Time{
|
|
"documents/a/revisions/r1": now.Add(-24 * time.Hour),
|
|
"documents/c/revisions/r1": now.Add(-30 * 24 * time.Hour),
|
|
}
|
|
|
|
got := Sample(existing, lastScrubbed, time.Hour, 2, now)
|
|
|
|
if len(got) != 2 {
|
|
t.Fatalf("erwartet 2 kandidaten, habe %d: %+v", len(got), got)
|
|
}
|
|
// "nie geprueft" (b) zaehlt als aeltestmoeglich, kommt vor "vor 30 tagen" (c).
|
|
if got[0].StorageKey != "documents/b/revisions/r1" || got[1].StorageKey != "documents/c/revisions/r1" {
|
|
t.Fatalf("falsche prioritaet, want [b, c], habe %+v", got)
|
|
}
|
|
}
|
|
|
|
// TestSample_RespectsCooldown ist der Nachweis, dass kuerzlich gepruefte
|
|
// Objekte NICHT erneut ausgewaehlt werden — sonst wuerde dieselbe Gruppe
|
|
// dauernd gescrubbt (genau der Fehler, den die Alt-Priorisierung
|
|
// verhindern soll).
|
|
func TestSample_RespectsCooldown(t *testing.T) {
|
|
existing := []reconcile.Finding{
|
|
{StorageKey: "documents/a/revisions/r1"},
|
|
{StorageKey: "documents/b/revisions/r1"},
|
|
}
|
|
lastScrubbed := map[string]time.Time{
|
|
"documents/a/revisions/r1": now.Add(-1 * time.Hour), // innerhalb cooldown
|
|
}
|
|
|
|
got := Sample(existing, lastScrubbed, 24*time.Hour, 10, now)
|
|
|
|
if len(got) != 1 || got[0].StorageKey != "documents/b/revisions/r1" {
|
|
t.Fatalf("erwartet nur b (a innerhalb cooldown), habe %+v", got)
|
|
}
|
|
}
|
|
|
|
// TestSample_LimitsToSampleSize ist der Nachweis, dass die
|
|
// Stichprobengroesse tatsaechlich begrenzt (kein Voll-Scrub jeden Lauf).
|
|
func TestSample_LimitsToSampleSize(t *testing.T) {
|
|
existing := []reconcile.Finding{
|
|
{StorageKey: "documents/a/revisions/r1"},
|
|
{StorageKey: "documents/b/revisions/r1"},
|
|
{StorageKey: "documents/c/revisions/r1"},
|
|
}
|
|
|
|
got := Sample(existing, map[string]time.Time{}, time.Hour, 1, now)
|
|
|
|
if len(got) != 1 {
|
|
t.Fatalf("erwartet genau 1 kandidat, habe %d", len(got))
|
|
}
|
|
}
|
|
|
|
// TestSample_DeterministicForIdenticalInput ist der Nachweis, dass zwei
|
|
// Laeufe mit identischer Eingabe dieselbe Reihenfolge liefern (kein
|
|
// Zufall im Sampling).
|
|
func TestSample_DeterministicForIdenticalInput(t *testing.T) {
|
|
existing := []reconcile.Finding{
|
|
{StorageKey: "documents/a/revisions/r1"},
|
|
{StorageKey: "documents/b/revisions/r1"},
|
|
{StorageKey: "documents/c/revisions/r1"},
|
|
}
|
|
lastScrubbed := map[string]time.Time{}
|
|
|
|
first := Sample(existing, lastScrubbed, time.Hour, 2, now)
|
|
second := Sample(existing, lastScrubbed, time.Hour, 2, now)
|
|
|
|
if len(first) != len(second) {
|
|
t.Fatal("unterschiedliche anzahl zwischen zwei laeufen mit identischer eingabe")
|
|
}
|
|
for i := range first {
|
|
if first[i].StorageKey != second[i].StorageKey {
|
|
t.Fatalf("reihenfolge nicht deterministisch: lauf1=%+v lauf2=%+v", first, second)
|
|
}
|
|
}
|
|
}
|