#!/usr/bin/env node import { execFileSync } from 'node:child_process'; import { readFile } from 'node:fs/promises'; const DEFAULT_CDP = '/Users/inmanx/.agents/skills/chrome-cdp/scripts/cdp.mjs'; const DEFAULT_PLATFORM_TARGET = 'DA600CC0'; const EXTENSION_ID = 'kafggjjlhebccdgkechmbgflkaifkccf'; function parseArgs(argv) { const args = { fixture: '', expectedRunId: '', platformTarget: DEFAULT_PLATFORM_TARGET, cdp: process.env.CDP_CLI || DEFAULT_CDP, timeoutMs: 45_000 }; for (let index = 0; index < argv.length; index += 1) { const value = argv[index]; if (value === '--fixture') args.fixture = argv[++index] || ''; else if (value === '--expected-run-id') args.expectedRunId = argv[++index] || ''; else if (value === '--platform-target') args.platformTarget = argv[++index] || ''; else if (value === '--cdp') args.cdp = argv[++index] || ''; else if (value === '--timeout-ms') args.timeoutMs = Number(argv[++index]); else throw new Error(`unknown argument: ${value}`); } if (!args.fixture) throw new Error('--fixture is required'); if (!args.expectedRunId) throw new Error('--expected-run-id is required'); if (!Number.isFinite(args.timeoutMs) || args.timeoutMs < 10_000) throw new Error('--timeout-ms must be at least 10000'); return args; } function cdp(args, command, target, ...rest) { return execFileSync(process.execPath, [args.cdp, command, target, ...rest], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 }).trim(); } function parseJson(value, label) { try { return JSON.parse(String(value || '').trim()); } catch { throw new Error(`${label} returned non-JSON output: ${String(value || '').slice(0, 500)}`); } } function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function validateOperation(operation, expectedRunId) { const context = operation?.source?.test_context || {}; const refs = operation?.data?.existing_refs || {}; const errors = []; if (!String(operation?.action || '').match(/^(?:passenger_list_import|arrangement_(?:guide|vehicle|hotel|transport|other)|order_update_(?:independent|shared_plan|shared_child)|order_(?:cancel|restore))$/)) errors.push('action_not_requery_allowlisted'); if (context.run_id !== expectedRunId) errors.push('run_id_mismatch'); if (context.marker !== 'TEST-202609' || refs.marker !== 'TEST-202609') errors.push('marker_mismatch'); if (context.account !== '测试ai员工账号' || refs.owner_account !== '测试ai员工账号') errors.push('owner_mismatch'); if (context.allow_live_write !== false) errors.push('read_only_context_required'); if (!/^2026-09-\d{2}$/.test(String(refs.departure_date || ''))) errors.push('date_outside_2026_09'); if (!String(refs.tid || '').match(/^\d+$/)) errors.push('tid_missing'); return errors; } const args = parseArgs(process.argv.slice(2)); const operation = JSON.parse(await readFile(args.fixture, 'utf8')); const validationErrors = validateOperation(operation, args.expectedRunId); if (validationErrors.length) throw new Error(`read-only lifecycle requery safety check failed: ${validationErrors.join(',')}`); parseJson(cdp(args, 'eval', args.platformTarget, `(async()=>JSON.stringify(await sendToExtension('PING',{},3000)))()`), 'extension ping'); const targetList = parseJson(cdp(args, 'evalraw', args.platformTarget, 'Target.getTargets', '{}'), 'target list'); const worker = targetList.targetInfos?.find((target) => target.type === 'service_worker' && target.url === `chrome-extension://${EXTENSION_ID}/background.js`); if (!worker?.targetId) throw new Error('extension_service_worker_missing'); const attached = parseJson(cdp(args, 'evalraw', args.platformTarget, 'Target.attachToTarget', JSON.stringify({ targetId: worker.targetId, flatten: false })), 'worker attach'); if (!attached.sessionId) throw new Error('extension_service_worker_attach_failed'); const probeKey = `READONLY-LIFECYCLE-${args.expectedRunId}-${Date.now()}`; const expression = `(async()=>{ const operation=${JSON.stringify(operation)}; const probeKey=${JSON.stringify(probeKey)}; let report; try{ const tab=await findOrOpenErpTab(probeKey); const routeResults=await runPageAction(tab.id,'prepareLifecycleOperation',[operation],{allFrames:false},probeKey); const routePreparation=normalizeLifecycleReport(routeResults[0]?.result||{ status:'lifecycle_route_blocked',blockers:['lifecycle_route_preparation_missing_result'],no_erp_write:true,write_attempted:false }); if(!['lifecycle_route_ready','lifecycle_route_not_required'].includes(routePreparation.status)){ report={status:'blocked',stage:'route_preparation',route_preparation:routePreparation,write_attempted:false,no_erp_write:true}; }else{ const discovery=await discoverExactLifecycleFrame(tab.id,operation,probeKey); if(discovery.status!=='ready'&&discovery.status!=='not_required'){ report={status:'blocked',stage:'frame_discovery',route_preparation:routePreparation,frame_discovery:discovery,write_attempted:false,no_erp_write:true}; }else{ const target=discovery.frame_id?{frameIds:[discovery.frame_id]}:{allFrames:true}; const verificationResults=await runPageAction(tab.id,'verifyLifecycleOperation',[operation],target,probeKey); const verification=selectLifecycleRequeryResult(verificationResults); report={ status:verification.status==='lifecycle_requery_matched'&&verification.requery?.matched===true?'completed':'not_matched', stage:'read_only_lifecycle_requery',route_preparation:routePreparation,frame_discovery:discovery, verification,write_attempted:false,no_erp_write:true }; } } }catch(error){ report={status:'blocked',stage:'read_only_lifecycle_requery_error',error_name:String(error?.name||'Error'),error_message:String(error?.message||error).slice(0,500),write_attempted:false,no_erp_write:true}; } const saved=await chrome.storage.local.get('businessTaskResults'); const resultMap=saved.businessTaskResults||{}; resultMap[probeKey]={...report,message:'read-only lifecycle requery completed'}; await chrome.storage.local.set({businessTaskResults:resultMap}); return true; })()`; const message = JSON.stringify({ id: 1, method: 'Runtime.evaluate', params: { expression, awaitPromise: true, returnByValue: true } }); cdp(args, 'evalraw', args.platformTarget, 'Target.sendMessageToTarget', JSON.stringify({ sessionId: attached.sessionId, message })); const startedAt = Date.now(); let result = null; while (Date.now() - startedAt < args.timeoutMs) { try { const response = parseJson(cdp(args, 'eval', args.platformTarget, `(async()=>JSON.stringify(await sendToExtension('GET_TASK_RESULT',{task_id:${JSON.stringify(probeKey)}},3000)))()`), 'probe result'); result = response?.result || null; if (result && ['completed', 'not_matched', 'blocked'].includes(result.status)) break; } catch { // The bounded read-only route and requery are still running. } await wait(500); } try { cdp(args, 'evalraw', args.platformTarget, 'Target.detachFromTarget', JSON.stringify({ sessionId: attached.sessionId })); } catch { // Detachment is cleanup only. } if (!result) throw new Error(`read-only lifecycle requery timed out after ${args.timeoutMs}ms`); console.log(JSON.stringify({ probe_key: probeKey, result }, null, 2)); if (result.status !== 'completed') process.exitCode = 2;