package migration import ( "context" "path/filepath" "testing" "time" "github.com/klarkxy/nekonest-cloud/relay/internal/controlplane" "github.com/klarkxy/nekonest-cloud/relay/internal/tenantbackup" "github.com/klarkxy/nekonest-cloud/relay/internal/tenantfs" "github.com/klarkxy/nekonest-cloud/relay/internal/tenantstore" "github.com/klarkxy/nekonest/relaycore/protocol" ) type fakeRegistry struct { paths tenantfs.Paths err error } func (registry fakeRegistry) Quiesce(string, int64) (tenantfs.Paths, error) { return registry.paths, registry.err } type fakeControl struct { updates []controlplane.MigrationAdvance } func (control *fakeControl) AdvanceMigration(_ context.Context, update controlplane.MigrationAdvance) error { control.updates = append(control.updates, update) return nil } func createSource(t *testing.T, root, tenantID string) tenantfs.Paths { t.Helper() paths, err := tenantfs.Resolve(root, tenantID) if err != nil { t.Fatal(err) } store, err := tenantstore.NewWithTransportMode(paths.Database, string(protocol.TransportSealed)) if err != nil { t.Fatal(err) } if _, err := store.RegisterDevice("host_0123456789abcdef0123456789abcdef", "Host", "linux"); err != nil { t.Fatal(err) } if err := store.Close(); err != nil { t.Fatal(err) } return paths } func TestManagerCreatesFencedBackupAndRestoresTarget(t *testing.T) { const tenantID = "tenant_0123456789abcdef0123456789abcdef" sourceRoot := t.TempDir() paths := createSource(t, sourceRoot, tenantID) backupRoot := t.TempDir() control := &fakeControl{} now := time.Date(2026, 8, 12, 12, 0, 0, 0, time.UTC) source, err := New(Config{ DataRoot: sourceRoot, BackupRoot: backupRoot, Registry: fakeRegistry{paths: paths}, ControlPlane: control, Now: func() time.Time { return now }, }) if err != nil { t.Fatal(err) } assignment := controlplane.MigrationAssignment{ MigrationID: "migration_0123456789abcdef0123456789abcdef", TenantID: tenantID, Role: "source", State: "quiescing", SourceGeneration: 3, TargetGeneration: 4, } if err := source.process(context.Background(), assignment); err != nil { t.Fatal(err) } if len(control.updates) != 1 || control.updates[0].Action != "quiesced" || control.updates[0].BackupRef == "" { t.Fatalf("source update = %#v", control.updates) } backupPath, err := tenantbackup.ResolveReference(backupRoot, control.updates[0].BackupRef) if err != nil || filepath.Base(backupPath) == "" { t.Fatalf("backup path = %q err=%v", backupPath, err) } targetRoot := t.TempDir() targetControl := &fakeControl{} target, err := New(Config{ DataRoot: targetRoot, BackupRoot: backupRoot, Registry: fakeRegistry{}, ControlPlane: targetControl, Now: func() time.Time { return now }, }) if err != nil { t.Fatal(err) } assignment.Role = "target" assignment.State = "copying" assignment.BackupRef = control.updates[0].BackupRef assignment.ManifestSHA256 = control.updates[0].ManifestSHA256 if err := target.process(context.Background(), assignment); err != nil { t.Fatal(err) } if err := target.process(context.Background(), assignment); err != nil { t.Fatal("copy retry was not idempotent:", err) } if len(targetControl.updates) != 2 || targetControl.updates[0].Action != "copied" { t.Fatalf("target updates = %#v", targetControl.updates) } }