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('右前翼子板');
|
|||
|
|
});
|
|||
|
|
});
|