Files
PlantAI-rag-new/tests/frontend/unit/b3/kg.pane.test.js
T
jerryandClaude Fable 5 0875bb036c P0/M1: 四部分重构——frontend→ui、backend→sdk 改名 + 引用修复 + 骨架
- 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>
2026-08-31 12:56:47 +08:00

142 lines
5.5 KiB
JavaScript
Raw 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.
// 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);
});
});