- git mv 保留历史:ui/(Vue3+Vite)、sdk/(BFF 中间层)
- 路径语义修正:db_path/figures_cache 改相对 sdk/(BFF cwd),
历史库 sdk/backend/data 迁移至 sdk/data/(会话/图片缓存无损)
- 引用修复 ~15 处:config.py、deploy/*.sh、vitest/playwright 配置、
单测导入路径、docs、README、gitignore、.env.example
- 骨架:agent/{qa,methodology,legacy-rag,vendor}、knowledge/manifest.yaml、
tooling/{compile,kb,crypto,assemble,audit,tools,scripts} + README
- 门禁验证:sdk pytest 125 通过、ui vitest 158 通过、
生产 BFF 脚本重启四上游全绿 + 静态托管 + 会话库无损
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
142 lines
5.5 KiB
JavaScript
142 lines
5.5 KiB
JavaScript
// kg.pane.test.js —— B3 单测:KgPane(节点配色稳定性、>50 节点降级、离线态、搜索流程 mock)
|
||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { createPinia, setActivePinia, getActivePinia } from 'pinia';
|
||
import { buildPalette } from '../../../../ui/src/components/bench/graphLayout.js';
|
||
import {
|
||
isDegradedGraph,
|
||
isKgOffline,
|
||
NODE_LIMIT_DESKTOP,
|
||
NODE_LIMIT_MOBILE,
|
||
} from '../../../../ui/src/components/bench/kgPaneCompat.js';
|
||
import { useBenchStore } from '../../../../ui/src/stores/bench.js';
|
||
|
||
vi.mock('../../../../ui/src/api/extraction.js', () => ({
|
||
listJobs: vi.fn(),
|
||
getJob: vi.fn(),
|
||
uploadPaper: vi.fn(),
|
||
getRunLog: vi.fn(),
|
||
getProcessingStatus: vi.fn(),
|
||
getSummary: vi.fn(),
|
||
}));
|
||
vi.mock('../../../../ui/src/api/kg.js', () => ({
|
||
kgStatus: vi.fn(),
|
||
searchEntities: vi.fn().mockResolvedValue({
|
||
query: 'OsSPL',
|
||
results: [
|
||
{ id: 'cent_ospl14', name: 'OsSPL14', type: 'gene', support_count: 18 },
|
||
{ id: 'cent_ospl7', name: 'OsSPL7', type: 'gene', support_count: 9 },
|
||
],
|
||
}),
|
||
getEntity: vi.fn(),
|
||
getNeighborhood: vi.fn().mockResolvedValue({
|
||
center: 'cent_ospl14',
|
||
center_name: 'OsSPL14',
|
||
depth: 1,
|
||
nodes: [{ id: 'cent_ospl14', label: 'OsSPL14', type: 'gene' }],
|
||
edges: [],
|
||
}),
|
||
}));
|
||
|
||
describe('KgPane 节点配色稳定性(FR-KG-002)', () => {
|
||
it('同一 type 在会话内颜色稳定(重复调用结果一致)', () => {
|
||
const types = ['gene', 'tissue', 'process', 'gene', 'molecule'];
|
||
const p1 = buildPalette(types);
|
||
const p2 = buildPalette(types);
|
||
expect(p1.get('gene')).toBe(p2.get('gene'));
|
||
expect(p1.get('tissue')).toBe(p2.get('tissue'));
|
||
expect(p1.get('process')).toBe(p2.get('process'));
|
||
});
|
||
|
||
it('不同 type 颜色互不相同(调色板无重复)', () => {
|
||
const types = ['gene', 'tissue', 'process', 'molecule', 'trait', 'experimental_material'];
|
||
const p = buildPalette(types);
|
||
const values = [...p.values()];
|
||
expect(new Set(values).size).toBe(values.length);
|
||
});
|
||
|
||
it('未知类型归入 unknown 色,且稳定', () => {
|
||
const p1 = buildPalette(['gene']);
|
||
const p2 = buildPalette(['gene']);
|
||
expect(p1.get('unknown')).toBe(p2.get('unknown'));
|
||
});
|
||
|
||
it('同一 type 在不同图之间配色一致(固定基数,会话内跨图稳定)', () => {
|
||
const a = buildPalette(['gene', 'tissue']);
|
||
const b = buildPalette(['gene']);
|
||
expect(a.get('gene')).toBe(b.get('gene'));
|
||
});
|
||
});
|
||
|
||
describe('KgPane 节点降级(FR-KG-001)', () => {
|
||
it('>50 节点(桌面)自动降级列表视图', () => {
|
||
expect(isDegradedGraph(50, NODE_LIMIT_DESKTOP)).toBe(false);
|
||
expect(isDegradedGraph(51, NODE_LIMIT_DESKTOP)).toBe(true);
|
||
});
|
||
|
||
it('移动端上限 30(FR-RESP-012)', () => {
|
||
expect(isDegradedGraph(30, NODE_LIMIT_MOBILE)).toBe(false);
|
||
expect(isDegradedGraph(31, NODE_LIMIT_MOBILE)).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('KgPane 离线降级(FR-KG-007)', () => {
|
||
it('kgReady=false 且状态已探测 → 离线(搜索/邻域禁用,答案图谱仍可用)', () => {
|
||
expect(isKgOffline(false, { ok: false })).toBe(true);
|
||
expect(isKgOffline(false, null)).toBe(false); // 尚未探测:不判离线
|
||
expect(isKgOffline(true, { ok: true })).toBe(false);
|
||
});
|
||
|
||
it('store.checkKgStatus 失败 → kgReady=false + kgError=true', async () => {
|
||
const { kgStatus } = await import('../../../../ui/src/api/kg.js');
|
||
kgStatus.mockRejectedValue({ code: 'UPSTREAM_DOWN', message: 'Neo4j 不可用', degraded: true });
|
||
setActivePinia(createPinia());
|
||
const store = useBenchStore();
|
||
await expect(store.checkKgStatus()).rejects.toBeTruthy();
|
||
expect(store.kgReady).toBe(false);
|
||
expect(store.kgError).toBe(true);
|
||
});
|
||
|
||
it('store.checkKgStatus 成功 → kgReady=true', async () => {
|
||
const { kgStatus } = await import('../../../../ui/src/api/kg.js');
|
||
kgStatus.mockResolvedValue({ ok: true, active: 'neo4j-plant-srt-direct-v1-full' });
|
||
setActivePinia(createPinia());
|
||
const store = useBenchStore();
|
||
await store.checkKgStatus();
|
||
expect(store.kgReady).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('KgPane 搜索流程(mock,FR-KG-004)', () => {
|
||
it('搜索返回 name_norm 匹配结果(精确/前缀),选中后加载邻域', async () => {
|
||
const { searchEntities, getNeighborhood } = await import('../../../../ui/src/api/kg.js');
|
||
const data = await searchEntities('OsSPL', 20);
|
||
expect(data.results).toHaveLength(2);
|
||
expect(data.results[0].name).toBe('OsSPL14');
|
||
|
||
const nb = await getNeighborhood('cent_ospl14', { depth: 1, limit: 50 });
|
||
expect(nb.center).toBe('cent_ospl14');
|
||
expect(nb.depth).toBe(1);
|
||
expect(nb.nodes.length).toBeGreaterThan(0);
|
||
expect(getNeighborhood).toHaveBeenCalledWith('cent_ospl14', { depth: 1, limit: 50 });
|
||
});
|
||
|
||
it('答案图谱在 Neo4j 不可用时仍可用(双数据源策略,FR-KG-007)', () => {
|
||
// 答案图谱来自消息 graph 块,不依赖 kg API;模拟 B2 消息存在时 currentGraph 返回
|
||
setActivePinia(createPinia());
|
||
const store = useBenchStore();
|
||
const chatStore = {
|
||
currentMessage: {
|
||
role: 'assistant',
|
||
blocks: [
|
||
{ type: 'graph', nodes: [{ id: 'OsSPL14', label: 'OsSPL14', type: 'Gene' }], edges: [] },
|
||
],
|
||
},
|
||
};
|
||
getActivePinia()._s.set('chat', chatStore);
|
||
expect(store.currentGraph).not.toBeNull();
|
||
expect(store.currentGraph.nodes).toHaveLength(1);
|
||
// 且此时 kgReady 为 false 也不影响答案图谱
|
||
expect(store.kgReady).toBe(false);
|
||
});
|
||
});
|