conversationsSidebarSlice.migration.test.ts8.1 KBView on GitHub
import { migrateV1Persisted, readPersisted } from '@/modules/conversationsPage/slice/conversationsSidebarSlice';

describe('conversationsSidebarSlice — v1 → v2 migration', () => {
  describe('migrateV1Persisted', () => {
    test('groupBy: starredAndResponse → sentinel', () => {
      const out = migrateV1Persisted({
        groupBy: 'starredAndResponse',
        sortBy: 'lastActivity',
        filters: {},
      });
      expect(out.schema).toBe(2);
      expect(out.groupBy).toEqual({ kind: 'sentinel', value: 'starredAndResponse' });
    });

    test('groupBy: awaitingResponse / none → sentinel', () => {
      expect(migrateV1Persisted({ groupBy: 'awaitingResponse' }).groupBy).toEqual({
        kind: 'sentinel',
        value: 'awaitingResponse',
      });
      expect(migrateV1Persisted({ groupBy: 'none' }).groupBy).toEqual({
        kind: 'sentinel',
        value: 'none',
      });
    });

    test('groupBy: status / priority → column', () => {
      expect(migrateV1Persisted({ groupBy: 'status' }).groupBy).toEqual({
        kind: 'column',
        columnId: 'status',
      });
      expect(migrateV1Persisted({ groupBy: 'priority' }).groupBy).toEqual({
        kind: 'column',
        columnId: 'priority',
      });
    });

    test('groupBy: owner → primaryUser column, aop → aopId column', () => {
      expect(migrateV1Persisted({ groupBy: 'owner' }).groupBy).toEqual({
        kind: 'column',
        columnId: 'primaryUser',
      });
      expect(migrateV1Persisted({ groupBy: 'aop' }).groupBy).toEqual({
        kind: 'column',
        columnId: 'aopId',
      });
    });

    test('groupBy: legacy "starred" collapses to starredAndResponse sentinel', () => {
      expect(migrateV1Persisted({ groupBy: 'starred' }).groupBy).toEqual({
        kind: 'sentinel',
        value: 'starredAndResponse',
      });
    });

    test('sortBy: lastActivity → lastContactedAt desc, priority 0', () => {
      const out = migrateV1Persisted({ groupBy: 'none', sortBy: 'lastActivity', filters: {} });
      expect(out.columns.lastContactedAt?.sort).toEqual({
        active: true,
        direction: 'desc',
        priority: 0,
      });
    });

    test('sortBy: dueDateAsc → nextStepDate asc', () => {
      const out = migrateV1Persisted({ groupBy: 'none', sortBy: 'dueDateAsc', filters: {} });
      expect(out.columns.nextStepDate?.sort).toEqual({
        active: true,
        direction: 'asc',
        priority: 0,
      });
    });

    test('sortBy: name / dealValueDesc map to name asc and dealValue desc', () => {
      expect(
        migrateV1Persisted({ groupBy: 'none', sortBy: 'name' }).columns.name?.sort?.direction,
      ).toBe('asc');
      expect(
        migrateV1Persisted({ groupBy: 'none', sortBy: 'dealValueDesc' }).columns.dealValue?.sort
          ?.direction,
      ).toBe('desc');
    });

    test('filters.status → columns.status.filter.selected', () => {
      const out = migrateV1Persisted({
        groupBy: 'none',
        filters: { status: ['active', 'qualification'] },
      });
      expect(out.columns.status?.filter?.selected).toEqual(['active', 'qualification']);
    });

    test('filters.priority + filters.aopIds + hasOpenTasks survive', () => {
      const out = migrateV1Persisted({
        groupBy: 'none',
        filters: {
          priority: ['high'],
          aopIds: ['aop-1', 'aop-2'],
          hasOpenTasks: true,
        },
      });
      expect(out.columns.priority?.filter?.selected).toEqual(['high']);
      expect(out.columns.aopId?.filter?.selected).toEqual(['aop-1', 'aop-2']);
      expect(out.columns.currentTasks?.filter?.binaryChoice).toBe('true');
    });

    test('filters.onlyStarred + filters.hasSlackChannel intentionally dropped (no server-side equivalent)', () => {
      const out = migrateV1Persisted({
        groupBy: 'none',
        filters: { onlyStarred: true, hasSlackChannel: true },
      });
      // Neither produces a columns entry.
      expect(Object.keys(out.columns)).toHaveLength(0);
    });

    test('collapsedGroupKeys is preserved', () => {
      const out = migrateV1Persisted({
        groupBy: 'none',
        collapsedGroupKeys: { foo: true, bar: false },
      });
      expect(out.collapsedGroupKeys).toEqual({ foo: true, bar: false });
    });

    test('unknown groupBy falls back to default sentinel', () => {
      expect(migrateV1Persisted({ groupBy: 'unknown-thing' as unknown }).groupBy).toEqual({
        kind: 'sentinel',
        value: 'starredAndResponse',
      });
    });
  });

  describe('readPersisted', () => {
    const STORAGE_KEY=[redacted];

    beforeEach(() => {
      window.localStorage.clear();
    });

    test('returns defaults when nothing is stored', () => {
      const out = readPersisted();
      expect(out.schema).toBe(2);
      expect(out.groupBy).toEqual({ kind: 'sentinel', value: 'starredAndResponse' });
      expect(out.columns).toEqual({});
      expect(out.collapsedGroupKeys).toEqual({});
    });

    test('round-trips a v2 blob', () => {
      const blob = {
        schema: 2,
        groupBy: { kind: 'column', columnId: 'status' },
        columns: { status: { filter: { selected: ['active'] } } },
        collapsedGroupKeys: { x: true },
      };
      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(blob));
      const out = readPersisted();
      expect(out.groupBy).toEqual({ kind: 'column', columnId: 'status' });
      expect(out.columns.status?.filter?.selected).toEqual(['active']);
      expect(out.collapsedGroupKeys).toEqual({ x: true });
    });

    test('migrates a v1 blob (no schema field) end-to-end', () => {
      const v1 = {
        groupBy: 'status',
        sortBy: 'name',
        filters: { status: ['active'], hasOpenTasks: true },
        collapsedGroupKeys: { foo: true },
      };
      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(v1));
      const out = readPersisted();
      expect(out.schema).toBe(2);
      expect(out.groupBy).toEqual({ kind: 'column', columnId: 'status' });
      expect(out.columns.status?.filter?.selected).toEqual(['active']);
      expect(out.columns.name?.sort).toEqual({ active: true, direction: 'asc', priority: 0 });
      expect(out.columns.currentTasks?.filter?.binaryChoice).toBe('true');
      expect(out.collapsedGroupKeys).toEqual({ foo: true });
    });

    test('returns defaults on JSON parse error', () => {
      window.localStorage.setItem(STORAGE_KEY, '{not-json');
      const out = readPersisted();
      expect(out.groupBy).toEqual({ kind: 'sentinel', value: 'starredAndResponse' });
    });

    test('rewrites legacy sortDimension "strength" → "signal" on read', () => {
      const blob = {
        schema: 2,
        groupBy: { kind: 'sentinel', value: 'none' },
        columns: {
          wm_score: {
            sort: { active: true, direction: 'desc', priority: 0, sortDimension: 'strength' },
          },
        },
        collapsedGroupKeys: {},
      };
      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(blob));
      const out = readPersisted();
      expect(out.columns.wm_score?.sort?.sortDimension).toBe('signal');
    });

    test('drops an unrecognized sortDimension so the value default applies', () => {
      const blob = {
        schema: 2,
        groupBy: { kind: 'sentinel', value: 'none' },
        columns: {
          wm_score: {
            sort: { active: true, direction: 'asc', priority: 0, sortDimension: 'bogus' },
          },
        },
        collapsedGroupKeys: {},
      };
      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(blob));
      const out = readPersisted();
      expect(out.columns.wm_score?.sort?.sortDimension).toBeUndefined();
    });

    test('leaves valid sortDimension untouched', () => {
      const blob = {
        schema: 2,
        groupBy: { kind: 'sentinel', value: 'none' },
        columns: {
          wm_score: {
            sort: { active: true, direction: 'desc', priority: 0, sortDimension: 'signal' },
          },
        },
        collapsedGroupKeys: {},
      };
      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(blob));
      const out = readPersisted();
      expect(out.columns.wm_score?.sort?.sortDimension).toBe('signal');
    });
  });
});