#!/usr/bin/env node import { createHash } from 'node:crypto'; 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 = 'D484CA70'; const DEFAULT_ERP_TARGET = 'DA600CC0'; const CREATE_ACTIONS = new Set([ 'team_order_create', 'team_order_batch_create', 'shared_plan_create', 'shared_child_order_create' ]); function parseArgs(argv) { const args = { fixture: '', taskId: '', confirm: false, allowLiveWrite: false, platformTarget: DEFAULT_PLATFORM_TARGET, erpTarget: DEFAULT_ERP_TARGET, cdp: process.env.CDP_CLI || DEFAULT_CDP, expectedRunId: '', parseTimeoutMs: 30_000, resultTimeoutMs: 120_000 }; for (let index = 0; index < argv.length; index += 1) { const value = argv[index]; if (value === '--fixture') args.fixture = argv[++index] || ''; else if (value === '--task-id') args.taskId = argv[++index] || ''; else if (value === '--confirm') args.confirm = true; else if (value === '--allow-live-write') args.allowLiveWrite = true; else if (value === '--platform-target') args.platformTarget = argv[++index] || ''; else if (value === '--erp-target') args.erpTarget = argv[++index] || ''; else if (value === '--cdp') args.cdp = argv[++index] || ''; else if (value === '--expected-run-id') args.expectedRunId = argv[++index] || ''; else if (value === '--parse-timeout-ms') args.parseTimeoutMs = Number(argv[++index]); else if (value === '--result-timeout-ms') args.resultTimeoutMs = 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.parseTimeoutMs) || args.parseTimeoutMs < 1_000) throw new Error('--parse-timeout-ms must be at least 1000'); if (!Number.isFinite(args.resultTimeoutMs) || args.resultTimeoutMs < 5_000) throw new Error('--result-timeout-ms must be at least 5000'); return args; } function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function cdp(args, command, target, ...rest) { return execFileSync(process.execPath, [args.cdp, command, target, ...rest], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 }).trim(); } function evaluate(args, target, expression) { return cdp(args, 'eval', target, expression); } function parseJsonOutput(value, label) { try { return JSON.parse(String(value || '').trim()); } catch (error) { throw new Error(`${label} returned non-JSON output: ${String(value || '').slice(0, 500)}`); } } function sha256(value) { return createHash('sha256').update(value).digest('hex'); } function coreOperation(operation, expected) { const copy = structuredClone(operation); if (copy.data && expected?.data) { if (!Object.hasOwn(expected.data, 'attachments')) delete copy.data.attachments; if (!Object.hasOwn(expected.data, 'system_defaults')) delete copy.data.system_defaults; } return copy; } function expectedPreConfirmationOperation(operation) { const copy = structuredClone(operation); if (!CREATE_ACTIONS.has(copy?.action) && copy?.source?.test_context) { copy.source.test_context.allow_live_write = false; } return copy; } function validateFixtureSafety(operation, expectedRunId, allowLiveWrite = false) { const context = operation?.source?.test_context || {}; const refs = operation?.data?.existing_refs || {}; const data = operation?.data || {}; const errors = []; if (CREATE_ACTIONS.has(operation?.action)) { if (allowLiveWrite) errors.push('creation_must_not_use_allow_live_write_flag'); const dates = Array.isArray(data.departure_dates) ? data.departure_dates : []; if (!dates.length || dates.some((date) => !/^2026-09-\d{2}$/.test(String(date || '')))) errors.push('creation_dates_outside_2026_09'); const contextDates = Array.isArray(context.target_dates) ? context.target_dates.map(String) : []; if (context.run_id !== expectedRunId) errors.push('creation_run_id_mismatch'); if (context.marker !== 'TEST-202609') errors.push('creation_marker_mismatch'); if (context.account !== '测试ai员工账号') errors.push('creation_account_mismatch'); if (!String(context.native_baseline_id || '').trim()) errors.push('creation_native_baseline_missing'); if (context.live_window !== '2026-09') errors.push('creation_live_window_mismatch'); if (dates.some((date) => !contextDates.includes(String(date)))) errors.push('creation_target_dates_mismatch'); if (context.allow_live_write === true) errors.push('creation_must_not_self_authorize_live_write'); const suffix = String(data.order_number?.suffix || ''); const parent = String(data.existing_refs?.parent_group_no || data.parent_group_no || ''); if (operation.action === 'shared_child_order_create') { if (!parent.includes('TEST-202609') || !String(data.special_requests || '').includes('TEST-202609')) errors.push('shared_child_creation_marker_mismatch'); } else if (!suffix.includes('TEST-202609')) errors.push('creation_suffix_marker_mismatch'); const splitOrder = data.split_order && typeof data.split_order === 'object' && !Array.isArray(data.split_order) ? data.split_order : {}; const sharedBatchSplitProbe = operation.action === 'shared_plan_create' && dates.length >= 2 && (splitOrder.customer !== undefined || splitOrder.passenger_counts !== undefined); if (sharedBatchSplitProbe) { if (context.phase !== 'shared_batch_split_order_probe') errors.push('shared_batch_probe_phase_mismatch'); } return errors; } 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'); const deleteLiveWrite = operation?.action === 'order_delete'; if (deleteLiveWrite && context.allow_live_write !== true) { errors.push('delete_fixture_allow_live_write_must_be_true'); } if (!deleteLiveWrite && context.allow_live_write === true && !allowLiveWrite) { errors.push('non_delete_live_write_requires_allow_live_write_flag'); } if (!deleteLiveWrite && context.allow_live_write !== true && allowLiveWrite) { errors.push('allow_live_write_flag_requires_fixture_authorization'); } if (context.live_window !== '2026-09') errors.push('live_window_mismatch'); const targetDate = String(refs.departure_date || ''); if (targetDate && !/^2026-09-\d{2}$/.test(targetDate)) errors.push('target_date_outside_2026_09'); if (!Array.isArray(context.created_refs) || !context.created_refs.some((item) => ( item?.identifier === refs.identifier && String(item?.tid || '') === String(refs.tid || '') && (!refs.ddid || String(item?.ddid || '') === String(refs.ddid)) ))) errors.push('target_not_in_created_refs'); return errors; } function localTaskExpression(taskId) { return `(()=>{const t=taskStore.find(x=>x.task_id===${JSON.stringify(taskId)});return JSON.stringify(t&&{task_id:t.task_id,status:t.status,stage:t.stage,message:t.message,parser:t.parse_response?.parser_prompt_version,action:t.operation?.action,data_keys:Object.keys(t.operation?.data||{}),failure:t.failure,success_receipt:t.success_receipt})})()`; } function exactRemoteTaskExpression(taskId) { return `(async()=>{const response=await fetch('/api/tasks/'+encodeURIComponent(${JSON.stringify(taskId)}),{credentials:'same-origin'});if(!response.ok)throw new Error('exact_task_http_'+response.status);const payload=await response.json();const t=payload?.task;if(!t)throw new Error('exact_task_missing');const index=taskStore.findIndex(x=>x.task_id===t.task_id);if(index>=0)taskStore[index]=t;else taskStore.unshift(t);return JSON.stringify({task_id:t.task_id,status:t.status,stage:t.stage,message:t.message,parser:t.parse_response?.parser_prompt_version,action:t.operation?.action,data_keys:Object.keys(t.operation?.data||{}),failure:t.failure,success_receipt:t.success_receipt})})()`; } async function waitForParsedTask(args, taskId) { const startedAt = Date.now(); let last = null; while (Date.now() - startedAt < args.parseTimeoutMs) { try { last = parseJsonOutput(evaluate(args, args.platformTarget, localTaskExpression(taskId)), 'task state'); } catch { last = null; } if (last && !['parse_queued', 'parse_running'].includes(last.status)) return last; try { last = parseJsonOutput(evaluate(args, args.platformTarget, exactRemoteTaskExpression(taskId)), 'exact remote task state'); if (last && !['parse_queued', 'parse_running'].includes(last.status)) return last; } catch { // The next exact read retries the same durable task; it never creates a duplicate. } await wait(500); } throw new Error(`task ${taskId} did not leave parse queue within ${args.parseTimeoutMs}ms; do not create a duplicate task`); } async function waitForCreatedTaskId(args, previousTaskId) { const startedAt = Date.now(); let latest = ''; while (Date.now() - startedAt < args.parseTimeoutMs) { try { const state = parseJsonOutput(evaluate( args, args.platformTarget, `JSON.stringify({task_id:String(currentTaskId||'')})` ), 'created task state'); latest = String(state?.task_id || ''); if (latest && latest !== previousTaskId) return latest; } catch { // The scheduled platform POST is still pending; do not schedule another. } await wait(500); } throw new Error(`scheduled task creation did not publish a new task id within ${args.parseTimeoutMs}ms; do not create a duplicate, query the expected run id`); } function terminalExtensionResult(result) { const status = String(result?.status || ''); const stage = String(result?.stage || ''); return /(?:completed|blocked|uncertain|pending)$/.test(status) || /(?:completed|blocked|uncertain|reconciliation)$/.test(stage); } async function waitForExtensionResult(args, taskId) { const startedAt = Date.now(); let latest = null; while (Date.now() - startedAt < args.resultTimeoutMs) { const expression = `(async()=>JSON.stringify(await sendToExtension('GET_TASK_RESULT',{task_id:${JSON.stringify(taskId)}},15000)))()`; try { const response = parseJsonOutput(evaluate(args, args.platformTarget, expression), 'extension result'); latest = response?.result || null; if (latest && terminalExtensionResult(latest)) return latest; } catch { // A transient bridge/page timeout does not authorize a resubmission. } await wait(2_000); } throw new Error(`task ${taskId} has no terminal extension result after ${args.resultTimeoutMs}ms; do not resubmit, inspect the same task`); } async function publishExistingResult(args) { try { evaluate(args, args.platformTarget, '(()=>{void (async()=>{await pollAllTaskResults();await syncRemoteTasks()})().catch(()=>{});return "scheduled"})()'); } catch { // The extension result remains durable even if this UI synchronization times out. } } function resultSummary(taskId, parsed, expectedHash, actualHash, result, platformTask) { const report = result?.report || result || {}; return { task_id: taskId, action: parsed.action, canonical_core_sha256: actualHash, expected_core_sha256: expectedHash, canonical_core_exact: expectedHash === actualHash, platform: platformTask, extension: { execution_id: result?.execution_id || report.execution_id || '', status: result?.status || '', stage: result?.stage || '', message: result?.message || '', write_attempted: result?.write_attempted === true, no_erp_write: result?.no_erp_write === true, blockers: result?.blockers || report.blockers || [], resolved_refs: result?.resolved_refs || report.resolved_refs || null, before_snapshot: result?.before_snapshot || report.before_snapshot || null, changes: report.changes || [], preflight: result?.preflight || report.preflight || null, server_response: result?.server_response || report.server_response || null, requery: result?.requery || report.requery || null, erp_receipt: result?.erp_receipt || report.erp_receipt || null, verification: result?.verification || report.verification || null, reconciliation: result?.reconciliation || report.reconciliation || null, submit: result?.submit || report.submit || null, side_effects: result?.side_effects || report.side_effects || [], warnings: result?.warnings || report.warnings || [], manual_review_required: result?.manual_review_required === true || report.manual_review_required === true } }; } const args = parseArgs(process.argv.slice(2)); const rawFixture = await readFile(args.fixture, 'utf8'); const fixture = JSON.parse(rawFixture); const fixtureSafetyErrors = validateFixtureSafety(fixture, args.expectedRunId, args.allowLiveWrite); if (fixtureSafetyErrors.length) throw new Error(`fixture safety check failed: ${fixtureSafetyErrors.join(',')}`); let taskId = String(args.taskId || ''); if (!taskId) { const encoded = Buffer.from(rawFixture).toString('base64'); const createExpression = `(()=>{const input=document.querySelector('#rawInstruction');if(!input)throw new Error('rawInstruction_missing');const previousTaskId=String(currentTaskId||'');input.value=new TextDecoder().decode(Uint8Array.from(atob(${JSON.stringify(encoded)}),c=>c.charCodeAt(0)));input.dispatchEvent(new Event('input',{bubbles:true}));void createTask().catch(()=>{});return JSON.stringify({scheduled:true,previous_task_id:previousTaskId})})()`; const scheduled = parseJsonOutput(evaluate(args, args.platformTarget, createExpression), 'task creation scheduling'); if (scheduled.scheduled !== true) throw new Error('task creation was not scheduled'); taskId = await waitForCreatedTaskId(args, String(scheduled.previous_task_id || '')); } const parsedState = await waitForParsedTask(args, taskId); if (parsedState.status !== 'awaiting_confirmation') { console.log(JSON.stringify({ task_id: taskId, status: parsedState.status, parsed_state: parsedState }, null, 2)); process.exitCode = 2; } else { const expectedPreConfirmation = expectedPreConfirmationOperation(fixture); const expectedCore = coreOperation(expectedPreConfirmation, fixture); const expectedHash = sha256(JSON.stringify(expectedCore)); const expectedHasAttachments = Object.hasOwn(fixture.data || {}, 'attachments'); const expectedHasSystemDefaults = Object.hasOwn(fixture.data || {}, 'system_defaults'); const hashExpression = `(async()=>{const t=taskStore.find(x=>x.task_id===${JSON.stringify(taskId)});if(!t)throw new Error('task_not_found');const a=structuredClone(t.operation);${expectedHasAttachments ? '' : 'delete a.data.attachments;'}${expectedHasSystemDefaults ? '' : 'delete a.data.system_defaults;'}const d=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(JSON.stringify(a)));return [...new Uint8Array(d)].map(b=>b.toString(16).padStart(2,'0')).join('')})()`; const actualHash = evaluate(args, args.platformTarget, hashExpression); if (actualHash !== expectedHash) throw new Error(`canonical core mismatch for ${taskId}: expected ${expectedHash}, actual ${actualHash}; task remains unconfirmed`); const guardExpression = `(async()=>{const t=taskStore.find(x=>x.task_id===${JSON.stringify(taskId)});return JSON.stringify({task_id:t?.task_id,status:t?.status,action:t?.operation?.action,parser:t?.parse_response?.parser_prompt_version,org_automation:organizationAutomationEnabled,required_version:REQUIRED_EXTENSION_VERSION,extension:await sendToExtension('PING')})})()`; const guard = parseJsonOutput(evaluate(args, args.platformTarget, guardExpression), 'platform guard'); const erpGuard = guard.extension?.erp_session || null; const guardErrors = []; if (guard.task_id !== taskId || guard.status !== 'awaiting_confirmation') guardErrors.push('task_not_awaiting_confirmation'); if (guard.action !== fixture.action || guard.parser !== 'canonical-json-v1') guardErrors.push('canonical_task_identity_mismatch'); if (guard.org_automation !== false) guardErrors.push('organization_automation_must_be_false'); if (!guard.extension?.ok || guard.extension.version !== guard.required_version || guard.extension.erp_automation_enabled !== true) guardErrors.push('extension_runtime_guard_failed'); if (!erpGuard?.ok || !erpGuard.tab_present || !erpGuard.account_matched || erpGuard.login_or_permission_error) { guardErrors.push('erp_test_account_not_present'); } if (guardErrors.length) throw new Error(`runtime safety check failed: ${guardErrors.join(',')}; task remains unconfirmed`); if (!args.confirm) { console.log(JSON.stringify({ task_id: taskId, status: parsedState.status, action: fixture.action, canonical_core_sha256: actualHash, canonical_core_exact: true, guard, erp_guard: erpGuard, confirmed: false }, null, 2)); } else { evaluate(args, args.platformTarget, `(()=>{const t=taskStore.find(x=>x.task_id===${JSON.stringify(taskId)});if(!t)throw new Error('task_not_found');if(t.status!=='awaiting_confirmation')throw new Error('task_not_awaiting_confirmation');void confirmAndSubmitToErpPlugin(t).catch(()=>{});return JSON.stringify({submitted:true,task_id:t.task_id})})()`); try { cdp(args, 'evalraw', args.erpTarget, 'Page.bringToFront', '{}'); } catch { // Foregrounding only prevents background timer throttling; it is not a write prerequisite. } const result = await waitForExtensionResult(args, taskId); await publishExistingResult(args); let platformTask = null; try { platformTask = parseJsonOutput(evaluate(args, args.platformTarget, localTaskExpression(taskId)), 'final task state'); } catch { platformTask = { task_id: taskId, status: 'ui_sync_unavailable' }; } const summary = resultSummary(taskId, fixture, expectedHash, actualHash, result, platformTask); console.log(JSON.stringify(summary, null, 2)); if (!/(?:completed)$/.test(String(result.status || ''))) process.exitCode = 2; } }