package nodes import ( "os" "path/filepath" "testing" "verstak/internal/core/storage" ) func openTestDB(t *testing.T) *storage.DB { t.Helper() dir := t.TempDir() db, err := storage.Open(filepath.Join(dir, "test.db")) if err != nil { t.Fatalf("open db: %v", err) } t.Cleanup(func() { db.Close() }) return db } func nodePtr(s string) *string { return &s } func TestSlugify(t *testing.T) { cases := []struct{ in, want string }{ {"ООО Ромашка", "ооо-ромашка"}, {"My Project!", "my-project"}, {"---", "untitled"}, {"", "untitled"}, {"A B", "a-b"}, {"hello_world", "hello-world"}, } for _, c := range cases { got := Slugify(c.in) if got != c.want { t.Errorf("Slugify(%q) = %q, want %q", c.in, got, c.want) } } } func TestCreateAndGet(t *testing.T) { db := openTestDB(t) repo := NewRepository(db) n, err := repo.Create(nil, TypeCase, "Test Case", 0, "", "") if err != nil { t.Fatalf("Create: %v", err) } if n.ID == "" { t.Fatal("empty ID") } if n.Slug != "test-case" { t.Errorf("slug = %q, want test-case", n.Slug) } got, err := repo.Get(n.ID) if err != nil { t.Fatalf("Get: %v", err) } if got.Title != "Test Case" { t.Errorf("title = %q", got.Title) } // GetActive on a live node. if _, err := repo.GetActive(n.ID); err != nil { t.Errorf("GetActive: %v", err) } } func TestCreateChild(t *testing.T) { db := openTestDB(t) repo := NewRepository(db) parent, err := repo.Create(nil, TypeFolder, "Folder", 0, "", "") if err != nil { t.Fatal(err) } child, err := repo.Create(nodePtr(parent.ID), TypeCase, "Child", 0, "", "") if err != nil { t.Fatal(err) } if child.ParentID == nil || *child.ParentID != parent.ID { t.Errorf("parent_id = %v, want %s", child.ParentID, parent.ID) } } func TestListChildren(t *testing.T) { db := openTestDB(t) repo := NewRepository(db) parent, _ := repo.Create(nil, TypeFolder, "Folder", 0, "", "") repo.Create(nodePtr(parent.ID), TypeCase, "A", 0, "", "") repo.Create(nodePtr(parent.ID), TypeCase, "B", 0, "", "") children, err := repo.ListChildren(parent.ID, false) if err != nil { t.Fatal(err) } if len(children) != 2 { t.Errorf("children = %d, want 2", len(children)) } // Ordered by title. if children[0].Title != "A" || children[1].Title != "B" { t.Errorf("order: %s, %s", children[0].Title, children[1].Title) } } func TestListRoots(t *testing.T) { db := openTestDB(t) repo := NewRepository(db) repo.Create(nil, TypeCase, "One", 0, "", "") repo.Create(nil, TypeCase, "Two", 0, "", "") roots, err := repo.ListRoots(false) if err != nil { t.Fatal(err) } if len(roots) != 2 { t.Errorf("roots = %d, want 2", len(roots)) } } func TestUpdateTitle(t *testing.T) { db := openTestDB(t) repo := NewRepository(db) n, _ := repo.Create(nil, TypeCase, "Old", 0, "", "") if err := repo.UpdateTitle(n.ID, "New Title"); err != nil { t.Fatal(err) } got, _ := repo.Get(n.ID) if got.Title != "New Title" { t.Errorf("title = %q", got.Title) } if got.Revision != 2 { t.Errorf("revision = %d, want 2", got.Revision) } } func TestMove(t *testing.T) { db := openTestDB(t) repo := NewRepository(db) a, _ := repo.Create(nil, TypeFolder, "A", 0, "", "") b, _ := repo.Create(nil, TypeFolder, "B", 0, "", "") child, _ := repo.Create(nodePtr(a.ID), TypeCase, "Child", 0, "", "") // Move child from A to B. if err := repo.Move(child.ID, nodePtr(b.ID), 0); err != nil { t.Fatal(err) } got, _ := repo.Get(child.ID) if got.ParentID == nil || *got.ParentID != b.ID { t.Errorf("parent = %v, want %s", got.ParentID, b.ID) } // Move to root. if err := repo.Move(child.ID, nil, 0); err != nil { t.Fatal(err) } got2, _ := repo.Get(child.ID) if got2.ParentID != nil { t.Errorf("parent = %v, want nil", got2.ParentID) } } func TestSoftDelete(t *testing.T) { db := openTestDB(t) repo := NewRepository(db) n, _ := repo.Create(nil, TypeCase, "To Delete", 0, "", "") if err := repo.SoftDelete(n.ID); err != nil { t.Fatal(err) } if _, err := repo.GetActive(n.ID); err != ErrNotFound { t.Errorf("GetActive returned %v, want ErrNotFound", err) } // Should still be fetchable via Get. got, err := repo.Get(n.ID) if err != nil { t.Fatal(err) } if !got.IsDeleted() { t.Error("node should be deleted") } // ListChildren without includeDeleted must skip it. parent, _ := repo.Create(nil, TypeFolder, "P", 0, "", "") child, _ := repo.Create(nodePtr(parent.ID), TypeCase, "Kid", 0, "", "") repo.SoftDelete(child.ID) kids, _ := repo.ListChildren(parent.ID, false) if len(kids) != 0 { t.Errorf("expected 0 children, got %d", len(kids)) } kidsAll, _ := repo.ListChildren(parent.ID, true) if len(kidsAll) != 1 { t.Errorf("expected 1 child with deleted, got %d", len(kidsAll)) } } func TestMetaKV(t *testing.T) { db := openTestDB(t) repo := NewRepository(db) n, _ := repo.Create(nil, TypeCase, "M", 0, "", "") if err := repo.MetaSet(n.ID, "status", "active"); err != nil { t.Fatal(err) } v, ok, err := repo.MetaGet(n.ID, "status") if err != nil { t.Fatal(err) } if !ok || v != "active" { t.Errorf("meta got %q, ok=%v", v, ok) } // Overwrite. repo.MetaSet(n.ID, "status", "archived") v, _, _ = repo.MetaGet(n.ID, "status") if v != "archived" { t.Errorf("meta = %q, want archived", v) } list, err := repo.MetaList(n.ID) if err != nil { t.Fatal(err) } if len(list) != 1 { t.Errorf("meta count = %d", len(list)) } } func TestNotFound(t *testing.T) { db := openTestDB(t) repo := NewRepository(db) if _, err := repo.Get("nonexistent"); err != ErrNotFound { t.Errorf("Get returned %v, want ErrNotFound", err) } if err := repo.UpdateTitle("nonexistent", "x"); err != ErrNotFound { t.Errorf("UpdateTitle returned %v, want ErrNotFound", err) } if err := repo.SoftDelete("nonexistent"); err != ErrNotFound { t.Errorf("SoftDelete returned %v, want ErrNotFound", err) } if err := repo.Move("nonexistent", nil, 0); err != ErrNotFound { t.Errorf("Move returned %v, want ErrNotFound", err) } } func TestInitEndToEnd(t *testing.T) { // Integration-like test: open a temp vault through storage, create and // read a node — proves the migration + repository stack works. dir := t.TempDir() dbPath := filepath.Join(dir, "index.db") db, err := storage.Open(dbPath) if err != nil { t.Fatalf("open db: %v", err) } defer db.Close() repo := NewRepository(db) n, err := repo.Create(nil, TypeCase, "Integration Case", 0, "", "") if err != nil { t.Fatal(err) } got, err := repo.Get(n.ID) if err != nil { t.Fatal(err) } if got.Title != "Integration Case" { t.Errorf("title = %q", got.Title) } } // Silence "os" import; keep unused-reference guard from breaking. var _ = os.Args