package app import ( "strings" "testing" "time" "gitea.maximumdirect.net/eric/distributor/internal/config" "gitea.maximumdirect.net/eric/distributor/internal/state" "gitea.maximumdirect.net/eric/distributor/internal/testutil" ) func TestPlanPruneDisabledPolicy(t *testing.T) { document := state.StateDocument{SingleOwner: &state.DistributorState{}} report, err := PlanPrune(document, config.PrunePolicy{}, PrunePlanOptions{ PipelineID: "reports", DestinationID: "archive", }) if err != nil { t.Fatalf("PlanPrune() error = %v", err) } if report.Enabled || report.CheckedCount != 0 || len(report.PrunedOutputs) != 0 { t.Fatalf("report = %#v, want disabled empty plan", report) } } func TestPlanPruneSingleOwnerOutputs(t *testing.T) { now := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC) olderThan := config.Duration(48 * time.Hour) destinationState := pruneSingleOwnerState(now) report, err := PlanPrune(state.StateDocument{SingleOwner: &destinationState}, config.PrunePolicy{ Enabled: true, OlderThan: &olderThan, }, PrunePlanOptions{ PipelineID: "reports", DestinationID: "archive", Now: now, }) if err != nil { t.Fatalf("PlanPrune() error = %v", err) } if got, want := pruneRecordPaths(report.PrunedOutputs), "old.txt"; got != want { t.Fatalf("pruned = %q, want %q", got, want) } if got, want := pruneRecordPaths(report.PreservedOutputs), "fresh.txt"; got != want { t.Fatalf("preserved = %q, want %q", got, want) } } func TestPlanPruneSharedRootCurrentOwnerOnly(t *testing.T) { now := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC) keepLatest := 0 sharedRoot := pruneSharedRootState(now) report, err := PlanPrune(state.StateDocument{SharedRoot: &sharedRoot}, config.PrunePolicy{ Enabled: true, KeepLatest: &keepLatest, }, PrunePlanOptions{ PipelineID: "reports", DestinationID: "archive", Now: now, }) if err != nil { t.Fatalf("PlanPrune() error = %v", err) } if got, want := report.CheckedCount, 1; got != want { t.Fatalf("checked count = %d, want %d", got, want) } if got, want := pruneRecordPaths(report.PrunedOutputs), "archive.txt"; got != want { t.Fatalf("pruned = %q, want %q", got, want) } } func pruneSingleOwnerState(now time.Time) state.DistributorState { manifest := testutil.ValidManifest(testutil.BundleOptions{}) publishedAt := now.Add(-96 * time.Hour) return state.DistributorState{ SchemaVersion: state.SchemaVersion, PipelineID: "reports", DestinationID: "archive", PublishedAt: publishedAt, CreatedAt: publishedAt, UpdatedAt: publishedAt, State: state.StatePolicy{Mode: state.StateModeSingleOwner}, Reconciliation: state.ReconciliationPolicy{Mode: config.ReconciliationModeReplace}, Source: state.SourceState{Manifest: manifest}, DistributorVersion: "test", Outputs: []state.OutputFile{{ Path: "old.txt", Kind: state.OutputKindSource, SourcePath: "report.md", SHA256: manifest.Files[0].SHA256, Size: manifest.Files[0].Size, CreatedAt: now.Add(-96 * time.Hour), UpdatedAt: now.Add(-72 * time.Hour), }, { Path: "fresh.txt", Kind: state.OutputKindSource, SourcePath: "summary.txt", SHA256: manifest.Files[1].SHA256, Size: manifest.Files[1].Size, CreatedAt: now.Add(-24 * time.Hour), UpdatedAt: now.Add(-24 * time.Hour), }}, } } func pruneSharedRootState(now time.Time) state.SharedRootState { manifest := testutil.ValidManifest(testutil.BundleOptions{}) archive := state.CurrentOwnerScope("reports", "archive") html := state.CurrentOwnerScope("reports", "html") return state.SharedRootState{ SchemaVersion: state.SharedRootSchemaVersion, DistributorVersion: "test", CreatedAt: now.Add(-96 * time.Hour), UpdatedAt: now.Add(-24 * time.Hour), State: state.StatePolicy{Mode: state.StateModeSharedRoot}, Owners: []state.OwnerRecord{{ Scope: archive, Reconciliation: state.ReconciliationPolicy{Mode: config.ReconciliationModeReplace}, Source: state.SourceState{Manifest: manifest}, }, { Scope: html, Reconciliation: state.ReconciliationPolicy{Mode: config.ReconciliationModeReplace}, Source: state.SourceState{Manifest: manifest}, }}, Outputs: []state.SharedRootOutputFile{{ Path: "archive.txt", Kind: state.OutputKindSource, SourcePath: "report.md", SHA256: manifest.Files[0].SHA256, Size: manifest.Files[0].Size, Owner: archive, SourceID: manifest.ID, SourceDigest: manifest.Digest, SourceCreated: manifest.Created, CreatedAt: now.Add(-96 * time.Hour), UpdatedAt: now.Add(-72 * time.Hour), }, { Path: "html.txt", Kind: state.OutputKindSource, SourcePath: "summary.txt", SHA256: manifest.Files[1].SHA256, Size: manifest.Files[1].Size, Owner: html, SourceID: manifest.ID, SourceDigest: manifest.Digest, SourceCreated: manifest.Created, CreatedAt: now.Add(-96 * time.Hour), UpdatedAt: now.Add(-72 * time.Hour), }}, } } func pruneRecordPaths(records []PruneOutputRecord) string { paths := make([]string, 0, len(records)) for _, record := range records { paths = append(paths, record.Path) } return strings.Join(paths, ",") }