fix(session-query): qualify persistence revisions by store

This commit is contained in:
Hypatia May
2026-07-15 12:32:18 +08:00
parent f88ca85ffd
commit 35edf2a825
13 changed files with 192 additions and 31 deletions

View File

@@ -1,7 +1,7 @@
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { existsSync } from 'node:fs'
import { mkdtemp, rm } from 'node:fs/promises'
import { mkdtemp, rm, symlink } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
@@ -245,12 +245,12 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
dbNewer.close()
expect(() => openDatabase(path, 'wal')).toThrow(/incompatible with this build/)
// A stale OLDER version (e.g. a pre-summary-drop v1 DB) is also rejected —
// we do not migrate (unreleased software, no backward-compat).
// The immediately preceding layout lacks the required store identity and is
// rejected rather than migrated (unreleased software, no backward-compat).
const olderPath = await freshDbPath()
openDatabase(olderPath, 'wal').close()
const dbOlder = openDatabase(olderPath, 'wal')
dbOlder.exec('PRAGMA user_version = 1')
dbOlder.exec(`PRAGMA user_version = ${SCHEMA_VERSION - 1}`)
dbOlder.close()
expect(() => openDatabase(olderPath, 'wal')).toThrow(/incompatible with this build/)
})
@@ -337,8 +337,46 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
await fiber2.dispose()
})
it('source-qualifies revisions across stores while preserving same-file reopen identity', async () => {
const pathA = await freshDbPath()
const pathB = await freshDbPath()
const m = meta('revision-source')
const a = await backend(pathA)
await a.ctx.sessionPersistence.create(m)
await a.ctx.sessionPersistence.append(m.id, oneTurnLog())
const revisionA = (await a.ctx.sessionPersistence.listSnapshots())[0]?.revision
await a.dispose()
const probeA = openDatabase(pathA, 'wal')
const storeIdA = (probeA.prepare(
'SELECT store_id FROM persistence_state WHERE singleton = 1',
).get() as { store_id: string }).store_id
probeA.close()
const aliasA = `${pathA}.alias`
await symlink(pathA, aliasA)
const reopenedA = await backend(aliasA)
expect((await reopenedA.ctx.sessionPersistence.listSnapshots())[0]?.revision).toBe(revisionA)
await reopenedA.dispose()
const b = await backend(pathB)
await b.ctx.sessionPersistence.create(m)
await b.ctx.sessionPersistence.append(m.id, oneTurnLog())
const revisionB = (await b.ctx.sessionPersistence.listSnapshots())[0]?.revision
const probeB = openDatabase(pathB, 'wal')
const storeIdB = (probeB.prepare(
'SELECT store_id FROM persistence_state WHERE singleton = 1',
).get() as { store_id: string }).store_id
probeB.close()
expect(storeIdB).not.toBe(storeIdA)
expect(revisionB).not.toBe(revisionA)
expect(String(revisionA)).toMatch(/:revision:1$/)
expect(String(revisionB)).toMatch(/:revision:1$/)
await b.dispose()
})
it('exposes the schema version constant', () => {
expect(SCHEMA_VERSION).toBe(5)
expect(SCHEMA_VERSION).toBe(6)
})
it('keeps the revision stable for an empty repair hook', async () => {
@@ -354,6 +392,17 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
})
describe('SessionPersistenceSqlite: edge cases', () => {
it('rejects and closes a current-schema database with an invalid store identity', async () => {
const path = await freshDbPath()
const db = openDatabase(path, 'wal')
db.exec("UPDATE persistence_state SET store_id = '' WHERE singleton = 1")
db.close()
const b = await backend(path)
await expect(b.ctx.sessionPersistence.listSnapshots()).rejects.toThrow(/no valid store identity/)
await expect(b.dispose()).resolves.toBeUndefined()
})
it('append rolls back and rethrows when an event INSERT fails inside the transaction', async () => {
const path = await freshDbPath()
const m = meta('rollback-insert')