|
| 1 | +import { afterAll, beforeAll, expect, test } from 'bun:test'; |
| 2 | +import { testServiceDeps } from '../../../../test/service-deps'; |
| 3 | +import type { ServiceDeps } from '../../../services'; |
| 4 | +import type { AssistantUIMessage } from './message'; |
| 5 | +import { loadMessages, saveMessages } from './persistence'; |
| 6 | + |
| 7 | +const ORGANIZATION_ID = 'chat-persistence-org'; |
| 8 | +const PROJECT_ID = 'chat-persistence-project'; |
| 9 | +const USER_ID = 'chat-persistence-user'; |
| 10 | +const CONVERSATION_ID = 'chat-persistence-conversation'; |
| 11 | + |
| 12 | +let deps: ServiceDeps; |
| 13 | + |
| 14 | +beforeAll(async () => { |
| 15 | + deps = await testServiceDeps(); |
| 16 | + await deps.db.organization.upsert({ |
| 17 | + where: { id: ORGANIZATION_ID }, |
| 18 | + create: { id: ORGANIZATION_ID, name: 'Chat org', timezone: 'UTC' }, |
| 19 | + update: {}, |
| 20 | + }); |
| 21 | + await deps.db.project.upsert({ |
| 22 | + where: { id: PROJECT_ID }, |
| 23 | + create: { id: PROJECT_ID, name: 'Chat', organizationId: ORGANIZATION_ID }, |
| 24 | + update: {}, |
| 25 | + }); |
| 26 | + await deps.db.user.upsert({ |
| 27 | + where: { id: USER_ID }, |
| 28 | + create: { id: USER_ID, email: 'chat-persistence@openpanel.test' }, |
| 29 | + update: {}, |
| 30 | + }); |
| 31 | +}); |
| 32 | + |
| 33 | +afterAll(async () => { |
| 34 | + await deps.db.conversation.deleteMany({ where: { id: CONVERSATION_ID } }); |
| 35 | + await deps.db.project.deleteMany({ where: { id: PROJECT_ID } }); |
| 36 | + await deps.db.organization.deleteMany({ where: { id: ORGANIZATION_ID } }); |
| 37 | + await deps.db.user.deleteMany({ where: { id: USER_ID } }); |
| 38 | +}); |
| 39 | + |
| 40 | +// Anthropic binds a thinking block to the bytes of the conversation before |
| 41 | +// it, so a tool input or output must come back exactly as the model saw it. |
| 42 | +test('message parts survive storage byte for byte, including key order', async () => { |
| 43 | + const message: AssistantUIMessage = { |
| 44 | + id: 'chat-persistence-assistant', |
| 45 | + role: 'assistant', |
| 46 | + parts: [ |
| 47 | + { type: 'step-start' }, |
| 48 | + { |
| 49 | + type: 'reasoning', |
| 50 | + text: 'Look at pages first.', |
| 51 | + providerMetadata: { anthropic: { signature: 'sig-1' } }, |
| 52 | + }, |
| 53 | + { |
| 54 | + type: 'tool-get_top_pages', |
| 55 | + toolCallId: 'call-1', |
| 56 | + state: 'output-available', |
| 57 | + input: { startDate: '2026-10-01', endDate: '2026-10-23', limit: 5 }, |
| 58 | + output: { |
| 59 | + rows: [{ views: 12, path: '/pricing' }], |
| 60 | + longerKeyAfterShort: true, |
| 61 | + a: 1, |
| 62 | + }, |
| 63 | + } as unknown as AssistantUIMessage['parts'][number], |
| 64 | + ], |
| 65 | + }; |
| 66 | + |
| 67 | + await saveMessages(deps, { |
| 68 | + conversationId: CONVERSATION_ID, |
| 69 | + projectId: PROJECT_ID, |
| 70 | + organizationId: ORGANIZATION_ID, |
| 71 | + userId: USER_ID, |
| 72 | + messages: [message], |
| 73 | + }); |
| 74 | + const [loaded] = await loadMessages(deps, CONVERSATION_ID); |
| 75 | + |
| 76 | + expect(JSON.stringify(loaded?.parts)).toBe(JSON.stringify(message.parts)); |
| 77 | +}); |
0 commit comments