diff --git a/controller/internal/appexport/export.go b/controller/internal/appexport/export.go index be9691f..4a9b69f 100644 --- a/controller/internal/appexport/export.go +++ b/controller/internal/appexport/export.go @@ -739,7 +739,9 @@ func (e *Exporter) exportVolumeData(stackName, dataDir string, manifest *Manifes // assertBundleDataComplete is the fail-loud post-export guard (v0.125.0, scenario B): every // manifest-CLAIMED data tar must exist non-empty in the staging tree before packaging. A // mismatch aborts the export — yesterday's outcome ("success" with a hollow bundle) is the -// one this exists to make impossible. +// one this exists to make impossible. v0.130.0 (C6B-F1 cause 3): also refuses a needs_hdd +// bundle that claims NO data at all — the claimed-tar checks pass trivially on 0 claims, which +// is how a discovery gap shipped hollow bundles right past the v0.125.0 net. func assertBundleDataComplete(tmpDir string, manifest *Manifest) error { for _, v := range manifest.VolumeNames { fi, err := os.Stat(filepath.Join(tmpDir, "data", "volumes", v+".tar")) @@ -753,6 +755,13 @@ func assertBundleDataComplete(tmpDir string, manifest *Manifest) error { return fmt.Errorf("bundle assertion: HDD subdir %q is claimed by the manifest but its tar is missing or empty", s) } } + // C6B-F1 cause 3 (v0.130.0): the claimed-tar checks above pass TRIVIALLY when discovery finds + // nothing (0 claims → 0 checks) — exactly how a 4.17 GB app shipped as a 2308-byte config-only + // bundle. A needs_hdd app with NO data of any kind is a hollow bundle by definition; refuse it + // loudly so a future discovery gap can never again ship silently. + if manifest.NeedsHDD && !manifest.HasHDDData && !manifest.HasVolumeData { + return fmt.Errorf("a mentés nem tartalmaz alkalmazásadatot (0 adatkönyvtár, 0 kötet egy adattárolós alkalmazásnál)") + } return nil } diff --git a/controller/internal/appexport/export_additive_test.go b/controller/internal/appexport/export_additive_test.go index 061a5c4..a63a619 100644 --- a/controller/internal/appexport/export_additive_test.go +++ b/controller/internal/appexport/export_additive_test.go @@ -268,3 +268,39 @@ func TestFabRoundTrip_UserdataPlacement(t *testing.T) { t.Fatalf("restored content differs: got %q want %q", got, marker) } } + +// Scenario D (§7) — the anti-hollow net (C6B-F1 cause 3): a needs_hdd app where discovery finds +// NOTHING (mounts absent on disk, no volumes) must FAIL the export with an honest Hungarian +// error — never success-with-a-hollow-bundle. RED-PROOF: remove the needs_hdd&&no-data assertion +// from assertBundleDataComplete → this test fails (the pre-fix silent hollow success). +func TestExport_NeedsHDDNoDataAtAllRefused(t *testing.T) { + srcStack := t.TempDir() + os.WriteFile(filepath.Join(srcStack, "docker-compose.yml"), + []byte("services:\n hdd-app:\n image: alpine\n"), 0644) + + hdd := t.TempDir() + prov := &hddProvider{ + rtProvider: &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: true}, + // the mount does NOT exist on disk and there are no volumes — total discovery blank + mounts: []string{filepath.Join(hdd, "userdata")}, hddPath: hdd, + } + drive := t.TempDir() + e := NewExporter(prov, log.New(io.Discard, "", 0), "test") + if err := e.StartExport(ExportRequest{StackName: "hdd-app", DestDrive: drive}); err != nil { + t.Fatalf("StartExport: %v", err) + } + job := waitJob(t, e) + msg := jobErr(job) + if msg == "" { + t.Fatal("a needs_hdd export with ZERO discovered data must FAIL — got success (the hollow bundle)") + } + if !strings.Contains(msg, "nem tartalmaz alkalmazásadatot") { + t.Errorf("expected the honest Hungarian no-app-data error, got %q", msg) + } + entries, _ := os.ReadDir(ExportDir(drive)) + for _, en := range entries { + if strings.HasSuffix(en.Name(), ".fab") { + t.Fatalf("a hollow bundle was produced: %s", en.Name()) + } + } +}