- Add "Reindex Vault" command to command palette and menu bar - Show "Indexed Xm ago" in status bar when idle, clickable to reindex - After git pull with updates, auto-trigger incremental reindex - Add lastIndexedTime state to useIndexing, populated from backend metadata - Add triggerFullReindex to useIndexing (retryIndexing is now an alias) - Add onSyncUpdated callback to useAutoSync - Extract formatIndexedElapsed to utils/indexingHelpers.ts - Tests: 20 new unit tests across 5 files, 2 Playwright smoke tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
146 lines
5.2 KiB
TypeScript
146 lines
5.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { renderHook, act } from '@testing-library/react'
|
|
import { useIndexing } from './useIndexing'
|
|
|
|
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() }))
|
|
vi.mock('@tauri-apps/api/event', () => ({ listen: vi.fn().mockResolvedValue(vi.fn()) }))
|
|
vi.mock('../mock-tauri', () => ({
|
|
isTauri: () => false,
|
|
mockInvoke: vi.fn().mockResolvedValue({
|
|
available: true,
|
|
qmd_installed: true,
|
|
collection_exists: true,
|
|
indexed_count: 100,
|
|
embedded_count: 80,
|
|
pending_embed: 0,
|
|
}),
|
|
}))
|
|
|
|
const { mockInvoke } = await import('../mock-tauri') as { mockInvoke: ReturnType<typeof vi.fn> }
|
|
|
|
describe('useIndexing', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
vi.clearAllMocks()
|
|
mockInvoke.mockResolvedValue({
|
|
available: true,
|
|
qmd_installed: true,
|
|
collection_exists: true,
|
|
indexed_count: 100,
|
|
embedded_count: 80,
|
|
pending_embed: 0,
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('starts with idle phase', () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
expect(result.current.progress.phase).toBe('idle')
|
|
})
|
|
|
|
it('auto-dismisses error phase after 15 seconds', async () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
|
|
// Simulate setting error state via retryIndexing
|
|
mockInvoke.mockRejectedValueOnce(new Error('qmd update failed'))
|
|
await act(async () => { await result.current.retryIndexing() })
|
|
expect(result.current.progress.phase).toBe('error')
|
|
|
|
act(() => { vi.advanceTimersByTime(15000) })
|
|
expect(result.current.progress.phase).toBe('idle')
|
|
})
|
|
|
|
it('sets unavailable phase for "not installed" errors', async () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
|
|
mockInvoke.mockRejectedValueOnce(new Error('bun not installed'))
|
|
await act(async () => { await result.current.retryIndexing() })
|
|
expect(result.current.progress.phase).toBe('unavailable')
|
|
})
|
|
|
|
it('sets unavailable phase for "not available" errors', async () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
|
|
mockInvoke.mockRejectedValueOnce(new Error('qmd not available: bun not found'))
|
|
await act(async () => { await result.current.retryIndexing() })
|
|
expect(result.current.progress.phase).toBe('unavailable')
|
|
})
|
|
|
|
it('auto-dismisses unavailable phase after 8 seconds', async () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
|
|
mockInvoke.mockRejectedValueOnce(new Error('bun not installed'))
|
|
await act(async () => { await result.current.retryIndexing() })
|
|
expect(result.current.progress.phase).toBe('unavailable')
|
|
|
|
act(() => { vi.advanceTimersByTime(8000) })
|
|
expect(result.current.progress.phase).toBe('idle')
|
|
})
|
|
|
|
it('exposes retryIndexing function', () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
expect(typeof result.current.retryIndexing).toBe('function')
|
|
})
|
|
|
|
it('exposes triggerFullReindex function', () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
expect(typeof result.current.triggerFullReindex).toBe('function')
|
|
})
|
|
|
|
it('retryIndexing is the same reference as triggerFullReindex', () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
expect(result.current.retryIndexing).toBe(result.current.triggerFullReindex)
|
|
})
|
|
|
|
it('triggerFullReindex sets scanning phase then completes', async () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
|
|
await act(async () => { await result.current.triggerFullReindex() })
|
|
// In non-Tauri mode, it goes to 'complete' then auto-dismisses
|
|
expect(result.current.progress.phase).toBe('complete')
|
|
})
|
|
|
|
it('triggerFullReindex sets lastIndexedTime on success', async () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
|
|
expect(result.current.lastIndexedTime).toBeNull()
|
|
await act(async () => { await result.current.triggerFullReindex() })
|
|
expect(result.current.lastIndexedTime).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('triggerFullReindex sets error phase on failure', async () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
|
|
mockInvoke.mockRejectedValueOnce(new Error('indexing failed'))
|
|
await act(async () => { await result.current.triggerFullReindex() })
|
|
expect(result.current.progress.phase).toBe('error')
|
|
})
|
|
|
|
it('populates lastIndexedTime from backend metadata on mount', async () => {
|
|
mockInvoke.mockResolvedValue({
|
|
available: true,
|
|
qmd_installed: true,
|
|
collection_exists: true,
|
|
indexed_count: 100,
|
|
embedded_count: 80,
|
|
pending_embed: 0,
|
|
last_indexed_commit: 'abc123',
|
|
last_indexed_at: 1709800000,
|
|
})
|
|
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
|
|
// Wait for the effect to run
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(10) })
|
|
expect(result.current.lastIndexedTime).toBe(1709800000000) // seconds → ms
|
|
})
|
|
|
|
it('starts with lastIndexedTime as null', () => {
|
|
const { result } = renderHook(() => useIndexing('/test/vault'))
|
|
expect(result.current.lastIndexedTime).toBeNull()
|
|
})
|
|
})
|