package main import ( "os" "path/filepath" "testing" "verstak/internal/core/actions" "verstak/internal/core/activity" "verstak/internal/core/files" "verstak/internal/core/nodes" "verstak/internal/core/notes" "verstak/internal/core/plugins" "verstak/internal/core/search" "verstak/internal/core/storage" syncsvc "verstak/internal/core/sync" "verstak/internal/core/templates" "verstak/internal/core/worklog" ) // setupTestApp creates a full App with a temp vault directory for testing. func setupTestApp(t *testing.T) (*App, string) { t.Helper() vaultRoot, err := os.MkdirTemp("", "verstak-test-*") if err != nil { t.Fatalf("mkdir temp: %v", err) } // Init vault structure if err := os.MkdirAll(filepath.Join(vaultRoot, ".verstak"), 0o750); err != nil { t.Fatalf("mkdir .verstak: %v", err) } if err := os.MkdirAll(filepath.Join(vaultRoot, ".verstak", "trash"), 0o750); err != nil { t.Fatalf("mkdir trash: %v", err) } dbPath := filepath.Join(vaultRoot, ".verstak", "vault.db") db, err := storage.Open(dbPath) if err != nil { t.Fatalf("open db: %v", err) } nodeRepo := nodes.NewRepository(db) fileSvc := files.NewService(db, vaultRoot, nodeRepo) noteSvc := notes.NewService(db, vaultRoot, nodeRepo, fileSvc) actionSvc := actions.NewService(db) activitySvc := activity.NewService(db) worklogSvc := worklog.NewService(db) searchSvc := search.NewService(db) pm := plugins.NewManager(vaultRoot) pm.Discover() templatesReg := templates.NewRegistry() if err := templatesReg.LoadSystem(); err != nil { t.Fatalf("load templates: %v", err) } syncSvc := syncsvc.NewService(db, "test-device") app := &App{ db: db, nodes: nodeRepo, files: fileSvc, notes: noteSvc, activity: activitySvc, actions: actionSvc, worklog: worklogSvc, search: searchSvc, plugins: pm, sync: syncSvc, templates: templatesReg, vault: vaultRoot, } t.Cleanup(func() { db.Close() os.RemoveAll(vaultRoot) }) return app, vaultRoot } func TestVaultLayout_CreateProjectTree(t *testing.T) { app, vault := setupTestApp(t) // 1. Create root "Проекты" from folder template proj, err := app.CreateNodeFromTemplate("", "Проекты", "folder.default") if err != nil { t.Fatalf("create Проекты: %v", err) } if proj.FsPath != "Проекты" { t.Errorf("expected fs_path 'Проекты', got %q", proj.FsPath) } if _, err := os.Stat(filepath.Join(vault, "Проекты")); os.IsNotExist(err) { t.Error("expected folder 'Проекты' to exist on disk") } // 2. Create child "Рабочие" inside Проекты work, err := app.CreateNodeFromTemplate(proj.ID, "Рабочие", "folder.default") if err != nil { t.Fatalf("create Рабочие: %v", err) } expectedWorkPath := "Проекты/Рабочие" if work.FsPath != expectedWorkPath { t.Errorf("expected fs_path %q, got %q", expectedWorkPath, work.FsPath) } if _, err := os.Stat(filepath.Join(vault, expectedWorkPath)); os.IsNotExist(err) { t.Error("expected folder 'Проекты/Рабочие' to exist on disk") } // 3. Create project "Разработка серверной" from project template server, err := app.CreateNodeFromTemplate(work.ID, "Разработка серверной", "project.default") if err != nil { t.Fatalf("create Разработка серверной: %v", err) } expectedServerPath := "Проекты/Рабочие/Разработка серверной" if server.FsPath != expectedServerPath { t.Errorf("expected fs_path %q, got %q", expectedServerPath, server.FsPath) } serverFolder := filepath.Join(vault, expectedServerPath) if _, err := os.Stat(serverFolder); os.IsNotExist(err) { t.Error("expected project folder on disk") } // 4. Verify template created Overview.md overviewPath := filepath.Join(serverFolder, "Overview.md") if _, err := os.Stat(overviewPath); os.IsNotExist(err) { t.Log("note: Overview.md from template not created (may not be implemented)") } } func TestVaultLayout_CreateNoteInsideProject(t *testing.T) { app, vault := setupTestApp(t) proj, err := app.CreateNodeFromTemplate("", "Тестовый проект", "project.default") if err != nil { t.Fatalf("create project: %v", err) } // Create a note inside the project noteNode, fileRec, err := app.notes.Create(proj.ID, "Моя заметка", "") if err != nil { t.Fatalf("create note: %v", err) } if noteNode == nil || fileRec == nil { t.Fatal("expected non-nil node and file record") } // Verify the note .md file is inside the project folder expectedPath := filepath.Join(vault, proj.FsPath, "Моя заметка.md") if _, err := os.Stat(expectedPath); os.IsNotExist(err) { // Try the safe-display-name variant expectedPath2 := filepath.Join(vault, proj.FsPath, "Моя_заметка.md") if _, err2 := os.Stat(expectedPath2); os.IsNotExist(err2) { // Show what actually exists entries, _ := os.ReadDir(filepath.Join(vault, proj.FsPath)) t.Errorf("expected note file in project folder, found: %v", listNames(entries)) } } } func TestVaultLayout_CopyFileIntoProject(t *testing.T) { app, vault := setupTestApp(t) proj, err := app.CreateNodeFromTemplate("", "Проект", "folder.default") if err != nil { t.Fatalf("create project: %v", err) } // Create a temp source file srcFile := filepath.Join(vault, "source.txt") if err := os.WriteFile(srcFile, []byte("hello"), 0o640); err != nil { t.Fatalf("write source: %v", err) } // Create a file node inside the project fileNode, err := app.nodes.Create(&proj.ID, "file", "test.txt", 0, "", "") if err != nil { t.Fatalf("create file node: %v", err) } // Copy the file into vault (using parent's fs_path) rec, err := app.files.CopyIntoVault(fileNode.ID, srcFile, proj.FsPath) if err != nil { t.Fatalf("copy into vault: %v", err) } // Verify file lands in project folder expectedPath := filepath.Join(vault, proj.FsPath, "source.txt") if _, err := os.Stat(expectedPath); os.IsNotExist(err) { t.Errorf("expected file at %s, record path = %s", expectedPath, rec.Path) } } func TestVaultLayout_RenameParentUpdatesDescendants(t *testing.T) { app, vault := setupTestApp(t) root, _ := app.CreateNodeFromTemplate("", "Root", "folder.default") child, _ := app.CreateNodeFromTemplate(root.ID, "Child", "folder.default") grandchild, _ := app.CreateNodeFromTemplate(child.ID, "Grandchild", "folder.default") // Rename root if err := app.RenameNode(root.ID, "RenamedRoot"); err != nil { t.Fatalf("rename root: %v", err) } // Verify child fs_path updated childUpdated, _ := app.nodes.GetActive(child.ID) expectedChildPath := "RenamedRoot/Child" if childUpdated.FsPath != expectedChildPath { t.Errorf("expected child fs_path %q, got %q", expectedChildPath, childUpdated.FsPath) } // Verify grandchild fs_path updated gcUpdated, _ := app.nodes.GetActive(grandchild.ID) expectedGCPath := "RenamedRoot/Child/Grandchild" if gcUpdated.FsPath != expectedGCPath { t.Errorf("expected grandchild fs_path %q, got %q", expectedGCPath, gcUpdated.FsPath) } // Verify physical folders if _, err := os.Stat(filepath.Join(vault, expectedChildPath)); os.IsNotExist(err) { t.Error("expected child folder on disk after rename") } if _, err := os.Stat(filepath.Join(vault, expectedGCPath)); os.IsNotExist(err) { t.Error("expected grandchild folder on disk after rename") } // Verify old path no longer exists if _, err := os.Stat(filepath.Join(vault, "Root")); !os.IsNotExist(err) { t.Error("expected old root path to not exist") } } func TestVaultLayout_MoveNode(t *testing.T) { app, vault := setupTestApp(t) folder1, _ := app.CreateNodeFromTemplate("", "Folder1", "folder.default") folder2, _ := app.CreateNodeFromTemplate("", "Folder2", "folder.default") child, _ := app.CreateNodeFromTemplate(folder1.ID, "Child", "folder.default") // Move child from Folder1 to Folder2 if err := app.MoveNode(child.ID, folder2.ID); err != nil { t.Fatalf("move node: %v", err) } moved, _ := app.nodes.GetActive(child.ID) expectedPath := "Folder2/Child" if moved.FsPath != expectedPath { t.Errorf("expected fs_path %q, got %q", expectedPath, moved.FsPath) } if _, err := os.Stat(filepath.Join(vault, expectedPath)); os.IsNotExist(err) { t.Error("expected child folder at new location on disk") } } func TestVaultLayout_DeleteMovesToTrash(t *testing.T) { app, vault := setupTestApp(t) node, _ := app.CreateNodeFromTemplate("", "ToDelete", "folder.default") nodePath := filepath.Join(vault, "ToDelete") if _, err := os.Stat(nodePath); os.IsNotExist(err) { t.Fatal("expected folder to exist before delete") } // Delete the node if err := app.DeleteNode(node.ID); err != nil { t.Fatalf("delete node: %v", err) } // Verify node folder is no longer in original location if _, err := os.Stat(nodePath); !os.IsNotExist(err) { t.Error("expected node folder to be removed from original location") } // Verify trash has the folder trashDir := filepath.Join(vault, ".verstak", "trash") entries, _ := os.ReadDir(trashDir) found := false for _, e := range entries { if e.IsDir() && contains(e.Name(), "ToDelete") { found = true break } } if !found { t.Errorf("expected deleted folder in trash, found: %v", listNames(entries)) } // Verify node is soft-deleted _, err := app.nodes.GetActive(node.ID) if err == nil { t.Error("expected node to be soft-deleted") } } func TestVaultLayout_NameConflict(t *testing.T) { app, vault := setupTestApp(t) node1, err := app.CreateNodeFromTemplate("", "SameName", "folder.default") if err != nil { t.Fatalf("create first: %v", err) } if node1.FsPath != "SameName" { t.Errorf("expected fs_path 'SameName', got %q", node1.FsPath) } node2, err := app.CreateNodeFromTemplate("", "SameName", "folder.default") if err != nil { t.Fatalf("create second: %v", err) } if node2.FsPath == "SameName" { t.Errorf("expected unique fs_path for second node, got same %q", node2.FsPath) } if node2.FsPath == node1.FsPath { t.Error("expected different fs_path for conflicting name") } // Both folders should exist on disk if _, err := os.Stat(filepath.Join(vault, node1.FsPath)); os.IsNotExist(err) { t.Errorf("expected first folder at %s", node1.FsPath) } if _, err := os.Stat(filepath.Join(vault, node2.FsPath)); os.IsNotExist(err) { t.Errorf("expected second folder at %s", node2.FsPath) } } func TestVaultLayout_VaultCheck(t *testing.T) { app, _ := setupTestApp(t) // Create a healthy vault structure app.CreateNodeFromTemplate("", "Healthy", "folder.default") child, _ := app.CreateNodeFromTemplate("", "Child", "folder.default") app.CreateNodeFromTemplate(child.ID, "Grandchild", "folder.default") // Run vault check result, err := app.VaultCheck() if err != nil { t.Fatalf("vault check: %v", err) } if !result.Healthy { t.Errorf("expected healthy vault, got errors: %v", result.Errors) } if result.TotalNodes == 0 { t.Error("expected at least 1 node in check") } } // --- helpers --- func listNames(entries []os.DirEntry) []string { var names []string for _, e := range entries { names = append(names, e.Name()) } return names } func contains(s, substr string) bool { return len(s) >= len(substr) && containsStr(s, substr) } func containsStr(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }