Skip to content

Commit 274cdf0

Browse files
committed
test(client): 新增 18 个测试文件,覆盖率达标(lines 60.1/funcs 50.7/branches 53.4)
1 parent e6629ee commit 274cdf0

20 files changed

Lines changed: 2237 additions & 5 deletions
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* 轻量应用事件总线单元测试
3+
* Unit tests for the lightweight app event bus
4+
*
5+
* @ai-context: 覆盖订阅/取消订阅、多监听器广播、监听器异常隔离
6+
* (console.error 且不影响其他监听器)与 clear 全量清理。纯内存实现,
7+
* 每个测试用 clear() 隔离。
8+
* @ai-context: Covers subscribe/unsubscribe, multi-listener broadcast,
9+
* listener error isolation (console.error without breaking others) and
10+
* full clear(). In-memory only; each test isolates via clear().
11+
*/
12+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
13+
import { assistantEventBus } from './eventBus';
14+
import type { TriggerContext } from '../types';
15+
16+
const ctx: TriggerContext = { currentHour: 10, sessionMinutes: 25 };
17+
18+
describe('assistantEventBus', () => {
19+
beforeEach(() => {
20+
assistantEventBus.clear();
21+
});
22+
afterEach(() => {
23+
assistantEventBus.clear();
24+
});
25+
26+
it('should deliver emitted context to a subscribed listener', () => {
27+
// Arrange
28+
const listener = vi.fn();
29+
assistantEventBus.on('session:end', listener);
30+
// Act
31+
assistantEventBus.emit('session:end', ctx);
32+
// Assert
33+
expect(listener).toHaveBeenCalledTimes(1);
34+
expect(listener).toHaveBeenCalledWith(ctx);
35+
});
36+
37+
it('should broadcast to multiple listeners of the same event', () => {
38+
// Arrange
39+
const a = vi.fn();
40+
const b = vi.fn();
41+
assistantEventBus.on('user:idle', a);
42+
assistantEventBus.on('user:idle', b);
43+
// Act
44+
assistantEventBus.emit('user:idle', ctx);
45+
// Assert
46+
expect(a).toHaveBeenCalledTimes(1);
47+
expect(b).toHaveBeenCalledTimes(1);
48+
});
49+
50+
it('should not deliver across different events', () => {
51+
// Arrange
52+
const listener = vi.fn();
53+
assistantEventBus.on('review:due', listener);
54+
// Act
55+
assistantEventBus.emit('app:startup', ctx);
56+
// Assert
57+
expect(listener).not.toHaveBeenCalled();
58+
});
59+
60+
it('should stop delivering after unsubscribe', () => {
61+
// Arrange
62+
const listener = vi.fn();
63+
const off = assistantEventBus.on('intention:due', listener);
64+
// Act
65+
off();
66+
assistantEventBus.emit('intention:due', ctx);
67+
// Assert
68+
expect(listener).not.toHaveBeenCalled();
69+
});
70+
71+
it('should be safe to emit with no listeners', () => {
72+
// Act/Assert:不抛错
73+
expect(() => assistantEventBus.emit('user:active', ctx)).not.toThrow();
74+
});
75+
76+
it('should isolate a throwing listener and keep others running', () => {
77+
// Arrange
78+
const boom = vi.fn(() => { throw new Error('listener exploded'); });
79+
const ok = vi.fn();
80+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
81+
assistantEventBus.on('cognitive:overload', boom);
82+
assistantEventBus.on('cognitive:overload', ok);
83+
// Act
84+
assistantEventBus.emit('cognitive:overload', ctx);
85+
// Assert:异常被捕获记录,不影响第二个监听器
86+
expect(errorSpy).toHaveBeenCalled();
87+
expect(ok).toHaveBeenCalledTimes(1);
88+
errorSpy.mockRestore();
89+
});
90+
91+
it('should clear all listeners', () => {
92+
// Arrange
93+
const listener = vi.fn();
94+
assistantEventBus.on('stuck:incubation', listener);
95+
// Act
96+
assistantEventBus.clear();
97+
assistantEventBus.emit('stuck:incubation', ctx);
98+
// Assert
99+
expect(listener).not.toHaveBeenCalled();
100+
});
101+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* 三维知识脑图派生层单元测试
3+
* Unit tests for the 3D knowledge map derivation layer
4+
*
5+
* @ai-context: 覆盖 moduleColor 的确定性色板映射与 deriveMapNodes 的
6+
* 冷启动短路、节点投影(z 轴随掌握度分层)、邻居连接聚合、费曼薄弱点/
7+
* 无溯源卡片的合成模块归属与位置确定性。纯函数、无副作用。
8+
* @ai-context: Covers deterministic module colors and deriveMapNodes:
9+
* cold-start short circuit, mastery-based z layering, neighbor link
10+
* aggregation, synthetic module assignment and positional determinism.
11+
*/
12+
import { describe, it, expect } from 'vitest';
13+
import { deriveMapNodes, moduleColor, MODULE_COLORS, UNKNOWN_MODULE, FEYNMAN_MODULE } from './mapData';
14+
import type { KnowledgeGraph } from './knowledgeGraph';
15+
16+
const graph = (overrides: Partial<KnowledgeGraph>): KnowledgeGraph => ({
17+
coldStart: false,
18+
nodes: [
19+
{ id: 'card:1', concept: '微积分', tier: '牢固', glow: 1, dimmed: false },
20+
{ id: 'card:2', concept: '导数', tier: '成长中', glow: 0.6, dimmed: false },
21+
{ id: 'feynman:wp1', concept: '极限', tier: '朦胧', glow: 0.3, dimmed: true },
22+
],
23+
links: [
24+
{ source: 'card:1', target: 'feynman:wp1', kind: 'weakpoint' },
25+
{ source: 'card:1', target: 'card:2', kind: 'shared-note' },
26+
],
27+
...overrides,
28+
});
29+
30+
describe('moduleColor', () => {
31+
it('should map any module to one of the palette colors', () => {
32+
expect(MODULE_COLORS).toHaveLength(8);
33+
for (const m of ['数学', '英语', '未分类', '费曼薄弱点', '']) {
34+
expect(MODULE_COLORS).toContain(moduleColor(m));
35+
}
36+
});
37+
38+
it('should be deterministic for the same module', () => {
39+
expect(moduleColor('线性代数')).toBe(moduleColor('线性代数'));
40+
});
41+
});
42+
43+
describe('deriveMapNodes', () => {
44+
it('should return empty for cold start or empty graph', () => {
45+
expect(deriveMapNodes(graph({ coldStart: true }))).toEqual([]);
46+
expect(deriveMapNodes(graph({ nodes: [], links: [] }))).toEqual([]);
47+
});
48+
49+
it('should project every node with mastery-based z layering', () => {
50+
// Act
51+
const nodes = deriveMapNodes(graph({}));
52+
// Assert
53+
expect(nodes).toHaveLength(3);
54+
// 牢固节点 z=3.2 顶层,朦胧节点 z≈0.96
55+
const solid = nodes.find((n) => n.id === 'card:1');
56+
const hazy = nodes.find((n) => n.id === 'feynman:wp1');
57+
expect(solid?.position3D[2]).toBe(3.2);
58+
expect(hazy?.position3D[2]).toBe(0.96);
59+
expect(solid?.mastery).toBe(1);
60+
});
61+
62+
it('should aggregate bidirectional connections from links', () => {
63+
// Act
64+
const nodes = deriveMapNodes(graph({}));
65+
// Assert:无向连接——两端都收录
66+
const c1 = nodes.find((n) => n.id === 'card:1');
67+
const wp1 = nodes.find((n) => n.id === 'feynman:wp1');
68+
expect(c1?.connections).toContain('feynman:wp1');
69+
expect(c1?.connections).toContain('card:2');
70+
expect(wp1?.connections).toContain('card:1');
71+
});
72+
73+
it('should assign source modules including synthetic fallbacks', () => {
74+
// Arrange:仅 card:1 有溯源
75+
const sourceRefs = new Map([['1', '数学']]);
76+
// Act
77+
const nodes = deriveMapNodes(graph({}), sourceRefs);
78+
// Assert
79+
expect(nodes.find((n) => n.id === 'card:1')?.sourceModule).toBe('数学');
80+
expect(nodes.find((n) => n.id === 'card:2')?.sourceModule).toBe(UNKNOWN_MODULE);
81+
expect(nodes.find((n) => n.id === 'feynman:wp1')?.sourceModule).toBe(FEYNMAN_MODULE);
82+
});
83+
84+
it('should produce identical positions for identical input', () => {
85+
// Act
86+
const first = deriveMapNodes(graph({}));
87+
const second = deriveMapNodes(graph({}));
88+
// Assert:确定性打散
89+
expect(first).toEqual(second);
90+
});
91+
92+
it('should keep x/y inside the world disc', () => {
93+
// Act
94+
const nodes = deriveMapNodes(graph({}));
95+
// Assert:半径 4.2 内
96+
for (const n of nodes) {
97+
const [x, y] = n.position3D;
98+
expect(Math.hypot(x, y)).toBeLessThanOrEqual(4.2);
99+
}
100+
});
101+
});

0 commit comments

Comments
 (0)