fix(session-query): close review edge cases (round 4)
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
normalizeSessionRequest,
|
||||
quoteFtsData,
|
||||
requestFingerprint,
|
||||
SQLITE_MAX_PAGE_LIMIT,
|
||||
type NormalizedEventRequest,
|
||||
type NormalizedSessionRequest,
|
||||
} from '../src/query.ts'
|
||||
@@ -88,6 +89,12 @@ describe('SQLite search request normalization', () => {
|
||||
expect(() => normalizeEventRequest({ sessionId: SessionId('s'), query: 'x', limit }, limits))
|
||||
.toThrow(expectCode('SESSION_QUERY_INVALID_LIMIT'))
|
||||
}
|
||||
expect(() => normalizeEventRequest({
|
||||
sessionId: SessionId('s'),
|
||||
query: 'x',
|
||||
limit: SQLITE_MAX_PAGE_LIMIT + 1,
|
||||
}, { defaultLimit: 1, maxLimit: SQLITE_MAX_PAGE_LIMIT + 1 }))
|
||||
.toThrow(expectCode('SESSION_QUERY_INVALID_LIMIT'))
|
||||
})
|
||||
|
||||
it('materializes owned filter values during normalization', () => {
|
||||
@@ -214,7 +221,8 @@ describe('SQLite query identity and presentation', () => {
|
||||
expect(makeSnippet(`abcde${FTS_HIGHLIGHT_START}f${FTS_HIGHLIGHT_END}`, 1)).toBe('…')
|
||||
expect(makeSnippet('abcdefghij', 5)).toBe('abcd…')
|
||||
expect(makeSnippet(`ab${FTS_HIGHLIGHT_START}c${FTS_HIGHLIGHT_END}defghij`, 5)).toBe('…bcd…')
|
||||
expect(makeSnippet(`abcde${FTS_HIGHLIGHT_START}f${FTS_HIGHLIGHT_END}`, 2)).toBe('a…')
|
||||
expect(makeSnippet(`ab${FTS_HIGHLIGHT_START}c${FTS_HIGHLIGHT_END}defghij`, 3)).toBe('…c…')
|
||||
expect(makeSnippet(`abcde${FTS_HIGHLIGHT_START}f${FTS_HIGHLIGHT_END}`, 2)).toBe('…f')
|
||||
expect(makeSnippet(`abcde${FTS_HIGHLIGHT_START}f${FTS_HIGHLIGHT_END}`, 5)).toBe('…cdef')
|
||||
expect(makeSnippet(` x—${FTS_HIGHLIGHT_START}café${FTS_HIGHLIGHT_END}\n y `, 20))
|
||||
.toBe('x—café y')
|
||||
|
||||
@@ -387,6 +387,8 @@ describe('SQLite session search', () => {
|
||||
{ path: '' },
|
||||
{ path: ':memory:', defaultLimit: 0 },
|
||||
{ path: ':memory:', maxLimit: 0 },
|
||||
{ path: ':memory:', defaultLimit: 1e100 },
|
||||
{ path: ':memory:', maxLimit: 1e100 },
|
||||
{ path: ':memory:', snippetChars: 0 },
|
||||
{ path: ':memory:', defaultLimit: 3, maxLimit: 2 },
|
||||
{ path: ':memory:', journalMode: 'memory' },
|
||||
@@ -462,6 +464,39 @@ describe('SQLite reconciliation and source lifecycle', () => {
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_SESSION_NOT_FOUND'))
|
||||
})
|
||||
|
||||
it('uses the reconciled persistence binding through the query boundary', async () => {
|
||||
const durable = header('post-reconcile-unmount')
|
||||
TestPersistence.reset([{ meta: durable, events: [
|
||||
...messageEvents('durable needle', 1),
|
||||
{ ...messageEvents('durable needle again', 2)[0]!, seq: 1 },
|
||||
] }])
|
||||
const ctx = await liveContext({ path: ':memory:', defaultLimit: 1, maxLimit: 2 })
|
||||
const persistence = await ctx.plugin(TestPersistence)
|
||||
const internals = ctx.sessionSearch as unknown as {
|
||||
_reconcile(signal: AbortSignal | undefined): Promise<{
|
||||
identity: symbol
|
||||
service?: SessionPersistence
|
||||
}>
|
||||
}
|
||||
const reconcile = internals._reconcile.bind(internals)
|
||||
const boundary = vi.spyOn(internals, '_reconcile').mockImplementation(async (signal) => {
|
||||
const binding = await reconcile(signal)
|
||||
await persistence.dispose()
|
||||
return binding
|
||||
})
|
||||
|
||||
const page = await ctx.sessionSearch.searchEvents({
|
||||
sessionId: durable.id,
|
||||
query: 'needle',
|
||||
limit: 1,
|
||||
})
|
||||
expect(page.items).toMatchObject([{ sessionId: durable.id }])
|
||||
expect(page.nextCursor).toEqual(expect.any(String))
|
||||
boundary.mockRestore()
|
||||
await expect(ctx.sessionSearch.searchEvents({ sessionId: durable.id, query: 'needle' }))
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_SESSION_NOT_FOUND'))
|
||||
})
|
||||
|
||||
it('discards a stale list rejection when persistence unmounts during observation', async () => {
|
||||
const durable = header('racing')
|
||||
TestPersistence.reset([{ meta: durable, events: messageEvents('durable needle') }])
|
||||
@@ -561,6 +596,22 @@ describe('SQLite reconciliation and source lifecycle', () => {
|
||||
expect(TestPersistence.loads.get(added.id)).toBe(1)
|
||||
})
|
||||
|
||||
it('fails after one retry when persistence snapshots keep changing', async () => {
|
||||
const durable = header('continuous-mutation')
|
||||
TestPersistence.reset([{ meta: durable, events: messageEvents('durable needle') }])
|
||||
const ctx = await liveContext()
|
||||
await ctx.plugin(TestPersistence)
|
||||
let lists = 0
|
||||
TestPersistence.snapshotEffect = () => {
|
||||
lists += 1
|
||||
TestPersistence.set({ meta: durable, events: messageEvents(`durable needle ${lists}`) })
|
||||
}
|
||||
|
||||
await expect(ctx.sessionSearch.searchSessions({ query: 'needle' }))
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_PERSISTENCE_FAILED'))
|
||||
expect(lists).toBe(4)
|
||||
})
|
||||
|
||||
it('retries if the persistence binding changes while live sessions are observed', async () => {
|
||||
const durable = header('live-boundary-retry')
|
||||
TestPersistence.reset([{ meta: durable, events: messageEvents('durable needle') }])
|
||||
|
||||
Reference in New Issue
Block a user