156 lines
4.8 KiB
TypeScript
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');
|
|
});
|
|
|