package stacks_test import ( "os" "path/filepath" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // TestDeployPathAgreesWithBackupHelpers is the Phase-1 GATE invariant: the host path an app actually // writes to — resolved from its compose ${HDD_PATH} bind mounts, exactly as deploy does — must be // byte-identical to where the app-data backup helpers look. Single-nested, no doubled felhom-data. // // Before the fix, catalog templates used ${HDD_PATH}/felhom-data/appdata/. On a Model-A in-guest // drive the guest mount /mnt/ ALREADY is the host's /felhom-data namespace, so that extra // segment produced .../felhom-data/felhom-data/... on disk, while NamespaceRoot(drive, true) resolved // the single-nested .../appdata/. Deploy and backup disagreed. The catalog templates now use // ${HDD_PATH}/appdata/; this test locks the two sides together so they can't drift again. func TestDeployPathAgreesWithBackupHelpers(t *testing.T) { const hddPath = "/mnt/felhom-usb" // an enrolled in-guest drive mount (basename is NOT felhom-data) const app = "romm" // A compose volumes block in the FIXED form (no felhom-data segment), mirroring the catalog template. compose := "services:\n" + " romm:\n" + " volumes:\n" + " - ${HDD_PATH}/appdata/romm/library:/romm/library\n" + " - ${HDD_PATH}/appdata/romm/resources:/romm/resources\n" + " - romm_config:/romm/config\n" + "volumes:\n" + " romm_config:\n" dir := t.TempDir() composePath := filepath.Join(dir, "docker-compose.yml") if err := os.WriteFile(composePath, []byte(compose), 0600); err != nil { t.Fatalf("writing temp compose: %v", err) } // Deploy side: where the app's bytes actually land (named volume romm_config is correctly excluded). mounts := stacks.ParseComposeHDDMounts(composePath, hddPath) if len(mounts) != 2 { t.Fatalf("expected 2 HDD bind mounts, got %d: %v", len(mounts), mounts) } // Backup side: the app-data dir the helpers resolve for the same in-guest drive. ns := appbackup.NamespaceRoot(hddPath, true) // in-guest Model-A drive → mount as-is, no felhom-data wantAppDir := filepath.ToSlash(appbackup.AppDataDir(ns, app)) if wantAppDir != "/mnt/felhom-usb/appdata/romm" { t.Fatalf("backup app-data dir unexpected: %q", wantAppDir) } for _, m := range mounts { s := filepath.ToSlash(m) // (1) No doubled felhom-data, and in guest-path space no felhom-data segment at all. if strings.Contains(s, appbackup.FelhomDataDir+"/"+appbackup.FelhomDataDir) { t.Errorf("deploy mount double-nests felhom-data: %q", s) } if n := strings.Count(s, appbackup.FelhomDataDir); n > 0 { t.Errorf("deploy mount unexpectedly carries %d felhom-data segment(s): %q", n, s) } // (2) Every app bind mount lives under the backup helpers' app-data dir — deploy and backup agree. if !strings.HasPrefix(s, wantAppDir+"/") { t.Errorf("deploy mount %q is not under backup app-data dir %q — the two sides diverge", s, wantAppDir) } } }