213 lines
7.8 KiB
TypeScript
213 lines
7.8 KiB
TypeScript
import { createExecuteSkillTool } from '../src/modules/netaclaw/tools/builtin/execute_skill';
|
||
|
||
describe('execute_skill process event bridge', () => {
|
||
it('bridges skill process events to tool updates', async () => {
|
||
let capturedParams: any;
|
||
const executor = {
|
||
execute: jest.fn(async (params: any) => {
|
||
capturedParams = params;
|
||
params.onProcessEvent?.({
|
||
source: 'skill',
|
||
targetType: 'skill',
|
||
stage: 'frames',
|
||
status: 'running',
|
||
message: 'frames extracted',
|
||
current: 1,
|
||
total: 2,
|
||
payload: { frameCount: 10 },
|
||
});
|
||
return { success: true, output: { ok: true }, duration: 1 };
|
||
}),
|
||
};
|
||
const updates: any[] = [];
|
||
|
||
const tool = createExecuteSkillTool(executor as any);
|
||
const result = await tool.execute(
|
||
'call-1',
|
||
{ name: 'demo', input: { taskId: 't1' } },
|
||
event => updates.push(event),
|
||
);
|
||
|
||
expect(capturedParams).toEqual(expect.objectContaining({
|
||
skillName: 'demo',
|
||
input: { taskId: 't1' },
|
||
toolCallId: 'call-1',
|
||
}));
|
||
expect(updates).toEqual([
|
||
expect.objectContaining({
|
||
source: 'skill',
|
||
targetType: 'skill',
|
||
stage: 'frames',
|
||
status: 'running',
|
||
message: 'frames extracted',
|
||
current: 1,
|
||
total: 2,
|
||
payload: {
|
||
frameCount: 10,
|
||
skillName: 'demo',
|
||
},
|
||
}),
|
||
]);
|
||
expect(result).toEqual({
|
||
type: 'text',
|
||
text: JSON.stringify({ ok: true }, null, 2),
|
||
});
|
||
});
|
||
|
||
it('does not fail execute_skill when process update observer throws', async () => {
|
||
const executor = {
|
||
execute: jest.fn(async (params: any) => {
|
||
params.onProcessEvent?.({
|
||
source: 'skill',
|
||
targetType: 'skill',
|
||
status: 'running',
|
||
message: 'still observational',
|
||
});
|
||
return { success: true, output: { ok: true }, duration: 1 };
|
||
}),
|
||
};
|
||
|
||
const tool = createExecuteSkillTool(executor as any);
|
||
const result = await tool.execute(
|
||
'call-1',
|
||
{ name: 'demo', input: {} },
|
||
() => {
|
||
throw new Error('observer failed');
|
||
},
|
||
);
|
||
|
||
expect(result).toEqual({
|
||
type: 'text',
|
||
text: JSON.stringify({ ok: true }, null, 2),
|
||
});
|
||
});
|
||
|
||
it('returns structured images when skill output includes bestFrameImages', async () => {
|
||
const output = {
|
||
success: true,
|
||
taskId: 'task_1',
|
||
summary: { mergedDamageCount: 1, bestFrameCount: 1, candidateDamageCount: 2, reviewImageCount: 1 },
|
||
vehicleInfo: { frontPlate: '粤B12345' },
|
||
candidates: [
|
||
{ id: 'cand_001', part: '前保险杠', type: '划痕' },
|
||
],
|
||
damages: [
|
||
{ id: 'damage_001', part: '前保险杠', type: '划痕', severity: '轻微' },
|
||
],
|
||
uncertainDamages: [
|
||
{ id: 'damage_002', part: '右前门', type: '凹陷', severity: '轻微' },
|
||
],
|
||
bestFrameImages: [
|
||
{
|
||
url: '/workspace/vehicle-damage-inspection/task_1/source/best_01_damage_001_2_00s.jpg',
|
||
mimeType: 'image/jpeg',
|
||
label: '前保险杠划痕 2s',
|
||
},
|
||
],
|
||
reviewImages: [
|
||
{
|
||
url: '/workspace/vehicle-damage-inspection/task_1/source/review_zoom_damage_001_2_00s.jpg',
|
||
mimeType: 'image/jpeg',
|
||
label: '模型不确定,放大后复核:前保险杠划痕',
|
||
purpose: '模型不确定,放大后复核',
|
||
reason: '放大后仍不够清晰,建议人工复核',
|
||
},
|
||
],
|
||
};
|
||
const executor = {
|
||
execute: jest.fn(async () => ({ success: true, output, duration: 1 })),
|
||
};
|
||
|
||
const tool = createExecuteSkillTool(executor as any);
|
||
const result = await tool.execute('call-1', { name: 'vehicle-damage-inspection', input: {} });
|
||
|
||
expect(result).toEqual({
|
||
type: 'images',
|
||
text: [
|
||
'Skill vehicle-damage-inspection 执行完成。',
|
||
'请只依据 finalResult.damages、finalResult.uncertainDamages、finalResult.bestFrameImages、finalResult.reviewImages 和 finalResult.summary 回答用户。',
|
||
'damages 是已确认旧伤;uncertainDamages 是放大后仍无法明确确认的疑似旧伤,需要提示人工复核,不能说成已确认旧伤。',
|
||
'reviewImages 是模型不清楚时生成的放大复核图;如存在,请说明“模型不清楚,已放大后判断”,并把图片返回给前端展示。',
|
||
'candidates 是已复核前的中间候选,不代表最终旧伤,不要把 candidates 当成检测结论。',
|
||
JSON.stringify({
|
||
success: true,
|
||
taskId: 'task_1',
|
||
finalResult: {
|
||
summary: { mergedDamageCount: 1, bestFrameCount: 1, candidateDamageCount: 2, reviewImageCount: 1 },
|
||
vehicleInfo: { frontPlate: '粤B12345' },
|
||
damages: [
|
||
{ id: 'damage_001', part: '前保险杠', type: '划痕', severity: '轻微' },
|
||
],
|
||
uncertainDamages: [
|
||
{ id: 'damage_002', part: '右前门', type: '凹陷', severity: '轻微' },
|
||
],
|
||
bestFrameImages: output.bestFrameImages,
|
||
reviewImages: output.reviewImages,
|
||
},
|
||
artifacts: undefined,
|
||
}, null, 2),
|
||
].join('\n'),
|
||
images: [
|
||
{
|
||
url: '/workspace/vehicle-damage-inspection/task_1/source/best_01_damage_001_2_00s.jpg',
|
||
mimeType: 'image/jpeg',
|
||
label: '前保险杠划痕 2s',
|
||
},
|
||
{
|
||
url: '/workspace/vehicle-damage-inspection/task_1/source/review_zoom_damage_001_2_00s.jpg',
|
||
mimeType: 'image/jpeg',
|
||
label: '模型不确定,放大后复核:前保险杠划痕',
|
||
purpose: '模型不确定,放大后复核',
|
||
reason: '放大后仍不够清晰,建议人工复核',
|
||
},
|
||
],
|
||
});
|
||
});
|
||
|
||
it('returns final-only text when reviewed skill output has rejected all candidates', async () => {
|
||
const output = {
|
||
success: true,
|
||
taskId: 'task_empty',
|
||
summary: { candidateDamageCount: 1, mergedDamageCount: 0, bestFrameCount: 0, reviewImageCount: 0 },
|
||
candidates: [
|
||
{ id: 'cand_001', part: '右前翼子板', type: '凹陷' },
|
||
],
|
||
damages: [],
|
||
uncertainDamages: [],
|
||
bestFrameImages: [],
|
||
reviewImages: [],
|
||
};
|
||
const executor = {
|
||
execute: jest.fn(async () => ({ success: true, output, duration: 1 })),
|
||
};
|
||
|
||
const tool = createExecuteSkillTool(executor as any);
|
||
const result = await tool.execute('call-1', { name: 'vehicle-damage-inspection', input: {} });
|
||
|
||
expect(result).toEqual({
|
||
type: 'text',
|
||
text: [
|
||
'Skill vehicle-damage-inspection 执行完成。',
|
||
'请只依据 finalResult.damages、finalResult.uncertainDamages、finalResult.bestFrameImages、finalResult.reviewImages 和 finalResult.summary 回答用户。',
|
||
'damages 是已确认旧伤;uncertainDamages 是放大后仍无法明确确认的疑似旧伤,需要提示人工复核,不能说成已确认旧伤。',
|
||
'reviewImages 是模型不清楚时生成的放大复核图;如存在,请说明“模型不清楚,已放大后判断”,并把图片返回给前端展示。',
|
||
'candidates 是已复核前的中间候选,不代表最终旧伤,不要把 candidates 当成检测结论。',
|
||
JSON.stringify({
|
||
success: true,
|
||
taskId: 'task_empty',
|
||
finalResult: {
|
||
summary: { candidateDamageCount: 1, mergedDamageCount: 0, bestFrameCount: 0, reviewImageCount: 0 },
|
||
vehicleInfo: undefined,
|
||
damages: [],
|
||
uncertainDamages: [],
|
||
bestFrameImages: [],
|
||
reviewImages: [],
|
||
},
|
||
artifacts: undefined,
|
||
}, null, 2),
|
||
].join('\n'),
|
||
});
|
||
expect((result as any).text).not.toContain('右前翼子板');
|
||
});
|
||
});
|