Newer
Older
navi-1 / webclient / tests / unit / stores / sessions.test.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy 6 hours ago 7 KB webclient: session-list polish from review pass
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useSessionsStore } from '@/stores/sessions.js'

vi.mock('@/api/index.js', () => ({
  getSessions: vi.fn(),
  createSession: vi.fn(),
  deleteSession: vi.fn(),
  pinSession: vi.fn(),
}))

import * as api from '@/api/index.js'

describe('Sessions store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    vi.clearAllMocks()
  })

  it('fetchSessions loads sessions', async () => {
    api.getSessions.mockResolvedValue({
      items: [{ session_id: 's1', profile_id: 'secretary', pinned: false }],
      has_more: true,
      next_offset: 1,
    })
    const store = useSessionsStore()
    await store.fetchSessions('developer')
    expect(api.getSessions).toHaveBeenCalledWith({ limit: 30, offset: 0, profileId: 'developer', search: null })
    expect(store.sessions).toHaveLength(1)
    expect(store.sessions[0].session_id).toBe('s1')
    expect(store.hasMore).toBe(true)
    expect(store.currentProfileId).toBe('developer')
    expect(store.loading).toBe(false)
  })

  it('fetchMoreSessions appends the next page', async () => {
    api.getSessions
      .mockResolvedValueOnce({
        items: [{ session_id: 's1', profile_id: 'secretary', pinned: false }],
        has_more: true,
        next_offset: 1,
      })
      .mockResolvedValueOnce({
        items: [{ session_id: 's2', profile_id: 'developer', pinned: false }],
        has_more: false,
        next_offset: 2,
      })
    const store = useSessionsStore()
    await store.fetchSessions('developer')
    await store.fetchMoreSessions()
    expect(api.getSessions).toHaveBeenLastCalledWith({ limit: 30, offset: 1, profileId: 'developer', search: null })
    expect(store.sessions.map(s => s.session_id)).toEqual(['s1', 's2'])
    expect(store.hasMore).toBe(false)
    expect(store.loadingMore).toBe(false)
  })

  it('fetchMoreSessions does nothing without more pages', async () => {
    api.getSessions.mockResolvedValue({
      items: [],
      has_more: false,
      next_offset: 0,
    })
    const store = useSessionsStore()
    await store.fetchSessions()
    await store.fetchMoreSessions()
    expect(api.getSessions).toHaveBeenCalledTimes(1)
  })

  it('createSession prepends placeholder and returns session', async () => {
    api.createSession.mockResolvedValue({
      session_id: 's2',
      profile_id: 'developer',
      created_at: '2024-01-01T00:00:00Z',
    })
    const store = useSessionsStore()
    const result = await store.createSession('developer')
    expect(api.createSession).toHaveBeenCalledWith('developer')
    expect(store.sessions).toHaveLength(1)
    expect(store.sessions[0].session_id).toBe('s2')
    expect(store.sessions[0].message_count).toBe(0)
    expect(result.session_id).toBe('s2')
  })

  it('deleteSession filters out the session', async () => {
    api.deleteSession.mockResolvedValue()
    const store = useSessionsStore()
    store.sessions = [
      { session_id: 's1' },
      { session_id: 's2' },
    ]
    await store.deleteSession('s1')
    expect(api.deleteSession).toHaveBeenCalledWith('s1')
    expect(store.sessions).toHaveLength(1)
    expect(store.sessions[0].session_id).toBe('s2')
  })

  it('pinSession sorts pinned to top', async () => {
    api.pinSession.mockResolvedValue()
    const store = useSessionsStore()
    store.sessions = [
      { session_id: 's1', pinned: false },
      { session_id: 's2', pinned: false },
    ]
    await store.pinSession('s2', true)
    expect(api.pinSession).toHaveBeenCalledWith('s2', true)
    expect(store.sessions[0].session_id).toBe('s2')
    expect(store.sessions[0].pinned).toBe(true)
  })

  it('updatePreview mutates session preview', () => {
    const store = useSessionsStore()
    store.sessions = [{ session_id: 's1', preview: '' }]
    store.updatePreview('s1', 'hello')
    expect(store.sessions[0].preview).toBe('hello')
  })

  it('updateName mutates session name', () => {
    const store = useSessionsStore()
    store.sessions = [{ session_id: 's1', name: null }]
    store.updateName('s1', 'Work')
    expect(store.sessions[0].name).toBe('Work')
  })

  it('refetch merges into existing items, preserving references', async () => {
    api.getSessions.mockResolvedValueOnce({
      items: [{ session_id: 's1', preview: 'old', pinned: false }],
      has_more: false,
      next_offset: 1,
    })
    const store = useSessionsStore()
    await store.fetchSessions('developer')
    const ref = store.sessions[0]
    api.getSessions.mockResolvedValueOnce({
      items: [{ session_id: 's1', preview: 'new', pinned: false }],
      has_more: false,
      next_offset: 1,
    })
    await store.fetchSessions('developer')
    expect(store.sessions[0]).toBe(ref) // same object reference — no row remount
    expect(store.sessions[0].preview).toBe('new')
  })

  it('refetch follows server order and drops sessions removed on the server', async () => {
    api.getSessions
      .mockResolvedValueOnce({
        items: [{ session_id: 's1' }, { session_id: 's2' }],
        has_more: false,
        next_offset: 2,
      })
      .mockResolvedValueOnce({
        items: [{ session_id: 's2' }, { session_id: 's3' }],
        has_more: false,
        next_offset: 2,
      })
    const store = useSessionsStore()
    await store.fetchSessions('developer')
    const ref2 = store.sessions[1]
    await store.fetchSessions('developer')
    expect(store.sessions.map(s => s.session_id)).toEqual(['s2', 's3'])
    expect(store.sessions[0]).toBe(ref2)
  })

  it('pinSession keeps item reference (in-place update + sort)', async () => {
    api.pinSession.mockResolvedValue()
    const store = useSessionsStore()
    store.sessions = [
      { session_id: 's1', pinned: false },
      { session_id: 's2', pinned: false },
    ]
    const ref2 = store.sessions[1]
    await store.pinSession('s2', true)
    expect(store.sessions[0].session_id).toBe('s2')
    expect(store.sessions[0]).toBe(ref2)
    expect(store.sessions[0].pinned).toBe(true)
  })

  it('createSession places the placeholder where the server list would (below pinned)', async () => {
    api.createSession.mockResolvedValue({
      session_id: 'new1',
      profile_id: 'developer',
      created_at: '2024-01-02T00:00:00Z',
    })
    const store = useSessionsStore()
    store.sessions = [{ session_id: 's1', pinned: true }]
    await store.createSession('developer')
    // placeholder lands after the pinned run, matching server's
    // pinned DESC, last_active DESC ordering
    expect(store.sessions.map(s => s.session_id)).toEqual(['s1', 'new1'])
  })

  it('rememberName persists names beyond the loaded page; fetchSessions fills the map', async () => {
    api.getSessions.mockResolvedValueOnce({
      items: [{ session_id: 's1', name: 'Work', preview: '', pinned: false }],
      has_more: false,
      next_offset: 1,
    })
    const store = useSessionsStore()
    await store.fetchSessions('developer')
    expect(store.nameById['s1']).toBe('Work')

    store.rememberName('s2', 'Deep session')
    expect(store.nameById['s2']).toBe('Deep session')

    api.getSessions.mockResolvedValueOnce({
      items: [{ session_id: 's3', name: null, preview: '', pinned: false }],
      has_more: false,
      next_offset: 1,
    })
    await store.fetchSessions('developer')
    // null name must not wipe remembered names
    expect(store.nameById['s2']).toBe('Deep session')

    store.updateName('s3', 'Named now')
    expect(store.nameById['s3']).toBe('Named now')
  })
})