mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
test(session-search): cover the legacy title filter while indexing is off
The panel test fails on the pre-fix code with no session rows rendered.
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import type { AiVaultSessionGroup } from './ai-vault-session-filters'
|
||||
|
||||
const mockState: {
|
||||
settings: { aiVaultSearch?: { enabled: boolean } }
|
||||
runtimeEnvironments: never[]
|
||||
folderWorkspaces: Record<string, never>
|
||||
projectGroups: never[]
|
||||
repos: never[]
|
||||
worktreesByRepo: Record<string, never>
|
||||
} = {
|
||||
settings: {},
|
||||
runtimeEnvironments: [],
|
||||
folderWorkspaces: {},
|
||||
projectGroups: [],
|
||||
repos: [],
|
||||
worktreesByRepo: {}
|
||||
}
|
||||
const updateSettingsOrThrow = vi.fn(async (next: { aiVaultSearch: { enabled: boolean } }) => {
|
||||
mockState.settings = next
|
||||
})
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: Object.assign((select: (state: typeof mockState) => unknown) => select(mockState), {
|
||||
getState: () => ({ ...mockState, updateSettingsOrThrow })
|
||||
})
|
||||
}))
|
||||
vi.mock('@/store/selectors', () => ({
|
||||
useActiveRepo: () => null,
|
||||
useActiveWorktree: () => null,
|
||||
useActiveWorktreeId: () => null,
|
||||
useAllWorktrees: () => [],
|
||||
useProjectHostSetupProjection: () => ({ projects: [], setups: [] }),
|
||||
useRepos: () => []
|
||||
}))
|
||||
|
||||
const sessions: AiVaultSession[] = [
|
||||
vaultSession('claude:1', 'Fix the foo pipeline'),
|
||||
vaultSession('claude:2', 'Rename the bar widget')
|
||||
]
|
||||
vi.mock('./ai-vault-session-refresh', () => ({
|
||||
useAiVaultSessionRefresh: () => ({
|
||||
error: null,
|
||||
loading: false,
|
||||
refresh: vi.fn(),
|
||||
scanResult: { sessions, issues: [], scannedAt: '2026-05-01T10:10:00.000Z' },
|
||||
sessions
|
||||
})
|
||||
}))
|
||||
vi.mock('./ai-vault-session-launch-actions', () => ({
|
||||
useAiVaultSessionLaunchActions: () => ({
|
||||
buildResumeStartup: vi.fn(),
|
||||
copyResumeCommand: vi.fn(),
|
||||
handleResume: vi.fn(),
|
||||
handleResumeInNewChat: vi.fn(),
|
||||
handleContinueInNewSession: vi.fn(),
|
||||
continuationRequest: null,
|
||||
handleContinuationDialogOpenChange: vi.fn()
|
||||
})
|
||||
}))
|
||||
vi.mock('./ai-vault-original-pane-actions', () => ({
|
||||
useAiVaultOriginalPaneActions: () => ({
|
||||
getOriginalPaneTarget: vi.fn(),
|
||||
getSessionLiveState: vi.fn(),
|
||||
jumpToOriginalPane: vi.fn(),
|
||||
jumpToWorktree: vi.fn()
|
||||
})
|
||||
}))
|
||||
vi.mock('./ai-vault-session-delete-action', () => ({
|
||||
useAiVaultSessionDeleteAction: () => vi.fn()
|
||||
}))
|
||||
// The virtualizer measures a zero-height viewport under happy-dom; the rows it would
|
||||
// choose are exactly the grouped sessions, so render those instead.
|
||||
vi.mock('./AiVaultSessionVirtualList', () => ({
|
||||
AiVaultSessionVirtualList: ({ groups }: { groups: readonly AiVaultSessionGroup[] }) => (
|
||||
<ul>
|
||||
{groups.flatMap((group) =>
|
||||
group.sessions.map((session) => <li key={session.id}>{session.title}</li>)
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}))
|
||||
|
||||
const searchSessions = vi.fn()
|
||||
|
||||
function vaultSession(id: string, title: string): AiVaultSession {
|
||||
return {
|
||||
id,
|
||||
executionHostId: 'local',
|
||||
agent: 'claude',
|
||||
sessionId: id,
|
||||
title,
|
||||
cwd: '/Users/ada/repo',
|
||||
branch: null,
|
||||
model: null,
|
||||
filePath: `/Users/ada/.claude/${id}.jsonl`,
|
||||
codexHome: null,
|
||||
createdAt: '2026-05-01T10:00:00.000Z',
|
||||
updatedAt: '2026-05-01T10:10:00.000Z',
|
||||
modifiedAt: '2026-05-01T10:10:00.000Z',
|
||||
messageCount: 4,
|
||||
totalTokens: 10,
|
||||
previewMessages: [],
|
||||
queuedMessageCount: 0,
|
||||
subagentTranscriptCount: 0,
|
||||
resumeCommand: 'claude --resume',
|
||||
subagent: null
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockState.settings = {}
|
||||
searchSessions.mockReset().mockResolvedValue({
|
||||
kind: 'results',
|
||||
hits: [],
|
||||
page: { cursor: null, hasMore: false },
|
||||
generation: 1,
|
||||
durationMs: 1,
|
||||
truncated: { candidates: false, snippets: 0, query: false, freshness: false }
|
||||
})
|
||||
Object.defineProperty(window, 'api', {
|
||||
configurable: true,
|
||||
value: { aiVault: { searchSessions }, ui: { writeClipboardText: vi.fn() } }
|
||||
})
|
||||
})
|
||||
afterEach(cleanup)
|
||||
|
||||
async function typeQuery(text: string) {
|
||||
const { default: AiVaultPanel } = await import('./AiVaultPanel')
|
||||
render(<AiVaultPanel />)
|
||||
await userEvent.type(screen.getByLabelText('Search sessions'), text)
|
||||
}
|
||||
|
||||
it('offers indexing above the title-filtered history instead of hiding every session', async () => {
|
||||
await typeQuery('foo')
|
||||
|
||||
expect(screen.getByRole('status').textContent).toContain('Enable full-text search?')
|
||||
expect(screen.getByRole('button', { name: 'Enable' })).toBeTruthy()
|
||||
expect(screen.getByText('Fix the foo pipeline')).toBeTruthy()
|
||||
expect(screen.queryByText('Rename the bar widget')).toBeNull()
|
||||
expect(searchSessions).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('switches to index search with the same query once indexing is enabled', async () => {
|
||||
await typeQuery('foo')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Enable' }))
|
||||
|
||||
expect(updateSettingsOrThrow).toHaveBeenCalledWith({
|
||||
aiVaultSearch: { enabled: true, historyDays: null }
|
||||
})
|
||||
await waitFor(() =>
|
||||
expect(searchSessions).toHaveBeenCalledWith(expect.objectContaining({ query: 'foo' }), 'local')
|
||||
)
|
||||
expect(screen.queryByRole('button', { name: 'Enable' })).toBeNull()
|
||||
})
|
||||
@@ -9,9 +9,10 @@ import type { ExecutionHostId, ExecutionHostScope } from '../../../../shared/exe
|
||||
import { searchHit, searchResults } from '../../../../shared/ai-vault-search-test-fixture'
|
||||
import { useAiVaultPanelSearch, useAiVaultSearch } from './use-ai-vault-search'
|
||||
|
||||
const mockSettings: { aiVaultSearch?: { enabled: boolean } } = {}
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (select: (state: { settings: undefined }) => unknown) =>
|
||||
select({ settings: undefined })
|
||||
useAppStore: (select: (state: { settings: typeof mockSettings }) => unknown) =>
|
||||
select({ settings: mockSettings })
|
||||
}))
|
||||
|
||||
const ALL_AGENTS = ['codex' as const]
|
||||
@@ -35,6 +36,7 @@ beforeEach(() => {
|
||||
value: { aiVault: { searchSessions } }
|
||||
})
|
||||
searchSessions.mockReset().mockResolvedValue(empty)
|
||||
delete mockSettings.aiVaultSearch
|
||||
})
|
||||
afterEach(() => vi.useRealTimers())
|
||||
async function debounce() {
|
||||
@@ -267,3 +269,42 @@ it('restarts page one under the all scope when the merged cursor goes stale', as
|
||||
expect(result.current.sessions.map((session) => session.executionHostId)).toEqual(['local'])
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('leaves the box as the legacy title filter while local indexing consent is pending', async () => {
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useAiVaultPanelSearch('needle', ALL_AGENTS, undefined, 'local')
|
||||
)
|
||||
await debounce()
|
||||
expect(searchSessions).not.toHaveBeenCalled()
|
||||
expect(result.current.searching).toBe(false)
|
||||
expect(result.current.consentOffer).toBe(true)
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.sessions).toEqual([])
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('searches the local index with the same query once consent is on', async () => {
|
||||
mockSettings.aiVaultSearch = { enabled: true }
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useAiVaultPanelSearch('needle', ALL_AGENTS, undefined, 'local')
|
||||
)
|
||||
await debounce()
|
||||
expect(searchSessions).toHaveBeenCalledExactlyOnceWith(
|
||||
{ ...ALL_REQUEST, cursor: undefined },
|
||||
'local'
|
||||
)
|
||||
expect(result.current.searching).toBe(true)
|
||||
expect(result.current.consentOffer).toBe(false)
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('offers nothing and searches nothing for a blank query', async () => {
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useAiVaultPanelSearch(' ', ALL_AGENTS, undefined, 'local')
|
||||
)
|
||||
await debounce()
|
||||
expect(searchSessions).not.toHaveBeenCalled()
|
||||
expect(result.current.searching).toBe(false)
|
||||
expect(result.current.consentOffer).toBe(false)
|
||||
unmount()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user