package backup import ( "io" "log" "path/filepath" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/config" ) // R-634 (v0.265.0). Measured 2026-09-23 on 9202: a whole-box backup pressed 20 s into outline's // deploy stopped it (`compose down`), dumped its half-made volumes and ran a SECOND `compose up -d` // beside the deploy's own — both failed, and the deploy recorded „not deployed". type deployingFake struct { volDumpFakeProvider deploying map[string]bool } func (f *deployingFake) IsDeploying(name string) bool { return f.deploying[name] } // Through the dumpVolumesSafe SEAM, never the real dump: the real one runs `docker run … tar` against a // named volume, which on a developer machine CREATES that volume (it did, once, on DooPlex while this // test was written). The seam is where the stop happens, so a call to it IS the leak. // // COMPANION RED-PROOF (REPORT.md): delete the stackIsDeploying block in runVolumeDumps — the dump // (and with it the stop) is called for outline and this fails with dumped=[outline]. func TestR634_VolumeLegNeverStopsADeployingApp(t *testing.T) { cfg := &config.Config{} cfg.Paths.SystemDataPath = filepath.Join(t.TempDir(), "sys") fake := &deployingFake{ volDumpFakeProvider: volDumpFakeProvider{ stacks: []StackSummary{{Name: "outline"}}, volumes: map[string][]string{"outline": {"outline_outline_data"}}, }, deploying: map[string]bool{"outline": true}, } m := &Manager{cfg: cfg, logger: log.New(io.Discard, "", 0), systemDataPath: cfg.Paths.SystemDataPath, stackProvider: fake} var calls []string m.dumpVolumesSafe = func(name string) error { calls = append(calls, name); return nil } summary, dumped, ok := m.runVolumeDumps() if len(calls) != 0 { t.Fatalf("a DEPLOYING app was stopped and dumped by the backup: dumped=%v (R-634's race)", calls) } if !ok || dumped != 0 { t.Errorf("a deploy in flight is a SKIP, never a failure that pages the operator: ok=%v dumped=%d %v", ok, dumped, summary) } if !containsSummary(summary, "SKIP outline volumes (deploying)") { t.Errorf("the skip must be visible in the run summary, got %v", summary) } // Control: the same app once its deploy has finished IS backed up (the skip is conditional). fake.deploying["outline"] = false m.runVolumeDumps() if len(calls) != 1 || calls[0] != "outline" { t.Errorf("control: a finished app must be dumped as before, dumped=%v", calls) } }