fix(web): converge search runtime boundaries (round 8)

This commit is contained in:
Hypatia May
2026-07-27 14:02:35 +08:00
parent 40b68cd8d5
commit ba2925c704
21 changed files with 469 additions and 54 deletions

View File

@@ -0,0 +1,45 @@
/**
* Node 22 startup-output smoke for first-search SQLite opening.
*
* The isolated subprocess omits NODE_OPTIONS so warning suppression cannot
* hide a static node:sqlite import.
*/
import { execFile } from 'node:child_process'
import { resolve } from 'node:path'
import { promisify } from 'node:util'
import { expect, it } from 'vitest'
const execFileAsync = promisify(execFile)
const root = resolve(import.meta.dirname, '../../../..')
it('mounts and disposes first-search mode without a SQLite experimental warning', async () => {
const script = `
import { Context } from 'cordis'
import SessionStore from '@deepseek-ai/dsh-session'
import SessionQuerySqlite from './packages/session-query/session-query-sqlite/src/index.ts'
const ctx = new Context()
const sessions = await ctx.plugin(SessionStore)
const search = await ctx.plugin(SessionQuerySqlite, {
path: ':memory:',
openAt: 'first-search',
})
await search.dispose()
await sessions.dispose()
`
const env = { ...process.env }
delete env.NODE_OPTIONS
const { stderr } = await execFileAsync(process.execPath, [
'--import',
'tsx',
'--input-type=module',
'--eval',
script,
], {
cwd: root,
env,
})
expect(stderr).not.toMatch(/ExperimentalWarning: SQLite/)
})

View File

@@ -177,16 +177,19 @@ async function liveContext(config: ConstructorParameters<typeof SessionQuerySqli
}
describe('SQLite session search', () => {
it('defaults and validates persisted inspection concurrency through its Cordis config', async () => {
it('defaults and validates opening policy and persisted inspection concurrency through its Cordis config', async () => {
const defaultCtx = await liveContext()
expect((defaultCtx.sessionQuery as SessionQuerySqlite).config.openAt).toBe('startup')
expect((defaultCtx.sessionQuery as SessionQuerySqlite).config.persistedInspectConcurrency)
.toBe(SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY)
const configuredValue = 2
const configured = new SessionQuerySqlite.Config({
path: ':memory:',
openAt: 'first-search',
persistedInspectConcurrency: configuredValue,
})
expect(configured.openAt).toBe('first-search')
expect(configured.persistedInspectConcurrency).toBe(configuredValue)
const configuredCtx = await liveContext(configured)
expect((configuredCtx.sessionQuery as SessionQuerySqlite).config.persistedInspectConcurrency)
@@ -198,6 +201,72 @@ describe('SQLite session search', () => {
persistedInspectConcurrency,
})).toThrow()
}
expect(() => new SessionQuerySqlite.Config({
path: ':memory:',
openAt: 'later' as never,
})).toThrow()
})
it('mounts and disposes first-search mode without opening its database', async () => {
const path = await temporaryPath('unopened.db')
const ctx = new Context()
await ctx.plugin(SessionStore)
const search = await ctx.plugin(SessionQuerySqlite, {
path,
openAt: 'first-search',
})
await expect(stat(path)).rejects.toMatchObject({ code: 'ENOENT' })
await search.dispose()
await expect(stat(path)).rejects.toMatchObject({ code: 'ENOENT' })
})
it('opens once on the first search and reuses readiness for later searches', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionQuerySqlite, {
path: ':memory:',
openAt: 'first-search',
})
const service = ctx.sessionQuery as SessionQuerySqlite
const internals = service as unknown as { _open(): Promise<void> }
const open = vi.spyOn(internals, '_open')
await expect(service.searchSessions({ query: 'first' })).resolves.toEqual({ items: [] })
await expect(service.searchSessions({ query: 'second' })).resolves.toEqual({ items: [] })
expect(open).toHaveBeenCalledOnce()
})
it('shares one readiness promise across concurrent first searches', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionQuerySqlite, {
path: ':memory:',
openAt: 'first-search',
})
const service = ctx.sessionQuery as SessionQuerySqlite
const internals = service as unknown as { _open(): Promise<void> }
const originalOpen = internals._open.bind(internals)
const release = Promise.withResolvers<undefined>()
const started = Promise.withResolvers<undefined>()
const open = vi.spyOn(internals, '_open').mockImplementation(async () => {
started.resolve(undefined)
await release.promise
await originalOpen()
})
const first = service.searchSessions({ query: 'first' })
const second = service.searchSessions({ query: 'second' })
await started.promise
expect(open).toHaveBeenCalledOnce()
release.resolve(undefined)
await expect(Promise.all([first, second])).resolves.toEqual([
{ items: [] },
{ items: [] },
])
expect(open).toHaveBeenCalledOnce()
})
it('searches two-character Unicode61 tokens in live-only sessions', async () => {
@@ -522,6 +591,7 @@ describe('SQLite session search', () => {
{ path: ':memory:', persistedInspectConcurrency: 0 },
{ path: ':memory:', persistedInspectConcurrency: Number.MAX_SAFE_INTEGER + 1 },
{ path: ':memory:', defaultLimit: 3, maxLimit: 2 },
{ path: ':memory:', openAt: 'later' },
{ path: ':memory:', journalMode: 'memory' },
]) {
const direct = new Context()
@@ -1238,6 +1308,30 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
}
})
it('defers an invalid database failure only in first-search mode', async () => {
const path = await temporaryPath('lazy-invalid.db')
const foreign = new DatabaseSync(path)
foreign.exec('CREATE TABLE canonical(value TEXT)')
foreign.close()
const lazyCtx = new Context()
await lazyCtx.plugin(SessionStore)
const lazy = await lazyCtx.plugin(SessionQuerySqlite, {
path,
openAt: 'first-search',
})
expect(lazyCtx.sessionQuery).toBeInstanceOf(SessionQuerySqlite)
await expect(lazyCtx.sessionQuery.searchSessions({ query: 'needle' }))
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
await lazy.dispose()
const eagerCtx = new Context()
await eagerCtx.plugin(SessionStore)
await expect(eagerCtx.plugin(SessionQuerySqlite, { path }))
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
expect(eagerCtx.sessionQuery).toBeUndefined()
})
it.each(['sessions', 'events'] as const)(
'forwards one exact reconciliation signal through both snapshot lists and persisted inspection for %s search',
async (scope) => {