import { describe, expect, it } from 'vitest' import { DEFAULT_USER_SETTINGS, UserSettingsSchema, mergeSettings, parseUserSettings, type UserSettings, } from '@/lib/user-settings' describe('mergeSettings', () => { it('returns the patch when previous is empty', () => { const result = mergeSettings({}, { views: { sprintBacklog: { sort: 'code' } } }) expect(result).toEqual({ views: { sprintBacklog: { sort: 'code' } } }) }) it('preserves existing keys when patch only sets new ones', () => { const prev: UserSettings = { views: { sprintBacklog: { sort: 'code' } } } const result = mergeSettings(prev, { views: { pbiList: { sort: 'date' } }, }) expect(result).toEqual({ views: { sprintBacklog: { sort: 'code' }, pbiList: { sort: 'date' }, }, }) }) it('merges nested objects without overwriting siblings', () => { const prev: UserSettings = { views: { sprintBacklog: { sort: 'code', sortDir: 'asc' } }, } const result = mergeSettings(prev, { views: { sprintBacklog: { sort: 'priority' } }, }) expect(result).toEqual({ views: { sprintBacklog: { sort: 'priority', sortDir: 'asc' } }, }) }) it('replaces arrays instead of appending', () => { const prev: UserSettings = { views: { sprintBacklog: { collapsedPbis: ['a', 'b'] } }, } const result = mergeSettings(prev, { views: { sprintBacklog: { collapsedPbis: ['c'] } }, }) expect(result.views?.sprintBacklog?.collapsedPbis).toEqual(['c']) }) it('does not mutate the previous object', () => { const prev: UserSettings = { views: { sprintBacklog: { sort: 'code' } } } const snapshot = JSON.parse(JSON.stringify(prev)) mergeSettings(prev, { views: { sprintBacklog: { sortDir: 'desc' } } }) expect(prev).toEqual(snapshot) }) it('skips undefined values in the patch', () => { const prev: UserSettings = { views: { sprintBacklog: { sort: 'code' } } } const result = mergeSettings(prev, { views: undefined }) expect(result).toEqual(prev) }) }) describe('parseUserSettings', () => { it('returns defaults for null', () => { expect(parseUserSettings(null)).toEqual(DEFAULT_USER_SETTINGS) }) it('returns defaults for undefined', () => { expect(parseUserSettings(undefined)).toEqual(DEFAULT_USER_SETTINGS) }) it('returns defaults for invalid input', () => { expect(parseUserSettings({ views: { sprintBacklog: { filterStatus: 'BOGUS' } } })) .toEqual(DEFAULT_USER_SETTINGS) }) it('passes valid settings through', () => { const valid = { views: { sprintBacklog: { sort: 'code' as const } } } expect(parseUserSettings(valid)).toEqual(valid) }) }) describe('UserSettingsSchema', () => { it('rejects unknown top-level keys', () => { const result = UserSettingsSchema.safeParse({ unknown: 1 }) expect(result.success).toBe(false) }) it('accepts an empty object', () => { expect(UserSettingsSchema.safeParse({}).success).toBe(true) }) it('accepts the full shape', () => { const result = UserSettingsSchema.safeParse({ views: { sprintBacklog: { filterPriority: 1, filterStatus: 'OPEN', sort: 'code', sortDir: 'asc', collapsedPbis: ['x'], filterPopoverOpen: true, }, pbiList: { sort: 'priority', filterPriority: 'all', filterStatus: 'ready', sortDir: 'desc' }, storyPanel: { sort: 'date' }, jobsColumns: { 'queue:active': { kinds: ['TASK_IMPLEMENTATION'], statuses: [] } }, jobs: { timeFilter: '24h' }, ideasList: { filterStatuses: ['draft', 'planned'] }, }, devTools: { debugMode: true }, layout: { splitPanePositions: { 'backlog-pid': [25, 35, 40] }, activeSprints: { 'product-1': 'sprint-1' }, }, }) expect(result.success).toBe(true) }) it('accepts views.jobs.timeFilter and returns it via parseUserSettings', () => { const input = { views: { jobs: { timeFilter: '1h' as const } } } const result = parseUserSettings(input) expect(result).toEqual(input) }) it('rejects an invalid views.jobs.timeFilter value', () => { const result = UserSettingsSchema.safeParse({ views: { jobs: { timeFilter: 'BOGUS' } } }) expect(result.success).toBe(false) }) it('accepts layout-only settings', () => { expect(UserSettingsSchema.safeParse({ layout: { splitPanePositions: { x: [50, 50] }, activeSprints: { p: 's' } }, }).success).toBe(true) }) it('accepts null values in activeSprints (explicit "no active sprint")', () => { const result = UserSettingsSchema.safeParse({ layout: { activeSprints: { 'product-1': null, 'product-2': 'sprint-2' } }, }) expect(result.success).toBe(true) if (result.success) { expect(result.data.layout?.activeSprints).toEqual({ 'product-1': null, 'product-2': 'sprint-2', }) } }) it('accepts pendingSprintDraft with per-PBI intent and overrides', () => { const result = UserSettingsSchema.safeParse({ workflow: { pendingSprintDraft: { 'product-1': { goal: 'Sprint goal', pbiIntent: { pbiA: 'all', pbiB: 'none' }, storyOverrides: { pbiA: { add: [], remove: ['story-1'] }, pbiB: { add: ['story-2'], remove: [] }, }, }, }, }, }) expect(result.success).toBe(true) }) it('fills empty defaults for pbiIntent and storyOverrides in draft', () => { const result = UserSettingsSchema.safeParse({ workflow: { pendingSprintDraft: { 'product-1': { goal: 'g' } } }, }) expect(result.success).toBe(true) if (result.success) { const draft = result.data.workflow?.pendingSprintDraft?.['product-1'] expect(draft?.pbiIntent).toEqual({}) expect(draft?.storyOverrides).toEqual({}) } }) it('rejects pendingSprintDraft with empty goal', () => { const result = UserSettingsSchema.safeParse({ workflow: { pendingSprintDraft: { 'p': { goal: '' } } }, }) expect(result.success).toBe(false) }) it('rejects an invalid ideasList.filterStatuses value', () => { const result = UserSettingsSchema.safeParse({ views: { ideasList: { filterStatuses: ['BOGUS'] } } }) expect(result.success).toBe(false) }) it('accepts an empty ideasList.filterStatuses array', () => { const result = UserSettingsSchema.safeParse({ views: { ideasList: { filterStatuses: [] } } }) expect(result.success).toBe(true) }) it('rejects unknown intent value', () => { const result = UserSettingsSchema.safeParse({ workflow: { pendingSprintDraft: { p: { goal: 'x', pbiIntent: { a: 'partial' } }, }, }, }) expect(result.success).toBe(false) }) })