package publish import ( "context" "encoding/json" "strings" "testing" "time" "gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/config" "gitea.maximumdirect.net/eric/distributor/internal/state" "gitea.maximumdirect.net/eric/distributor/internal/storage" "gitea.maximumdirect.net/eric/distributor/internal/storage/fake" "gitea.maximumdirect.net/eric/distributor/internal/testutil" ) func TestBuildSharedRootTreatsAbsentOwnerAsPublishable(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}}, }) destinationBackend := fake.New() writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, false)) req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace) plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if plan.Action != ActionPublishNew { t.Fatalf("plan action = %s, want publish_new", plan.Action) } if plan.OwnerScope != state.CurrentOwnerScope("reports", "archive") { t.Fatalf("owner scope = %#v, want reports/archive", plan.OwnerScope) } if got, want := len(plan.OtherOwnerOutputs), 1; got != want { t.Fatalf("other owner output count = %d, want %d", got, want) } if got, want := len(plan.OwnerOutputsToWrite), 1; got != want { t.Fatalf("owner output write count = %d, want %d", got, want) } if len(plan.OwnerOutputsToDelete) != 0 || len(plan.RetainedOwnerOutputs) != 0 { t.Fatalf("delete=%#v retained=%#v, want none", plan.OwnerOutputsToDelete, plan.RetainedOwnerOutputs) } } func TestBuildSharedRootReplaceDeletesOnlyCurrentOwnerOmittedOutputs(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\nNew.\n"}}, }) destinationBackend := fake.New() writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, true)) req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace) plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if plan.Action != ActionReplaceOlder { t.Fatalf("plan action = %s, want replace_older", plan.Action) } if got, want := sharedRootOutputPathList(plan.OwnerOutputsToDelete), "old.md"; got != want { t.Fatalf("owner outputs to delete = %q, want %q", got, want) } if got, want := sharedRootOutputPathList(plan.OtherOwnerOutputs), "other/report.md"; got != want { t.Fatalf("other owner outputs = %q, want %q", got, want) } if len(plan.RetainedOwnerOutputs) != 0 { t.Fatalf("retained owner outputs = %#v, want none", plan.RetainedOwnerOutputs) } } func TestBuildSharedRootMergeRetainsCurrentOwnerOmittedOutputs(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\nNew.\n"}}, }) destinationBackend := fake.New() writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, true)) req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeMerge) plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if got, want := sharedRootOutputPathList(plan.RetainedOwnerOutputs), "old.md"; got != want { t.Fatalf("retained owner outputs = %q, want %q", got, want) } if len(plan.OwnerOutputsToDelete) != 0 { t.Fatalf("owner outputs to delete = %#v, want none", plan.OwnerOutputsToDelete) } } func TestBuildSharedRootRejectsOtherOwnerPathConflict(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}}, }) destinationBackend := fake.New() sharedRoot := sharedRootStateWithOwners(t, sourceBundle.Manifest, false) sharedRoot.Outputs[0].Path = "report.md" writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRoot) req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace) plan, err := Build(context.Background(), req) if err == nil || !strings.Contains(err.Error(), "fail_conflict") { t.Fatalf("Build() error = %v, want fail_conflict", err) } if plan.Action != ActionFailConflict { t.Fatalf("plan action = %s, want fail_conflict", plan.Action) } } func TestBuildSharedRootPlansOutputTakeoverByPolicy(t *testing.T) { tests := []struct { name string takeover config.TakeoverPolicy ownerScope state.OwnerScope sameSource bool wantAction Action wantErr string }{ { name: "default same pipeline allows different destination", takeover: config.TakeoverPolicy{}, ownerScope: state.CurrentOwnerScope("reports", "web"), wantAction: ActionReplaceTakeover, }, { name: "same pipeline refuses different pipeline", takeover: config.TakeoverPolicy{Mode: config.TakeoverModeSamePipeline}, ownerScope: state.CurrentOwnerScope("other", "archive"), wantErr: "fail_conflict", }, { name: "same source allows different pipeline", takeover: config.TakeoverPolicy{Mode: config.TakeoverModeSameSource}, ownerScope: state.CurrentOwnerScope("other", "archive"), sameSource: true, wantAction: ActionReplaceTakeover, }, { name: "same source refuses different source", takeover: config.TakeoverPolicy{Mode: config.TakeoverModeSameSource}, ownerScope: state.CurrentOwnerScope("reports", "web"), wantErr: "fail_conflict", }, { name: "any managed allows different pipeline", takeover: config.TakeoverPolicy{Mode: config.TakeoverModeAnyManaged}, ownerScope: state.CurrentOwnerScope("other", "archive"), wantAction: ActionReplaceTakeover, }, { name: "never refuses same pipeline", takeover: config.TakeoverPolicy{Mode: config.TakeoverModeNever}, ownerScope: state.CurrentOwnerScope("reports", "web"), wantErr: "fail_conflict", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}}, }) destinationBackend := fake.New() sharedRoot := sharedRootStateWithOwners(t, sourceBundle.Manifest, false) ownerManifest := sharedRoot.Owners[0].Source.Manifest if tt.sameSource { ownerManifest = sourceBundle.Manifest } setSharedRootOwnerOutput(t, &sharedRoot, 0, tt.ownerScope, ownerManifest, "report.md") writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRoot) req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace) req.Takeover = tt.takeover plan, err := Build(context.Background(), req) if tt.wantErr != "" { if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("Build() error = %v, want %q", err, tt.wantErr) } if plan.Action != ActionFailConflict { t.Fatalf("plan action = %s, want fail_conflict", plan.Action) } return } if err != nil { t.Fatalf("Build() error = %v", err) } if plan.Action != tt.wantAction { t.Fatalf("plan action = %s, want %s", plan.Action, tt.wantAction) } if got, want := sharedRootOutputPathList(plan.TakenOverOwnerOutputs), "report.md"; got != want { t.Fatalf("taken over outputs = %q, want %q", got, want) } }) } } func TestBuildSharedRootRejectsUnmanagedPathCollision(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}}, }) destinationBackend := fake.New() writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, false)) testutil.WriteFakeFile(t, destinationBackend, "bundle/report.md", "unmanaged") req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace) plan, err := Build(context.Background(), req) if err == nil || !strings.Contains(err.Error(), "fail_unmanaged") { t.Fatalf("Build() error = %v, want fail_unmanaged", err) } if plan.Action != ActionFailUnmanaged { t.Fatalf("plan action = %s, want fail_unmanaged", plan.Action) } } func TestExecuteSharedRootPublishesOwnerAndPreservesOtherOwners(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}}, }) destinationBackend := fake.New() writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, false)) req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace) plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "bundle/report.md", "# Report\n") testutil.AssertFakeFile(t, destinationBackend, "bundle/other/report.md", "old") destinationState := readFakeSharedRootState(t, destinationBackend, "bundle") if got, want := len(destinationState.Owners), 2; got != want { t.Fatalf("owner count = %d, want %d", got, want) } if got, want := destinationState.State.Mode, state.StateModeSharedRoot; got != want { t.Fatalf("state mode = %q, want %q", got, want) } if _, ok := destinationState.Owner(state.CurrentOwnerScope("other", "archive")); !ok { t.Fatal("other owner missing from shared-root state") } if _, ok := destinationState.Owner(state.CurrentOwnerScope("reports", "archive")); !ok { t.Fatal("current owner missing from shared-root state") } if got, want := strings.Join(destinationState.AllManagedOutputPaths(), ","), "other/report.md,report.md"; got != want { t.Fatalf("managed paths = %q, want %q", got, want) } } func TestExecuteSharedRootReplaceDeletesOnlyCurrentOwnerOmittedOutputs(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\nNew.\n"}}, }) destinationBackend := fake.New() existing := sharedRootStateWithOwners(t, sourceBundle.Manifest, true) writeFakeSharedRootState(t, destinationBackend, "bundle", existing) req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace) plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "bundle/report.md", "# Report\nNew.\n") testutil.AssertFakeMissing(t, destinationBackend, "bundle/old.md") testutil.AssertFakeFile(t, destinationBackend, "bundle/other/report.md", "old") destinationState := readFakeSharedRootState(t, destinationBackend, "bundle") if got, want := strings.Join(destinationState.AllManagedOutputPaths(), ","), "other/report.md,report.md"; got != want { t.Fatalf("managed paths = %q, want %q", got, want) } if !destinationState.CreatedAt.Equal(existing.CreatedAt) { t.Fatalf("created_at = %s, want %s", destinationState.CreatedAt, existing.CreatedAt) } output, ok := findSharedRootOutputForTest(destinationState.Outputs, "report.md") if !ok { t.Fatal("report.md missing from shared-root outputs") } if !output.CreatedAt.Equal(existing.Outputs[1].CreatedAt) || !output.UpdatedAt.After(existing.Outputs[1].UpdatedAt) { t.Fatalf("report.md timestamps = created:%s updated:%s, want preserved created and newer updated", output.CreatedAt, output.UpdatedAt) } } func TestExecuteSharedRootTakeoverReassignsPathAndPreservesUnrelatedOutputs(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\nNew.\n"}}, }) destinationBackend := fake.New() sharedRoot := sharedRootStateWithOwners(t, sourceBundle.Manifest, false) previousScope := state.CurrentOwnerScope("reports", "web") setSharedRootOwnerOutput(t, &sharedRoot, 0, previousScope, sharedRoot.Owners[0].Source.Manifest, "report.md") keepOutput := sharedRoot.Outputs[0] keepOutput.Path = "web/keep.md" keepOutput.SourcePath = "web/keep.md" sharedRoot.Outputs = append(sharedRoot.Outputs, keepOutput) writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRoot) req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace) plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if plan.Action != ActionReplaceTakeover { t.Fatalf("plan action = %s, want %s", plan.Action, ActionReplaceTakeover) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "bundle/report.md", "# Report\nNew.\n") testutil.AssertFakeFile(t, destinationBackend, "bundle/web/keep.md", "old") destinationState := readFakeSharedRootState(t, destinationBackend, "bundle") reportOutput, ok := findSharedRootOutputForTest(destinationState.Outputs, "report.md") if !ok { t.Fatal("report.md missing from shared-root outputs") } if reportOutput.Owner != state.CurrentOwnerScope("reports", "archive") { t.Fatalf("report.md owner = %#v, want reports/archive", reportOutput.Owner) } keep, ok := findSharedRootOutputForTest(destinationState.Outputs, "web/keep.md") if !ok { t.Fatal("web/keep.md missing from shared-root outputs") } if keep.Owner != previousScope { t.Fatalf("web/keep.md owner = %#v, want reports/web", keep.Owner) } } func TestExecuteSharedRootTakeoverMergeDoesNotRetainOldSourceOutputs(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\nNew.\n"}}, }) destinationBackend := fake.New() sharedRoot := sharedRootStateWithOwners(t, sourceBundle.Manifest, false) previousScope := state.CurrentOwnerScope("reports", "web") setSharedRootOwnerOutput(t, &sharedRoot, 0, previousScope, sharedRoot.Owners[0].Source.Manifest, "report.md") createdAt := sharedRoot.CreatedAt oldManifest := sourceBundle.Manifest oldManifest.ID = "old.source" oldManifest.Created = oldManifest.Created.Add(-time.Hour) oldManifest.Files = []bundle.ManifestFile{{ Path: "old.md", SHA256: bundle.FileDigest([]byte("old\n")), Size: int64(len("old\n")), }} oldManifest.Digest = bundle.BundleDigest(oldManifest.Files) currentScope := state.CurrentOwnerScope("reports", "archive") sharedRoot.Owners = append(sharedRoot.Owners, state.OwnerRecord{ Scope: currentScope, Reconciliation: state.ReconciliationPolicy{Mode: config.ReconciliationModeMerge}, Source: state.SourceState{Manifest: oldManifest}, }) sharedRoot.Outputs = append(sharedRoot.Outputs, state.SharedRootOutputFile{ Path: "old.md", Kind: state.OutputKindSource, SourcePath: "old.md", SHA256: oldManifest.Files[0].SHA256, Size: oldManifest.Files[0].Size, Owner: currentScope, SourceID: oldManifest.ID, SourceDigest: oldManifest.Digest, SourceCreated: oldManifest.Created, CreatedAt: createdAt, UpdatedAt: createdAt, }) writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRoot) req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeMerge) req.PathMapping = config.PathMappingFixed plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if plan.Action != ActionReplaceTakeover { t.Fatalf("plan action = %s, want %s", plan.Action, ActionReplaceTakeover) } if got, want := sharedRootOutputPathList(plan.OwnerOutputsToDelete), "old.md"; got != want { t.Fatalf("owner outputs to delete = %q, want %q", got, want) } if len(plan.RetainedOwnerOutputs) != 0 { t.Fatalf("retained owner outputs = %#v, want none", plan.RetainedOwnerOutputs) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "bundle/report.md", "# Report\nNew.\n") testutil.AssertFakeMissing(t, destinationBackend, "bundle/old.md") destinationState := readFakeSharedRootState(t, destinationBackend, "bundle") if got, want := strings.Join(destinationState.AllManagedOutputPaths(), ","), "report.md"; got != want { t.Fatalf("managed paths = %q, want %q", got, want) } } func TestExecuteSharedRootMergeRetainsCurrentOwnerOmittedOutputs(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\nNew.\n"}}, }) destinationBackend := fake.New() writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, true)) req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeMerge) plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "bundle/report.md", "# Report\nNew.\n") testutil.AssertFakeFile(t, destinationBackend, "bundle/old.md", "old") testutil.AssertFakeFile(t, destinationBackend, "bundle/other/report.md", "old") destinationState := readFakeSharedRootState(t, destinationBackend, "bundle") if got, want := strings.Join(destinationState.AllManagedOutputPaths(), ","), "other/report.md,report.md,old.md"; got != want { t.Fatalf("managed paths = %q, want %q", got, want) } } func TestExecuteSharedRootForceReplaceDeletesOnlyBundlePath(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}}, }) destinationBackend := fake.New() testutil.WriteFakeFile(t, destinationBackend, "bundle/unmanaged.txt", "unmanaged") testutil.WriteFakeFile(t, destinationBackend, "bundle/nested/old.txt", "old") testutil.WriteFakeFile(t, destinationBackend, "bundle-sibling/keep.txt", "keep") req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace) req.Force = true plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if plan.Action != ActionForceReplace { t.Fatalf("plan action = %s, want force_replace", plan.Action) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "bundle/report.md", "# Report\n") testutil.AssertFakeMissing(t, destinationBackend, "bundle/unmanaged.txt") testutil.AssertFakeMissing(t, destinationBackend, "bundle/nested/old.txt") testutil.AssertFakeFile(t, destinationBackend, "bundle-sibling/keep.txt", "keep") destinationState := readFakeSharedRootState(t, destinationBackend, "bundle") if got, want := len(destinationState.Owners), 1; got != want { t.Fatalf("owner count = %d, want %d", got, want) } } func sharedRootRequest(sourceBackend, destinationBackend *fake.Backend, sourceBundle bundle.Bundle, reconciliationMode string) Request { return Request{ PipelineID: "reports", DestinationID: "archive", SourceBundle: sourceBundle, SourceBackend: sourceBackend, DestinationBackend: destinationBackend, DestinationBundlePath: sourceBundle.RootRelativePath, Publish: config.PublishPolicy{Source: true}, State: config.StatePolicy{Mode: config.StateModeSharedRoot}, Reconciliation: config.ReconciliationPolicy{Mode: reconciliationMode}, Transfer: defaultTransfer(), DistributorVersion: "test", } } func writeFakeSharedRootState(t *testing.T, backend *fake.Backend, relative string, sharedRoot state.SharedRootState) { t.Helper() data, err := json.MarshalIndent(sharedRoot, "", " ") if err != nil { t.Fatalf("marshal shared-root state: %v", err) } statePath, err := storage.StatePath(relative) if err != nil { t.Fatalf("state path: %v", err) } testutil.WriteFakeFile(t, backend, statePath, string(append(data, '\n'))) for _, output := range sharedRoot.Outputs { path, err := storage.Join(relative, output.Path) if err != nil { t.Fatalf("join output path: %v", err) } testutil.WriteFakeFile(t, backend, path, "old") } } func readFakeSharedRootState(t *testing.T, backend *fake.Backend, relative string) state.SharedRootState { t.Helper() statePath, err := storage.StatePath(relative) if err != nil { t.Fatalf("state path: %v", err) } data, err := backend.ReadFile(context.Background(), statePath) if err != nil { t.Fatalf("read shared-root state: %v", err) } destinationState, err := state.ParseSharedRoot(data) if err != nil { t.Fatalf("parse shared-root state: %v", err) } return destinationState } func findSharedRootOutputForTest(outputs []state.SharedRootOutputFile, path string) (state.SharedRootOutputFile, bool) { for _, output := range outputs { if output.Path == path { return output, true } } return state.SharedRootOutputFile{}, false } func setSharedRootOwnerOutput(t *testing.T, sharedRoot *state.SharedRootState, index int, scope state.OwnerScope, manifest bundle.Manifest, path string) { t.Helper() sharedRoot.Owners[index].Scope = scope sharedRoot.Owners[index].Source = state.SourceState{Manifest: manifest} sourcePath := path if len(manifest.Files) > 0 { sourcePath = manifest.Files[0].Path } sharedRoot.Outputs[index].Path = path sharedRoot.Outputs[index].SourcePath = sourcePath sharedRoot.Outputs[index].Owner = scope sharedRoot.Outputs[index].SourceID = manifest.ID sharedRoot.Outputs[index].SourceDigest = manifest.Digest sharedRoot.Outputs[index].SourceCreated = manifest.Created if len(manifest.Files) > 0 { sharedRoot.Outputs[index].SHA256 = manifest.Files[0].SHA256 sharedRoot.Outputs[index].Size = manifest.Files[0].Size } } func sharedRootStateWithOwners(t *testing.T, current bundle.Manifest, includeCurrent bool) state.SharedRootState { t.Helper() createdAt := time.Date(2026, 5, 30, 11, 12, 0, 0, time.UTC) otherManifest := testutil.ValidManifest(testutil.BundleOptions{ ID: "other.source", Files: []testutil.SourceFile{{Path: "other/report.md", Data: "# Other\n"}}, }) sharedRoot := state.SharedRootState{ SchemaVersion: state.SharedRootSchemaVersion, DistributorVersion: "test", CreatedAt: createdAt, UpdatedAt: createdAt, State: state.StatePolicy{Mode: state.StateModeSharedRoot}, Owners: []state.OwnerRecord{{ Scope: state.CurrentOwnerScope("other", "archive"), Reconciliation: state.ReconciliationPolicy{Mode: config.ReconciliationModeReplace}, Source: state.SourceState{Manifest: otherManifest}, }}, Outputs: []state.SharedRootOutputFile{{ Path: "other/report.md", Kind: state.OutputKindSource, SourcePath: "other/report.md", SHA256: otherManifest.Files[0].SHA256, Size: otherManifest.Files[0].Size, Owner: state.CurrentOwnerScope("other", "archive"), SourceID: otherManifest.ID, SourceDigest: otherManifest.Digest, SourceCreated: otherManifest.Created, CreatedAt: createdAt, UpdatedAt: createdAt, }}, } if !includeCurrent { return sharedRoot } older := current older.Created = older.Created.Add(-time.Hour) older.Files = append([]bundle.ManifestFile(nil), current.Files...) older.Files = append(older.Files, bundle.ManifestFile{ Path: "old.md", SHA256: bundle.FileDigest([]byte("old\n")), Size: int64(len("old\n")), }) older.Digest = bundle.BundleDigest(older.Files) scope := state.CurrentOwnerScope("reports", "archive") sharedRoot.Owners = append(sharedRoot.Owners, state.OwnerRecord{ Scope: scope, Reconciliation: state.ReconciliationPolicy{Mode: config.ReconciliationModeReplace}, Source: state.SourceState{Manifest: older}, }) for _, file := range older.Files { sharedRoot.Outputs = append(sharedRoot.Outputs, state.SharedRootOutputFile{ Path: file.Path, Kind: state.OutputKindSource, SourcePath: file.Path, SHA256: file.SHA256, Size: file.Size, Owner: scope, SourceID: older.ID, SourceDigest: older.Digest, SourceCreated: older.Created, CreatedAt: createdAt, UpdatedAt: createdAt, }) } return sharedRoot } func sharedRootOutputPathList(outputs []state.SharedRootOutputFile) string { paths := make([]string, 0, len(outputs)) for _, output := range outputs { paths = append(paths, output.Path) } return strings.Join(paths, ",") }