fix(import): preserve Discord DM channel identity

Retain the channel ID for stable matching of short and non-overlapping private exports. Add a real SQLite regression test covering repeated import deduplication and incremental append.
This commit is contained in:
digua committed 2026-10-23 21:40:10 +08:00
1 parent 2322f7ea48
commit d1e3cbde47
3 files changed
+67 -2

No files matched your search

@@ -4,6 +4,7 @@ import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { CHAT_DB_SCHEMA } from '@openchatlab/core'
import { parseFileSync } from '@openchatlab/parser'
import { MessageType, type ParsedMessage } from '@openchatlab/shared-types'
import { openBetterSqliteDatabase } from '../better-sqlite3-adapter'
import { resolveAutoImportTarget, resolveAutoImportTargetPlan, type AutoImportMatcherDeps } from './auto-import-matcher'
@@ -186,6 +187,69 @@ test('matches a unique group session by stable group id', async (t) => {
)
})
test('reimports short Discord DMs and appends non-overlapping exports to the same session', async (t) => {
const tempDir = makeTempDir()
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
const sourcePath = path.join(tempDir, 'discord.json')
const writeExport = (id: number) =>
fs.writeFileSync(
sourcePath,
JSON.stringify({
guild: { id: '0', name: 'Direct Messages' },
channel: { id: 'dm-channel-1', type: 'DirectTextChat', name: 'Alice' },
messages: [
{
id: String(id),
type: 'Default',
timestamp: `2026-01-01T00:00:0${id}Z`,
content: `Message ${id}`,
author: { id: 'alice', name: 'Alice', discriminator: '0000', isBot: false, roles: [] },
attachments: [],
embeds: [],
stickers: [],
},
],
}),
'utf8'
)
writeExport(1)
const parsed = await parseFileSync(sourcePath)
const dbPath = path.join(tempDir, 'existing.db')
seedSession(dbPath, parsed.meta, parsed.members, parsed.messages)
for (const id of [1, 2]) {
writeExport(id)
const result = await autoImportFile(sourcePath, {
...createDeps(tempDir, ['existing']),
sessionExists: (sessionId) => sessionId === 'existing',
createSession: async () => {
throw new Error('must not create a duplicate Discord DM')
},
appendSession: (sessionId, filePath) =>
incrementalImport(sessionId, filePath, {
openDatabase: (_sessionId, readonly = false) => openBetterSqliteDatabase(dbPath, { readonly, nativeBinding }),
onProgress: () => {},
}),
})
assert.equal(result.success, true)
assert.equal(result.sessionId, 'existing')
assert.equal(result.importMode, 'incremental')
assert.equal(result.matchedBy, 'stable-id')
assert.equal(result.newMessageCount, id === 1 ? 0 : 1)
assert.equal(result.duplicateCount, id === 1 ? 1 : 0)
}
const db = openBetterSqliteDatabase(dbPath, { readonly: true, nativeBinding })
try {
assert.deepEqual(db.prepare('SELECT platform_message_id FROM message ORDER BY ts').all(), [
{ platform_message_id: '1' },
{ platform_message_id: '2' },
])
} finally {
db.close()
}
})
test('matches a unique private session by stable owner and participant ids', async (t) => {
const tempDir = makeTempDir()
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
@@ -48,7 +48,7 @@ for (const { channelType, guildName, expectedType } of cases) {
assert.equal(result.meta.platform, 'discord')
const isPrivate = expectedType === ChatType.PRIVATE
assert.equal(result.meta.name, isPrivate ? 'Alice' : `${guildName} - Alice`)
assert.equal(result.meta.groupId, isPrivate ? undefined : 'channel-1')
assert.equal(result.meta.groupId, 'channel-1')
assert.equal(result.meta.groupAvatar, isPrivate ? undefined : 'https://example.com/icon.png')
assert.deepEqual(
result.members.map((member) => member.platformId),
@@ -307,7 +307,8 @@ async function* parseDiscordExporter(options: ParseOptions): AsyncGenerator<Pars
name: chatName,
platform: KNOWN_PLATFORMS.DISCORD,
type: isPrivate ? ChatType.PRIVATE : ChatType.GROUP,
groupId: isPrivate ? undefined : channel?.id,
// Keep the channel ID for stable auto-import matching, including direct messages.
groupId: channel?.id,
groupAvatar: isPrivate ? undefined : guild?.iconUrl,
}