{"record":{"id":"0bffa4159102aa8f","repo":"jackwener/OpenCLI","slug":"mercury-is-showing-a-submit-button-without-a-recog","errorCode":null,"errorMessage":"Mercury is showing a submit button without a recognizable expense form; refusing to click","messagePattern":"Mercury is showing a submit button without a recognizable expense form; refusing to click","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/mercury/utils.js","lineNumber":215,"sourceCode":"        throw new CommandExecutionError('Mercury is already showing a submit/review surface; refusing to click a possible final Submit expense button');\n    }\n    return payload;\n}\n\nexport async function assertCreateExpenseSurface(page) {\n    const state = await page.evaluate(`(() => {\n        const text = document.body?.innerText || '';\n        return {\n            url: location.href,\n            title: document.title,\n            hasFinalSubmit: /Submit expense/i.test(text) && /Review|receipt|amount|merchant|category|notes/i.test(text),\n            hasForm: /receipt|amount|merchant|category|notes|expense date/i.test(text),\n            bodyPreview: text.replace(/\\\\s+/g, ' ').trim().slice(0, 600)\n        };\n    })()`);\n    const payload = assertBooleanFields(assertMercuryState(state, 'expense form state'), 'expense form state', ['hasFinalSubmit', 'hasForm']);\n    if (payload.hasFinalSubmit && !payload.hasForm) {\n        throw new CommandExecutionError('Mercury is showing a submit button without a recognizable expense form; refusing to click');\n    }\n    return payload;\n}\n\nexport async function fillReimbursementFields(page, input) {\n    const result = await page.evaluate(`(() => {\n        const payload = ${JSON.stringify(input)};\n        const setNativeValue = (el, value) => {\n            const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;\n            const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;\n            setter?.call(el, value);\n            el.dispatchEvent(new Event('input', { bubbles: true }));\n            el.dispatchEvent(new Event('change', { bubbles: true }));\n            el.dispatchEvent(new Event('blur', { bubbles: true }));\n        };\n        const visible = (nodes) => nodes.find((node) => {\n            const style = window.getComputedStyle(node);\n            return style.visibility !== 'hidden' && style.display !== 'none' && node.offsetParent !== null;","sourceCodeStart":197,"sourceCodeEnd":233,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/mercury/utils.js#L197-L233","documentation":"assertCreateExpenseSurface() checks the page text for a final 'Submit expense' button (hasFinalSubmit) and for a recognizable expense form (hasForm: receipt/amount/merchant/category/notes/expense date). If it sees a submit button but no recognizable form, the library refuses to proceed — the click could be a dangerous final submit on an unexpected page rather than part of a normal expense form.","triggerScenarios":"page body text matches /Submit expense/i together with /Review|receipt|amount|merchant|category|notes/i (hasFinalSubmit=true) but does not match /receipt|amount|merchant|category|notes|expense date/i (hasForm=false), typically right after the expense was already submitted or on a confirmation/review page.","commonSituations":"The expense was already submitted and the page shows only a confirmation with a Submit-style control; Mercury UI copy changed so form keywords no longer appear; automation resumed on a stale/redirected page.","solutions":["Check the flow state: if the expense was already submitted, skip creating a new one instead of retrying.","Reload the create-expense page and re-run assertCreateExpenseSurface() before proceeding.","If Mercury's UI wording changed, update the regexes (hasFinalSubmit/hasForm) in clis/mercury/utils.js to match the new copy.","Wrap the call in try/catch for CommandExecutionError and treat it as 'already at review/confirmation stage'."],"exampleFix":"// before\nawait assertCreateExpenseSurface(page);\nawait fillReimbursementFields(page, input);\n// after\nlet surface;\ntry {\n  surface = await assertCreateExpenseSurface(page);\n} catch (err) {\n  // already showing a submit/review surface — expense likely already created\n  return { skipped: true, reason: 'review-surface' };\n}\nawait fillReimbursementFields(page, input);","handlingStrategy":"try-catch","validationCode":"const state = await assertCreateExpenseSurface(page);\nif (state.hasFinalSubmit && !state.hasForm) {\n  // already at review/confirmation — do not proceed with fill/submit\n  return;\n}","typeGuard":"function isSafeExpenseSurface(p) {\n  return p != null && typeof p === 'object'\n    && !(p.hasFinalSubmit === true && p.hasForm === false);\n}","tryCatchPattern":"try {\n  await assertCreateExpenseSurface(page);\n  await fillReimbursementFields(page, input);\n} catch (err) {\n  if (err instanceof CommandExecutionError && /without a recognizable expense form/.test(err.message)) {\n    // likely already submitted; check expenses list via inspectMercury\n    return { skipped: true, reason: 'review-surface' };\n  }\n  throw err;\n}","preventionTips":["Call assertCreateExpenseSurface immediately before filling/submitting, not minutes earlier.","Treat this error as 'expense already submitted' rather than retrying the flow.","After any redirect or timeout, reload the create-expense page and re-assert the surface.","If Mercury's UI copy changes, update the hasForm/hasFinalSubmit regexes."],"tags":["safety-guard","browser-automation","duplicate-submission","mercury"],"backgroundTag":"unsafe-click-refused","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}