fix(session-query): harden SQLite search reconciliation

This commit is contained in:
Hypatia May
2026-07-15 12:10:24 +08:00
parent ecf90ff382
commit f88ca85ffd
40 files changed
+1315 -227

No files matched your search

@@ -10,6 +10,8 @@ import SessionQueryService, {
extractSessionEventText,
filterSessionEventDocuments,
filterSessionResults,
materializeSessionEventResultFilters,
materializeSessionResultFilters,
SessionSearchService,
type SessionEventSearchHit,
type SessionEventSearchRequest,
@@ -177,6 +179,31 @@ describe('session-query document and filter helpers', () => {
expect(() => buildSessionEventRecords(id, malformed)).toThrow(expectCode('SESSION_QUERY_INVALID_SURFACE'))
})
it('owns filters and rejects malformed runtime filter shapes deterministically', () => {
expect(materializeSessionResultFilters([{ kind: 'created-at', to: 2 }]))
.toEqual([{ kind: 'created-at', to: 2 }])
expect(() => materializeSessionResultFilters('not-an-array' as never))
.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
expect(() => materializeSessionResultFilters([{ kind: 'id', values: 'bad' } as never]))
.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
expect(() => materializeSessionResultFilters([{ kind: 'id', values: [1] } as never]))
.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
expect(() => materializeSessionResultFilters([{ kind: 'cwd', values: 'bad' } as never]))
.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
expect(() => materializeSessionResultFilters([{ kind: 'parent', values: [1] } as never]))
.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
expect(() => materializeSessionResultFilters([{} as never]))
.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
expect(() => materializeSessionEventResultFilters([{ kind: 'text', text: 1 } as never]))
.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
expect(() => materializeSessionEventResultFilters([{ kind: 'future' } as never]))
.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
expect(() => filterSessionResults([], [{ kind: 'future' } as never]))
.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
expect(() => filterSessionEventDocuments([], [{ kind: 'future' } as never]))
.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
})
it('exposes the scan path on the concrete exact-read service', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
@@ -1,9 +1,10 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context, type Fiber } from 'cordis'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader, SessionId as SessionIdType } from '@deepseek-ai/dsh-session'
import SessionPersistence from '@deepseek-ai/dsh-session-persistence'
import SessionPersistence, { SessionPersistenceRevision } from '@deepseek-ai/dsh-session-persistence'
import SessionQueryService, {
type SessionEventSurface,
type SessionQueryErrorCode,
} from '@deepseek-ai/dsh-session-query'
@@ -59,6 +60,14 @@ class TestPersistence extends SessionPersistence {
TestPersistence.afterList?.()
return Promise.resolve(headers)
}
async listSnapshots() {
return [...TestPersistence.entries.values()].map(entry => ({
header: structuredClone(entry.meta),
revision: SessionPersistenceRevision(`events:${entry.events.length}`),
}))
}
}
async function liveContext(config: ConstructorParameters<typeof SessionQueryService>[1] = {}): Promise<Context> {
@@ -94,6 +103,38 @@ describe('session-query exact reads', () => {
expect(older.header.createdAt).toBe(1)
})
it('filters sessions symmetrically and owns mutable filter values immediately', async () => {
const durable = header('durable-filter', 1)
TestPersistence.reset([{ meta: durable, events: eventLog('durable') }])
const ctx = await liveContext()
const live = ctx.sessions.create(SessionId('live-filter'), { meta: { createdAt: 2 } })
live.append(
'user/message',
{ content: [{ type: 'text', text: 'live' }], source: { kind: 'user' } },
{ surfaceOp: 'append' },
)
const persistence = await ctx.plugin(TestPersistence)
const ids = [durable.id]
const filtered = ctx.sessionQuery.filterSessions([{ kind: 'id', values: ids }])
ids[0] = live.id
await expect(filtered).resolves.toEqual([{
header: durable,
live: false,
persisted: true,
}])
const surfaces: SessionEventSurface[] = ['current']
const events = ctx.sessionQuery.filterEvents(live.id, [{ kind: 'surface', values: surfaces }])
surfaces[0] = 'shadowed'
await expect(events).resolves.toMatchObject([{ sessionId: live.id, surface: 'current', text: 'live' }])
await expect(ctx.sessionQuery.filterSessions([{ kind: 'future' } as never]))
.rejects.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
await expect(ctx.sessionQuery.filterEvents(live.id, [{ kind: 'future' } as never]))
.rejects.toThrow(expectCode('SESSION_QUERY_INVALID_FILTER'))
await persistence.dispose()
})
it('classifies current, shadowed, and raw-log-only events through foldSurface', async () => {
const ctx = await liveContext()
const session = ctx.sessions.create(SessionId('surface'))
@@ -267,4 +308,26 @@ describe('session-query exact reads', () => {
await fiber.dispose()
expect(ctx.sessionQuery).toBeUndefined()
})
it('awaits optional-persistence child-fiber quiescence on disposal', async () => {
TestPersistence.reset()
const ctx = new Context()
await ctx.plugin(SessionStore)
const query = await ctx.plugin(SessionQueryService)
const persistence = await ctx.plugin(TestPersistence)
const optional = (ctx.sessionQuery as unknown as {
_corpus: { _optionalPersistenceFiber: Fiber }
})._corpus._optionalPersistenceFiber
let release!: () => void
const cleanup = new Promise<void>((resolve) => { release = resolve })
optional.ctx.effect(() => () => cleanup)
let settled = false
const disposing = query.dispose().then(() => { settled = true })
await Promise.resolve()
expect(settled).toBe(false)
release()
await disposing
await persistence.dispose()
})
})