Skip to content

Commit a47ca82

Browse files
waleedlatif1claude
andcommitted
test(copilot): unit tests for messages dual-write helpers
Cover row shape, ordering, options propagation, ON CONFLICT semantics, and error swallowing for appendCopilotChatMessages / replaceCopilotChatMessages. Also adds copilotChatMessages to the central schemaMock so the imports resolve. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8557d13 commit a47ca82

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
vi.mock('@sim/db', () => dbChainMock)
8+
9+
import {
10+
appendCopilotChatMessages,
11+
replaceCopilotChatMessages,
12+
} from '@/lib/copilot/chat/messages-dual-write'
13+
import type { PersistedMessage } from '@/lib/copilot/chat/persisted-message'
14+
15+
const userMsg: PersistedMessage = {
16+
id: 'msg-user-1',
17+
role: 'user',
18+
content: 'Hello',
19+
timestamp: '2026-01-01T00:00:00.000Z',
20+
}
21+
22+
const assistantMsg: PersistedMessage = {
23+
id: 'msg-asst-1',
24+
role: 'assistant',
25+
content: 'Hi back',
26+
timestamp: '2026-01-01T00:00:01.000Z',
27+
}
28+
29+
describe('messages-dual-write', () => {
30+
beforeEach(() => {
31+
vi.clearAllMocks()
32+
resetDbChainMock()
33+
})
34+
35+
describe('appendCopilotChatMessages', () => {
36+
it('is a no-op on empty array', async () => {
37+
await appendCopilotChatMessages('chat-1', [])
38+
expect(dbChainMockFns.insert).not.toHaveBeenCalled()
39+
})
40+
41+
it('inserts rows built from PersistedMessage shape', async () => {
42+
await appendCopilotChatMessages('chat-1', [userMsg, assistantMsg])
43+
44+
expect(dbChainMockFns.insert).toHaveBeenCalledTimes(1)
45+
expect(dbChainMockFns.values).toHaveBeenCalledTimes(1)
46+
const rows = dbChainMockFns.values.mock.calls[0][0]
47+
expect(rows).toHaveLength(2)
48+
49+
expect(rows[0]).toMatchObject({
50+
chatId: 'chat-1',
51+
messageId: 'msg-user-1',
52+
role: 'user',
53+
content: userMsg,
54+
model: null,
55+
streamId: null,
56+
})
57+
expect(rows[0].createdAt).toEqual(new Date(userMsg.timestamp))
58+
expect(rows[0].updatedAt).toEqual(new Date(userMsg.timestamp))
59+
60+
expect(rows[1]).toMatchObject({
61+
chatId: 'chat-1',
62+
messageId: 'msg-asst-1',
63+
role: 'assistant',
64+
content: assistantMsg,
65+
})
66+
expect(rows[1].createdAt).toEqual(new Date(assistantMsg.timestamp))
67+
})
68+
69+
it('preserves per-message ordering via timestamp', async () => {
70+
await appendCopilotChatMessages('chat-1', [userMsg, assistantMsg])
71+
const rows = dbChainMockFns.values.mock.calls[0][0]
72+
expect(rows[0].createdAt.getTime()).toBeLessThan(rows[1].createdAt.getTime())
73+
})
74+
75+
it('passes chatModel and streamId options to every row', async () => {
76+
await appendCopilotChatMessages('chat-1', [userMsg, assistantMsg], {
77+
chatModel: 'claude-sonnet-4-5',
78+
streamId: 'stream-xyz',
79+
})
80+
81+
const rows = dbChainMockFns.values.mock.calls[0][0]
82+
expect(rows[0].model).toBe('claude-sonnet-4-5')
83+
expect(rows[0].streamId).toBe('stream-xyz')
84+
expect(rows[1].model).toBe('claude-sonnet-4-5')
85+
expect(rows[1].streamId).toBe('stream-xyz')
86+
})
87+
88+
it('uses ON CONFLICT DO UPDATE with chat_id + message_id target', async () => {
89+
await appendCopilotChatMessages('chat-1', [userMsg])
90+
91+
expect(dbChainMockFns.onConflictDoUpdate).toHaveBeenCalledTimes(1)
92+
const conflictArg = dbChainMockFns.onConflictDoUpdate.mock.calls[0][0]
93+
expect(conflictArg.target).toHaveLength(2)
94+
expect(conflictArg.set).toHaveProperty('content')
95+
expect(conflictArg.set).toHaveProperty('role')
96+
expect(conflictArg.set).toHaveProperty('model')
97+
expect(conflictArg.set).toHaveProperty('streamId')
98+
expect(conflictArg.set).toHaveProperty('updatedAt')
99+
})
100+
101+
it('swallows DB errors so the legacy JSONB write stays canonical', async () => {
102+
dbChainMockFns.onConflictDoUpdate.mockRejectedValueOnce(new Error('connection lost'))
103+
104+
await expect(appendCopilotChatMessages('chat-1', [userMsg])).resolves.toBeUndefined()
105+
})
106+
})
107+
108+
describe('replaceCopilotChatMessages', () => {
109+
it('deletes all chat rows when given an empty snapshot', async () => {
110+
await replaceCopilotChatMessages('chat-1', [])
111+
112+
expect(dbChainMockFns.transaction).toHaveBeenCalledTimes(1)
113+
expect(dbChainMockFns.delete).toHaveBeenCalledTimes(1)
114+
expect(dbChainMockFns.insert).not.toHaveBeenCalled()
115+
})
116+
117+
it('deletes only rows whose message_id is not in the new snapshot, then upserts', async () => {
118+
await replaceCopilotChatMessages('chat-1', [userMsg, assistantMsg])
119+
120+
expect(dbChainMockFns.delete).toHaveBeenCalledTimes(1)
121+
expect(dbChainMockFns.insert).toHaveBeenCalledTimes(1)
122+
123+
const rows = dbChainMockFns.values.mock.calls[0][0]
124+
expect(rows).toHaveLength(2)
125+
expect(rows.map((r: { messageId: string }) => r.messageId)).toEqual([
126+
'msg-user-1',
127+
'msg-asst-1',
128+
])
129+
130+
expect(dbChainMockFns.onConflictDoUpdate).toHaveBeenCalledTimes(1)
131+
const conflictArg = dbChainMockFns.onConflictDoUpdate.mock.calls[0][0]
132+
expect(conflictArg.set).toHaveProperty('streamId')
133+
expect(conflictArg.set).toHaveProperty('model')
134+
})
135+
136+
it('passes chatModel to every row in the snapshot', async () => {
137+
await replaceCopilotChatMessages('chat-1', [userMsg], {
138+
chatModel: 'gpt-4o-mini',
139+
})
140+
141+
const rows = dbChainMockFns.values.mock.calls[0][0]
142+
expect(rows[0].model).toBe('gpt-4o-mini')
143+
})
144+
145+
it('swallows DB errors so the legacy JSONB write stays canonical', async () => {
146+
dbChainMockFns.transaction.mockRejectedValueOnce(new Error('tx aborted'))
147+
148+
await expect(replaceCopilotChatMessages('chat-1', [userMsg])).resolves.toBeUndefined()
149+
})
150+
})
151+
})

packages/testing/src/mocks/schema.mock.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,21 @@ export const schemaMock = {
715715
createdAt: 'createdAt',
716716
updatedAt: 'updatedAt',
717717
},
718+
copilotChatMessages: {
719+
id: 'id',
720+
chatId: 'chatId',
721+
messageId: 'messageId',
722+
role: 'role',
723+
content: 'content',
724+
streamId: 'streamId',
725+
parentMessageId: 'parentMessageId',
726+
model: 'model',
727+
tokensIn: 'tokensIn',
728+
tokensOut: 'tokensOut',
729+
deletedAt: 'deletedAt',
730+
createdAt: 'createdAt',
731+
updatedAt: 'updatedAt',
732+
},
718733
copilotWorkflowReadHashes: {
719734
id: 'id',
720735
chatId: 'chatId',

0 commit comments

Comments
 (0)