|
@@ -1,5 +1,5 @@
|
|
|
// @vitest-environment node
|
|
// @vitest-environment node
|
|
|
-import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
|
|
|
|
|
+import { mkdtempSync, rmSync } from 'node:fs'
|
|
|
import { tmpdir } from 'node:os'
|
|
import { tmpdir } from 'node:os'
|
|
|
import { join } from 'node:path'
|
|
import { join } from 'node:path'
|
|
|
import { DatabaseSync } from 'node:sqlite'
|
|
import { DatabaseSync } from 'node:sqlite'
|
|
@@ -30,82 +30,6 @@ afterEach(() => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
describe('durable source=web chat cache', () => {
|
|
describe('durable source=web chat cache', () => {
|
|
|
- it.each(['prefer-local', 'shadow', 'upstream-only'] as const)(
|
|
|
|
|
- 'preserves sessions and cached files after repeated read 404s in %s mode', async mode => {
|
|
|
|
|
- const f = fixture()
|
|
|
|
|
- const coordinator = new ChatCacheCoordinator(f.store, f.coordinator.upstream, mode)
|
|
|
|
|
- const sourcePath = '/tmp/final-report.png'
|
|
|
|
|
- try {
|
|
|
|
|
- f.store.recordCommand(owner, profile, sessionID, 'session.create', { source: 'web' })
|
|
|
|
|
- f.store.putSnapshot(owner, 'tail', 'messages', profile, sessionID, response({
|
|
|
|
|
- messages: [{ id: 'latest', role: 'assistant', content: `` }],
|
|
|
|
|
- pagination: { total: 2, offset: 0, has_more: true },
|
|
|
|
|
- }))
|
|
|
|
|
- f.store.storeAttachment(owner, sourcePath, response('report-bytes', 200, 'image/png'))
|
|
|
|
|
- const asset = f.store.attachment(owner, sourcePath)!
|
|
|
|
|
- const changed = vi.fn()
|
|
|
|
|
- coordinator.onSynchronized = changed
|
|
|
|
|
- const result = await coordinator.messages(owner, 'older', profile, sessionID, 1, 1, async () => {
|
|
|
|
|
- f.store.recordCommand(owner, profile, sessionID, 'prompt.submit', { text: '新消息' })
|
|
|
|
|
- return response({ detail: 'Session not found' }, 404)
|
|
|
|
|
- })
|
|
|
|
|
- expect(result.response.status).toBe(404)
|
|
|
|
|
- expect(f.store.ownsSession(owner, profile, sessionID)).toBe(true)
|
|
|
|
|
- expect(JSON.parse(f.store.messagePage(owner, profile, sessionID, 0, 1)!.response.body.toString()).messages)
|
|
|
|
|
- .toEqual([expect.objectContaining({ id: 'latest' })])
|
|
|
|
|
- expect(readFileSync(asset.localPath, 'utf8')).toBe('report-bytes')
|
|
|
|
|
- expect(changed).not.toHaveBeenCalled()
|
|
|
|
|
-
|
|
|
|
|
- await coordinator.messages(owner, 'older', profile, sessionID, 1, 1,
|
|
|
|
|
- async () => response({ detail: 'Session not found' }, 404))
|
|
|
|
|
- expect(f.store.ownsSession(owner, profile, sessionID)).toBe(true)
|
|
|
|
|
- expect(readFileSync(asset.localPath, 'utf8')).toBe('report-bytes')
|
|
|
|
|
- expect(changed).not.toHaveBeenCalled()
|
|
|
|
|
- } finally { f.store.close() }
|
|
|
|
|
- },
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- it('serves older cached sessions without probing or automatically cleaning them', async () => {
|
|
|
|
|
- const f = fixture()
|
|
|
|
|
- try {
|
|
|
|
|
- f.store.recordCommand(owner, profile, sessionID, 'session.create', { source: 'web' })
|
|
|
|
|
- f.store.putSnapshot(owner, 'detail', 'detail', profile, sessionID, response({ id: sessionID, title: '保留' }))
|
|
|
|
|
- f.store.db.prepare('UPDATE chat_sessions SET owned_at=?').run(Date.now() - 120_000)
|
|
|
|
|
- const load = vi.fn(async () => response({ sessions: [], total: 0 }))
|
|
|
|
|
- vi.mocked(f.coordinator.upstream.request).mockResolvedValue(response({}, 404))
|
|
|
|
|
- const result = await f.coordinator.read(owner, 'list', 'list', profile, undefined, load, true)
|
|
|
|
|
- expect(result.source).toBe('local')
|
|
|
|
|
- expect(JSON.parse(result.response.body.toString()).sessions).toContainEqual(expect.objectContaining({ id: sessionID }))
|
|
|
|
|
- expect(load).not.toHaveBeenCalled()
|
|
|
|
|
- expect(f.coordinator.upstream.request).not.toHaveBeenCalled()
|
|
|
|
|
- expect(f.store.ownsSession(owner, profile, sessionID)).toBe(true)
|
|
|
|
|
- } finally { f.store.close() }
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- it('retains a cached session when reconciliation receives a 404', async () => {
|
|
|
|
|
- const f = fixture()
|
|
|
|
|
- f.store.recordCommand(owner, profile, sessionID, 'session.create', { source: 'web' })
|
|
|
|
|
- f.store.putSnapshot(owner, 'detail', 'detail', profile, sessionID, response({ id: sessionID, title: 'ghost' }))
|
|
|
|
|
- vi.mocked(f.coordinator.upstream.request).mockResolvedValue(response({ detail: 'Session not found' }, 404))
|
|
|
|
|
- await expect(f.coordinator.reconcile(owner, profile, sessionID)).rejects.toThrow('Unable to synchronize conversation metadata')
|
|
|
|
|
- expect(f.store.ownsSession(owner, profile, sessionID)).toBe(true)
|
|
|
|
|
- expect(f.store.localDetail(owner, profile, sessionID)).toBeDefined()
|
|
|
|
|
- f.store.close()
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- it('keeps acknowledged flags across aliases and rejects a deleted session sync', () => {
|
|
|
|
|
- const f = fixture()
|
|
|
|
|
- f.store.recordCommand(owner, profile, sessionID, 'session.create', { source: 'web' })
|
|
|
|
|
- f.store.putSnapshot(owner, 'detail', 'detail', profile, sessionID, response({ id: sessionID, is_pinned: false }))
|
|
|
|
|
- const revision = f.store.revision(owner, profile, sessionID)
|
|
|
|
|
- f.store.recordSessionPatch(owner, profile, sessionID, { pinned: true })
|
|
|
|
|
- expect(JSON.parse(f.store.localDetail(owner, profile, sessionID)!.response.body.toString()).pinned).toBe(true)
|
|
|
|
|
- expect(f.store.applySync(owner, profile, sessionID, revision, [], false)).toBe(false)
|
|
|
|
|
- f.store.deleteSession(owner, profile, sessionID)
|
|
|
|
|
- expect(f.store.applySync(owner, profile, sessionID, 0, [], true)).toBe(false)
|
|
|
|
|
- f.store.close()
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
it('keeps session metadata when message envelopes only contain session_id and advances activity from messages', () => {
|
|
it('keeps session metadata when message envelopes only contain session_id and advances activity from messages', () => {
|
|
|
const f = fixture()
|
|
const f = fixture()
|
|
|
f.store.recordRoute(owner, profile, sessionID, 'runtime-web')
|
|
f.store.recordRoute(owner, profile, sessionID, 'runtime-web')
|