verstak/cmd/verstak-gui/vault_layout_test.go

372 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}