283 lines
8.3 KiB
TypeScript
283 lines
8.3 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
|
|
import type { ChatMessage } from '../types/chat'
|
|
import { mapBackendMessage } from '../lib/collabSync'
|
|
import { analyzeCheckpointMessages } from './checkpointUtils'
|
|
import { __chatStoreTestUtils } from './ChatStore'
|
|
|
|
const persistentTimestamps = __chatStoreTestUtils.latestPersistentMessageTimestamps([
|
|
{
|
|
id: 'db-assistant-1',
|
|
channelId: 'session:read-test',
|
|
sender: 'assistant',
|
|
senderName: 'OPC',
|
|
content: 'First persisted reply',
|
|
timestamp: 10,
|
|
mentions: [],
|
|
},
|
|
{
|
|
id: 'msg-local-only',
|
|
channelId: 'session:read-test',
|
|
sender: 'user',
|
|
senderName: 'You',
|
|
content: 'Optimistic message',
|
|
timestamp: 30,
|
|
mentions: [],
|
|
metadata: { ui_message_id: 'ui-local-only' },
|
|
},
|
|
{
|
|
id: 'db-assistant-2',
|
|
channelId: 'session:read-test',
|
|
sender: 'assistant',
|
|
senderName: 'OPC',
|
|
content: 'Latest persisted reply',
|
|
timestamp: 20,
|
|
mentions: [],
|
|
},
|
|
])
|
|
|
|
assert.equal(persistentTimestamps['session:read-test'], 20)
|
|
|
|
const unreadState = { 'session:read-test': 10 }
|
|
const advancedReadState = __chatStoreTestUtils.advanceReadTimestamp(
|
|
unreadState,
|
|
'session:read-test',
|
|
persistentTimestamps['session:read-test'],
|
|
)
|
|
assert.notEqual(advancedReadState, unreadState)
|
|
assert.equal(advancedReadState['session:read-test'], 20)
|
|
assert.equal(
|
|
__chatStoreTestUtils.advanceReadTimestamp(advancedReadState, 'session:read-test', 20),
|
|
advancedReadState,
|
|
'marking an already-read channel must preserve state identity',
|
|
)
|
|
assert.equal(
|
|
__chatStoreTestUtils.advanceReadTimestamp(advancedReadState, 'session:read-test', 15),
|
|
advancedReadState,
|
|
'an older snapshot must not move the read cursor backwards',
|
|
)
|
|
|
|
const syntheticCheckpoint: ChatMessage = {
|
|
id: 'checkpoint::cp-delivery',
|
|
channelId: 'session:task-1',
|
|
sender: 'assistant',
|
|
senderName: 'Company Member',
|
|
content: 'Human review requested.',
|
|
timestamp: 1,
|
|
mentions: [],
|
|
metadata: {
|
|
checkpoint_type: 'company_delivery_feedback',
|
|
checkpoint_id: 'cp-delivery',
|
|
summary: 'Pending review',
|
|
},
|
|
}
|
|
|
|
const backendCheckpointUpdate: ChatMessage = {
|
|
id: 'db-message-1',
|
|
channelId: 'session:task-1',
|
|
sender: 'assistant',
|
|
senderName: 'Company Member',
|
|
content: 'Human review requested.',
|
|
timestamp: 2,
|
|
mentions: [],
|
|
metadata: {
|
|
checkpoint_type: 'company_delivery_feedback',
|
|
checkpoint_id: 'cp-delivery',
|
|
checkpoint_status: 'ignored',
|
|
checkpoint_reply_kind: 'ignore',
|
|
},
|
|
}
|
|
|
|
const mergedCheckpoint = __chatStoreTestUtils.dedupeMessages([
|
|
syntheticCheckpoint,
|
|
backendCheckpointUpdate,
|
|
])
|
|
|
|
assert.equal(mergedCheckpoint.length, 1)
|
|
assert.equal(mergedCheckpoint[0].id, 'db-message-1')
|
|
assert.equal(mergedCheckpoint[0].timestamp, 1, 'checkpoint status updates must keep their original timeline position')
|
|
assert.equal(mergedCheckpoint[0].metadata?.checkpoint_status, 'ignored')
|
|
assert.deepEqual([...analyzeCheckpointMessages(mergedCheckpoint).pendingMessageIds], [])
|
|
assert.deepEqual([...analyzeCheckpointMessages(mergedCheckpoint).respondedMessageIds], ['db-message-1'])
|
|
|
|
const terminalSyntheticCheckpoint: ChatMessage = {
|
|
...syntheticCheckpoint,
|
|
timestamp: 2,
|
|
metadata: {
|
|
...syntheticCheckpoint.metadata,
|
|
checkpoint_status: 'ignored',
|
|
checkpoint_reply_kind: 'ignore',
|
|
},
|
|
}
|
|
|
|
const mergedSameIdCheckpoint = __chatStoreTestUtils.dedupeMessages([
|
|
syntheticCheckpoint,
|
|
terminalSyntheticCheckpoint,
|
|
])
|
|
|
|
assert.equal(mergedSameIdCheckpoint.length, 1)
|
|
assert.equal(mergedSameIdCheckpoint[0].id, 'checkpoint::cp-delivery')
|
|
assert.equal(mergedSameIdCheckpoint[0].metadata?.checkpoint_status, 'ignored')
|
|
assert.deepEqual([...analyzeCheckpointMessages(mergedSameIdCheckpoint).pendingMessageIds], [])
|
|
|
|
const optimisticUserMessage: ChatMessage = {
|
|
id: 'msg-local',
|
|
channelId: 'session:task-1',
|
|
sender: 'user',
|
|
senderName: 'You',
|
|
content: 'New requirement',
|
|
timestamp: 3,
|
|
mentions: [],
|
|
metadata: {
|
|
ui_message_id: 'ui-1',
|
|
},
|
|
}
|
|
|
|
const backendUserMessage: ChatMessage = {
|
|
id: 'db-user-1',
|
|
channelId: 'session:task-1',
|
|
sender: 'user',
|
|
senderName: 'You',
|
|
content: 'New requirement',
|
|
timestamp: 4,
|
|
mentions: [],
|
|
metadata: {
|
|
ui_message_id: 'ui-1',
|
|
},
|
|
}
|
|
|
|
const mergedUserMessage = __chatStoreTestUtils.dedupeMessages([
|
|
optimisticUserMessage,
|
|
backendUserMessage,
|
|
])
|
|
|
|
assert.equal(mergedUserMessage.length, 1)
|
|
assert.equal(mergedUserMessage[0].metadata?.ui_message_id, 'ui-1')
|
|
assert.equal(mergedUserMessage[0].timestamp, 4, 'backend acknowledgement must replace the optimistic client clock')
|
|
|
|
const mirroredUserMessages = __chatStoreTestUtils.dedupeMessages([
|
|
backendUserMessage,
|
|
{ ...backendUserMessage, id: 'db-user-mirror', channelId: 'session:child-task' },
|
|
])
|
|
assert.equal(
|
|
mirroredUserMessages.length,
|
|
2,
|
|
'ui_message_id mirrors in different channels must remain available to each channel projection',
|
|
)
|
|
|
|
assert.deepEqual(
|
|
__chatStoreTestUtils.unreadMessageCounts([
|
|
{
|
|
id: 'msg-local-system',
|
|
channelId: 'session:task-1',
|
|
sender: 'system',
|
|
senderName: 'System',
|
|
content: 'Local task assignment notice',
|
|
timestamp: 100,
|
|
mentions: [],
|
|
},
|
|
{
|
|
id: 'db-assistant-unread',
|
|
channelId: 'session:task-1',
|
|
sender: 'assistant',
|
|
senderName: 'OPC',
|
|
content: 'Persisted reply',
|
|
timestamp: 90,
|
|
mentions: [],
|
|
},
|
|
], {}),
|
|
{ 'session:task-1': 1 },
|
|
'local-only system rows must not become unread entries that markRead can never cover',
|
|
)
|
|
|
|
const nativeCompanyRawTurn: ChatMessage = {
|
|
id: 'native-raw-1',
|
|
channelId: 'session:task-1',
|
|
sender: 'assistant',
|
|
senderName: 'Task Generalist',
|
|
content: '最终分析已经完成,结论如下。',
|
|
timestamp: 5,
|
|
mentions: [],
|
|
metadata: {
|
|
source: 'engine',
|
|
transcript_kind: 'runtime_v2_assistant',
|
|
},
|
|
}
|
|
|
|
const companyRoleResult: ChatMessage = {
|
|
id: 'role-result-1',
|
|
channelId: 'session:task-1',
|
|
sender: 'chao',
|
|
senderName: 'Chao',
|
|
content: '最终分析已经完成,结论如下。',
|
|
timestamp: 6,
|
|
mentions: [],
|
|
metadata: {
|
|
source: 'engine',
|
|
transcript_kind: 'company_role_result',
|
|
},
|
|
}
|
|
|
|
const mergedNativeCompanyDuplicate = __chatStoreTestUtils.dedupeMessages([
|
|
nativeCompanyRawTurn,
|
|
companyRoleResult,
|
|
])
|
|
|
|
assert.equal(mergedNativeCompanyDuplicate.length, 1)
|
|
assert.equal(mergedNativeCompanyDuplicate[0].id, 'role-result-1')
|
|
assert.equal(mergedNativeCompanyDuplicate[0].senderName, 'Chao')
|
|
assert.equal(mergedNativeCompanyDuplicate[0].timestamp, 5, 'semantic result replacement must retain its original timeline position')
|
|
assert.equal(
|
|
mergedNativeCompanyDuplicate[0].metadata?.ui_timeline_id,
|
|
'message:native-raw-1',
|
|
'semantic result replacement must retain the already-mounted row identity',
|
|
)
|
|
const repeatedNativeCompanySync = __chatStoreTestUtils.dedupeMessages([
|
|
...mergedNativeCompanyDuplicate,
|
|
nativeCompanyRawTurn,
|
|
companyRoleResult,
|
|
])
|
|
assert.equal(repeatedNativeCompanySync.length, 1)
|
|
assert.equal(repeatedNativeCompanySync[0].metadata?.ui_timeline_id, 'message:native-raw-1')
|
|
assert.equal(repeatedNativeCompanySync[0].timestamp, 5)
|
|
|
|
const mountedHighPriorityResult: ChatMessage = {
|
|
...companyRoleResult,
|
|
id: 'mounted-high-result',
|
|
timestamp: 10,
|
|
metadata: { source: 'engine', transcript_kind: 'child_task_result' },
|
|
}
|
|
const olderLowPrioritySurface: ChatMessage = {
|
|
...nativeCompanyRawTurn,
|
|
id: 'older-low-result',
|
|
timestamp: 4,
|
|
}
|
|
const historyBackfillMerge = __chatStoreTestUtils.mergeMessagesIntoExisting(
|
|
[mountedHighPriorityResult],
|
|
[olderLowPrioritySurface],
|
|
)
|
|
assert.equal(historyBackfillMerge.length, 1)
|
|
assert.equal(historyBackfillMerge[0].id, 'mounted-high-result')
|
|
assert.equal(historyBackfillMerge[0].timestamp, 10, 'history backfill must not move an already-mounted result row')
|
|
assert.equal(
|
|
historyBackfillMerge[0].metadata?.ui_timeline_id,
|
|
'message:mounted-high-result',
|
|
'history backfill must retain the mounted high-priority result key',
|
|
)
|
|
|
|
const mappedTaskGeneralistMessage = mapBackendMessage({
|
|
message_id: 'legacy-task-generalist',
|
|
channel_id: 'session:task-1',
|
|
sender: 'task_generalist',
|
|
sender_name: 'Task Generalist',
|
|
content: 'Legacy native task result.',
|
|
created_at: 10,
|
|
metadata: {
|
|
transcript_kind: 'runtime_v2_company_assistant',
|
|
},
|
|
})
|
|
|
|
assert.equal(mappedTaskGeneralistMessage.senderName, 'OPC')
|
|
|
|
console.log('ChatStore.test.ts: OK (read cursors, optimistic, checkpoint, and company result identity merging)')
|