fix(web): support large search corpora (round 4)

This commit is contained in:
Hypatia May
2026-07-27 13:05:59 +08:00
parent bd42204e53
commit 0dc66d5b77
8 changed files with 96 additions and 39 deletions

View File

@@ -107,7 +107,6 @@ describe('session.search', () => {
},
},
],
nextCursor: 'more' as never,
}))
ctx.provide('sessionQuery', { searchSessions } as never)
const api = createApiProxy(ctx, defaults)
@@ -119,7 +118,7 @@ describe('session.search', () => {
ok: true,
value: {
items: [{ sessionId: 'cold', snippet: 'the matching answer' }],
hasMore: true,
hasMore: false,
},
})
expect(searchSessions).toHaveBeenCalledOnce()
@@ -129,7 +128,6 @@ describe('session.search', () => {
]
expect(query).toEqual({
query: 'matching answer',
sessionFilters: [{ kind: 'id', values: ['live', 'cold'] }],
eventFilters: [
{
kind: 'type',
@@ -179,7 +177,6 @@ describe('session.search', () => {
withBestMatch(2, { type: 'tool/result' }),
withBestMatch(3, { type: 'steering/message', snippet: 'allowed snippet' }),
],
nextCursor: 'more',
}),
} as never)
@@ -192,19 +189,25 @@ describe('session.search', () => {
ok: true,
value: {
items: [{ sessionId: 'visible', snippet: 'allowed snippet' }],
hasMore: true,
hasMore: false,
},
})
})
it('enforces the 20-item Host boundary even if a provider overproduces', async () => {
it('pages the globally ranked stream until the 20-item Host boundary is known', async () => {
const ctx = await baseContext()
const items = Array.from({ length: 21 }, (_, index) => hit(`visible-${index}`, index))
for (const item of items) {
ctx.sessions.create(item.header.id, { meta: item.header })
}
const searchSessions = vi.fn()
.mockResolvedValueOnce({
items: [hit('hidden-ranked-first'), ...items.slice(0, 19)],
nextCursor: 'page-2',
})
.mockResolvedValueOnce({ items: items.slice(19) })
ctx.provide('sessionQuery', {
searchSessions: () => Promise.resolve({ items }),
searchSessions,
} as never)
const response = await createApiProxy(ctx, defaults).sessions.search(
request('match'),
@@ -218,6 +221,39 @@ describe('session.search', () => {
if (!response.result.ok) throw new Error('unreachable')
expect(response.result.value.items).toHaveLength(20)
expect(response.result.value.items.at(-1)?.sessionId).toBe('visible-19')
expect(searchSessions).toHaveBeenCalledTimes(2)
expect(searchSessions.mock.calls[1]?.[0]).toMatchObject({ cursor: 'page-2' })
})
it('keeps visibility sets above SQLite variable limits out of provider bindings', async () => {
const ctx = await baseContext()
const cold = Array.from(
{ length: 32_751 },
(_, index) => header(`cold-${index}`, `/cold-${index}`),
)
ctx.provide('sessionPersistence', {
list: () => Promise.resolve(cold),
locate: () => undefined,
} as never)
const searchSessions = vi.fn(() => Promise.resolve({
items: [hit('cold-32750')],
}))
ctx.provide('sessionQuery', { searchSessions } as never)
const response = await createApiProxy(ctx, defaults).sessions.search(
request('large corpus'),
new AbortController().signal,
)
expect(response.result).toEqual({
ok: true,
value: {
items: [{ sessionId: 'cold-32750', snippet: 'match 0' }],
hasMore: false,
},
})
expect(searchSessions).toHaveBeenCalledOnce()
expect(searchSessions.mock.calls[0]?.[0]).not.toHaveProperty('sessionFilters')
})
it('propagates cancellation through visible-session collection and stops cold-summary work', async () => {