mirror of
https://github.com/lfnovo/open-notebook.git
synced 2026-10-19 09:13:16 +00:00
fix(search): open matched records and deduplicate sources (#1345)
Some checks failed
Development Build / extract-version (push) Has been cancelled
Development Build / changes (push) Has been cancelled
Tests / Backend Tests (push) Has been cancelled
Tests / Backend Lint (push) Has been cancelled
Tests / Backend Typecheck (push) Has been cancelled
Tests / Frontend Tests (push) Has been cancelled
Tests / Frontend Lint (push) Has been cancelled
Tests / Frontend Build (push) Has been cancelled
Development Build / build-regular (push) Has been cancelled
Development Build / build-single (push) Has been cancelled
Development Build / summary (push) Has been cancelled
Some checks failed
Development Build / extract-version (push) Has been cancelled
Development Build / changes (push) Has been cancelled
Tests / Backend Tests (push) Has been cancelled
Tests / Backend Lint (push) Has been cancelled
Tests / Backend Typecheck (push) Has been cancelled
Tests / Frontend Tests (push) Has been cancelled
Tests / Frontend Lint (push) Has been cancelled
Tests / Frontend Build (push) Has been cancelled
Development Build / build-regular (push) Has been cancelled
Development Build / build-single (push) Has been cancelled
Development Build / summary (push) Has been cancelled
This commit is contained in:
parent
83edd89356
commit
ae7b96d376
4 changed files with 169 additions and 10 deletions
89
frontend/src/app/(dashboard)/search/page.test.tsx
Normal file
89
frontend/src/app/(dashboard)/search/page.test.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import SearchPage from './page'
|
||||
import { useSearch } from '@/lib/hooks/use-search'
|
||||
import { useModalManager } from '@/lib/hooks/use-modal-manager'
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
usePathname: () => '/search',
|
||||
useSearchParams: () => new URLSearchParams('mode=search'),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/layout/AppShell', () => ({
|
||||
AppShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/search/NotebookScopeSelector', () => ({
|
||||
NotebookScopeSelector: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/search/StreamingResponse', () => ({
|
||||
StreamingResponse: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/search/AdvancedModelsDialog', () => ({
|
||||
AdvancedModelsDialog: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/search/SaveToNotebooksDialog', () => ({
|
||||
SaveToNotebooksDialog: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/hooks/use-search', () => ({
|
||||
useSearch: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/hooks/use-ask', () => ({
|
||||
useAsk: () => ({
|
||||
sendAsk: vi.fn(),
|
||||
isStreaming: false,
|
||||
strategy: null,
|
||||
answers: [],
|
||||
finalAnswer: '',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/hooks/use-models', () => ({
|
||||
useModelDefaults: () => ({ data: null, isLoading: false }),
|
||||
useModels: () => ({ data: [], isLoading: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/hooks/use-modal-manager', () => ({
|
||||
useModalManager: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockUseSearch = vi.mocked(useSearch)
|
||||
const mockUseModalManager = vi.mocked(useModalManager)
|
||||
const openModal = vi.fn()
|
||||
|
||||
describe('SearchPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseSearch.mockReturnValue({
|
||||
data: {
|
||||
results: [{
|
||||
id: 'source_insight:matched-insight',
|
||||
parent_id: 'source:parent-source',
|
||||
title: 'Matched insight',
|
||||
final_score: 0.9,
|
||||
created: '2026-01-01T00:00:00Z',
|
||||
updated: '2026-01-01T00:00:00Z',
|
||||
}],
|
||||
total_count: 1,
|
||||
search_type: 'text',
|
||||
},
|
||||
isPending: false,
|
||||
mutate: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useSearch>)
|
||||
mockUseModalManager.mockReturnValue({ openModal } as unknown as ReturnType<typeof useModalManager>)
|
||||
})
|
||||
|
||||
it('opens the record that matched instead of its parent source', () => {
|
||||
render(<SearchPage />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Matched insight' }))
|
||||
|
||||
expect(openModal).toHaveBeenCalledWith('insight', 'matched-insight')
|
||||
})
|
||||
})
|
||||
|
|
@ -466,13 +466,7 @@ export default function SearchPage() {
|
|||
) : (
|
||||
<div className="space-y-2">
|
||||
{searchMutation.data.results.map((result, index) => {
|
||||
// Parse type from parent_id (format: "source:id" or "note:id" or "source_insight:id")
|
||||
// Handle null parent_id gracefully (orphaned records)
|
||||
if (!result.parent_id) {
|
||||
console.warn('Search result with null parent_id:', result)
|
||||
return null
|
||||
}
|
||||
const [type, id] = result.parent_id.split(':')
|
||||
const [type, id] = result.id.split(':')
|
||||
const modalType = type === 'source_insight' ? 'insight' : type as 'source' | 'note' | 'insight'
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AddExistingSourceDialog } from './AddExistingSourceDialog'
|
||||
import { searchApi } from '@/lib/api/search'
|
||||
import { sourcesApi } from '@/lib/api/sources'
|
||||
|
||||
vi.mock('use-debounce', () => ({
|
||||
useDebounce: (value: string) => [value],
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/api/search', () => ({
|
||||
searchApi: { search: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/api/sources', () => ({
|
||||
sourcesApi: { list: vi.fn() },
|
||||
}))
|
||||
|
||||
const mutateAsync = vi.fn()
|
||||
vi.mock('@/lib/hooks/use-sources', () => ({
|
||||
useSources: () => ({ data: [] }),
|
||||
useAddSourcesToNotebook: () => ({ mutateAsync, isPending: false }),
|
||||
}))
|
||||
|
||||
const mockSearch = vi.mocked(searchApi.search)
|
||||
const mockList = vi.mocked(sourcesApi.list)
|
||||
|
||||
describe('AddExistingSourceDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockList.mockResolvedValue([])
|
||||
mockSearch.mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
id: 'source:shared',
|
||||
parent_id: 'source:shared',
|
||||
title: 'Direct source match',
|
||||
final_score: 0.9,
|
||||
created: '2026-01-01T00:00:00Z',
|
||||
updated: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'source_insight:child',
|
||||
parent_id: 'source:shared',
|
||||
title: 'Insight from same source',
|
||||
final_score: 0.8,
|
||||
created: '2026-01-01T00:00:00Z',
|
||||
updated: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
total_count: 2,
|
||||
search_type: 'text',
|
||||
})
|
||||
})
|
||||
|
||||
it('shows a source only once when several search hits have the same parent', async () => {
|
||||
render(
|
||||
<AddExistingSourceDialog
|
||||
open={true}
|
||||
onOpenChange={vi.fn()}
|
||||
notebookId="notebook:1"
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('sources.searchPlaceholder'), {
|
||||
target: { value: 'shared' },
|
||||
})
|
||||
|
||||
await waitFor(() => expect(mockSearch).toHaveBeenCalled())
|
||||
expect(screen.getByText('Direct source match')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Insight from same source')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -91,9 +91,12 @@ export function AddExistingSourceDialog({
|
|||
minimum_score: 0.01,
|
||||
})
|
||||
|
||||
// Since we set search_sources=true and search_notes=false,
|
||||
// the API only returns sources, no need to filter
|
||||
const sources = response.results.map(r => ({
|
||||
const sourceIds = new Set<string>()
|
||||
const sources = response.results.filter(r => {
|
||||
if (sourceIds.has(r.parent_id)) return false
|
||||
sourceIds.add(r.parent_id)
|
||||
return true
|
||||
}).map(r => ({
|
||||
id: r.parent_id,
|
||||
title: r.title || 'Untitled',
|
||||
topics: [],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue