feat: v0.7.4 时序语义修正 · 防线实效补漏 · 全量测试翻倍 — 2406 用例 + jsdom 组件测试全量回归
P1 修复面收口: - 超时三态区分(aborted→USER_INTERRUPT / ETIMEDOUT→TIMEOUT / 其余→ERROR), 根治"真实网络超时被误报为用户中断" - 流空闲超时统一(SSE/Ollama/Anthropic 读循环 60s 无数据抛 504 进重试通道) - 同会话并发 sendMessage 防重入(isRunning 守卫)+ 会话存在性预检 + 前置调用移入 try(ERROR+DONE 双事件保证,根治 isStreaming 假死) - 清空审计后 resetChainCache(根治 verifyChain 误报 TAMPERED) - DONE 不再提前清理 TRACE(TERMINATED 统一收尾,补全最终迭代录制) - IME 合成回车不发送(普通 Enter + Cmd/Ctrl+Enter 双分支)+ handleSend 闭包修复 P2 安全纵深: - preload 移除原始 electronAPI 暴露(渲染层零使用,关掉 XSS invoke 任意通道单点风险) - CORS 同源回显根治(仅当前浏览页面 Origin,did-navigate 同步) - MEMORY.md 命令保护正则扩展(括号/$/反引号/< 重定向边界 + 前导路径) - write_file append TOCTOU 统一(open 后 realpath 校验,新文件分支补漏) - 敏感键归一化(authKey 驼峰/连字符命中)+ MCP headers 鉴权值加密落库 - ReDoS 检测共享化(search_files/file_editor 统一拦截) - run_tests/lint_code 升风险 + 需确认 + npx --no-install(执行边界对齐 run_command) - MCP/SearXNG/llm.baseURL/updateFeedUrl 配置类 URL 高危目标校验(IPv6 去括号 + 十六进制映射解析 + 尾点剥离) P3 架构还债: - temperature/maxTokens 热生效(引擎/编排器/SubAgent 三处接线)+ setBatch 单事务落盘 - SessionRecorder flush 竞态根治(flushPromise 等待 + 超限内联落盘 + stopRecording async) - 内存收口(lastConsolidationBySession LRU / subTraces 清理 / 会话删除 disposeEngine) - i18n 全量收口(28 组件 + 353 key 双字典,状态标签改渲染时函数) - 死代码清理(updateTraceStep/HEADER_HEIGHT/void preA/失实注释) - 斜杠菜单 MUI 化 + 删除逻辑收敛 resetSessionState + Blob URL 统一释放 + 用户消息"仅保存"落库(saveMessage 透传前端 id 修复 id 错位) P4 能力演进: - 死循环检测拆分(驻留前置 + 乒乓后置带进度信号,合法交替不误报) - run-lock 30s 超时强制 abort(旧 run 卡死不无限排队) - RETRY 双通道 stream_reset(前端按 run 归属精确清空,根治重试文本重复) - FTS5 trigram 中文子串搜索(迁移 9 版本化 SCHEMA_VERSION=2,≤2 字符 LIKE 回退) - getContextWindow 兜底 1M→128K(未知模型防 413) 测试: - 855 → 2406 用例(+1551,2.8 倍):服务层 +325(含 MemoryManager 51 新用例)、 工具实体 +483、IPC/适配器 +390(含 OpenAI/Anthropic/Ollama 独立套件)、 纯函数表格化 +330;引入 jsdom + @testing-library(14 组件测试文件 249 用例) - 修复 R1(saveMessage id 透传)/ R2(stream_reset 精确归属)两个回归缺陷 - 遗留低危项清零:git-tools 顺序耦合 / web-fetch 真实时间退避 / slo 内存断言 / mcp-security 多余 skipIf / deepseek-balance 命名误导 / 组件 mock 注入脆弱性 版本: 0.7.4; README 同步(工具风险表/版本徽章); 依赖: 移除 @electron-toolkit/preload, 新增 jsdom/@testing-library(devDependencies 不打包) 回归: typecheck 双端 0 错误; ESLint 0/0; Electron ABI 全量 2406/2406 零跳过; 系统 Node 2110 通过 296 跳过(better-sqlite3 ABI)
This commit is contained in:
@@ -443,3 +443,427 @@ describe.skipIf(!dbAvailable)('session_summaries 级联删除(F-3)', () => {
|
||||
expect(remaining.map((r) => r.session_id)).toEqual(['s_keep']);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* maybeSummarize 触发与增量重摘要(P2-11 滚动摘要的核心触发语义)
|
||||
*
|
||||
* 触发条件(源码契约):
|
||||
* 1. 总消息数 >= MIN_MESSAGES_TO_SUMMARIZE(50)
|
||||
* 2. 未总结条数 > TAIL_KEEP + MIN_NEW_TO_SUMMARIZE(35),且
|
||||
* 3. 摘出后 toSummarize(未总结前 N-20 条)>= MIN_NEW_TO_SUMMARIZE(15)
|
||||
* LLM 返回空串/抛错 → 静默降级不落库。
|
||||
*/
|
||||
describe.skipIf(!dbAvailable)('maybeSummarize — 触发阈值与增量', () => {
|
||||
let db: any;
|
||||
let sessionService: any;
|
||||
let summaryService: any;
|
||||
let sendMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
const createSuite = async (): Promise<void> => {
|
||||
const { SessionService } = await import('../session.service');
|
||||
const { SessionSummaryService } = await import('../session-summary.service');
|
||||
db = new Database(':memory:');
|
||||
db.exec(`
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT DEFAULT '新会话',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
message_count INTEGER DEFAULT 0,
|
||||
pinned INTEGER DEFAULT 0,
|
||||
archived INTEGER DEFAULT 0,
|
||||
metadata TEXT DEFAULT '{}'
|
||||
);
|
||||
CREATE TABLE messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT,
|
||||
reasoning_content TEXT,
|
||||
tool_calls TEXT,
|
||||
tool_result TEXT,
|
||||
attachments TEXT,
|
||||
iteration INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE session_summaries (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
summary TEXT NOT NULL,
|
||||
summarized_until_rowid INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
sessionService = new SessionService(() => db);
|
||||
sendMock = vi.fn().mockResolvedValue({ content: '滚动摘要内容' });
|
||||
const adapter = {
|
||||
providerId: 'mock',
|
||||
supportedModels: [],
|
||||
supportsToolCalling: false,
|
||||
supportsThinking: false,
|
||||
getContextWindow: () => 1000,
|
||||
send: sendMock,
|
||||
sendStream: vi.fn(),
|
||||
healthCheck: vi.fn().mockResolvedValue(true),
|
||||
};
|
||||
summaryService = new SessionSummaryService(
|
||||
() => db,
|
||||
sessionService,
|
||||
() => adapter,
|
||||
);
|
||||
};
|
||||
|
||||
const insertMany = (sessionId: string, n: number, start = 1): void => {
|
||||
for (let i = start; i < start + n; i++) {
|
||||
db.prepare(
|
||||
`INSERT INTO messages (id, session_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)`,
|
||||
).run(`${sessionId}_m${i}`, sessionId, i % 2 === 1 ? 'user' : 'assistant', `消息 ${i}`, i);
|
||||
}
|
||||
};
|
||||
|
||||
it('消息数 < 50 时触发摘要,LLM 不被调用', async () => {
|
||||
await createSuite();
|
||||
db.prepare(`INSERT INTO sessions (id, created_at, updated_at) VALUES ('thr_49', 1, 1)`).run();
|
||||
insertMany('thr_49', 49);
|
||||
await summaryService.maybeSummarize('thr_49');
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
expect(summaryService.getSummary('thr_49')).toBeNull();
|
||||
db.close();
|
||||
});
|
||||
|
||||
it('消息数 >= 50 时触发摘要并保存摘要与游标', async () => {
|
||||
await createSuite();
|
||||
db.prepare(`INSERT INTO sessions (id, created_at, updated_at) VALUES ('thr_50', 1, 1)`).run();
|
||||
insertMany('thr_50', 50);
|
||||
await summaryService.maybeSummarize('thr_50');
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
const summary = summaryService.getSummary('thr_50');
|
||||
expect(summary).not.toBeNull();
|
||||
// 摘出 30 条(50-20),游标指向第 30 条 rowid
|
||||
const lastToSummarize = db
|
||||
.prepare(
|
||||
`SELECT MAX(rowid) AS r FROM messages WHERE session_id = 'thr_50' AND id = 'thr_50_m30'`,
|
||||
)
|
||||
.get() as { r: number };
|
||||
expect(summary.summarizedUntilRowid).toBe(lastToSummarize.r);
|
||||
db.close();
|
||||
});
|
||||
|
||||
it('85 条消息时摘要覆盖前 65 条、保留尾部 20 条原文', async () => {
|
||||
await createSuite();
|
||||
db.prepare(`INSERT INTO sessions (id, created_at, updated_at) VALUES ('thr_85', 1, 1)`).run();
|
||||
insertMany('thr_85', 85);
|
||||
await summaryService.maybeSummarize('thr_85');
|
||||
const summary = summaryService.getSummary('thr_85');
|
||||
expect(summary).not.toBeNull();
|
||||
const tailStart = db
|
||||
.prepare(`SELECT rowid AS r FROM messages WHERE session_id = 'thr_85' AND id = 'thr_85_m66'`)
|
||||
.get() as { r: number };
|
||||
expect(summary.summarizedUntilRowid).toBe(tailStart.r - 1);
|
||||
// 分层加载:摘要 + 尾部 20 条原文
|
||||
const history = summaryService.buildHistoryMessages('thr_85');
|
||||
expect(history.length).toBe(21); // 1 摘要 + 20 原文
|
||||
db.close();
|
||||
});
|
||||
|
||||
it('增量重摘要:已有摘要后新增 35 条再次摘要(游标前移)', async () => {
|
||||
await createSuite();
|
||||
db.prepare(`INSERT INTO sessions (id, created_at, updated_at) VALUES ('incr', 1, 1)`).run();
|
||||
insertMany('incr', 85);
|
||||
await summaryService.maybeSummarize('incr');
|
||||
const first = summaryService.getSummary('incr');
|
||||
expect(first).not.toBeNull();
|
||||
|
||||
// 再插入 35 条(rowid 86-120)
|
||||
insertMany('incr', 35, 86);
|
||||
await summaryService.maybeSummarize('incr');
|
||||
expect(sendMock).toHaveBeenCalledTimes(2);
|
||||
const second = summaryService.getSummary('incr')!;
|
||||
expect(second.summarizedUntilRowid).toBeGreaterThan(first!.summarizedUntilRowid);
|
||||
// 第二轮请求携带前序摘要(滚动合并)
|
||||
const priorPrompt = sendMock.mock.calls[1][0] as { messages: Array<{ content: string }> };
|
||||
expect(priorPrompt.messages[0].content).toContain('## Prior summary');
|
||||
expect(priorPrompt.messages[0].content).toContain('滚动摘要内容');
|
||||
db.close();
|
||||
});
|
||||
|
||||
it('增量不足(新增 <= 35 条)不重复调用 LLM', async () => {
|
||||
await createSuite();
|
||||
db.prepare(`INSERT INTO sessions (id, created_at, updated_at) VALUES ('noincr', 1, 1)`).run();
|
||||
insertMany('noincr', 85);
|
||||
await summaryService.maybeSummarize('noincr');
|
||||
const calls = sendMock.mock.calls.length;
|
||||
// 只加 5 条
|
||||
insertMany('noincr', 5, 86);
|
||||
await summaryService.maybeSummarize('noincr');
|
||||
expect(sendMock.mock.calls.length).toBe(calls);
|
||||
db.close();
|
||||
});
|
||||
|
||||
it('LLM 返回空串 → 不保存摘要(静默降级)', async () => {
|
||||
await createSuite();
|
||||
db.prepare(`INSERT INTO sessions (id, created_at, updated_at) VALUES ('empty', 1, 1)`).run();
|
||||
insertMany('empty', 60);
|
||||
sendMock.mockResolvedValueOnce({ content: ' ' });
|
||||
await expect(summaryService.maybeSummarize('empty')).resolves.toBeUndefined();
|
||||
expect(summaryService.getSummary('empty')).toBeNull();
|
||||
db.close();
|
||||
});
|
||||
|
||||
it('LLM 抛错 → 不抛异常、不保存摘要', async () => {
|
||||
await createSuite();
|
||||
db.prepare(`INSERT INTO sessions (id, created_at, updated_at) VALUES ('fail', 1, 1)`).run();
|
||||
insertMany('fail', 60);
|
||||
sendMock.mockRejectedValueOnce(new Error('timeout'));
|
||||
await expect(summaryService.maybeSummarize('fail')).resolves.toBeUndefined();
|
||||
expect(summaryService.getSummary('fail')).toBeNull();
|
||||
db.close();
|
||||
});
|
||||
|
||||
it('摘要请求参数锁定:maxTokens=2048、temperature=0、stream=false', async () => {
|
||||
await createSuite();
|
||||
db.prepare(`INSERT INTO sessions (id, created_at, updated_at) VALUES ('param', 1, 1)`).run();
|
||||
insertMany('param', 60);
|
||||
await summaryService.maybeSummarize('param');
|
||||
const req = sendMock.mock.calls[0][0] as {
|
||||
params: { maxTokens: number; temperature: number; stream: boolean };
|
||||
};
|
||||
expect(req.params.maxTokens).toBe(2048);
|
||||
expect(req.params.temperature).toBe(0);
|
||||
expect(req.params.stream).toBe(false);
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!dbAvailable)('buildHistoryMessages — 无摘要与 role 过滤', () => {
|
||||
let db: any;
|
||||
let sessionService: any;
|
||||
let summaryService: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { SessionService } = await import('../session.service');
|
||||
const { SessionSummaryService } = await import('../session-summary.service');
|
||||
db = new Database(':memory:');
|
||||
db.exec(`
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT DEFAULT '新会话',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
message_count INTEGER DEFAULT 0,
|
||||
pinned INTEGER DEFAULT 0,
|
||||
archived INTEGER DEFAULT 0,
|
||||
metadata TEXT DEFAULT '{}'
|
||||
);
|
||||
CREATE TABLE messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT,
|
||||
reasoning_content TEXT,
|
||||
tool_calls TEXT,
|
||||
tool_result TEXT,
|
||||
attachments TEXT,
|
||||
iteration INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE session_summaries (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
summary TEXT NOT NULL,
|
||||
summarized_until_rowid INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
db.prepare(
|
||||
'INSERT INTO sessions (id, created_at, updated_at, message_count) VALUES (?, ?, ?, 0)',
|
||||
).run('s_plain', Date.now(), Date.now());
|
||||
db.prepare(
|
||||
'INSERT INTO sessions (id, created_at, updated_at, message_count) VALUES (?, ?, ?, 0)',
|
||||
).run('s_role', Date.now(), Date.now());
|
||||
sessionService = new SessionService(() => db);
|
||||
summaryService = new SessionSummaryService(
|
||||
() => db,
|
||||
sessionService,
|
||||
() => null as unknown as Parameters<typeof Object>[0],
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
db?.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
|
||||
const insertMessage = (sessionId: string, id: string, role: string, content: string): void => {
|
||||
db.prepare(
|
||||
`INSERT INTO messages (id, session_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)`,
|
||||
).run(id, sessionId, role, content, Date.now());
|
||||
};
|
||||
|
||||
it('无摘要时返回全量原文(不注入摘要消息)', () => {
|
||||
insertMessage('s_plain', 'p1', 'user', '甲');
|
||||
insertMessage('s_plain', 'p2', 'assistant', '乙');
|
||||
const history = summaryService.buildHistoryMessages('s_plain');
|
||||
expect(history).toHaveLength(2);
|
||||
expect(history.some((m: any) => (m.content as string).startsWith('[Context Summary]'))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('system 角色消息被过滤(不进 LLM 历史)', () => {
|
||||
insertMessage('s_role', 'r1', 'system', '系统提示');
|
||||
insertMessage('s_role', 'r2', 'user', '用户消息');
|
||||
const history = summaryService.buildHistoryMessages('s_role');
|
||||
expect(history).toHaveLength(1);
|
||||
expect(history[0].content).toBe('用户消息');
|
||||
});
|
||||
|
||||
it('消息按插入顺序返回且携带 timestamp 元数据', () => {
|
||||
insertMessage('s_plain', 'p3', 'user', '丙');
|
||||
const history = summaryService.buildHistoryMessages('s_plain');
|
||||
const contents = history.map((m: any) => m.content);
|
||||
expect(contents).toEqual(['甲', '乙', '丙']);
|
||||
expect(history.every((m: any) => typeof m.timestamp === 'number')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!dbAvailable)('restoreHistoryImages — 更多边界', () => {
|
||||
let db: any;
|
||||
let sessionService: any;
|
||||
let summaryService: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { SessionService } = await import('../session.service');
|
||||
const { SessionSummaryService } = await import('../session-summary.service');
|
||||
db = new Database(':memory:');
|
||||
db.exec(`
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT DEFAULT '新会话',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
message_count INTEGER DEFAULT 0,
|
||||
pinned INTEGER DEFAULT 0,
|
||||
archived INTEGER DEFAULT 0,
|
||||
metadata TEXT DEFAULT '{}'
|
||||
);
|
||||
CREATE TABLE messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT,
|
||||
reasoning_content TEXT,
|
||||
tool_calls TEXT,
|
||||
tool_result TEXT,
|
||||
attachments TEXT,
|
||||
iteration INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE session_summaries (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
summary TEXT NOT NULL,
|
||||
summarized_until_rowid INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
sessionService = new SessionService(() => db);
|
||||
summaryService = new SessionSummaryService(
|
||||
() => db,
|
||||
sessionService,
|
||||
() => null as unknown as Parameters<typeof Object>[0],
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
db?.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
|
||||
const insertWithAttachments = (
|
||||
sessionId: string,
|
||||
id: string,
|
||||
content: string,
|
||||
attachments: Array<{ type: string; preview?: string; textContent?: string }>,
|
||||
): void => {
|
||||
db.prepare(
|
||||
`INSERT INTO messages (id, session_id, role, content, attachments, created_at) VALUES (?, ?, 'user', ?, ?, ?)`,
|
||||
).run(id, sessionId, content, JSON.stringify(attachments), Date.now());
|
||||
};
|
||||
|
||||
it('空 preview 的 image 附件不恢复为图片', () => {
|
||||
db.prepare(`DELETE FROM messages WHERE session_id = 's_img2'`).run();
|
||||
db.prepare(
|
||||
`INSERT INTO sessions (id, created_at, updated_at, message_count) VALUES ('s_img2', ?, ?, 0)`,
|
||||
).run(Date.now(), Date.now());
|
||||
insertWithAttachments('s_img2', 'img2_a', '空 preview 图', [{ type: 'image', preview: '' }]);
|
||||
const history = summaryService.buildHistoryMessages('s_img2');
|
||||
expect(history[0].images).toBeUndefined();
|
||||
});
|
||||
|
||||
it('单条消息含多张图:全部恢复(同消息内按数组序)', () => {
|
||||
db.prepare(`DELETE FROM messages WHERE session_id = 's_img3'`).run();
|
||||
db.prepare(
|
||||
`INSERT INTO sessions (id, created_at, updated_at, message_count) VALUES ('s_img3', ?, ?, 0)`,
|
||||
).run(Date.now(), Date.now());
|
||||
insertWithAttachments('s_img3', 'img3_a', '三图消息', [
|
||||
{ type: 'image', preview: 'PIC1' },
|
||||
{ type: 'image', preview: 'PIC2' },
|
||||
{ type: 'image', preview: 'PIC3' },
|
||||
]);
|
||||
const history = summaryService.buildHistoryMessages('s_img3');
|
||||
const urls = (history[0].images as Array<{ url: string }>).map((i) => i.url);
|
||||
expect(urls).toEqual(['PIC1', 'PIC2', 'PIC3']);
|
||||
});
|
||||
|
||||
it('10 张上限从最新收集:单消息 6 张 + 后 1 消息 6 张 → 后消息全收、前消息收 4 张', () => {
|
||||
db.prepare(`DELETE FROM messages WHERE session_id = 's_img4'`).run();
|
||||
db.prepare(
|
||||
`INSERT INTO sessions (id, created_at, updated_at, message_count) VALUES ('s_img4', ?, ?, 0)`,
|
||||
).run(Date.now(), Date.now());
|
||||
const six = Array.from({ length: 6 }, (_, i) => ({ type: 'image', preview: `OLD${i}` }));
|
||||
insertWithAttachments('s_img4', 'img4_old', '旧消息 6 图', six);
|
||||
const sixNew = Array.from({ length: 6 }, (_, i) => ({ type: 'image', preview: `NEW${i}` }));
|
||||
insertWithAttachments('s_img4', 'img4_new', '新消息 6 图', sixNew);
|
||||
|
||||
const history = summaryService.buildHistoryMessages('s_img4');
|
||||
const oldMsg = history.find((m: any) => m.content === '旧消息 6 图');
|
||||
const newMsg = history.find((m: any) => m.content === '新消息 6 图');
|
||||
expect((newMsg!.images as unknown[]).length).toBe(6);
|
||||
// 剩 4 张配额给更早的消息(取靠后 4 张)
|
||||
expect((oldMsg!.images as unknown[]).length).toBe(4);
|
||||
const oldUrls = (oldMsg!.images as Array<{ url: string }>).map((i) => i.url);
|
||||
expect(oldUrls).toEqual(['OLD2', 'OLD3', 'OLD4', 'OLD5']);
|
||||
});
|
||||
|
||||
it('非 image 类型附件不恢复(text/file 类型忽略)', () => {
|
||||
db.prepare(`DELETE FROM messages WHERE session_id = 's_img5'`).run();
|
||||
db.prepare(
|
||||
`INSERT INTO sessions (id, created_at, updated_at, message_count) VALUES ('s_img5', ?, ?, 0)`,
|
||||
).run(Date.now(), Date.now());
|
||||
insertWithAttachments('s_img5', 'img5_a', '混合附件', [
|
||||
{ type: 'text', textContent: 't' },
|
||||
{ type: 'file', preview: 'FILEPREVIEW' },
|
||||
{ type: 'image', preview: 'REALIMG' },
|
||||
]);
|
||||
const history = summaryService.buildHistoryMessages('s_img5');
|
||||
const images = history[0].images as Array<{ url: string }>;
|
||||
expect(images).toHaveLength(1);
|
||||
expect(images[0].url).toBe('REALIMG');
|
||||
});
|
||||
|
||||
it('无任何附件消息不产生 images 字段', () => {
|
||||
db.prepare(`DELETE FROM messages WHERE session_id = 's_img6'`).run();
|
||||
db.prepare(
|
||||
`INSERT INTO sessions (id, created_at, updated_at, message_count) VALUES ('s_img6', ?, ?, 0)`,
|
||||
).run(Date.now(), Date.now());
|
||||
db.prepare(
|
||||
`INSERT INTO messages (id, session_id, role, content, created_at) VALUES ('img6_a', 's_img6', 'user', '纯文本', ?)`,
|
||||
).run(Date.now());
|
||||
const history = summaryService.buildHistoryMessages('s_img6');
|
||||
expect(history[0].images).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user