/ tests / unit / workspace / groups.test.ts
groups.test.ts
  1  /**
  2   * Tests for workspace groups - organizational subgroups within parent workspaces
  3   * Following TDD: Write failing tests first, then implement
  4   */
  5  
  6  import { describe, it, expect } from 'vitest';
  7  import { validateWorkspaceGroups } from '../../../src/lib/workspace/validation';
  8  import type {
  9    WorkspaceGroup,
 10    ParentWorkspace,
 11    ChildWorkspace,
 12  } from '../../../src/lib/types';
 13  
 14  // Helper to create valid metadata
 15  const createMetadata = () => ({
 16    created: new Date().toISOString(),
 17    lastModified: new Date().toISOString(),
 18    lastAccessed: new Date().toISOString(),
 19    switchCount: 0,
 20    totalTimeActive: 0,
 21  });
 22  
 23  // Helper to create a parent workspace with optional groups
 24  function createParent(
 25    id: string,
 26    childIds: string[] = [],
 27    groups?: WorkspaceGroup[]
 28  ): ParentWorkspace {
 29    return {
 30      id,
 31      name: `Parent ${id}`,
 32      type: 'parent',
 33      parentId: null,
 34      isTranscendent: false,
 35      openInContextSwitch: true,
 36      children: childIds,
 37      groups,
 38      metadata: createMetadata(),
 39    };
 40  }
 41  
 42  // Helper to create a child workspace with optional groupId
 43  function createChild(
 44    id: string,
 45    parentId: string,
 46    groupId?: string | null
 47  ): ChildWorkspace {
 48    return {
 49      id,
 50      name: `Child ${id}`,
 51      type: 'child',
 52      parentId,
 53      isTranscendent: false,
 54      openInContextSwitch: true,
 55      tabs: [],
 56      groupId,
 57      metadata: createMetadata(),
 58    };
 59  }
 60  
 61  describe('WorkspaceGroup value object', () => {
 62    it('should have required id and name fields', () => {
 63      const group: WorkspaceGroup = {
 64        id: 'group-1',
 65        name: 'My Group',
 66      };
 67  
 68      expect(group.id).toBe('group-1');
 69      expect(group.name).toBe('My Group');
 70    });
 71  
 72    it('should default collapsed to undefined (treated as false)', () => {
 73      const group: WorkspaceGroup = {
 74        id: 'group-1',
 75        name: 'My Group',
 76      };
 77  
 78      expect(group.collapsed).toBeUndefined();
 79    });
 80  
 81    it('should allow optional collapsed field', () => {
 82      const collapsedGroup: WorkspaceGroup = {
 83        id: 'group-1',
 84        name: 'My Group',
 85        collapsed: true,
 86      };
 87  
 88      const expandedGroup: WorkspaceGroup = {
 89        id: 'group-2',
 90        name: 'Another Group',
 91        collapsed: false,
 92      };
 93  
 94      expect(collapsedGroup.collapsed).toBe(true);
 95      expect(expandedGroup.collapsed).toBe(false);
 96    });
 97  
 98    it('should allow optional color', () => {
 99      const coloredGroup: WorkspaceGroup = {
100        id: 'group-1',
101        name: 'My Group',
102        color: '#ff5500',
103      };
104  
105      expect(coloredGroup.color).toBe('#ff5500');
106    });
107  });
108  
109  describe('ParentWorkspace with groups', () => {
110    it('should accept optional groups array', () => {
111      // Parent without groups is valid (backwards compatible)
112      const parentWithoutGroups = createParent('parent-1', ['child-1']);
113      expect(parentWithoutGroups.groups).toBeUndefined();
114  
115      // Parent with groups
116      const parentWithGroups = createParent('parent-2', ['child-1', 'child-2'], [
117        { id: 'group-1', name: 'Group A' },
118        { id: 'group-2', name: 'Group B' },
119      ]);
120      expect(parentWithGroups.groups).toHaveLength(2);
121      expect(parentWithGroups.groups![0].name).toBe('Group A');
122    });
123  
124    it('should accept empty groups array', () => {
125      const parent = createParent('parent-1', [], []);
126      expect(parent.groups).toEqual([]);
127    });
128  });
129  
130  describe('ChildWorkspace with groupId', () => {
131    it('should accept optional groupId field', () => {
132      // Child without groupId is valid (ungrouped)
133      const childWithoutGroup = createChild('child-1', 'parent-1');
134      expect(childWithoutGroup.groupId).toBeUndefined();
135  
136      // Child with groupId
137      const childWithGroup = createChild('child-2', 'parent-1', 'group-1');
138      expect(childWithGroup.groupId).toBe('group-1');
139    });
140  
141    it('should allow null groupId for explicitly ungrouped children', () => {
142      const child = createChild('child-1', 'parent-1', null);
143      expect(child.groupId).toBeNull();
144    });
145  });
146  
147  describe('validateWorkspaceGroups', () => {
148    it('should pass when child groupId references existing group in parent', () => {
149      const parent = createParent('parent-1', ['child-1'], [
150        { id: 'group-1', name: 'Group A' },
151      ]);
152      const child = createChild('child-1', 'parent-1', 'group-1');
153  
154      const result = validateWorkspaceGroups(parent, [child]);
155      expect(result.valid).toBe(true);
156      expect(result.errors).toHaveLength(0);
157    });
158  
159    it('should fail when child groupId references non-existent group', () => {
160      const parent = createParent('parent-1', ['child-1'], [
161        { id: 'group-1', name: 'Group A' },
162      ]);
163      const child = createChild('child-1', 'parent-1', 'non-existent-group');
164  
165      const result = validateWorkspaceGroups(parent, [child]);
166      expect(result.valid).toBe(false);
167      expect(result.errors.some(e => e.message.includes('non-existent group'))).toBe(true);
168      expect(result.errors[0].workspaceId).toBe('child-1');
169    });
170  
171    it('should pass when child has no groupId (ungrouped)', () => {
172      const parent = createParent('parent-1', ['child-1'], [
173        { id: 'group-1', name: 'Group A' },
174      ]);
175      const childWithoutGroup = createChild('child-1', 'parent-1');
176  
177      const result = validateWorkspaceGroups(parent, [childWithoutGroup]);
178      expect(result.valid).toBe(true);
179    });
180  
181    it('should pass when child has null groupId (explicitly ungrouped)', () => {
182      const parent = createParent('parent-1', ['child-1'], [
183        { id: 'group-1', name: 'Group A' },
184      ]);
185      const child = createChild('child-1', 'parent-1', null);
186  
187      const result = validateWorkspaceGroups(parent, [child]);
188      expect(result.valid).toBe(true);
189    });
190  
191    it('should fail when groups array has duplicate IDs', () => {
192      const parent = createParent('parent-1', [], [
193        { id: 'group-1', name: 'Group A' },
194        { id: 'group-1', name: 'Group B' }, // Duplicate ID!
195      ]);
196  
197      const result = validateWorkspaceGroups(parent, []);
198      expect(result.valid).toBe(false);
199      expect(result.errors.some(e => e.message.includes('Duplicate group ID'))).toBe(true);
200      expect(result.errors[0].workspaceId).toBe('parent-1');
201    });
202  
203    it('should pass for parent without groups (backwards compatible)', () => {
204      const parent = createParent('parent-1', ['child-1']);
205      const child = createChild('child-1', 'parent-1');
206  
207      const result = validateWorkspaceGroups(parent, [child]);
208      expect(result.valid).toBe(true);
209    });
210  
211    it('should validate multiple children correctly', () => {
212      const parent = createParent('parent-1', ['child-1', 'child-2', 'child-3'], [
213        { id: 'group-1', name: 'Group A' },
214        { id: 'group-2', name: 'Group B' },
215      ]);
216      const child1 = createChild('child-1', 'parent-1', 'group-1');
217      const child2 = createChild('child-2', 'parent-1', 'group-2');
218      const child3 = createChild('child-3', 'parent-1'); // Ungrouped
219  
220      const result = validateWorkspaceGroups(parent, [child1, child2, child3]);
221      expect(result.valid).toBe(true);
222    });
223  
224    it('should report multiple errors when multiple children reference invalid groups', () => {
225      const parent = createParent('parent-1', ['child-1', 'child-2'], [
226        { id: 'group-1', name: 'Group A' },
227      ]);
228      const child1 = createChild('child-1', 'parent-1', 'invalid-1');
229      const child2 = createChild('child-2', 'parent-1', 'invalid-2');
230  
231      const result = validateWorkspaceGroups(parent, [child1, child2]);
232      expect(result.valid).toBe(false);
233      expect(result.errors).toHaveLength(2);
234    });
235  });