package infra import ( "os" "path/filepath" "testing" ) // mkVaultDir creates a temporary directory tree for tests. // entries is a list of relative paths to create. // Paths ending in "/" are directories; others are files with placeholder content. func mkVaultDir(t *testing.T, entries []string) string { t.Helper() root := t.TempDir() for _, e := range entries { full := filepath.Join(root, filepath.FromSlash(e)) if e[len(e)-1] == '/' { if err := os.MkdirAll(full, 0o755); err != nil { t.Fatalf("mkVaultDir: mkdir %q: %v", full, err) } } else { if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatalf("mkVaultDir: mkdir parent %q: %v", full, err) } if err := os.WriteFile(full, []byte("test\n"), 0o644); err != nil { t.Fatalf("mkVaultDir: write %q: %v", full, err) } } } return root } func TestVaultLayoutEnsure_DryRun_NoChange(t *testing.T) { root := mkVaultDir(t, []string{ "raw/", "raw/file1.csv", "processed/", }) before := snapshotDir(t, root) report, err := VaultLayoutEnsure(root, true) if err != nil { t.Fatalf("unexpected error: %v", err) } if !report.DryRun { t.Error("DryRun flag not set in report") } after := snapshotDir(t, root) if !mapEqual(before, after) { t.Errorf("dry-run modified disk: before=%v after=%v", before, after) } // Should have planned a migration for raw and processed. if len(report.Migrated) == 0 { t.Error("expected Migrated to be non-empty in dry-run plan") } } func TestVaultLayoutEnsure_FreshDir_CreatesLayout(t *testing.T) { root := mkVaultDir(t, []string{}) // empty vault report, err := VaultLayoutEnsure(root, false) if err != nil { t.Fatalf("unexpected error: %v", err) } // All standard dirs should be created. wantCreated := []string{ "data", "knowledge", filepath.Join("data", "raw"), filepath.Join("data", "processed"), filepath.Join("data", "exports"), filepath.Join("knowledge", "decisions"), filepath.Join("knowledge", "domains"), filepath.Join("knowledge", "models"), filepath.Join("knowledge", "benchmarks"), filepath.Join("knowledge", "test_documents"), } createdSet := toSet(report.Created) for _, w := range wantCreated { if _, ok := createdSet[w]; !ok { t.Errorf("expected Created to contain %q, got %v", w, report.Created) } } // All directories must actually exist on disk. for _, w := range wantCreated { full := filepath.Join(root, w) info, err := os.Stat(full) if err != nil { t.Errorf("expected %q to exist: %v", full, err) continue } if !info.IsDir() { t.Errorf("%q should be a directory", full) } } } func TestVaultLayoutEnsure_LegacyDataLayout_Migrates(t *testing.T) { root := mkVaultDir(t, []string{ "raw/", "raw/file1.parquet", "raw/file2.parquet", "processed/", "processed/clean.csv", "exports/", }) report, err := VaultLayoutEnsure(root, false) if err != nil { t.Fatalf("unexpected error: %v", err) } // raw and processed should appear in Migrated (as dirs, top-level rename). migratedSet := toSet(report.Migrated) for _, pair := range []string{ "raw -> " + filepath.Join("data", "raw"), "processed -> " + filepath.Join("data", "processed"), } { if _, ok := migratedSet[pair]; !ok { t.Errorf("expected Migrated to contain %q, got %v", pair, report.Migrated) } } // Files must have moved. for _, f := range []string{ filepath.Join("data", "raw", "file1.parquet"), filepath.Join("data", "raw", "file2.parquet"), filepath.Join("data", "processed", "clean.csv"), } { if _, err := os.Stat(filepath.Join(root, f)); err != nil { t.Errorf("expected %q to exist after migration: %v", f, err) } } // Old dirs must be gone. for _, d := range []string{"raw", "processed"} { if pathExists(filepath.Join(root, d)) { t.Errorf("expected legacy dir %q to be removed", d) } } } func TestVaultLayoutEnsure_LegacyKnowledgeLayout_Migrates(t *testing.T) { root := mkVaultDir(t, []string{ "decisions/", "decisions/2024-01.md", "models/", "models/ner_v1.pkl", "README.md", }) report, err := VaultLayoutEnsure(root, false) if err != nil { t.Fatalf("unexpected error: %v", err) } // decisions and models should appear in Migrated. migratedSet := toSet(report.Migrated) for _, pair := range []string{ "decisions -> " + filepath.Join("knowledge", "decisions"), "models -> " + filepath.Join("knowledge", "models"), "README.md -> " + filepath.Join("knowledge", "README.md"), } { if _, ok := migratedSet[pair]; !ok { t.Errorf("expected Migrated to contain %q, got %v", pair, report.Migrated) } } // Files must be at new location. for _, f := range []string{ filepath.Join("knowledge", "decisions", "2024-01.md"), filepath.Join("knowledge", "models", "ner_v1.pkl"), filepath.Join("knowledge", "README.md"), } { if _, err := os.Stat(filepath.Join(root, f)); err != nil { t.Errorf("expected %q to exist after migration: %v", f, err) } } } func TestVaultLayoutEnsure_AlreadyMigrated_Idempotent(t *testing.T) { root := mkVaultDir(t, []string{ "data/", "data/raw/", "data/raw/file.csv", "data/processed/", "data/exports/", "knowledge/", "knowledge/decisions/", "knowledge/domains/", "knowledge/models/", "knowledge/benchmarks/", "knowledge/test_documents/", }) report1, err := VaultLayoutEnsure(root, false) if err != nil { t.Fatalf("first run error: %v", err) } if len(report1.Migrated) != 0 { t.Errorf("first run on fully-migrated vault should have no migrations, got %v", report1.Migrated) } before := snapshotDir(t, root) report2, err := VaultLayoutEnsure(root, false) if err != nil { t.Fatalf("second run error: %v", err) } after := snapshotDir(t, root) if !mapEqual(before, after) { t.Error("second run modified disk (not idempotent)") } if len(report2.Migrated) != 0 { t.Errorf("second run should produce no migrations, got %v", report2.Migrated) } if len(report2.AlreadyOK) == 0 { t.Error("second run should report existing dirs as AlreadyOK") } } func TestVaultLayoutEnsure_Mixed_PartialMigration(t *testing.T) { // data/raw already migrated; exports still at root; knowledge dirs in legacy positions. root := mkVaultDir(t, []string{ "data/", "data/raw/", "data/raw/already_here.csv", "exports/", "exports/report.pdf", "decisions/", "decisions/2023-note.md", }) report, err := VaultLayoutEnsure(root, false) if err != nil { t.Fatalf("unexpected error: %v", err) } // data/raw should be AlreadyOK. if !sliceContains(report.AlreadyOK, filepath.Join("data", "raw")) { t.Errorf("data/raw should be AlreadyOK, got AlreadyOK=%v", report.AlreadyOK) } // exports should be migrated. exportsMigrated := false for _, m := range report.Migrated { if m == "exports -> "+filepath.Join("data", "exports") { exportsMigrated = true } } if !exportsMigrated { t.Errorf("exports should be migrated, Migrated=%v", report.Migrated) } // decisions should be migrated. decisionsMigrated := false for _, m := range report.Migrated { if m == "decisions -> "+filepath.Join("knowledge", "decisions") { decisionsMigrated = true } } if !decisionsMigrated { t.Errorf("decisions should be migrated, Migrated=%v", report.Migrated) } } func TestVaultLayoutEnsure_MergeConflict_Errors(t *testing.T) { // Both src (raw/) and dst (data/raw/) exist and have a file with the same name. root := mkVaultDir(t, []string{ "raw/", "raw/collision.csv", "data/", "data/raw/", "data/raw/collision.csv", // same name -> conflict }) _, err := VaultLayoutEnsure(root, false) if err == nil { t.Fatal("expected error for merge conflict, got nil") } if !contains(err.Error(), "conflict") && !contains(err.Error(), "collision.csv") { t.Errorf("error should mention conflict or the file name, got: %v", err) } } func TestVaultLayoutEnsure_UnknownFiles_Skipped(t *testing.T) { root := mkVaultDir(t, []string{ ".git/", "vault_index.db", "my_custom_notes.txt", "raw/", }) report, err := VaultLayoutEnsure(root, false) if err != nil { t.Fatalf("unexpected error: %v", err) } skippedSet := toSet(report.Skipped) for _, name := range []string{".git", "vault_index.db", "my_custom_notes.txt"} { if _, ok := skippedSet[name]; !ok { t.Errorf("expected %q in Skipped, got %v", name, report.Skipped) } } // raw should NOT be in Skipped (it's a known bucket). if _, ok := skippedSet["raw"]; ok { t.Error("raw should not appear in Skipped — it is a known bucket") } } func TestVaultLayoutEnsure_NotADir_Errors(t *testing.T) { t.Run("non-existent path", func(t *testing.T) { _, err := VaultLayoutEnsure("/tmp/does_not_exist_fn_registry_test_xyz", false) if err == nil { t.Fatal("expected error for non-existent path") } }) t.Run("path is a file", func(t *testing.T) { f, err := os.CreateTemp("", "vault_layout_*.txt") if err != nil { t.Fatal(err) } f.Close() defer os.Remove(f.Name()) _, err = VaultLayoutEnsure(f.Name(), false) if err == nil { t.Fatal("expected error when vaultPath is a file, not a dir") } if !contains(err.Error(), "not a directory") { t.Errorf("error should mention 'not a directory', got: %v", err) } }) } // --- helpers --- // snapshotDir returns a map of relative path -> exists for all entries under root. func snapshotDir(t *testing.T, root string) map[string]bool { t.Helper() snap := make(map[string]bool) err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return err } rel, _ := filepath.Rel(root, path) snap[rel] = true return nil }) if err != nil { t.Fatalf("snapshotDir: %v", err) } return snap } func mapEqual(a, b map[string]bool) bool { if len(a) != len(b) { return false } for k := range a { if !b[k] { return false } } return true } func toSet(ss []string) map[string]struct{} { m := make(map[string]struct{}, len(ss)) for _, s := range ss { m[s] = struct{}{} } return m } func sliceContains(ss []string, target string) bool { for _, s := range ss { if s == target { return true } } return false } func contains(s, sub string) bool { return len(s) >= len(sub) && (s == sub || len(sub) == 0 || func() bool { for i := 0; i <= len(s)-len(sub); i++ { if s[i:i+len(sub)] == sub { return true } } return false }()) }