package workspace import ( "os" "path/filepath" "testing" ) func TestLoad_DefaultRootNode(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") verstakDir := filepath.Join(vaultDir, ".verstak") os.MkdirAll(verstakDir, 0o755) m := NewManager(vaultDir) if err := m.Load(); err != nil { t.Fatalf("Load: %v", err) } tree := m.GetTree() if len(tree.Nodes) != 1 { t.Fatalf("expected 1 root node, got %d", len(tree.Nodes)) } if tree.Nodes[0].Type != TypeSpace { t.Errorf("root type: got %s, want %s", tree.Nodes[0].Type, TypeSpace) } if tree.Nodes[0].Title != "My Workspace" { t.Errorf("root title: got %q, want %q", tree.Nodes[0].Title, "My Workspace") } if tree.CurrentNodeID != tree.Nodes[0].ID { t.Errorf("current node should be root") } } func TestCreateNode_Case(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") verstakDir := filepath.Join(vaultDir, ".verstak") os.MkdirAll(verstakDir, 0o755) m := NewManager(vaultDir) if err := m.Load(); err != nil { t.Fatalf("Load: %v", err) } rootID := m.GetTree().Nodes[0].ID node, err := m.CreateNode(rootID, TypeCase, "Test Case") if err != nil { t.Fatalf("CreateNode: %v", err) } if node.Type != TypeCase { t.Errorf("type: got %s, want %s", node.Type, TypeCase) } if node.Title != "Test Case" { t.Errorf("title: got %q, want %q", node.Title, "Test Case") } if node.ParentID != rootID { t.Errorf("parentID: got %q, want %q", node.ParentID, rootID) } if node.Status != StatusActive { t.Errorf("status: got %s, want %s", node.Status, StatusActive) } if node.Path != filepath.Join("My Workspace", "Test Case") { t.Errorf("path: got %q, want %q", node.Path, filepath.Join("My Workspace", "Test Case")) } if _, err := os.Stat(filepath.Join(vaultDir, node.Path)); err != nil { t.Fatalf("expected workspace folder to exist: %v", err) } // Verify persisted tree := m.GetTree() if len(tree.Nodes) != 2 { t.Errorf("expected 2 nodes, got %d", len(tree.Nodes)) } } func TestCreateNode_DuplicateTitlesGetUniquePaths(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) if err := m.Load(); err != nil { t.Fatalf("Load: %v", err) } rootID := m.GetTree().Nodes[0].ID first, err := m.CreateNode(rootID, TypeCase, "SameName") if err != nil { t.Fatalf("CreateNode first: %v", err) } second, err := m.CreateNode(rootID, TypeCase, "SameName") if err != nil { t.Fatalf("CreateNode second: %v", err) } if first.Path == second.Path { t.Fatalf("expected unique paths, got %q", first.Path) } if second.Path != filepath.Join("My Workspace", "SameName (2)") { t.Errorf("second path: got %q, want %q", second.Path, filepath.Join("My Workspace", "SameName (2)")) } } func TestCreateNode_InvalidType(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) m.Load() _, err := m.CreateNode("", NodeType("note"), "My Note") if err == nil { t.Error("expected error for invalid type 'note'") } } func TestCreateNode_EmptyTitle(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) m.Load() _, err := m.CreateNode("", TypeCase, "") if err == nil { t.Error("expected error for empty title") } _, err = m.CreateNode("", TypeCase, " ") if err == nil { t.Error("expected error for whitespace-only title") } } func TestRenameNode(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) m.Load() rootID := m.GetTree().Nodes[0].ID node, _ := m.CreateNode(rootID, TypeCase, "Original") if err := m.RenameNode(node.ID, "Renamed"); err != nil { t.Fatalf("RenameNode: %v", err) } renamed, _ := m.GetNode(node.ID) if renamed.Title != "Renamed" { t.Errorf("title: got %q, want %q", renamed.Title, "Renamed") } if renamed.UpdatedAt == node.UpdatedAt { t.Error("updatedAt should change after rename") } } func TestMoveNode(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) m.Load() rootID := m.GetTree().Nodes[0].ID folder, _ := m.CreateNode(rootID, TypeFolder, "Folder") c, _ := m.CreateNode(rootID, TypeCase, "Case") // Move case into folder if err := m.MoveNode(c.ID, folder.ID); err != nil { t.Fatalf("MoveNode: %v", err) } moved, _ := m.GetNode(c.ID) if moved.ParentID != folder.ID { t.Errorf("parentID: got %q, want %q", moved.ParentID, folder.ID) } if moved.Path != filepath.Join("My Workspace", "Folder", "Case") { t.Errorf("path: got %q, want %q", moved.Path, filepath.Join("My Workspace", "Folder", "Case")) } if _, err := os.Stat(filepath.Join(vaultDir, moved.Path)); err != nil { t.Fatalf("expected moved folder to exist: %v", err) } } func TestMoveNode_CannotMoveIntoSelf(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) m.Load() rootID := m.GetTree().Nodes[0].ID node, _ := m.CreateNode(rootID, TypeCase, "Case") err := m.MoveNode(node.ID, node.ID) if err == nil { t.Error("expected error when moving node into itself") } } func TestMoveNode_SameParentKeepsPath(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) m.Load() rootID := m.GetTree().Nodes[0].ID node, _ := m.CreateNode(rootID, TypeCase, "Case") if err := m.MoveNode(node.ID, rootID); err != nil { t.Fatalf("MoveNode: %v", err) } moved, _ := m.GetNode(node.ID) if moved.Path != node.Path { t.Errorf("path changed on same-parent move: got %q, want %q", moved.Path, node.Path) } } func TestMoveNode_CannotMoveIntoDescendant(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) m.Load() rootID := m.GetTree().Nodes[0].ID folder, _ := m.CreateNode(rootID, TypeFolder, "Folder") child, _ := m.CreateNode(folder.ID, TypeCase, "Child") // Try to move folder into its own child err := m.MoveNode(folder.ID, child.ID) if err == nil { t.Error("expected error when moving node into descendant") } } func TestArchiveNode(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) m.Load() rootID := m.GetTree().Nodes[0].ID node, _ := m.CreateNode(rootID, TypeCase, "To Archive") if err := m.ArchiveNode(node.ID); err != nil { t.Fatalf("ArchiveNode: %v", err) } archived, _ := m.GetNode(node.ID) if archived.Status != StatusArchived { t.Errorf("status: got %s, want %s", archived.Status, StatusArchived) } } func TestSetCurrentNode(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) m.Load() rootID := m.GetTree().Nodes[0].ID node, _ := m.CreateNode(rootID, TypeCase, "My Case") if err := m.SetCurrentNode(node.ID); err != nil { t.Fatalf("SetCurrentNode: %v", err) } current, err := m.GetCurrentNode() if err != nil { t.Fatalf("GetCurrentNode: %v", err) } if current.ID != node.ID { t.Errorf("current: got %s, want %s", current.ID, node.ID) } } func TestGetTree_StableAfterReopen(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) // Create and populate m1 := NewManager(vaultDir) m1.Load() rootID := m1.GetTree().Nodes[0].ID m1.CreateNode(rootID, TypeCase, "Case 1") m1.CreateNode(rootID, TypeFolder, "Folder 1") m1.CreateNode(rootID, TypeCase, "Case 2") // Reopen m2 := NewManager(vaultDir) if err := m2.Load(); err != nil { t.Fatalf("reopen Load: %v", err) } tree := m2.GetTree() // root + 3 created = 4 if len(tree.Nodes) != 4 { t.Fatalf("expected 4 nodes after reopen, got %d", len(tree.Nodes)) } // Check order: children of root should be sorted by order children := m2.ListChildren(rootID) if len(children) != 3 { t.Fatalf("expected 3 children, got %d", len(children)) } if children[0].Title != "Case 1" { t.Errorf("first child: got %q, want %q", children[0].Title, "Case 1") } if children[1].Title != "Folder 1" { t.Errorf("second child: got %q, want %q", children[1].Title, "Folder 1") } if children[2].Title != "Case 2" { t.Errorf("third child: got %q, want %q", children[2].Title, "Case 2") } } func TestCorruptWorkspaceJSON(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") verstakDir := filepath.Join(vaultDir, ".verstak") os.MkdirAll(verstakDir, 0o755) // Write corrupt JSON corruptPath := filepath.Join(verstakDir, "workspace.json") os.WriteFile(corruptPath, []byte("{not valid json"), 0o600) m := NewManager(vaultDir) err := m.Load() if err == nil { t.Error("expected error for corrupt workspace.json") } // Should have created a backup entries, _ := os.ReadDir(verstakDir) backupFound := false for _, e := range entries { if filepath.Ext(e.Name()) == ".corrupt" || len(e.Name()) > 14 && e.Name()[14] == '-' { backupFound = true break } } // Also check for .corrupt.* pattern for _, e := range entries { name := e.Name() if len(name) > 20 && name[:14] == "workspace.json" { backupFound = true break } } _ = backupFound // backup may have different naming // Should have created a valid default tree tree := m.GetTree() if len(tree.Nodes) != 1 { t.Errorf("expected 1 default node, got %d", len(tree.Nodes)) } } func TestListChildren_EmptyParent(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) m.Load() // Root has no parent, so ListChildren("") should return root-level nodes children := m.ListChildren("") if len(children) != 1 { t.Errorf("expected 1 root-level node, got %d", len(children)) } } func TestCreateNode_InvalidParent(t *testing.T) { dir := t.TempDir() vaultDir := filepath.Join(dir, "vault") os.MkdirAll(filepath.Join(vaultDir, ".verstak"), 0o755) m := NewManager(vaultDir) m.Load() _, err := m.CreateNode("nonexistent-id", TypeCase, "Orphan") if err == nil { t.Error("expected error for nonexistent parent") } }