import { normalizeSkillProcessLine } from '../src/modules/netaclaw/service/skill_executor'; describe('skill process events', () => { it('only parses explicit process_event JSON lines', () => { const event = normalizeSkillProcessLine( 'vehicle-damage-inspection', '{"type":"process_event","stage":"inspect","message":"Inspecting","taskId":"task-1","status":"completed","level":"warning","current":2,"total":4,"percent":55,"timestamp":"2026-05-06T00:00:00.000Z","note":"kept"}', 7, ); expect(event).toEqual({ source: 'skill', targetType: 'skill', taskId: 'task-1', stage: 'inspect', status: 'completed', level: 'warning', message: 'Inspecting', timestamp: '2026-05-06T00:00:00.000Z', sequence: 7, current: 2, total: 4, percent: 55, payload: { note: 'kept' }, }); }); it('supports bracketed stderr prefixes before JSON', () => { const event = normalizeSkillProcessLine( 'vehicle-damage-inspection', '[vehicle-damage-inspection] {"type":"process_event","stage":"upload","time":"2026-05-06T00:00:00.000Z"}', 1, ); expect(event).toEqual(expect.objectContaining({ source: 'skill', targetType: 'skill', stage: 'upload', status: 'running', level: 'info', message: 'upload', timestamp: '2026-05-06T00:00:00.000Z', sequence: 1, })); }); it('derives percent from batch and totalBatches', () => { const event = normalizeSkillProcessLine( 'vehicle-damage-inspection', '{"type":"process_event","stage":"batching","batch":1,"totalBatches":3}', 2, ); expect(event).toEqual(expect.objectContaining({ current: 1, total: 3, percent: 33, })); }); it('sanitizes payload before emitting process events', () => { const event = normalizeSkillProcessLine( 'vehicle-damage-inspection', JSON.stringify({ type: 'process_event', message: 'request completed', apiKey: 'secret', rawText: 'x'.repeat(600), nested: { value: 1 }, }), 3, ); expect(event?.payload).toEqual({ apiKey: '[filtered]', rawText: `${'x'.repeat(500)}...`, nested: '[object]', }); }); it('returns null for ordinary stderr', () => { expect(normalizeSkillProcessLine( 'vehicle-damage-inspection', 'Traceback: failed to import module', 1, )).toBeNull(); }); it('returns null for ordinary JSON without process_event type', () => { expect(normalizeSkillProcessLine( 'vehicle-damage-inspection', '{"stage":"inspect","message":"ordinary json"}', 1, )).toBeNull(); }); }); describe('vehicle damage inspection process event output', () => { it('main skill progress logs are explicit process events', async () => { const scriptPath = require.resolve('../skills/vehicle-damage-inspection/scripts/index.cjs'); const originalWrite = process.stderr.write; const writes: string[] = []; jest.resetModules(); process.stderr.write = jest.fn((chunk: any) => { writes.push(String(chunk)); return true; }) as any; try { const skill = require(scriptPath); await skill.run( { videoUrl: 'demo.mp4', taskId: 'task-1', mode: 'frames-only' }, { RZYX_AI_WORKSPACE_ROOT: 'C:/data/workspace/vehicle-damage-inspection', RZYX_AI_UPLOAD_ROOT: 'C:/data/uploads', }, { createWorkspace: () => ({ taskId: 'task-1', workspacePath: 'C:/data/workspace/vehicle-damage-inspection/task-1', }), resolveVideoPath: () => 'C:/data/uploads/demo.mp4', extractFrames: async () => ({ videoInfo: { duration: 1, resolution: '960x544', extractedFrames: 1 }, frames: [{ index: 0, timestamp: 0, path: 'frame.jpg' }], }), writeJson: () => undefined, }, ); } finally { process.stderr.write = originalWrite; } const parsed = writes .map(line => line.match(/\[vehicle-damage-inspection\]\s*(\{.*\})/)?.[1]) .filter(Boolean) .map(json => JSON.parse(json as string)); expect(parsed.length).toBeGreaterThan(0); expect(parsed.every(item => item.type === 'process_event')).toBe(true); }); });