jackwener/OpenCLI · error · CommandExecutionError

Mercury is showing a submit button without a recognizable ex

Error message

Mercury is showing a submit button without a recognizable expense form; refusing to click

What it means

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.

Source

Thrown at clis/mercury/utils.js:215

        throw new CommandExecutionError('Mercury is already showing a submit/review surface; refusing to click a possible final Submit expense button');
    }
    return payload;
}

export async function assertCreateExpenseSurface(page) {
    const state = await page.evaluate(`(() => {
        const text = document.body?.innerText || '';
        return {
            url: location.href,
            title: document.title,
            hasFinalSubmit: /Submit expense/i.test(text) && /Review|receipt|amount|merchant|category|notes/i.test(text),
            hasForm: /receipt|amount|merchant|category|notes|expense date/i.test(text),
            bodyPreview: text.replace(/\\s+/g, ' ').trim().slice(0, 600)
        };
    })()`);
    const payload = assertBooleanFields(assertMercuryState(state, 'expense form state'), 'expense form state', ['hasFinalSubmit', 'hasForm']);
    if (payload.hasFinalSubmit && !payload.hasForm) {
        throw new CommandExecutionError('Mercury is showing a submit button without a recognizable expense form; refusing to click');
    }
    return payload;
}

export async function fillReimbursementFields(page, input) {
    const result = await page.evaluate(`(() => {
        const payload = ${JSON.stringify(input)};
        const setNativeValue = (el, value) => {
            const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
            const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
            setter?.call(el, value);
            el.dispatchEvent(new Event('input', { bubbles: true }));
            el.dispatchEvent(new Event('change', { bubbles: true }));
            el.dispatchEvent(new Event('blur', { bubbles: true }));
        };
        const visible = (nodes) => nodes.find((node) => {
            const style = window.getComputedStyle(node);
            return style.visibility !== 'hidden' && style.display !== 'none' && node.offsetParent !== null;

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Check the flow state: if the expense was already submitted, skip creating a new one instead of retrying.
  2. Reload the create-expense page and re-run assertCreateExpenseSurface() before proceeding.
  3. If Mercury's UI wording changed, update the regexes (hasFinalSubmit/hasForm) in clis/mercury/utils.js to match the new copy.
  4. Wrap the call in try/catch for CommandExecutionError and treat it as 'already at review/confirmation stage'.

Example fix

// before
await assertCreateExpenseSurface(page);
await fillReimbursementFields(page, input);
// after
let surface;
try {
  surface = await assertCreateExpenseSurface(page);
} catch (err) {
  // already showing a submit/review surface — expense likely already created
  return { skipped: true, reason: 'review-surface' };
}
await fillReimbursementFields(page, input);
Defensive patterns

Strategy: try-catch

Validate before calling

const state = await assertCreateExpenseSurface(page);
if (state.hasFinalSubmit && !state.hasForm) {
  // already at review/confirmation — do not proceed with fill/submit
  return;
}

Type guard

function isSafeExpenseSurface(p) {
  return p != null && typeof p === 'object'
    && !(p.hasFinalSubmit === true && p.hasForm === false);
}

Try / catch

try {
  await assertCreateExpenseSurface(page);
  await fillReimbursementFields(page, input);
} catch (err) {
  if (err instanceof CommandExecutionError && /without a recognizable expense form/.test(err.message)) {
    // likely already submitted; check expenses list via inspectMercury
    return { skipped: true, reason: 'review-surface' };
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/0bffa4159102aa8f. Report an issue: GitHub.