import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import test from 'node:test'; import vm from 'node:vm'; const source = await readFile(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8'); const timing = createRequire(import.meta.url)('../chrome-extension/ltjt-order-assistant/operation-timing.js'); const slice = (start, end) => { const first = source.indexOf(start); const last = source.indexOf(end, first); assert.ok(first >= 0 && last > first); return source.slice(first, last); }; const removed = () => new Error('Frame with ID 0 was removed.'); const operation = { action: 'passenger_list_import', data: {} }; const ready = () => [{ frameId: 0, result: { status: 'erp_resolution_ready', resolved_operation: operation } }]; function harness(options = {}) { let clock = Date.parse('2026-09-15T03:51:14.000Z'); const state = { calls: [], probes: 0, injections: 0, lookups: 0, details: [], saved: {}, published: [], record: { execution_id: 'execution-fixture', state: 'running' }, tab: { id: 7, status: 'complete', url: 'https://erp.example.invalid/System/Mainlt.asp' }, recovery: { recovery_status: 'success', recovery_reason: 'erp_page_access_denied', recovery_trigger: 'session_status', recovery_attempt_at: '2026-09-15T03:51:13.000Z', recovery_reload_requested_at: '2026-09-15T03:51:13.100Z', recovery_completed_at: '2026-09-15T03:51:14.000Z', recovery_tab_id: 7, last_tab_url: 'SECRET_URL', last_error: 'SECRET_ERROR', passenger: 'SECRET_PERSON' } }; const context = { Error, Promise, setTimeout, clearTimeout, Date: class extends Date { static now() { return clock; } }, performance: { now: () => clock }, nowIso: () => new Date(clock).toISOString(), throwIfTaskCancelled: () => { if (state.cancelled) throw Object.assign(new Error('cancelled'), { code: 'task_cancelled' }); }, delay: async (ms) => { clock += ms; options.onDelay?.(state); }, getExecutionRecord: async () => state.record ? { ...state.record } : null, readErpKeepaliveState: async () => ({ ...state.recovery }), isApprovedErpPageUrl: (url) => String(url).startsWith('https://erp.example.invalid/'), canReusePageActionScripts: async () => false, pageActionFiles: () => ['operation-plans.js', 'inpage.js'], timingActionCode: (action) => action === 'resolveLifecycleOperation' ? 'resolve_lifecycle_operation' : 'other', queueOperationTimingDetails: (_task, details) => state.details.push(...details), queuePageActionResultTiming() {}, executeErpScript: async (_tab, spec) => { if (spec.files) { state.injections += 1; if (options.failInjection && state.injections === 1) throw removed(); return [{ frameId: 0 }]; } const action = spec.args[0]; state.calls.push(action); if (action === 'resolveLifecycleOperation') { state.lookups += 1; return options.lookup ? options.lookup(state) : ready(); } if (action === 'prepareLifecycleOperation') { if (options.failRoute) throw removed(); return [{ frameId: 0, result: { status: 'lifecycle_route_ready' } }]; } if (action === 'liveSubmitLifecycleOperation') throw removed(); throw new Error(`unexpected action ${action}`); }, chrome: { tabs: { get: async () => { if (options.closed) throw new Error('No tab with id: 7.'); return { ...state.tab }; } }, scripting: { executeScript: async (spec) => { state.probes += 1; assert.equal(spec.target.frameIds[0], 0); assert.equal(spec.files, undefined); assert.match(spec.func.toString(), /document.readyState/); if (options.probeError) throw options.probeError(); const documentId = options.documents ? options.documents(state.probes) : 'document-new'; return [{ frameId: 0, documentId, result: { ready: true, url: state.tab.url } }]; } }, storage: { local: { set: async (patch) => Object.assign(state.saved, patch) } } }, operationPlans: { normalizeOperation: (value) => value, publicPlan: (value) => value, validateOperation: () => ({ ok: true }) }, summaryFromOperation: () => ({}), findOrOpenErpTab: async () => { if (options.failFind) throw removed(); return state.tab; }, setResult: async (_task, value) => { const advanced = timing.advance(state.timingState, { now_ms: clock, stage: value.stage, status: value.status, details: state.details.splice(0) }); state.timingState = advanced.state; state.timing = advanced.snapshot; }, publish: async (_task, _operation, stage, report, extra) => { state.published.push({ stage, report, ...extra }); await context.setResult(_task, { stage, ...extra }); }, LIFECYCLE_EDIT_DIALOG_ACTIONS: new Set(), cacheResolvedExecutionOperation: async () => {}, preflightLifecycleAfterRoute: async () => { if (options.failPreflight) throw removed(); return { status: 'lifecycle_preflight_ready', no_erp_write: true, write_attempted: false }; }, lifecyclePreflightBusinessFailure: () => null, transitionCurrentExecution: async (_task, next) => { state.record.state = next; }, lifecycleResult: { isVerifiedResult: () => false } }; vm.createContext(context); vm.runInContext([ slice('async function withDeadline', 'function taskRootId'), slice('const LIFECYCLE_EVIDENCE_DEFAULTS', 'function withStorageMutation'), slice('function pageActionError', 'function pickResult'), slice('function withoutResolvedOperation', 'function browserExecutionAction') ].join('\n'), context); return { state, resolve: () => context.resolveOperationInErp(7, 'task-fixture', operation), execute: () => context.executeLifecycleOperation('task-fixture', operation, { ok: true }), elapsed: () => clock - Date.parse('2026-09-15T03:51:14.000Z') }; } test('normal lookup does not wait; business no-match and ambiguous results never retry', async () => { for (const count of [null, 0, 2]) { const h = harness({ lookup: () => count === null ? ready() : [{ result: { status: 'erp_resolution_blocked', blockers: [`erp_resolution_candidate_count:${count}`] } }] }); const result = await h.resolve(); assert.equal(result.ok, count === null); assert.equal(h.state.lookups, 1); assert.equal(h.state.probes, 0); } }); test('removed query frame waits for a stable current document then reinjects and queries once', async () => { const h = harness({ lookup: (state) => { if (state.lookups === 1) throw removed(); return ready(); }, documents: (count) => count === 1 ? 'old' : 'new' }); const result = await h.resolve(); assert.equal(result.ok, true); assert.equal(h.state.injections, 2); assert.equal(h.state.lookups, 2); assert.equal(h.state.probes, 3); assert.equal(h.elapsed(), 500); const diagnostic = result.report.preflight.navigation_retry; assert.equal(diagnostic.attempts, 2); assert.equal(diagnostic.failures[0].step, 'action_roundtrip.resolve_lifecycle_operation'); assert.equal(diagnostic.failures[0].recovery.same_tab, true); h.state.recovery.recovery_status = 'running'; assert.equal(diagnostic.failures[0].recovery.recovery_status, 'success'); assert.doesNotMatch(JSON.stringify(diagnostic), /SECRET_|erp.example|document-new/); }); test('frame removal during injection also safely recovers and identifies injection as failed step', async () => { const h = harness({ failInjection: true }); const result = await h.resolve(); assert.equal(result.ok, true); assert.equal(h.state.injections, 2); assert.equal(h.state.lookups, 1); assert.equal(result.report.preflight.navigation_retry.failures[0].step, 'script_injection'); }); test('repeated frame removal stops after two lookups with actual resolution failure stage and no-write evidence', async () => { const h = harness({ lookup: () => { throw removed(); } }); await h.execute(); assert.equal(h.state.lookups, 2); const result = h.state.published.at(-1); assert.equal(result.stage, 'erp_resolution'); assert.equal(result.failure_stage, 'erp_resolution'); assert.equal(result.error_code, 'erp_resolution_page_changed'); assert.equal(result.report.status, 'erp_resolution_blocked'); assert.equal(result.report.preflight.stage, 'erp_readonly_resolution'); assert.equal(result.report.preflight.write_attempted, false); assert.equal(result.report.no_erp_write, true); assert.equal(result.report.native_request, null); assert.equal(result.report.server_response, null); assert.equal(result.report.requery, null); assert.equal(result.report.side_effects.length, 4); assert.equal(h.state.timing.stages.at(-1).stage, 'erp_resolution'); assert.equal(h.state.timing.stages.at(-1).status, 'blocked'); assert.equal(h.state.saved.lastLifecyclePreflight, undefined); assert.equal(h.state.calls.includes('liveSubmitLifecycleOperation'), false); }); test('moving/loading/missing-identity documents cannot pass readiness and the wait is bounded', async () => { for (const mode of ['moving', 'loading', 'no-document-id', 'frame-removed']) { const h = harness({ lookup: () => { throw removed(); }, documents: (count) => mode === 'no-document-id' ? undefined : `new-${count}`, probeError: mode === 'frame-removed' ? removed : null }); if (mode === 'loading') h.state.tab.status = 'loading'; const result = await h.resolve(); assert.equal(result.ok, false); assert.equal(h.state.lookups, 1); assert.equal(h.elapsed(), 8000); assert.match(result.report.blockers[0], /erp_resolution_document_not_stable/); } }); test('permanent errors, closed tabs, and disallowed navigation do not rerun queries', async () => { for (const mode of ['permission', 'closed', 'unapproved', 'pending-unapproved', 'probe-permission']) { const h = harness({ closed: mode === 'closed', lookup: () => { throw mode === 'permission' ? new Error('Cannot access contents of the page.') : removed(); }, probeError: mode === 'probe-permission' ? () => new Error('permission denied') : null }); if (mode === 'unapproved') h.state.tab.url = 'https://elsewhere.invalid/'; if (mode === 'pending-unapproved') h.state.tab.pendingUrl = 'https://elsewhere.invalid/'; assert.equal((await h.resolve()).ok, false); assert.equal(h.state.lookups, 1); assert.equal(h.elapsed(), 0); } }); test('retry requires the same running execution without a prior write boundary; cancellation propagates', async () => { for (const mode of ['missing', 'write-started', 'old-write', 'replaced', 'write-during-wait', 'cancelled']) { const h = harness({ lookup: () => { throw removed(); }, onDelay: (state) => { if (mode === 'replaced') state.record.execution_id = 'other'; if (mode === 'write-during-wait') state.record.state = 'write_started'; if (mode === 'cancelled') state.cancelled = true; } }); if (mode === 'missing') h.state.record = null; if (mode === 'write-started') h.state.record.state = 'write_started'; if (mode === 'old-write') h.state.record.write_started_at = '2026-09-15T03:50:00Z'; if (mode === 'cancelled') await assert.rejects(h.resolve(), (error) => error.code === 'task_cancelled'); else assert.match((await h.resolve()).report.blockers[0], /erp_resolution_retry_not_safe/); assert.equal(h.state.lookups, 1); } }); test('exceptions before route, during route and during preflight retain the actual failure phase', async () => { for (const [option, stage] of [['failFind', 'erp_resolution'], ['failRoute', 'lifecycle_route_preparation'], ['failPreflight', 'lifecycle_preflight']]) { const h = harness({ [option]: true }); await h.execute(); assert.equal(h.state.published.at(-1).stage, stage); assert.equal(h.state.published.at(-1).no_erp_write, true); assert.equal(h.state.probes, 0); assert.equal(h.state.calls.includes('liveSubmitLifecycleOperation'), false); } }); test('frame removal at live submit is uncertain and never retried', async () => { const h = harness(); const result = await h.execute(); assert.equal(result.status, 'execution_uncertain'); assert.equal(result.report.no_erp_write, false); assert.equal(result.report.write_attempted, true); assert.equal(h.state.calls.filter((action) => action === 'liveSubmitLifecycleOperation').length, 1); assert.equal(h.state.probes, 0); });