- #7a reveal 超时:长文按总时长 ≤8s 反推 interval(下限 12ms),短路径 30ms/字符不变(step=1);跳过按钮与 reduced-motion 行为不变 - #7b 标题编号:heading_open 注入 h1「一、」/h2「1.1」/h3「1.2.3」, 层级变化归零、自带编号跳过、md-hN class 恒挂、计数器逐次渲染重置; DocPane md 回退预览与私有参考块用 renderMarkdownPlain 关闭注入 (防止编号被序列化写回正本) - #27 会话聚合:删除前端二次聚合,会话域直接以后端 sources 为准; SourcesPane/SourceRow 以 paper_id/id 为 key 与高亮依据,徽标显示 真实 paper_id(截断+悬浮),无 paper_id 退 index+1; setHighlightSource 参数 index→key,chip 编号契约保持 - #31 节点详情:KgEntityDetail fetchExtra 序号守卫;KgPane :key 强制 重建;KgGraphView pointer 事件重构(pointerdown 不捕获、位移>4px 才捕获+标记 dragging、pointerup release、onClickNode dragging 拦截) - #39 日志刷新:打开总是重拉+「刷新日志」按钮+有 running 时 5s 独立 轮询(onBeforeUnmount 清理) - #40 轮询暂停:任何离开 extract 的切换 pausePolling;详情/日志展开态 提升到 store(detailOpenId/logOpen/runLog/logError),切 TAB 保留 - #42 模板选择:Composer docgen 模式模板下拉(自动推荐+内置+as_template 懒加载,失败降级),template_id 经 sendDocgen 传入 - #47b 会话切换 resetSession(selectSession 动态 import,只清不丢) - #55 文档 TAB:docgen store viewMode + DocPane「资料库」/「关闭文档」/ 「返回文档」按钮 - #28 检索文案:searchNoAnswer/searchTimeoutEmpty;200 降级体按 payload.degraded 分流,不再走 _errorMarker TIMEOUT 报错 - #52 异步轮询:api getTask/listTasks;sendDocgen 兼容 200/202, 202 后释放 busy 锁(生成期间可继续对话)、消息块记录 task_id、 2s 轮询 getTask、progress 驱动「生成中 x/y 节」、done 复用现有 artifact 挂块+落库+adoptArtifact+切 TAB、error 可重试; 会话恢复时 running 块含 task_id 续轮询(不误标「已中断」) 自测:29 个新增 r3debug 单测 + 23 个既有 private_docs 测试全绿; npm run build 通过;playwright 冒烟(dev server 5199)无 JS 错误, 模板下拉/提取面板/日志刷新按钮渲染正常。 未 push;Debug-2 9090 未就绪,#52/#28 联调留待 9090 就绪后执行。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
138 lines
4.9 KiB
JavaScript
138 lines
4.9 KiB
JavaScript
// tests/r3debug/bench.test.js —— #27 聚合 ID / #40 轮询暂停策略
|
||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||
import { setActivePinia, createPinia } from 'pinia';
|
||
import { useBenchStore, __setHttpGet } from '../../src/stores/bench.js';
|
||
|
||
function makePiniaWithChat(chatState) {
|
||
const pinia = createPinia();
|
||
setActivePinia(pinia);
|
||
// chat store 未注册时 currentSources 回退 null——注册一个假 chat store 提供 currentMessage
|
||
pinia.state.value.chat = chatState;
|
||
return pinia;
|
||
}
|
||
|
||
describe('#27 会话级来源聚合(后端结果为准)', () => {
|
||
beforeEach(() => {
|
||
vi.useFakeTimers();
|
||
});
|
||
afterEach(() => {
|
||
vi.useRealTimers();
|
||
vi.restoreAllMocks();
|
||
});
|
||
|
||
it('sessionSources.sources 原样透出(不重复聚合、不 +1)', () => {
|
||
makePiniaWithChat({ messages: [] });
|
||
const bench = useBenchStore();
|
||
bench.sourceScope = 'session';
|
||
bench.sessionSources = {
|
||
ok: true,
|
||
sources: [
|
||
{ paper_id: 'P1', id: 'x1', title: 'A', count: 2 },
|
||
{ paper_id: 'P2', id: 'x2', title: 'B', count: 1 },
|
||
],
|
||
};
|
||
const items = bench.sources;
|
||
expect(items).toHaveLength(2);
|
||
expect(items[0].src.paper_id).toBe('P1');
|
||
expect(items[0].label).toBe(2);
|
||
expect(items[1].label).toBe(1);
|
||
});
|
||
|
||
it('后端已去重:重复 paper_id 不再被前端二次聚合', () => {
|
||
makePiniaWithChat({ messages: [] });
|
||
const bench = useBenchStore();
|
||
bench.sourceScope = 'session';
|
||
bench.sessionSources = {
|
||
sources: [
|
||
{ paper_id: 'P1', count: 1 },
|
||
{ paper_id: 'P1', count: 1 }, // 后端若漏去重(防御):前端也不合并(修复后语义)
|
||
],
|
||
};
|
||
// 修复后:前端不再做 Map 聚合,直接透传
|
||
expect(bench.sources).toHaveLength(2);
|
||
});
|
||
|
||
it('setHighlightSource 改 key 语义:key 匹配 paper_id/id,旧 index 兼容', () => {
|
||
makePiniaWithChat({ messages: [] });
|
||
const bench = useBenchStore();
|
||
bench.setHighlightSource('P-123');
|
||
expect(bench.highlightSource.key).toBe('P-123');
|
||
bench.setHighlightSource(2); // 数字 → 兼容旧 index 调用
|
||
expect(bench.highlightSource.index).toBe(2);
|
||
bench.setHighlightSource(null); // 清除
|
||
expect(bench.highlightSource).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('#40 轮询暂停/恢复策略', () => {
|
||
beforeEach(() => {
|
||
vi.useFakeTimers();
|
||
__setHttpGet(null);
|
||
});
|
||
afterEach(() => {
|
||
vi.useRealTimers();
|
||
vi.restoreAllMocks();
|
||
__setHttpGet(null);
|
||
});
|
||
|
||
it('任何离开 extract 的切换 → pollPaused 且定时器清空', () => {
|
||
const pinia = createPinia();
|
||
setActivePinia(pinia);
|
||
const bench = useBenchStore();
|
||
bench.activeTab = 'extract';
|
||
bench.jobs = [{ job_id: 'j1', status: 'running' }];
|
||
const started = bench.startPolling({ timeoutFn: setTimeout, clearFn: clearTimeout, delay: 5000 });
|
||
expect(started).toBe(true);
|
||
expect(bench.isPolling()).toBe(true);
|
||
bench.setActiveTab('retrieval');
|
||
expect(bench.pollPaused).toBe(true);
|
||
expect(bench.isPolling()).toBe(false);
|
||
// fake timer 前进:无残留轮询请求(loadJobs 不会被调)
|
||
const spy = vi.spyOn(bench, 'loadJobs');
|
||
vi.advanceTimersByTime(20000);
|
||
expect(spy).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('进入 extract 恢复轮询资格(pollPaused=false);有 running 时 pane 会重启', () => {
|
||
const pinia = createPinia();
|
||
setActivePinia(pinia);
|
||
const bench = useBenchStore();
|
||
bench.activeTab = 'sources';
|
||
bench.pollPaused = true;
|
||
bench.jobs = [{ job_id: 'j1', status: 'running' }];
|
||
bench.setActiveTab('extract');
|
||
expect(bench.pollPaused).toBe(false);
|
||
expect(bench.activeTab).toBe('extract');
|
||
const started = bench.startPolling({ timeoutFn: setTimeout, clearFn: clearTimeout, delay: 5000 });
|
||
expect(started).toBe(true);
|
||
});
|
||
|
||
it('全部终态:_tickPoll 不再续定时器', async () => {
|
||
const pinia = createPinia();
|
||
setActivePinia(pinia);
|
||
const bench = useBenchStore();
|
||
bench.activeTab = 'extract';
|
||
bench.jobs = [{ job_id: 'j1', status: 'completed' }];
|
||
bench.startPolling({ timeoutFn: setTimeout, clearFn: clearTimeout, delay: 5000 });
|
||
const spy = vi.spyOn(bench, 'loadJobs').mockResolvedValue({ jobs: [] });
|
||
vi.advanceTimersByTime(5000);
|
||
await Promise.resolve();
|
||
expect(spy).not.toHaveBeenCalled(); // 无 active 任务:tick 直接 return 不再续
|
||
expect(bench.isPolling()).toBe(false);
|
||
});
|
||
|
||
it('#40 详情/日志展开态存 store:resetSessionData 清理', () => {
|
||
const pinia = createPinia();
|
||
setActivePinia(pinia);
|
||
const bench = useBenchStore();
|
||
bench.detailOpenId = 'job-1';
|
||
bench.logOpen = true;
|
||
bench.runLog = 'log line';
|
||
expect(bench.detailOpenId).toBe('job-1');
|
||
bench.resetSessionData();
|
||
expect(bench.detailOpenId).toBeNull();
|
||
expect(bench.logOpen).toBe(false);
|
||
expect(bench.runLog).toBe('');
|
||
});
|
||
});
|