Files
beaver_project/app-instance/frontend/lib/task-timeline.test.ts
steven_li fc9fd93c36 feat: 支持多语言提示词本地化和界面优化
- 添加 prompt_locale 参数支持简体中文、繁体中文和英文提示词本地化
- 移除内置 agents 配置以简化系统架构
- 更新 ContextBuilder 使用动态提示词模板而非硬编码内容
- 在 AgentLoop、Web 接口和 AgentService 中传递 locale 参数
- 添加输出语言指令确保用户界面内容按指定语言生成
- 扩展前端 LanguageSwitcher 组件支持三种语言选项
- 优化 Header 和侧边栏组件的响应式布局和文本截断处理
- 更新测试用例验证不同语言环境下的提示词正确性
2026-06-10 16:11:05 +08:00

512 lines
16 KiB
TypeScript
Raw Permalink 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.

import { describe, expect, it } from 'vitest';
import { buildTaskTimelineCards } from '@/lib/task-timeline';
import type { BackendTask, ProcessArtifact, ProcessEvent, ProcessRun } from '@/types';
function makeTask(overrides: Partial<BackendTask> = {}): BackendTask {
return {
task_id: 'task-1',
session_id: 'web:default',
description: 'Research the market',
short_title: 'Market research',
is_open: true,
goal: 'Summarize the market',
constraints: [],
priority: 1,
status: 'running',
creator: 'user',
created_at: '2026-05-26T10:00:00.000Z',
updated_at: '2026-05-26T10:00:00.000Z',
run_ids: ['run-main'],
skill_names: [],
feedback: [],
metadata: {},
...overrides,
};
}
describe('buildTaskTimelineCards', () => {
it('builds ordered timeline cards from task process data', () => {
const task = makeTask();
const processRuns: ProcessRun[] = [
{
run_id: 'run-main',
parent_run_id: null,
session_id: 'web:default',
actor_type: 'agent',
actor_id: 'main-agent',
actor_name: 'Main Agent',
title: 'Plan and coordinate',
status: 'running',
started_at: '2026-05-26T10:00:30.000Z',
},
{
run_id: 'run-research',
parent_run_id: 'run-main',
session_id: 'web:default',
actor_type: 'agent',
actor_id: 'research-agent',
actor_name: 'Research Agent',
title: 'Read source documents',
status: 'done',
started_at: '2026-05-26T10:05:00.000Z',
finished_at: '2026-05-26T10:05:30.000Z',
summary: 'Finished reading source documents.',
},
];
const processEvents: ProcessEvent[] = [
{
event_id: 'evt-plan',
run_id: 'run-main',
parent_run_id: null,
kind: 'task_planned',
actor_type: 'agent',
actor_id: 'main-agent',
actor_name: 'Main Agent',
text: 'Plan created.',
created_at: '2026-05-26T10:01:00.000Z',
},
{
event_id: 'evt-skill',
run_id: 'run-main',
parent_run_id: null,
kind: 'skill_selected',
actor_type: 'system',
actor_id: 'skill-router',
actor_name: 'Skill Router',
text: 'Research skill selected.',
created_at: '2026-05-26T10:02:00.000Z',
metadata: {
selected_skill_names: ['research'],
reason: 'Need source review.',
},
},
{
event_id: 'evt-tool-start',
run_id: 'run-research',
parent_run_id: 'run-main',
kind: 'tool_call_started',
actor_type: 'mcp',
actor_id: 'document-reader',
actor_name: 'Document Reader',
text: 'Reading source documents.',
created_at: '2026-05-26T10:03:00.000Z',
},
{
event_id: 'evt-tool-finish',
run_id: 'run-research',
parent_run_id: 'run-main',
kind: 'tool_call_finished',
actor_type: 'mcp',
actor_id: 'document-reader',
actor_name: 'Document Reader',
text: 'Documents read.',
created_at: '2026-05-26T10:04:00.000Z',
metadata: {
result_summary: '2 documents read successfully.',
},
},
];
const processArtifacts: ProcessArtifact[] = [
{
artifact_id: 'artifact-summary',
run_id: 'run-research',
actor_type: 'agent',
actor_id: 'research-agent',
actor_name: 'Research Agent',
title: 'Research summary',
artifact_type: 'markdown',
content: '# Summary',
created_at: '2026-05-26T10:06:00.000Z',
},
];
const cards = buildTaskTimelineCards({
task,
processRuns,
processEvents,
processArtifacts,
});
expect(cards.map((card) => card.type)).toEqual([
'task_created',
'plan',
'skill',
'tool_call',
'tool_result',
'agent_progress',
'artifact',
]);
expect(cards[1].title).toBe('执行计划');
expect(cards[2].title).toBe('选择 Skill');
expect(cards[4].summary).toBe('2 documents read successfully.');
expect(cards[6].relatedArtifactIds).toEqual(['artifact-summary']);
});
it('localizes generated milestone titles for English and Traditional Chinese', () => {
const task = makeTask();
const processEvents: ProcessEvent[] = [
{
event_id: 'evt-plan',
run_id: 'run-main',
parent_run_id: null,
kind: 'task_planned',
actor_type: 'agent',
actor_id: 'planner',
actor_name: 'Task Planner',
text: 'Plan created.',
created_at: '2026-05-26T10:01:00.000Z',
},
{
event_id: 'evt-tool-start',
run_id: 'run-main',
parent_run_id: null,
kind: 'tool_call_started',
actor_type: 'mcp',
actor_id: 'user_files_list',
actor_name: 'user_files_list',
text: 'Calling tool: user_files_list.',
created_at: '2026-05-26T10:02:00.000Z',
},
];
const englishCards = buildTaskTimelineCards({ task, processEvents, locale: 'en-US' });
const traditionalCards = buildTaskTimelineCards({ task, processEvents, locale: 'zh-Hant' });
expect(englishCards.map((card) => card.title)).toEqual([
'Task created',
'Execution plan',
'Calling tool: user_files_list',
]);
expect(traditionalCards.map((card) => card.title)).toEqual([
'任務已創建',
'執行計劃',
'調用工具user_files_list',
]);
});
it('appends result and acceptance cards for closed tasks with feedback', () => {
const task = makeTask({
is_open: false,
status: 'closed',
updated_at: '2026-05-26T10:04:00.000Z',
closed_at: '2026-05-26T10:04:00.000Z',
feedback: [
{
acceptance_type: 'accept',
comment: '可以',
created_at: '2026-05-26T10:05:00.000Z',
run_id: 'run-main',
},
],
});
const cards = buildTaskTimelineCards({ task });
expect(cards.at(-2)?.type).toBe('result');
expect(cards.at(-1)?.type).toBe('acceptance');
expect(cards.at(-1)?.summary).toContain('可以');
});
it('uses the latest assistant message from the acceptance run as the result body', () => {
const task = makeTask({
status: 'awaiting_acceptance',
updated_at: '2026-05-26T10:04:00.000Z',
run_ids: ['run-main'],
runs: [
{
run_id: 'run-main',
title: '主 Agent',
session_id: 'web:default',
messages: [
{ role: 'assistant', content: 'Draft answer', created_at: '2026-05-26T10:03:00.000Z' },
{ role: 'assistant', content: 'Final user-visible answer', created_at: '2026-05-26T10:04:00.000Z' },
],
},
],
});
const processEvents: ProcessEvent[] = [
{
event_id: 'evt-result-ready',
run_id: 'run-main',
parent_run_id: null,
kind: 'task_result_ready',
actor_type: 'system',
actor_id: 'evidence',
actor_name: 'Evidence',
text: 'The task result is ready for user acceptance.',
created_at: '2026-05-26T10:04:00.000Z',
metadata: {
result_summary: 'Summary should not replace the final answer.',
},
},
];
const cards = buildTaskTimelineCards({ task, processEvents });
const result = cards.find((card) => card.type === 'result');
expect(result?.summary).toBe('Final user-visible answer');
expect(result?.details?.result_summary).toBe('Summary should not replace the final answer.');
});
it('collapses previous result and acceptance cards into a history pack', () => {
const task = makeTask({
status: 'awaiting_acceptance',
updated_at: '2026-05-26T10:12:00.000Z',
run_ids: ['run-1', 'run-2'],
feedback: [
{
acceptance_type: 'revise',
comment: 'Add decisions',
created_at: '2026-05-26T10:06:00.000Z',
run_id: 'run-1',
},
],
runs: [
{
run_id: 'run-1',
title: '主 Agent',
session_id: 'web:default',
messages: [{ role: 'assistant', content: 'Version one answer', created_at: '2026-05-26T10:05:00.000Z' }],
},
{
run_id: 'run-2',
title: '主 Agent',
session_id: 'web:default',
messages: [{ role: 'assistant', content: 'Version two answer', created_at: '2026-05-26T10:12:00.000Z' }],
},
],
});
const processEvents: ProcessEvent[] = [
{
event_id: 'evt-result-1',
run_id: 'run-1',
parent_run_id: null,
kind: 'task_result_ready',
actor_type: 'system',
actor_id: 'evidence',
actor_name: 'Evidence',
text: 'Result one ready.',
created_at: '2026-05-26T10:05:00.000Z',
},
{
event_id: 'evt-plan-2',
run_id: 'run-2',
parent_run_id: null,
kind: 'task_planned',
actor_type: 'system',
actor_id: 'planner',
actor_name: 'Task Planner',
text: 'Second attempt planned.',
created_at: '2026-05-26T10:08:00.000Z',
},
{
event_id: 'evt-result-2',
run_id: 'run-2',
parent_run_id: null,
kind: 'task_result_ready',
actor_type: 'system',
actor_id: 'evidence',
actor_name: 'Evidence',
text: 'Result two ready.',
created_at: '2026-05-26T10:12:00.000Z',
},
];
const cards = buildTaskTimelineCards({ task, processEvents });
expect(cards.map((card) => card.type)).toEqual([
'task_created',
'result_history',
'plan',
'result',
]);
const history = cards.find((card) => card.type === 'result_history');
expect(history?.summary).toBe('1 历史结果版本');
expect(history?.details?.versions).toEqual([
expect.objectContaining({
runId: 'run-1',
result: 'Version one answer',
acceptanceType: 'revise',
comment: 'Add decisions',
}),
]);
expect(cards.find((card) => card.id === 'evt-plan-2')).toBeTruthy();
expect(cards.at(-1)?.summary).toBe('Version two answer');
});
it('does not add fallback progress when a child run already has progress events', () => {
const task = makeTask();
const processRuns: ProcessRun[] = [
{
run_id: 'run-research',
parent_run_id: 'run-main',
session_id: 'web:default',
actor_type: 'agent',
actor_id: 'research-agent',
actor_name: 'Research Agent',
title: 'Read source documents',
status: 'running',
started_at: '2026-05-26T10:01:00.000Z',
},
];
const processEvents: ProcessEvent[] = [
{
event_id: 'evt-progress',
run_id: 'run-research',
parent_run_id: 'run-main',
kind: 'run_progress',
actor_type: 'agent',
actor_id: 'research-agent',
actor_name: 'Research Agent',
text: 'Reading source documents.',
created_at: '2026-05-26T10:02:00.000Z',
},
];
const cards = buildTaskTimelineCards({ task, processRuns, processEvents });
expect(cards.filter((card) => card.runId === 'run-research' && card.type === 'agent_progress')).toHaveLength(1);
expect(cards.map((card) => card.id)).not.toContain('run-research:fallback-progress');
});
it('marks a tool call as finished when a matching tool result exists', () => {
const task = makeTask();
const processEvents: ProcessEvent[] = [
{
event_id: 'evt-tool-start',
run_id: 'run-main',
parent_run_id: null,
kind: 'tool_call_started',
actor_type: 'mcp',
actor_id: 'web_search',
actor_name: 'web_search',
text: 'Calling tool: web_search.',
status: 'running',
created_at: '2026-05-26T10:02:00.000Z',
metadata: {
tool_call_id: 'call-1',
tool_name: 'web_search',
},
},
{
event_id: 'evt-tool-finish',
run_id: 'run-main',
parent_run_id: null,
kind: 'tool_call_finished',
actor_type: 'mcp',
actor_id: 'web_search',
actor_name: 'web_search',
text: 'Search failed.',
status: 'error',
created_at: '2026-05-26T10:03:00.000Z',
metadata: {
tool_call_id: 'call-1',
tool_name: 'web_search',
result_summary: 'Search failed.',
},
},
];
const cards = buildTaskTimelineCards({ task, processEvents });
expect(cards.find((card) => card.id === 'evt-tool-start')?.status).toBe('error');
expect(cards.find((card) => card.id === 'evt-tool-finish')?.type).toBe('tool_result');
expect(cards.find((card) => card.id === 'evt-tool-finish')?.summary).toBe('Search failed.');
});
it('maps agent_finished events without timeline metadata to agent progress cards', () => {
const task = makeTask();
const processEvents: ProcessEvent[] = [
{
event_id: 'evt-agent-finished',
run_id: 'run-research',
parent_run_id: 'run-main',
kind: 'agent_finished',
actor_type: 'agent',
actor_id: 'research-agent',
actor_name: 'Research Agent',
text: 'Finished reading source documents.',
status: 'done',
created_at: '2026-05-26T10:02:00.000Z',
},
];
const cards = buildTaskTimelineCards({ task, processEvents });
expect(cards.find((card) => card.id === 'evt-agent-finished')?.type).toBe('agent_progress');
});
it('sorts invalid timestamps after valid timestamps while preserving insertion order', () => {
const task = makeTask();
const processEvents: ProcessEvent[] = [
{
event_id: 'evt-invalid-date',
run_id: 'run-main',
parent_run_id: null,
kind: 'task_planned',
actor_type: 'agent',
actor_id: 'main-agent',
actor_name: 'Main Agent',
text: 'Plan created.',
created_at: 'not-a-date',
},
];
const cards = buildTaskTimelineCards({ task, processEvents });
expect(cards.map((card) => card.id)).toEqual(['task-1:created', 'evt-invalid-date']);
});
it('dedupes synthetic result and acceptance milestones when lifecycle events exist', () => {
const task = makeTask({
is_open: false,
status: 'closed',
updated_at: '2026-05-26T10:04:00.000Z',
closed_at: '2026-05-26T10:04:00.000Z',
feedback: [
{
acceptance_type: 'accept',
comment: '可以',
created_at: '2026-05-26T10:05:00.000Z',
run_id: 'run-main',
},
],
});
const processEvents: ProcessEvent[] = [
{
event_id: 'evt-result-ready',
run_id: 'run-main',
parent_run_id: null,
kind: 'task_result_ready',
actor_type: 'agent',
actor_id: 'main-agent',
actor_name: 'Main Agent',
text: 'Result is ready.',
created_at: '2026-05-26T10:04:00.000Z',
},
{
event_id: 'evt-acceptance-recorded',
run_id: 'run-main',
parent_run_id: null,
kind: 'task_acceptance_recorded',
actor_type: 'user',
actor_id: 'user-acceptance',
actor_name: 'User Acceptance',
text: '可以',
created_at: '2026-05-26T10:05:02.000Z',
metadata: {
acceptance_type: 'accept',
},
},
];
const cards = buildTaskTimelineCards({ task, processEvents });
expect(cards.filter((card) => card.type === 'result')).toHaveLength(1);
expect(cards.filter((card) => card.type === 'acceptance')).toHaveLength(1);
expect(cards.map((card) => card.id)).toContain('evt-result-ready');
expect(cards.map((card) => card.id)).toContain('evt-acceptance-recorded');
});
});