944 lines
44 KiB
JavaScript
944 lines
44 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { dirname, resolve } from 'node:path';
|
|
|
|
const DEFAULT_PORT = process.env.LWLT_CDP_PORT || '9223';
|
|
|
|
function parseArgs(argv) {
|
|
const args = { _: [] };
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const arg = argv[i];
|
|
if (!arg.startsWith('--')) {
|
|
args._.push(arg);
|
|
continue;
|
|
}
|
|
const key = arg.slice(2);
|
|
const next = argv[i + 1];
|
|
if (!next || next.startsWith('--')) args[key] = true;
|
|
else {
|
|
args[key] = next;
|
|
i += 1;
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function usage() {
|
|
return [
|
|
'Usage:',
|
|
' node tools/browser_order_add_raw_instruction_test.mjs --input samples/raw_instruction_team_order_20260811.json --open-form --intercept-submit --out reports/raw-instruction.json',
|
|
'',
|
|
'Options:',
|
|
` --port <number> Chrome DevTools port. Default: ${DEFAULT_PORT}`,
|
|
' --input <path> Parsed raw-instruction JSON.',
|
|
' --open-form Navigate the LTJT main iframe to a fresh orders_add.asp form first.',
|
|
' --intercept-submit Call SubmitInfoForm() with DoInfoJH AJAX intercepted before network.',
|
|
' --out <path> Write redacted raw-instruction preflight report.',
|
|
' --approved-preflight-out <path> Write a browser_preflight_passed-compatible redacted report.',
|
|
' --approved-intercept-out <path> Write a submit_intercept_captured-compatible redacted report.',
|
|
' --help Show this help.',
|
|
'',
|
|
'This tool derives route/customer/trip/order-prefix from the chosen product template in the browser, validates linked choices, fills obvious test markers, and never lets DoInfoJH reach the network.'
|
|
].join('\n');
|
|
}
|
|
|
|
function readJson(path) {
|
|
return JSON.parse(readFileSync(path, 'utf8'));
|
|
}
|
|
|
|
function writeJson(path, data) {
|
|
mkdirSync(dirname(resolve(path)), { recursive: true });
|
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
}
|
|
|
|
function isBlank(value) {
|
|
return value == null || String(value).trim() === '';
|
|
}
|
|
|
|
function toNumber(value, fallback = 0) {
|
|
const num = Number(value);
|
|
return Number.isFinite(num) ? num : fallback;
|
|
}
|
|
|
|
function dateYyyyMD(value) {
|
|
if (typeof value !== 'string') return value;
|
|
const match = value.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
if (!match) return value;
|
|
return `${match[1]}-${Number(match[2])}-${Number(match[3])}`;
|
|
}
|
|
|
|
function passengerTotal(counts = {}) {
|
|
return ['adult', 'child_bed', 'child_no_bed', 'infant', 'leader']
|
|
.reduce((sum, key) => sum + Math.max(0, Math.trunc(toNumber(counts[key]))), 0);
|
|
}
|
|
|
|
function roomTotal(roomCounts = {}) {
|
|
return ['SGL', 'TWN', 'TRP', 'DBL', 'HNM', 'TL']
|
|
.reduce((sum, key) => sum + Math.max(0, Math.trunc(toNumber(roomCounts[key]))), 0);
|
|
}
|
|
|
|
function localValidate(operation) {
|
|
const blockers = [];
|
|
const warnings = [];
|
|
const data = operation.data || {};
|
|
const counts = data.passenger_counts || {};
|
|
if (operation.action !== 'team_order_create') blockers.push(`Unsupported action ${operation.action || '<missing>'}; expected team_order_create.`);
|
|
if (operation.submit_mode !== 'dry_run') blockers.push('submit_mode must be dry_run before the approved live-submit gate.');
|
|
if (operation.order_nature !== 'test') blockers.push('order_nature must be test for this live ERP test workflow.');
|
|
if (data.order_mode !== '团队-单个下单') warnings.push('data.order_mode is not 团队-单个下单; continuing because the target page is the independent group order form.');
|
|
if (isBlank(data.product?.name)) blockers.push('data.product.name is required.');
|
|
if (!Array.isArray(data.departure_dates) || data.departure_dates.length !== 1 || isBlank(data.departure_dates[0])) blockers.push('Exactly one data.departure_dates value is required.');
|
|
if (passengerTotal(counts) <= 0) blockers.push('Passenger total must be greater than zero.');
|
|
if (counts.expected_total != null && Math.trunc(toNumber(counts.expected_total)) !== passengerTotal(counts)) {
|
|
blockers.push(`passenger_counts.expected_total (${counts.expected_total}) does not equal computed total (${passengerTotal(counts)}).`);
|
|
}
|
|
if (roomTotal(data.room_counts || {}) <= 0) warnings.push('Computed room total is zero.');
|
|
if (isBlank(data.op_user?.name)) blockers.push('data.op_user.name is required.');
|
|
if (isBlank(data.sales_user?.name)) blockers.push('data.sales_user.name is required.');
|
|
if (isBlank(data.test_marker)) blockers.push('data.test_marker is required so ERP test data remains obvious.');
|
|
return { blockers, warnings };
|
|
}
|
|
|
|
async function getJson(url) {
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${url}`);
|
|
return res.json();
|
|
}
|
|
|
|
async function getTarget(port) {
|
|
const targets = await getJson(`http://127.0.0.1:${port}/json`);
|
|
const target = targets.find((item) => item.type === 'page' && item.url.includes('ltjt.yunzhi.run'));
|
|
if (!target) throw new Error(`No ltjt.yunzhi.run page target on port ${port}`);
|
|
return target;
|
|
}
|
|
|
|
class CDP {
|
|
constructor(wsUrl) {
|
|
this.wsUrl = wsUrl;
|
|
this.id = 0;
|
|
this.pending = new Map();
|
|
}
|
|
|
|
connect() {
|
|
return new Promise((resolve, reject) => {
|
|
this.ws = new WebSocket(this.wsUrl);
|
|
this.ws.onopen = resolve;
|
|
this.ws.onerror = (event) => reject(new Error(event.message || event.type || 'WebSocket error'));
|
|
this.ws.onmessage = (event) => {
|
|
const msg = JSON.parse(event.data);
|
|
if (!msg.id || !this.pending.has(msg.id)) return;
|
|
const { resolve: ok, reject: fail, timer } = this.pending.get(msg.id);
|
|
clearTimeout(timer);
|
|
this.pending.delete(msg.id);
|
|
msg.error ? fail(new Error(msg.error.message)) : ok(msg.result);
|
|
};
|
|
});
|
|
}
|
|
|
|
send(method, params = {}, timeoutMs = 30000) {
|
|
const id = ++this.id;
|
|
this.ws.send(JSON.stringify({ id, method, params }));
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
this.pending.delete(id);
|
|
reject(new Error(`Timeout: ${method}`));
|
|
}, timeoutMs);
|
|
this.pending.set(id, { resolve, reject, timer });
|
|
});
|
|
}
|
|
|
|
close() {
|
|
this.ws?.close();
|
|
}
|
|
}
|
|
|
|
async function evaluate(cdp, expression, timeoutMs = 30000) {
|
|
const result = await cdp.send('Runtime.evaluate', {
|
|
expression,
|
|
returnByValue: true,
|
|
awaitPromise: true
|
|
}, timeoutMs);
|
|
if (result.exceptionDetails) {
|
|
const detail = result.exceptionDetails.exception?.description
|
|
|| result.exceptionDetails.exception?.value
|
|
|| result.exceptionDetails.text
|
|
|| 'Runtime.evaluate failed';
|
|
throw new Error(detail);
|
|
}
|
|
return result.result.value;
|
|
}
|
|
|
|
function buildCompatiblePreflightReport(output) {
|
|
const submitPayloadSha256 = output.submit_intercept?.intercepted_submits?.[0]?.payload_sha256 || '';
|
|
return {
|
|
generated_at: new Date().toISOString(),
|
|
status: output.status === 'raw_instruction_test_passed' ? 'browser_preflight_passed' : 'browser_preflight_blocked',
|
|
source: 'browser_order_add_raw_instruction_test',
|
|
source_input: output.source_input,
|
|
page: output.page,
|
|
parsed_instruction: output.parsed_instruction,
|
|
lookup_checks: output.lookup_checks,
|
|
product_side_effect: output.product_side_effect,
|
|
consistency_checks: output.consistency_checks,
|
|
logical_checks: output.logical_checks,
|
|
final_form: {
|
|
...output.final_form,
|
|
submit_payload_sha256: submitPayloadSha256 || undefined,
|
|
submit_function_mutates_payload: Boolean(submitPayloadSha256 && submitPayloadSha256 !== output.final_form?.serialized_form_sha256)
|
|
},
|
|
blockers: output.blockers || [],
|
|
warnings: output.warnings || [],
|
|
submit_safety: {
|
|
live_submit_attempted: false,
|
|
live_submit_supported_by_this_tool: false,
|
|
note: 'Derived from raw-instruction redacted preflight. The live DoInfoJH request was not sent.'
|
|
},
|
|
test_markers: output.test_markers
|
|
};
|
|
}
|
|
|
|
function buildCompatibleInterceptReport(output) {
|
|
const submitIntercept = output.submit_intercept || {};
|
|
return {
|
|
generated_at: new Date().toISOString(),
|
|
status: submitIntercept.status || 'submit_intercept_blocked',
|
|
source: 'browser_order_add_raw_instruction_test',
|
|
source_input: output.source_input,
|
|
page: output.page,
|
|
intercepted_submit_count: submitIntercept.intercepted_submit_count || 0,
|
|
intercepted_submits: submitIntercept.intercepted_submits || [],
|
|
passthrough_ajax_count: 0,
|
|
alerts: submitIntercept.alerts || [],
|
|
blockers: submitIntercept.status === 'submit_intercept_captured' ? [] : ['raw-instruction submit intercept did not pass'],
|
|
submit_safety: {
|
|
live_submit_attempted: false,
|
|
DoInfoJH_network_prevented: submitIntercept.DoInfoJH_network_prevented === true,
|
|
note: 'Derived from raw-instruction preflight. jQuery.ajax intercepted DoInfoJH before network.'
|
|
},
|
|
test_markers: output.test_markers
|
|
};
|
|
}
|
|
|
|
function browserExpression({ operation, openForm, interceptSubmit }) {
|
|
return `(async () => {
|
|
const operation = ${JSON.stringify(operation)};
|
|
const openForm = ${JSON.stringify(Boolean(openForm))};
|
|
const interceptSubmit = ${JSON.stringify(Boolean(interceptSubmit))};
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const fabu = operation.data?.system_defaults?.fabudanwei || '老挝联泰';
|
|
const marker = String(operation.data?.test_marker || 'RAWTEST-ERP').trim();
|
|
const markerSuffix = marker + '-' + Date.now();
|
|
const entryUrl = 'https://ltjt.yunzhi.run/System/Business/orders_add.asp?fabudanwei=' + encodeURIComponent(fabu) + '©=0&ddid=0&_=' + Date.now();
|
|
const entryStamp = new URL(entryUrl).searchParams.get('_');
|
|
|
|
function topState() {
|
|
return { url: location.href, title: document.title };
|
|
}
|
|
|
|
function findOrderWindow() {
|
|
const queue = [window];
|
|
while (queue.length) {
|
|
const win = queue.shift();
|
|
let doc;
|
|
try { doc = win.document; } catch (err) { continue; }
|
|
if (doc.querySelector('#ListForm') && doc.location.href.includes('/orders_add.asp')) return win;
|
|
Array.from(win.frames).forEach((frame) => queue.push(frame));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (/login/i.test(location.href) || /云智办公i-5.1/.test(document.title || '')) {
|
|
return { ok: false, status: 'login_required', blockers: ['login_required'], top: topState() };
|
|
}
|
|
|
|
if (openForm) {
|
|
const frame = document.getElementById('Iframe_Home') || document.querySelector('iframe[name="MainIframe"], iframe');
|
|
if (!frame) return { ok: false, status: 'main_iframe_not_found', blockers: ['main_iframe_not_found'], top: topState() };
|
|
frame.src = entryUrl;
|
|
}
|
|
|
|
const started = Date.now();
|
|
let orderWin = findOrderWindow();
|
|
while ((!orderWin || !orderWin.document.querySelector('#ListForm') || orderWin.document.querySelector('#ListForm').elements.length < 800 || (openForm && !orderWin.document.location.href.includes('_=' + entryStamp))) && Date.now() - started < 25000) {
|
|
await sleep(500);
|
|
orderWin = findOrderWindow();
|
|
}
|
|
if (!orderWin) return { ok: false, status: 'orders_add_form_not_found', blockers: ['orders_add_form_not_found'], top: topState() };
|
|
|
|
const doc = orderWin.document;
|
|
const form = doc.querySelector('#ListForm');
|
|
const blockers = [];
|
|
const warnings = [];
|
|
const setResults = [];
|
|
const ajaxRecords = [];
|
|
const fabudanwei = form.elements.fabudanwei?.value || fabu;
|
|
|
|
function toNumber(value, fallback = 0) {
|
|
const num = Number(value);
|
|
return Number.isFinite(num) ? num : fallback;
|
|
}
|
|
|
|
function integerString(value) {
|
|
return String(Math.max(0, Math.trunc(toNumber(value))));
|
|
}
|
|
|
|
function compactNumber(value) {
|
|
const num = toNumber(value);
|
|
if (Number.isInteger(num)) return String(num);
|
|
return String(Math.round((num + Number.EPSILON) * 100) / 100);
|
|
}
|
|
|
|
function dateYyyyMD(value) {
|
|
if (typeof value !== 'string') return value;
|
|
const match = value.match(/^(\\d{4})-(\\d{1,2})-(\\d{1,2})$/);
|
|
if (!match) return value;
|
|
return match[1] + '-' + Number(match[2]) + '-' + Number(match[3]);
|
|
}
|
|
|
|
function passengerTotal(counts = {}) {
|
|
return ['adult', 'child_bed', 'child_no_bed', 'infant', 'leader']
|
|
.reduce((sum, key) => sum + Math.max(0, Math.trunc(toNumber(counts[key]))), 0);
|
|
}
|
|
|
|
function roomTotal(roomCounts = {}) {
|
|
return ['SGL', 'TWN', 'TRP', 'DBL', 'HNM', 'TL']
|
|
.reduce((sum, key) => sum + Math.max(0, Math.trunc(toNumber(roomCounts[key]))), 0);
|
|
}
|
|
|
|
function splitRows(text) {
|
|
const str = String(text || '');
|
|
if (!str) return [];
|
|
if (str.includes('◇')) return str.split('◇').map((row) => row.trim()).filter(Boolean);
|
|
if (/\\r?\\n/.test(str)) return str.split(/\\r?\\n/).map((row) => row.trim()).filter(Boolean);
|
|
return [str.trim()].filter(Boolean);
|
|
}
|
|
|
|
function splitColumns(row) {
|
|
return String(row || '').split('◆').map((part) => part.trim());
|
|
}
|
|
|
|
function rows(text) {
|
|
return splitRows(text).map((row, rowIndex) => ({ rowIndex, columns: splitColumns(row) }));
|
|
}
|
|
|
|
function summarizeText(text) {
|
|
const parsedRows = splitRows(text);
|
|
const histogram = {};
|
|
parsedRows.slice(0, 200).forEach((row) => {
|
|
const count = splitColumns(row).length;
|
|
histogram[count] = (histogram[count] || 0) + 1;
|
|
});
|
|
return { byte_length: new Blob([String(text || '')]).size, row_count: parsedRows.length, column_count_histogram_first_200_rows: histogram, value_redacted: true };
|
|
}
|
|
|
|
async function sha256(text) {
|
|
const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
|
|
return Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
async function fetchText(path) {
|
|
const url = new URL(path, doc.location.href).href;
|
|
const res = await orderWin.fetch(url, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
|
|
});
|
|
const text = await res.text();
|
|
return { ok: res.ok, status: res.status, text };
|
|
}
|
|
|
|
function exactMatches(text, expected, column) {
|
|
const expectedText = String(expected || '').trim();
|
|
if (!expectedText) return [];
|
|
return rows(text)
|
|
.filter((row) => String(row.columns[column] || '').trim() === expectedText);
|
|
}
|
|
|
|
function containsMatches(text, expected, column) {
|
|
const expectedText = String(expected || '').trim();
|
|
if (!expectedText) return [];
|
|
return rows(text)
|
|
.filter((row) => String(row.columns[column] || '').trim().includes(expectedText));
|
|
}
|
|
|
|
function selectUniqueExistingOption(text, expected, column, name) {
|
|
const normalizedColumn = Number(column);
|
|
const exactByColumn = exactMatches(text, expected, normalizedColumn);
|
|
if (exactByColumn.length === 1) {
|
|
return {
|
|
match: exactByColumn[0],
|
|
rule: 'exact',
|
|
exact_count: exactByColumn.length,
|
|
contains_count: containsMatches(text, expected, normalizedColumn).length,
|
|
blocker: ''
|
|
};
|
|
}
|
|
const contains = containsMatches(text, expected, normalizedColumn);
|
|
if (exactByColumn.length === 0 && contains.length === 1) return { match: contains[0], rule: 'unique_contains', exact_count: 0, contains_count: contains.length, blocker: '' };
|
|
return {
|
|
match: null,
|
|
rule: exactByColumn.length > 1 ? 'ambiguous_exact' : 'blocked',
|
|
exact_count: exactByColumn.length,
|
|
contains_count: contains.length,
|
|
blocker: exactByColumn.length > 1
|
|
? name + ': expected exactly one existing LTJT option, found multiple exact matches'
|
|
: name + ': no exact match and unique contains fallback did not produce exactly one option'
|
|
};
|
|
}
|
|
|
|
function getValue(name) {
|
|
const el = form.elements[name];
|
|
if (!el) return '';
|
|
if (el.length && !el.tagName) return Array.from(el).find((item) => item.checked)?.value || el[0]?.value || '';
|
|
return el.value ?? '';
|
|
}
|
|
|
|
function setValue(name, value, source) {
|
|
const elements = Array.from(form.elements).filter((el) => el.name === name);
|
|
if (!elements.length) {
|
|
blockers.push('Cannot set missing form field ' + name + ' from ' + source);
|
|
return;
|
|
}
|
|
const textValue = String(value ?? '');
|
|
const type = String(elements[0].type || '').toLowerCase();
|
|
if (type === 'radio') {
|
|
const matched = elements.find((el) => String(el.value) === textValue) || elements[0];
|
|
elements.forEach((el) => { el.checked = el === matched; });
|
|
} else if (type === 'checkbox') {
|
|
elements[0].checked = Boolean(value);
|
|
if (textValue) elements[0].value = textValue;
|
|
} else {
|
|
elements[0].value = textValue;
|
|
}
|
|
setResults.push({ name, source, type, value_length: textValue.length, value_redacted: true });
|
|
}
|
|
|
|
function summarizeValues(values) {
|
|
const normalized = values.map((value) => String(value ?? ''));
|
|
return {
|
|
value_count: normalized.length,
|
|
nonempty_count: normalized.filter((value) => value !== '').length,
|
|
total_value_length: normalized.reduce((sum, value) => sum + value.length, 0),
|
|
value_redacted: true
|
|
};
|
|
}
|
|
|
|
function snapshotForm() {
|
|
const snapshot = {};
|
|
Array.from(form.elements).forEach((el, index) => {
|
|
const name = el.name || el.id || '__element_' + index;
|
|
const type = String(el.type || '').toLowerCase();
|
|
const value = (type === 'checkbox' || type === 'radio') ? (el.checked ? el.value : '') : (el.value ?? '');
|
|
if (!snapshot[name]) snapshot[name] = [];
|
|
snapshot[name].push(String(value));
|
|
});
|
|
return snapshot;
|
|
}
|
|
|
|
function compareSnapshots(before, after) {
|
|
const names = Array.from(new Set([...Object.keys(before), ...Object.keys(after)])).sort();
|
|
return names
|
|
.filter((name) => JSON.stringify(before[name] || []) !== JSON.stringify(after[name] || []))
|
|
.map((name) => ({ name, before: summarizeValues(before[name] || []), after: summarizeValues(after[name] || []) }));
|
|
}
|
|
|
|
function groupChangedFields(changes) {
|
|
const groups = {};
|
|
for (const change of changes) {
|
|
let group = 'other';
|
|
if (/^(Text|shuoming|oldxingcheng|xingcheng)/.test(change.name)) group = 'itinerary_text';
|
|
else if (/^(zao|zhong|wan|zhusu|jingdian)/.test(change.name)) group = 'resources';
|
|
else if (/^(ys_|youyingshou|baojia|yingfu)/.test(change.name)) group = 'pricing_receivable';
|
|
else if (/^(TianShu|chanpinming|tuanxuhao|zhuanxianming|zutuanshe|bizhong|lianxiren)/.test(change.name)) group = 'core';
|
|
groups[group] = (groups[group] || 0) + 1;
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
function pushCheck(checks, check) {
|
|
checks.push(check);
|
|
if (check.required && !check.passed) blockers.push(check.name + ': ' + check.blocker);
|
|
}
|
|
|
|
const endpoints = {
|
|
product: '../dat/AjaxPublicFun.asp?Act=ProGetProductname&fls=1&fabudanwei=' + encodeURIComponent(fabudanwei),
|
|
route: '../dat/AjaxPublicFun.asp?Act=GetInformation&fl=3&fls=1&fabudanwei=' + encodeURIComponent(fabudanwei),
|
|
customer: '../dat/AjaxPublicFun.asp?Act=ProTravel&fabudanwei=' + encodeURIComponent(fabudanwei),
|
|
staff: '../dat/AjaxPublicFun.asp?Act=ProDanwei_Yuangong&fabudanwei=' + encodeURIComponent(fabudanwei)
|
|
};
|
|
const fetched = {};
|
|
for (const [key, path] of Object.entries(endpoints)) fetched[key] = await fetchText(path);
|
|
|
|
const productSelection = selectUniqueExistingOption(fetched.product.text, operation.data?.product?.name, 1, 'product');
|
|
if (!productSelection.match) blockers.push(productSelection.blocker);
|
|
const productInitialCheck = {
|
|
name: 'product_initial_selection',
|
|
endpoint_key: 'product',
|
|
match_rule: productSelection.rule,
|
|
exact_match_count: productSelection.exact_count,
|
|
contains_match_count: productSelection.contains_count,
|
|
match_shape: productSelection.match ? { row_index: productSelection.match.rowIndex, column_count: productSelection.match.columns.length, set_value_targets: ['chanpinming'] } : null,
|
|
summary: summarizeText(fetched.product.text)
|
|
};
|
|
|
|
const $ = orderWin.jQuery || orderWin.$;
|
|
const originalAjax = $?.ajax;
|
|
if (!$ || typeof originalAjax !== 'function') blockers.push('jQuery.ajax is not available in order form');
|
|
if (typeof orderWin.Find_product !== 'function') blockers.push('Find_product() is not available');
|
|
|
|
if (!blockers.length) {
|
|
$.ajax = function ajaxWrapper(options, ...rest) {
|
|
const config = typeof options === 'string' ? { url: options } : { ...(options || {}) };
|
|
const isProductEffect = String(config.url || '').includes('Act=GetProduct');
|
|
if (!isProductEffect) return originalAjax.call(this, options, ...rest);
|
|
const record = {
|
|
url_path_redacted: '../dat/AjaxPublicFun.asp?Act=GetProduct&cpm=[redacted]',
|
|
method: config.type || config.method || 'GET',
|
|
dataType: config.dataType || '',
|
|
completed: false,
|
|
ok: false,
|
|
text_status: '',
|
|
response_byte_length: 0,
|
|
contains_login_timeout_text: false
|
|
};
|
|
ajaxRecords.push(record);
|
|
const originalSuccess = config.success;
|
|
const originalError = config.error;
|
|
const originalComplete = config.complete;
|
|
config.success = function successWrapper(data, textStatus) {
|
|
const text = String(data || '');
|
|
record.ok = true;
|
|
record.text_status = textStatus || '';
|
|
record.response_byte_length = new Blob([text]).size;
|
|
record.contains_login_timeout_text = /登陆|登录|login/i.test(text);
|
|
return originalSuccess ? originalSuccess.apply(this, arguments) : undefined;
|
|
};
|
|
config.error = function errorWrapper(jqXHR, textStatus, errorThrown) {
|
|
record.ok = false;
|
|
record.text_status = textStatus || '';
|
|
record.error_text_redacted = errorThrown ? String(errorThrown).slice(0, 80) : '';
|
|
return originalError ? originalError.apply(this, arguments) : undefined;
|
|
};
|
|
config.complete = function completeWrapper() {
|
|
record.completed = true;
|
|
return originalComplete ? originalComplete.apply(this, arguments) : undefined;
|
|
};
|
|
return originalAjax.call(this, config, ...rest);
|
|
};
|
|
}
|
|
|
|
const beforeProduct = snapshotForm();
|
|
if (!blockers.length) {
|
|
setValue('chanpinming', productSelection.match.columns[1], 'product_lookup_selection');
|
|
try {
|
|
orderWin.Find_product();
|
|
} catch (err) {
|
|
blockers.push('Find_product() threw: ' + (err.message || String(err)).slice(0, 160));
|
|
}
|
|
}
|
|
const effectStarted = Date.now();
|
|
while (Date.now() - effectStarted < 15000) {
|
|
if (ajaxRecords.some((record) => record.completed)) break;
|
|
await sleep(250);
|
|
}
|
|
await sleep(1000);
|
|
if ($ && originalAjax) $.ajax = originalAjax;
|
|
|
|
const afterProduct = snapshotForm();
|
|
const productChanges = compareSnapshots(beforeProduct, afterProduct);
|
|
const productAjax = ajaxRecords.find((record) => record.url_path_redacted.includes('GetProduct')) || null;
|
|
if (!productAjax) blockers.push('GetProduct ajax request was not observed');
|
|
else if (!productAjax.ok) blockers.push('GetProduct ajax request did not complete successfully');
|
|
else if (productAjax.contains_login_timeout_text) blockers.push('GetProduct response looked like a login-timeout script');
|
|
if (!productChanges.length) blockers.push('Find_product/GetProduct completed without changing any serialized form fields');
|
|
|
|
const core = {
|
|
product: getValue('chanpinming'),
|
|
trip: getValue('TianShu'),
|
|
route: getValue('zhuanxianming'),
|
|
routePrefix: getValue('tuanxuhao1'),
|
|
customer: getValue('zutuanshe'),
|
|
customerId: getValue('zutuansheid'),
|
|
currency: getValue('bizhong')
|
|
};
|
|
|
|
const staticTrip = '1D◇2D1N◇3D2N◇4D3N◇5D4N◇6D5N◇7D6N◇8D7N◇9D8N◇10D9N';
|
|
const productAfterMatches = exactMatches(fetched.product.text, core.product, 1);
|
|
const tripMatches = exactMatches(staticTrip, core.trip, 0);
|
|
const routeMatches = exactMatches(fetched.route.text, core.route, 1);
|
|
const customerMatches = exactMatches(fetched.customer.text, core.customer, 2);
|
|
const opSelection = selectUniqueExistingOption(fetched.staff.text, operation.data?.op_user?.name, 0, 'op_user');
|
|
const salesSelection = selectUniqueExistingOption(fetched.staff.text, operation.data?.sales_user?.name, 0, 'sales_user');
|
|
if (!opSelection.match) blockers.push(opSelection.blocker);
|
|
if (!salesSelection.match) blockers.push(salesSelection.blocker);
|
|
const lookupChecks = [
|
|
productInitialCheck,
|
|
{ name: 'product_after_effect', endpoint_key: 'product', exact_match_count: productAfterMatches.length, match_shape: productAfterMatches.length === 1 ? { row_index: productAfterMatches[0].rowIndex, column_count: productAfterMatches[0].columns.length } : null, summary: summarizeText(fetched.product.text) },
|
|
{ name: 'trip_after_effect', endpoint_key: '', exact_match_count: tripMatches.length, match_shape: tripMatches.length === 1 ? { row_index: tripMatches[0].rowIndex, column_count: tripMatches[0].columns.length } : null, summary: summarizeText(staticTrip) },
|
|
{ name: 'route_after_effect', endpoint_key: 'route', exact_match_count: routeMatches.length, match_shape: routeMatches.length === 1 ? { row_index: routeMatches[0].rowIndex, column_count: routeMatches[0].columns.length } : null, summary: summarizeText(fetched.route.text) },
|
|
{ name: 'customer_after_effect', endpoint_key: 'customer', exact_match_count: customerMatches.length, match_shape: customerMatches.length === 1 ? { row_index: customerMatches[0].rowIndex, column_count: customerMatches[0].columns.length } : null, summary: summarizeText(fetched.customer.text) },
|
|
{ name: 'op_user', endpoint_key: 'staff', exact_match_count: opSelection.exact_count, contains_match_count: opSelection.contains_count, match_rule: opSelection.rule, match_shape: opSelection.match ? { row_index: opSelection.match.rowIndex, column_count: opSelection.match.columns.length, set_value_targets: ['gendanren'] } : null, summary: summarizeText(fetched.staff.text) },
|
|
{ name: 'sales_user', endpoint_key: 'staff', exact_match_count: salesSelection.exact_count, contains_match_count: salesSelection.contains_count, match_rule: salesSelection.rule, match_shape: salesSelection.match ? { row_index: salesSelection.match.rowIndex, column_count: salesSelection.match.columns.length, set_value_targets: ['xiaoshouren'] } : null, summary: summarizeText(fetched.staff.text) }
|
|
];
|
|
for (const check of lookupChecks.slice(1, 5)) {
|
|
if (check.exact_match_count !== 1) blockers.push(check.name + ': expected exactly one existing LTJT option, found ' + check.exact_match_count);
|
|
}
|
|
|
|
const consistencyChecks = [];
|
|
pushCheck(consistencyChecks, {
|
|
name: 'product_template_product_nonempty',
|
|
field: 'chanpinming',
|
|
passed: Boolean(String(core.product || '').trim()),
|
|
required: true,
|
|
blocker: 'product template did not populate product',
|
|
actual_redacted: true,
|
|
actual_length: String(core.product || '').length
|
|
});
|
|
pushCheck(consistencyChecks, {
|
|
name: 'product_template_trip_nonempty',
|
|
field: 'TianShu',
|
|
passed: Boolean(String(core.trip || '').trim()),
|
|
required: true,
|
|
blocker: 'product template did not populate trip',
|
|
actual_redacted: true,
|
|
actual_length: String(core.trip || '').length
|
|
});
|
|
pushCheck(consistencyChecks, {
|
|
name: 'product_template_route_nonempty',
|
|
field: 'zhuanxianming',
|
|
passed: Boolean(String(core.route || '').trim()),
|
|
required: true,
|
|
blocker: 'product template did not populate route',
|
|
actual_redacted: true,
|
|
actual_length: String(core.route || '').length
|
|
});
|
|
pushCheck(consistencyChecks, {
|
|
name: 'product_template_route_prefix_nonempty',
|
|
field: 'tuanxuhao1',
|
|
passed: Boolean(String(core.routePrefix || '').trim()),
|
|
required: true,
|
|
blocker: 'product template did not populate route prefix',
|
|
actual_redacted: true,
|
|
actual_length: String(core.routePrefix || '').length
|
|
});
|
|
pushCheck(consistencyChecks, {
|
|
name: 'product_template_customer_nonempty',
|
|
field: 'zutuanshe',
|
|
passed: Boolean(String(core.customer || '').trim()),
|
|
required: true,
|
|
blocker: 'product template did not populate customer',
|
|
actual_redacted: true,
|
|
actual_length: String(core.customer || '').length
|
|
});
|
|
pushCheck(consistencyChecks, {
|
|
name: 'product_template_customer_id_nonempty',
|
|
field: 'zutuansheid',
|
|
passed: Boolean(String(core.customerId || '').trim()),
|
|
required: true,
|
|
blocker: 'product template did not populate customer id',
|
|
actual_redacted: true,
|
|
actual_length: String(core.customerId || '').length
|
|
});
|
|
if (customerMatches.length === 1 && String(customerMatches[0].columns[1] || '').trim() !== String(core.customerId || '').trim()) {
|
|
blockers.push('customer_after_effect: product template customer id does not match the existing customer option id');
|
|
}
|
|
if (routeMatches.length === 1 && String(routeMatches[0].columns[3] || '').trim() !== String(core.routePrefix || '').trim()) {
|
|
blockers.push('route_after_effect: product template route prefix does not match the existing route option prefix');
|
|
}
|
|
|
|
const counts = operation.data?.passenger_counts || {};
|
|
const rooms = operation.data?.room_counts || {};
|
|
const prices = operation.data?.prices || {};
|
|
const passengerSum = passengerTotal(counts);
|
|
const expectedPassengerTotal = counts.expected_total == null ? passengerSum : Math.trunc(toNumber(counts.expected_total));
|
|
const computedRoomTotal = roomTotal(rooms);
|
|
const logicalChecks = [
|
|
{ name: 'passenger_total', passed: passengerSum > 0 && passengerSum === expectedPassengerTotal, expected: expectedPassengerTotal, actual: passengerSum },
|
|
{ name: 'room_total', passed: computedRoomTotal >= 0, actual: computedRoomTotal },
|
|
{ name: 'departure_date_count', passed: Array.isArray(operation.data?.departure_dates) && operation.data.departure_dates.length === 1, actual: Array.isArray(operation.data?.departure_dates) ? operation.data.departure_dates.length : 0 }
|
|
];
|
|
logicalChecks.forEach((check) => {
|
|
if (!check.passed) blockers.push(check.name + ': raw instruction logical validation failed');
|
|
});
|
|
|
|
const receivableCategories = [
|
|
['adult', '成人团费'],
|
|
['child_bed', '小童占床'],
|
|
['child_no_bed', '小童不占床'],
|
|
['infant', '婴儿'],
|
|
['leader', '领队']
|
|
];
|
|
const receivableRows = [];
|
|
for (const [key, label] of receivableCategories) {
|
|
const quantity = toNumber(counts[key]);
|
|
const unitPrice = toNumber(prices[key]);
|
|
if (quantity > 0 && unitPrice > 0) {
|
|
receivableRows.push({
|
|
key,
|
|
label,
|
|
quantity,
|
|
unit_price: unitPrice,
|
|
amount: quantity * unitPrice
|
|
});
|
|
} else if (quantity > 0 && unitPrice <= 0) {
|
|
blockers.push('price for ' + key + ' must be greater than zero when quantity is greater than zero');
|
|
}
|
|
}
|
|
if (!receivableRows.length) blockers.push('No receivable rows were generated from passenger counts and prices');
|
|
if (receivableRows.length > 10) blockers.push('receivable row count exceeds LTJT max 10');
|
|
|
|
if (!blockers.length) {
|
|
const note = marker + '|测试订单|' + String(operation.data?.special_requests || '');
|
|
const currency = core.currency || prices.currency || operation.data?.system_defaults?.currency || 'USD';
|
|
setValue('chufa_ri', dateYyyyMD(operation.data.departure_dates[0]), 'raw_instruction_departure_date');
|
|
setValue('tuanxuhao2', markerSuffix, 'raw_instruction_obvious_test_suffix');
|
|
setValue('gendanren', opSelection.match.columns[0], 'raw_instruction_existing_op_user');
|
|
setValue('xiaoshouren', salesSelection.match.columns[0], 'raw_instruction_existing_sales_user');
|
|
setValue('darenshu', integerString(counts.adult), 'raw_instruction_passenger_counts');
|
|
setValue('xiaorenshu', integerString(counts.child_bed), 'raw_instruction_passenger_counts');
|
|
setValue('ertrenshu', integerString(counts.child_no_bed), 'raw_instruction_passenger_counts');
|
|
setValue('yingrenshu', integerString(counts.infant), 'raw_instruction_passenger_counts');
|
|
setValue('quanrenshu', integerString(counts.leader), 'raw_instruction_passenger_counts');
|
|
setValue('frenshu0', integerString(rooms.SGL), 'raw_instruction_room_counts');
|
|
setValue('frenshu1', integerString(rooms.TWN), 'raw_instruction_room_counts');
|
|
setValue('frenshu2', integerString(rooms.TRP), 'raw_instruction_room_counts');
|
|
setValue('frenshu3', integerString(rooms.DBL), 'raw_instruction_room_counts');
|
|
setValue('frenshu4', integerString(rooms.HNM), 'raw_instruction_room_counts');
|
|
setValue('frenshu5', integerString(rooms.TL), 'raw_instruction_room_counts');
|
|
setValue('frenshu6', integerString(computedRoomTotal), 'raw_instruction_room_counts');
|
|
setValue('danzhuangtai', operation.data?.system_defaults?.business_status || '预订', 'raw_instruction_status');
|
|
setValue('yaobeian', operation.data?.system_defaults?.filing_required === false ? '' : '要备案', 'raw_instruction_checkbox');
|
|
setValue('xiadanbeizhu', note, 'raw_instruction_obvious_test_note');
|
|
|
|
const receivableTargets = ['ys_danwei','ys_danweiid','ys_shuoming','ys_zhanwei','ys_xiangmu','ys_fangshi','ys_bizhong','ys_shuliang','ys_danjia','ys_jine','ys_yishoufu','ys_beizhu','ys_id','ys_shoufulei','ys_caozuoren','ys_shenheren'];
|
|
for (let index = 0; index < 10; index += 1) {
|
|
receivableTargets.forEach((prefix) => setValue(prefix + index, '', 'raw_instruction_clear_receivables'));
|
|
}
|
|
receivableRows.forEach((row, index) => {
|
|
setValue('ys_danwei' + index, core.customer, 'raw_instruction_receivable_customer');
|
|
setValue('ys_danweiid' + index, core.customerId, 'raw_instruction_receivable_customer_id');
|
|
setValue('ys_xiangmu' + index, marker + ' ' + row.label, 'raw_instruction_receivable_item');
|
|
setValue('ys_shuoming' + index, '人', 'raw_instruction_receivable_unit');
|
|
setValue('ys_bizhong' + index, currency, 'raw_instruction_receivable_currency');
|
|
setValue('ys_shuliang' + index, compactNumber(row.quantity), 'raw_instruction_receivable_quantity');
|
|
setValue('ys_danjia' + index, compactNumber(row.unit_price), 'raw_instruction_receivable_unit_price');
|
|
setValue('ys_jine' + index, compactNumber(row.amount), 'raw_instruction_receivable_amount');
|
|
setValue('ys_yishoufu' + index, '0', 'raw_instruction_receivable_paid');
|
|
setValue('ys_beizhu' + index, note, 'raw_instruction_receivable_remark');
|
|
setValue('ys_shoufulei' + index, '0', 'raw_instruction_receivable_payment_type');
|
|
setValue('ys_caozuoren' + index, opSelection.match.columns[0], 'raw_instruction_receivable_operator');
|
|
});
|
|
}
|
|
|
|
const requiredTargets = ['chufa_ri','zhuanxianming','TianShu','zutuanshe','chanpinming','gendanren','tuanxuhao1','tuanxuhao2','darenshu','xiaorenshu','ertrenshu','yingrenshu','quanrenshu','xiaoshouren'];
|
|
const requiredMissing = requiredTargets.filter((name) => String(getValue(name) || '').trim() === '');
|
|
if (requiredMissing.length) blockers.push('Required fields still blank: ' + requiredMissing.join(', '));
|
|
|
|
const serializedForm = orderWin.jQuery
|
|
? orderWin.jQuery(form).serialize()
|
|
: Array.from(new FormData(form).entries()).map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value)).join('&');
|
|
const serializedParams = new URLSearchParams(serializedForm);
|
|
|
|
let submitIntercept = null;
|
|
if (interceptSubmit && !blockers.length) {
|
|
const originalAjaxForSubmit = $.ajax;
|
|
const originalAlert = orderWin.alert;
|
|
const originalDialogAlert = $.dialog?.alert;
|
|
const intercepted = [];
|
|
const alerts = [];
|
|
orderWin.alert = function interceptedAlert(message) {
|
|
alerts.push({ type: 'alert', message_length: String(message || '').length, message_redacted: true });
|
|
return undefined;
|
|
};
|
|
if ($.dialog && typeof $.dialog.alert === 'function') {
|
|
$.dialog.alert = function interceptedDialogAlert(message) {
|
|
alerts.push({ type: 'dialog.alert', message_length: String(message || '').length, message_redacted: true });
|
|
return undefined;
|
|
};
|
|
}
|
|
$.ajax = function ajaxIntercept(options, ...rest) {
|
|
const config = typeof options === 'string' ? { url: options } : { ...(options || {}) };
|
|
const url = String(config.url || '');
|
|
const data = String(config.data || '');
|
|
const isSubmit = /\\/System\\/DAT\\/orders\\.asp/i.test(url) && data.startsWith('Act=DoInfoJH&');
|
|
if (!isSubmit) return originalAjaxForSubmit.call(this, options, ...rest);
|
|
const params = new URLSearchParams(data.slice('Act=DoInfoJH&'.length));
|
|
intercepted.push({
|
|
url_redacted: '/System/DAT/orders.asp',
|
|
method: config.type || config.method || 'GET',
|
|
dataType: config.dataType || '',
|
|
prevented_from_network: true,
|
|
payload: data,
|
|
payload_summary: {
|
|
starts_with_DoInfoJH: true,
|
|
byte_length: new Blob([data]).size,
|
|
field_count: Array.from(params.keys()).length,
|
|
field_names: Array.from(new Set(Array.from(params.keys()))).sort(),
|
|
value_redacted: true
|
|
}
|
|
});
|
|
return { readyState: 4, status: 0, statusText: 'intercepted_by_raw_instruction_test', abort() {} };
|
|
};
|
|
let thrown = '';
|
|
try {
|
|
orderWin.SubmitInfoForm();
|
|
} catch (err) {
|
|
thrown = err.message || String(err);
|
|
}
|
|
await sleep(1000);
|
|
$.ajax = originalAjaxForSubmit;
|
|
orderWin.alert = originalAlert;
|
|
if ($.dialog && typeof originalDialogAlert === 'function') $.dialog.alert = originalDialogAlert;
|
|
for (const item of intercepted) {
|
|
item.payload_sha256 = await sha256(item.payload || '');
|
|
item.payload_redacted = true;
|
|
delete item.payload;
|
|
}
|
|
submitIntercept = {
|
|
status: intercepted.length === 1 && !alerts.length && !thrown ? 'submit_intercept_captured' : 'submit_intercept_blocked',
|
|
intercepted_submit_count: intercepted.length,
|
|
intercepted_submits: intercepted,
|
|
alerts,
|
|
thrown_redacted: Boolean(thrown),
|
|
thrown_length: String(thrown || '').length,
|
|
DoInfoJH_network_prevented: true
|
|
};
|
|
if (submitIntercept.status !== 'submit_intercept_captured') blockers.push('submit intercept did not capture exactly one clean DoInfoJH branch');
|
|
}
|
|
|
|
const receivableAmountTotal = receivableRows.reduce((sum, row) => sum + row.amount, 0);
|
|
return {
|
|
ok: true,
|
|
status: blockers.length ? 'raw_instruction_test_blocked' : 'raw_instruction_test_passed',
|
|
page: { url: doc.location.href, title: doc.title, form_element_count: form.elements.length },
|
|
parsed_instruction: {
|
|
action: operation.action,
|
|
order_nature: operation.order_nature,
|
|
order_mode: operation.data?.order_mode || '',
|
|
departure_date: dateYyyyMD(operation.data?.departure_dates?.[0] || ''),
|
|
passenger_total: passengerSum,
|
|
room_total: computedRoomTotal,
|
|
receivable_row_count: receivableRows.length,
|
|
receivable_amount_total: receivableAmountTotal,
|
|
values_are_test_only: true
|
|
},
|
|
lookup_checks: lookupChecks,
|
|
product_side_effect: {
|
|
ajax_records: ajaxRecords,
|
|
changed_field_count: productChanges.length,
|
|
changed_field_groups: groupChangedFields(productChanges),
|
|
changed_field_names: productChanges.map((change) => change.name)
|
|
},
|
|
consistency_checks: consistencyChecks,
|
|
core_after_product: Object.fromEntries(Object.entries(core).map(([key, value]) => [key, { nonempty: Boolean(String(value || '').trim()), value_length: String(value || '').length, value_redacted: true }])),
|
|
logical_checks: logicalChecks,
|
|
test_markers: {
|
|
suffix_marker: marker,
|
|
note_marker: marker,
|
|
full_suffix_marker: markerSuffix,
|
|
values_are_test_only: true
|
|
},
|
|
set_summary: {
|
|
set_field_count: setResults.length,
|
|
set_field_names: Array.from(new Set(setResults.map((item) => item.name))).sort(),
|
|
set_results: setResults
|
|
},
|
|
final_form: {
|
|
serialized_field_count: Array.from(serializedParams.keys()).length,
|
|
serialized_form_sha256: await sha256('Act=DoInfoJH&' + serializedForm),
|
|
serialized_form_redacted: true,
|
|
required_missing: requiredMissing
|
|
},
|
|
submit_intercept: submitIntercept,
|
|
blockers,
|
|
warnings,
|
|
submit_safety: {
|
|
live_submit_attempted: false,
|
|
DoInfoJH_network_prevented: Boolean(submitIntercept?.DoInfoJH_network_prevented),
|
|
note: 'This raw-instruction test uses existing LTJT options and product template side effects in browser memory. It never lets DoInfoJH reach the network.'
|
|
}
|
|
};
|
|
})()`;
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help) {
|
|
console.log(usage());
|
|
return;
|
|
}
|
|
|
|
const inputPath = args.input || args._[0];
|
|
if (!inputPath) {
|
|
console.error(usage());
|
|
process.exit(1);
|
|
}
|
|
|
|
const operation = readJson(inputPath);
|
|
const local = localValidate(operation);
|
|
if (local.blockers.length) {
|
|
const output = {
|
|
generated_at: new Date().toISOString(),
|
|
status: 'raw_instruction_test_blocked',
|
|
source_input: inputPath,
|
|
blockers: local.blockers,
|
|
warnings: local.warnings,
|
|
submit_safety: {
|
|
live_submit_attempted: false
|
|
}
|
|
};
|
|
if (args.out) writeJson(args.out, output);
|
|
console.log(JSON.stringify({ status: output.status, blockers: output.blockers.length, warnings: output.warnings.length, out: args.out || '' }, null, 2));
|
|
process.exit(2);
|
|
}
|
|
|
|
const target = await getTarget(args.port || DEFAULT_PORT);
|
|
const cdp = new CDP(target.webSocketDebuggerUrl);
|
|
await cdp.connect();
|
|
try {
|
|
await cdp.send('Runtime.enable');
|
|
const output = await evaluate(cdp, browserExpression({
|
|
operation,
|
|
openForm: args['open-form'],
|
|
interceptSubmit: args['intercept-submit']
|
|
}), 120000);
|
|
output.generated_at = new Date().toISOString();
|
|
output.source_input = inputPath;
|
|
output.warnings = [...(output.warnings || []), ...local.warnings];
|
|
if (args.out) writeJson(args.out, output);
|
|
if (args['approved-preflight-out']) writeJson(args['approved-preflight-out'], buildCompatiblePreflightReport(output));
|
|
if (args['approved-intercept-out']) writeJson(args['approved-intercept-out'], buildCompatibleInterceptReport(output));
|
|
console.log(JSON.stringify({
|
|
status: output.status,
|
|
blockers: output.blockers?.length || 0,
|
|
warnings: output.warnings?.length || 0,
|
|
lookup_checks: output.lookup_checks?.map((check) => ({
|
|
name: check.name,
|
|
exact_match_count: check.exact_match_count,
|
|
contains_match_count: check.contains_match_count,
|
|
match_rule: check.match_rule
|
|
})) || [],
|
|
product_changed_fields: output.product_side_effect?.changed_field_count || 0,
|
|
product_changed_groups: output.product_side_effect?.changed_field_groups || {},
|
|
passenger_total: output.parsed_instruction?.passenger_total,
|
|
room_total: output.parsed_instruction?.room_total,
|
|
receivable_amount_total: output.parsed_instruction?.receivable_amount_total,
|
|
serialized_field_count: output.final_form?.serialized_field_count || 0,
|
|
submit_intercept_status: output.submit_intercept?.status || '',
|
|
intercepted_submit_count: output.submit_intercept?.intercepted_submit_count || 0,
|
|
approved_preflight_out: args['approved-preflight-out'] || '',
|
|
approved_intercept_out: args['approved-intercept-out'] || '',
|
|
out: args.out || ''
|
|
}, null, 2));
|
|
if (output.blockers?.length) process.exit(2);
|
|
} finally {
|
|
cdp.close();
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error.stack || error.message);
|
|
process.exit(1);
|
|
});
|