import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { afterAll, describe, expect, it, vi } from 'vitest'
import type { GroupDailyReport, GroupReportMetadata } from '../../src/shared/group-report'
const root = mkdtempSync(join(tmpdir(), 'tracememo-report-history-'))
vi.mock('electron', () => ({
app: { getPath: () => root },
nativeImage: {
createFromPath: () => ({
isEmpty: () => false,
getSize: () => ({ width: 430, height: 1200 })
})
}
}))
const reportSnapshot = {
overview: '已生成的结构化日报',
topics: [],
resources: [],
importantMessages: [],
quotes: [],
qa: [],
todos: [],
unresolved: [],
storylines: [],
reversals: [],
participantChains: [],
analytics: {
topicHeat: [],
activeTimeline: '',
topSpeakers: [],
voiceLeaderboard: []
},
keywords: [],
media: { gallery: [], visionGallery: [], voiceHighlights: [], funBadges: [] }
} satisfies GroupDailyReport
const reportMetadata = {
groupName: '测试群',
reportDate: '2026-08-12',
dateRange: '今天',
messageCount: 10,
activeUsers: 3,
timeSpan: '09:00–18:00',
generatedAt: '2026-08-12T10:00:00.000Z',
recordNote: '',
footerNote: '',
heroParticipants: [],
avatars: {}
} satisfies GroupReportMetadata
describe('generated report template history', () => {
afterAll(() => {
rmSync(root, { recursive: true, force: true })
})
it('replaces the current report assets while preserving its structured snapshot and id', async () => {
const originalHtml = join(root, 'original.html')
const switchedHtml = join(root, 'switched.html')
writeFileSync(originalHtml, '
Mobile 01
')
writeFileSync(switchedHtml, 'Mobile 03
')
const { saveGeneratedReport, updateGeneratedReportTemplate } =
await import('../../src/main/report-history-service')
const saved = await saveGeneratedReport({
contactId: 'group-md5',
contactName: '测试群',
dateRange: '今天',
messageCount: 10,
generatedAt: '2026-08-12T10:00:00.000Z',
generatedImage: `data:image/png;base64,${Buffer.from('mobile-01').toString('base64')}`,
htmlPath: originalHtml,
reportSnapshot,
reportMetadata,
templateId: 'mobile-feed'
})
expect(saved.success).toBe(true)
expect(saved.record).toBeDefined()
const updated = await updateGeneratedReportTemplate({
reportId: saved.record!.id,
templateId: 'mobile-dashboard',
generatedImage: `data:image/png;base64,${Buffer.from('mobile-03').toString('base64')}`,
htmlPath: switchedHtml
})
expect(updated.success).toBe(true)
expect(updated.record).toMatchObject({
id: saved.record!.id,
templateId: 'mobile-dashboard',
reportSnapshot,
reportMetadata
})
expect(readFileSync(updated.record!.htmlPath!, 'utf8')).toContain('Mobile 03')
expect(readFileSync(updated.record!.pngPath!).toString()).toBe('mobile-03')
expect(JSON.parse(readFileSync(updated.record!.jsonPath!, 'utf8'))).toMatchObject({
id: saved.record!.id,
templateId: 'mobile-dashboard',
reportSnapshot,
reportMetadata
})
})
it('keeps legacy records viewable but rejects a lossless template switch', async () => {
const legacyHtml = join(root, 'legacy.html')
writeFileSync(legacyHtml, 'Legacy
')
const { saveGeneratedReport, updateGeneratedReportTemplate } =
await import('../../src/main/report-history-service')
const saved = await saveGeneratedReport({
contactId: 'legacy-group',
contactName: '旧报告',
dateRange: '今天',
messageCount: 5,
generatedAt: '2026-08-12T11:00:00.000Z',
generatedImage: `data:image/png;base64,${Buffer.from('legacy').toString('base64')}`,
htmlPath: legacyHtml
})
const updated = await updateGeneratedReportTemplate({
reportId: saved.record!.id,
templateId: 'desktop-editorial',
generatedImage: `data:image/png;base64,${Buffer.from('new').toString('base64')}`,
htmlPath: legacyHtml
})
expect(updated).toEqual({
success: false,
error: '旧报告未保存结构化数据,无法无损切换模板'
})
})
it('extracts and persists a render snapshot once for legacy template switching', async () => {
const legacyHtml = join(root, 'legacy-with-html.html')
writeFileSync(legacyHtml, 'Legacy source
')
const { saveGeneratedReport, prepareGeneratedReportTemplateSwitch } =
await import('../../src/main/report-history-service')
const saved = await saveGeneratedReport({
contactId: 'legacy-snapshot-group',
contactName: '旧报告快照',
dateRange: '今天',
messageCount: 8,
generatedAt: '2026-08-12T12:00:00.000Z',
generatedImage: `data:image/png;base64,${Buffer.from('legacy-snapshot').toString('base64')}`,
htmlPath: legacyHtml
})
const extractSnapshot = vi.fn(async () => ({
groupName: '旧报告快照',
reportDate: '2026-08-12',
values: { REPORT_TITLE: '旧报告快照日报', TOPIC_CARDS: '已有主题
' }
}))
const first = await prepareGeneratedReportTemplateSwitch(saved.record!.id, extractSnapshot)
const second = await prepareGeneratedReportTemplateSwitch(saved.record!.id, extractSnapshot)
expect(first).toEqual(second)
expect(extractSnapshot).toHaveBeenCalledTimes(1)
expect(JSON.parse(readFileSync(saved.record!.jsonPath!, 'utf8'))).toMatchObject({
id: saved.record!.id,
reportRenderSnapshot: first.snapshot
})
})
it('persists the classic v1 template as a selectable history template', async () => {
const classicHtml = join(root, 'classic.html')
writeFileSync(classicHtml, '经典日报
')
const { saveGeneratedReport } = await import('../../src/main/report-history-service')
const saved = await saveGeneratedReport({
contactId: 'classic-group',
contactName: '经典日报群',
dateRange: '今天',
messageCount: 6,
generatedAt: '2026-08-12T13:00:00.000Z',
generatedImage: `data:image/png;base64,${Buffer.from('classic').toString('base64')}`,
htmlPath: classicHtml,
reportSnapshot,
reportMetadata,
templateId: 'v1'
})
expect(saved.success).toBe(true)
expect(saved.record?.templateId).toBe('v1')
expect(JSON.parse(readFileSync(saved.record!.jsonPath!, 'utf8')).templateId).toBe('v1')
})
})