Files
Agent-System/web/src/engine/anchorNavigator.test.ts
T

156 lines
4.8 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
parseAnchorHash,
formatAnchorHash,
navigateToAnchor,
resolveTurnAnchorId,
} from './anchorNavigator';
test('parseAnchorHash: 正确解析标准深链 hash', () => {
const t1 = parseAnchorHash('#/s/sess_001/m/1024');
assert.deepEqual(t1, { sessionId: 'sess_001', messageId: 1024 });
const t2 = parseAnchorHash('/s/sess_benchmark_long_001/m/23592');
assert.deepEqual(t2, { sessionId: 'sess_benchmark_long_001', messageId: 23592 });
assert.equal(parseAnchorHash(''), null);
assert.equal(parseAnchorHash('#/random/route'), null);
assert.equal(parseAnchorHash('#/s/sess_001/m/abc'), null);
assert.equal(parseAnchorHash('#/s/sess_001/m/-1'), null);
});
test('formatAnchorHash: 正确构造深链格式', () => {
const hash = formatAnchorHash('sess_001', 1024);
assert.equal(hash, '#/s/sess_001/m/1024');
});
test('navigateToAnchor: 快路径直接命中 DOM 节点', async () => {
let scrolled = false;
let blockOption = '';
let behaviorOption = '';
const mockElement = {
classList: {
classes: new Set<string>(),
add(c: string) {
this.classes.add(c);
},
remove(c: string) {
this.classes.delete(c);
},
contains(c: string) {
return this.classes.has(c);
},
},
scrollIntoView(options: { block?: string; behavior?: string }) {
scrolled = true;
blockOption = options?.block || '';
behaviorOption = options?.behavior || '';
},
offsetWidth: 100,
};
(globalThis as any).document = {
getElementById(id: string) {
if (id === 'chat-anchor-1024') return mockElement;
return null;
},
};
const success = await navigateToAnchor({
sessionId: 'sess_001',
messageId: 1024,
highlightMs: 50,
});
assert.equal(success, true);
assert.equal(scrolled, true);
assert.equal(blockOption, 'start');
assert.equal(behaviorOption, 'auto'); // 严格无动画
assert.equal(mockElement.classList.contains('chat-anchor-pulse'), true);
delete (globalThis as any).document;
});
test('navigateToAnchor: 慢路径拉取分片后命中', async () => {
let loadCalled = false;
let elementMounted = false;
const mockElement = {
classList: {
classes: new Set<string>(),
add(c: string) {
this.classes.add(c);
},
remove(c: string) {
this.classes.delete(c);
},
contains(c: string) {
return this.classes.has(c);
},
},
scrollIntoView() {},
offsetWidth: 100,
};
(globalThis as any).document = {
getElementById(id: string) {
if (id === 'chat-anchor-9999' && elementMounted) return mockElement;
return null;
},
};
const ctx = {
sessionId: 'sess_001',
loadDayForAnchor: async (msgId: number) => {
loadCalled = true;
if (msgId === 9999) {
elementMounted = true;
return true;
}
return false;
},
};
const success = await navigateToAnchor(
{
sessionId: 'sess_001',
messageId: 9999,
highlightMs: 50,
},
ctx
);
assert.equal(loadCalled, true);
assert.equal(success, true);
delete (globalThis as any).document;
});
test('resolveTurnAnchorId: 方案 A 轮次归一化测试', () => {
const mockMessages: any[] = [
{ id: 'm1', serverId: 101, role: 'user', content: '请问超声刀注册申报要求', category: 'consult' },
{ id: 'm2', serverId: 102, role: 'assistant', content: '超声刀软组织设备申报流程如下...', category: 'consult' },
{ id: 'm3', serverId: 103, role: 'user', content: '生成立项申报任务', category: 'task' },
{ id: 'm4', serverId: 104, role: 'assistant', content: '已生成任务卡片', category: 'task' },
{ id: 'm5', serverId: 105, role: 'system', content: '系统维护提示', category: 'system' },
];
// 1. 若目标是 assistant 回复,必须向上溯源至该轮用户提问
assert.equal(resolveTurnAnchorId(102, mockMessages), 101, 'assistant msg 102 should resolve to user prompt 101');
assert.equal(resolveTurnAnchorId(104, mockMessages), 103, 'assistant msg 104 should resolve to user prompt 103');
// 2. 若自身就是 user 提问,保持不变
assert.equal(resolveTurnAnchorId(101, mockMessages), 101, 'user msg 101 should remain 101');
assert.equal(resolveTurnAnchorId(103, mockMessages), 103, 'user msg 103 should remain 103');
// 3. 孤立系统消息无前序用户提问,回退自身
assert.equal(resolveTurnAnchorId(105, mockMessages), 105, 'system msg 105 should fallback to itself');
// 4. 空消息列表或未匹配,安全返回原 ID
assert.equal(resolveTurnAnchorId(999, mockMessages), 999, 'unmatched msg returns raw id');
assert.equal(resolveTurnAnchorId(102, []), 102, 'empty messages returns raw id');
});