package appexport import ( "io" "log" "os" "path/filepath" "strings" "testing" ) // busyGuard is the app-stop seam with another operation holding the marker. type busyGuard struct{ begins []string } func (g *busyGuard) Begin(opID string, _ []string) error { g.begins = append(g.begins, opID) return nil } func (g *busyGuard) End() {} func (g *busyGuard) ReleaseFailed(...string) {} func (g *busyGuard) BusyWith() string { return "unit-restore:bookstack" } // R-1253: an export that stops its app is refused BEFORE anything starts while another operation holds the marker; // an export that does not stop its app writes no marker and still runs. RED-PROOF (2026-10-24): with the BusyWith check // removed from StartExport, the stop-first export starts (StartExport returns nil) and this fails. func TestR1253_AStopFirstExportWaitsForTheOtherOperation(t *testing.T) { stackDir := filepath.Join(t.TempDir(), "immich") if err := os.MkdirAll(stackDir, 0o755); err != nil { t.Fatal(err) } prov := &rtProvider{stackDir: stackDir, stacksDir: filepath.Dir(stackDir), deployed: true, running: true} e := NewExporter(prov, log.New(io.Discard, "", 0), "test") g := &busyGuard{} e.SetStopGuard(g) err := e.StartExport(ExportRequest{StackName: "immich", DestDrive: t.TempDir(), StopApp: true}) if err == nil { t.Fatal("a stop-first export must be refused while a restore holds the app-stop marker") } if k, ok := err.(interface{ Key() string }); !ok || k.Key() != "err.appexport.another_operation_running" { t.Fatalf("the refusal must carry its household sentence key, got %v", err) } if !strings.Contains(err.Error(), "visszaállítás") { t.Fatalf("the Hungarian sentence must name the cause: %q", err.Error()) } if len(g.begins) != 0 { t.Fatalf("nothing may be marked or stopped, begins=%v", g.begins) } }