Files
PlantAI-rag-new/ui/tests/r3debug/bench.test.js
adminandClaude Fable 5 671783eb1e fix(ui): r3 前端 10 项缺陷修复(#7/#27/#28/#31/#39/#40/#42/#47b/#52/#55)
- #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>
2026-09-10 16:17:11 +08:00

138 lines
4.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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('');
});
});