GPU_GUARD_MONOREPO/packages/backend/test/execute_skill_process_bridge.test.ts

213 lines
7.8 KiB
TypeScript
Raw Permalink Normal View History

2026-05-20 21:39:12 +08:00
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('右前翼子板');
});
});