jackwener/OpenCLI · error · CommandExecutionError

Mercury did not reach the Review step with the final Submit

Error message

Mercury did not reach the Review step with the final Submit expense button visible

What it means

After the Review click succeeds, the automation snapshots the Review step and requires both a Review indicator (hasReview) and the final 'Submit expense' button to be present. If either is missing, the flow did not actually reach the expected Review step, so it aborts rather than clicking an unknown button.

Source

Thrown at clis/mercury/reimbursement-draft.js:105

              .filter((node) => {
                const style = window.getComputedStyle(node);
                return style.visibility !== 'hidden' && style.display !== 'none' && node.offsetParent !== null;
              });
            const el = candidates.find((node) => norm(node.innerText || node.textContent || '') === 'review');
            if (!el) return { clicked: false };
            el.click();
            return { clicked: true, text: el.innerText || el.textContent || '' };
        })()`);
        if (!reviewClick || typeof reviewClick !== 'object' || typeof reviewClick.clicked !== 'boolean') {
            throw new CommandExecutionError('Mercury returned malformed Review click result');
        }
        if (!reviewClick.clicked) {
            throw new CommandExecutionError('Mercury Review button was not clicked; inspect the page for validation errors');
        }
        await page.wait({ time: 2 });
        const review = await reviewSnapshot(page);
        if (!review.hasReview || !review.hasSubmitExpenseButton) {
            throw new CommandExecutionError('Mercury did not reach the Review step with the final Submit expense button visible');
        }

        if (input.closeAfterReview) {
            await page.evaluate(`(() => {
                const norm = (s) => String(s || '').replace(/\\s+/g, ' ').trim().toLowerCase();
                const el = Array.from(document.querySelectorAll('button, a, [role="button"], [role="link"]'))
                  .find((node) => norm(node.innerText || node.textContent || '') === 'close');
                el?.click();
                return { clicked: Boolean(el) };
            })()`);
            await page.wait({ time: 1 });
        }

        return [{
            status: 'review_ready',
            url: review.url,
            receipt: receiptBasename(input.receipt),
            uploaded: true,

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Increase the wait after the Review click (e.g. page.wait({ time: 5 })) or poll with waitFor until hasReview && hasSubmitExpenseButton is true.
  2. Inspect the page state at failure — check for validation banners, session-expiry prompts, or interstitial modals.
  3. Re-authenticate if the Mercury session expired mid-flow.
  4. If Mercury changed markup, update reviewSnapshot's selectors for the Review step / 'Submit expense' button in reimbursement-draft.js.

Example fix

// before
await page.wait({ time: 2 });
const review = await reviewSnapshot(page);
if (!review.hasReview || !review.hasSubmitExpenseButton) {
    throw new CommandExecutionError('Mercury did not reach the Review step with the final Submit expense button visible');
}
// after
let review = null;
for (let i = 0; i < 10; i++) {
    await page.wait({ time: 1 });
    review = await reviewSnapshot(page);
    if (review.hasReview && review.hasSubmitExpenseButton) break;
}
if (!review || !review.hasReview || !review.hasSubmitExpenseButton) {
    throw new CommandExecutionError('Mercury did not reach the Review step with the final Submit expense button visible');
}
Defensive patterns

Strategy: retry

Validate before calling

// poll for the Review step instead of a fixed wait
let review = null;
for (let i = 0; i < 10 && !review; i++) {
    await page.wait({ time: 1 });
    const s = await reviewSnapshot(page);
    if (s.hasReview && s.hasSubmitExpenseButton) review = s;
}

Type guard

function reachedReviewStep(s) { return Boolean(s) && s.hasReview === true && s.hasSubmitExpenseButton === true; }

Try / catch

try {
    await draftReimbursement(input);
} catch (err) {
    if (err.message.includes('did not reach the Review step')) {
        await page.wait({ time: 5 });
        const s = await reviewSnapshot(page);
        if (!(s.hasReview && s.hasSubmitExpenseButton)) throw err;
        // continue the flow manually
    } else throw err;
}

Prevention

When it happens

Trigger: reviewSnapshot(page) returns { hasReview: false } or { hasSubmitExpenseButton: false } — the click happened but the page did not transition to the Review step within the 2-second wait, or the step rendered without the final Submit expense button.

Common situations: Mercury is slow and 2 seconds is not enough for the Review step to render; a validation or server-side error appeared after clicking Review; Mercury UI change altered the 'Submit expense' button text/testid; a modal or interstitial (2FA, session expiry) intercepted navigation.

Related errors


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