fix(web): align session search contracts

This commit is contained in:
Hypatia May
2026-07-27 18:38:00 +08:00
parent 621f414407
commit 4727d742db
42 changed files with 235 additions and 196 deletions

View File

@@ -536,12 +536,10 @@ describe('session.search', () => {
expect(searchSessions).toHaveBeenCalledOnce()
})
it('rejects an oversized provider page before iterating its items', async () => {
it('rejects an oversized provider page', async () => {
const ctx = await baseContext()
ctx.sessions.create(sid('visible'), { meta: header('visible') })
const oversized = new Array<SessionSearchHit>(21)
const iterate = vi.fn(() => oversized.values())
Object.defineProperty(oversized, Symbol.iterator, { value: iterate })
const oversized = Array.from({ length: 21 }, (_, index) => hit(`oversized-${index}`))
const searchSessions = vi.fn(() => Promise.resolve({ items: oversized }))
ctx.provide('sessionQuery', { searchSessions } as never)
@@ -554,15 +552,12 @@ describe('session.search', () => {
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error).toMatchObject({ code: 'internal' })
expect(response.result.error.message).toContain('returned 21 items; maximum is 20')
expect(iterate).not.toHaveBeenCalled()
})
it('uses the learned provider limit for the overproduction guard', async () => {
const ctx = await baseContext()
ctx.sessions.create(sid('visible'), { meta: header('visible') })
const oversized = new Array<SessionSearchHit>(11)
const iterate = vi.fn(() => oversized.values())
Object.defineProperty(oversized, Symbol.iterator, { value: iterate })
const oversized = Array.from({ length: 11 }, (_, index) => hit(`oversized-${index}`))
const searchSessions = vi.fn((providerRequest: SessionSearchRequest) => {
if (providerRequest.limit === 20) {
return Promise.reject(new SessionQueryError(
@@ -584,7 +579,6 @@ describe('session.search', () => {
expect(response.result.error).toMatchObject({ code: 'internal' })
expect(response.result.error.message).toContain('returned 11 items; maximum is 10')
expect(searchSessions).toHaveBeenCalledTimes(2)
expect(iterate).not.toHaveBeenCalled()
})
it('bounds provider snippets to 240 Unicode code points without splitting astral text', async () => {
@@ -617,58 +611,6 @@ describe('session.search', () => {
})
})
it('fails closed when the provider returns a non-string snippet', async () => {
const ctx = await baseContext()
const visible = hit('visible')
ctx.sessions.create(visible.header.id, { meta: visible.header })
ctx.provide('sessionQuery', {
searchSessions: () => Promise.resolve({
items: [{
...visible,
bestMatch: { ...visible.bestMatch, snippet: 42 },
}],
}),
} as never)
const response = await createApiProxy(ctx, defaults).sessions.search(
request('malformed-snippet'),
new AbortController().signal,
)
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('internal')
expect(response.result.error.message).toContain('non-string snippet')
expect(response.result).not.toHaveProperty('value')
})
it('inspects only numerically stored items when a compliant page overrides iteration', async () => {
const ctx = await baseContext()
const visible = Array.from({ length: 21 }, (_, index) => hit(`visible-${index}`, index))
for (const item of visible) {
ctx.sessions.create(item.header.id, { meta: item.header })
}
const stored = visible.slice(0, 1)
const iterate = vi.fn(() => visible.values())
Object.defineProperty(stored, Symbol.iterator, { value: iterate })
const searchSessions = vi.fn(() => Promise.resolve({ items: stored }))
ctx.provide('sessionQuery', { searchSessions } as never)
const response = await createApiProxy(ctx, defaults).sessions.search(
request('custom-iterator'),
new AbortController().signal,
)
expect(response.result).toEqual({
ok: true,
value: {
items: [{ sessionId: 'visible-0', snippet: 'match 0' }],
hasMore: false,
},
})
expect(iterate).not.toHaveBeenCalled()
})
it('fails closed when the provider repeats a continuation cursor', async () => {
const ctx = await baseContext()
ctx.sessions.create(sid('visible'), { meta: header('visible') })