{"record":{"id":"f223efc900801053","repo":"moeru-ai/airi","slug":"failed-to-retweet-errortomessage-error","errorCode":null,"errorMessage":"Failed to retweet: ${errorToMessage(error)}","messagePattern":"Failed to retweet: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"integrations/twitter-services/src/core/services/tweet.ts","lineNumber":174,"sourceCode":"      }\n\n      await retweetButton.click()\n\n      // Wait for retweet confirmation dialog and click it\n      await page.waitForSelector('[data-testid=\"retweetConfirm\"]')\n      await page.click('[data-testid=\"retweetConfirm\"]')\n\n      // Wait for the retweet to register\n      await page.waitForFunction(\n        `document.querySelector('${SELECTORS.TIMELINE.RETWEET_BUTTON}')?.getAttribute('aria-pressed') === 'true'`,\n        { timeout: 5000 },\n      )\n\n      return true\n    }\n    catch (error: unknown) {\n      console.error('Error retweeting:', error)\n      throw new Error(`Failed to retweet: ${errorToMessage(error)}`)\n    }\n  }\n\n  /**\n   * Posts a new tweet with the given content and options\n   */\n  async function postTweet(content: string, options: PostOptions = {}): Promise<string> {\n    try {\n      const page = ctx.page\n      // Go to home page where you can compose a tweet\n      await page.goto(TWITTER_HOME_URL)\n\n      // Wait for the tweet composer to load\n      await page.waitForSelector(SELECTORS.COMPOSE.TWEET_INPUT)\n\n      // Type the tweet content\n      await page.click(SELECTORS.COMPOSE.TWEET_INPUT)\n      await page.type(SELECTORS.COMPOSE.TWEET_INPUT, content)","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/integrations/twitter-services/src/core/services/tweet.ts#L156-L192","documentation":"This error wraps any failure that occurs during the retweet flow in the Twitter services module, which drives Twitter's web UI through a headless browser (Puppeteer-style page automation). The retweet sequence clicks the confirm button and then polls the timeline retweet button's aria-pressed attribute to verify success within a 5 second window. errorToMessage normalizes the underlying cause (timeout, navigation error, closed page, selector mismatch) into the thrown string.","triggerScenarios":"Calling retweet(tweetUrl) when the session cookie is expired so Twitter shows a login wall instead of the retweet confirm; the data-testid=\"retweetConfirm\" attribute was renamed by Twitter so page.click rejects; the waitForFunction exceeds its 5000ms timeout because aria-pressed never flips to 'true' (slow network, rate-limit interstitial, or already-retweeted state); ctx.page was closed or navigated away mid-flow.","commonSituations":"Twitter ships a DOM/A-B test that renames data-testid values; long-running bot sessions lose their auth cookie; running against a flaky proxy that stalls page loads; calling retweet on a tweet you already retweeted so the confirm path differs.","solutions":["Re-verify the session is authenticated (navigate to home, check for the compose box) before calling retweet.","Confirm SELECTORS.TIMELINE.RETWEET_BUTTON and data-testid=\"retweetConfirm\" still match the live DOM; update them if Twitter changed the UI.","Raise the waitForFunction timeout above 5000ms or retry once on timeout, since retweets sometimes register slowly.","Guard ctx.page for isClosed() before entry and re-create the page if it was torn down."],"exampleFix":"// before\nawait page.waitForFunction(\n  `document.querySelector('${SELECTORS.TIMELINE.RETWEET_BUTTON}')?.getAttribute('aria-pressed') === 'true'`,\n  { timeout: 5000 },\n)\n\n// after\nawait page.waitForFunction(\n  `document.querySelector('${SELECTORS.TIMELINE.RETWEET_BUTTON}')?.getAttribute('aria-pressed') === 'true'`,\n  { timeout: 15000 },\n).catch(async () => {\n  // re-check via the API-free heuristic: reload the tweet and inspect retweeted state\n  await page.reload({ waitUntil: 'networkidle' })\n})","handlingStrategy":"try-catch","validationCode":"if (!ctx.page || ctx.page.isClosed?.()) {\n  throw new Error('Page is not available; recreate it before retweeting')\n}\n// optional session pre-check\nawait ctx.page.goto(TWITTER_HOME_URL)\nconst loggedIn = await ctx.page.$('[data-testid=\"SideNav_NewTweet_Button\"]')\nif (!loggedIn) throw new Error('Session expired; re-authenticate before retweeting')","typeGuard":"function isRetweetable(page: { click: (s: string) => Promise<unknown> } | null | undefined): page is { click: (s: string) => Promise<unknown> } {\n  return !!page && typeof page.click === 'function'\n}","tryCatchPattern":"try {\n  await retweet(tweetUrl)\n}\ncatch (err) {\n  const msg = (err as Error).message\n  if (/timeout|aria-pressed/i.test(msg)) {\n    // transient: wait and retry once\n    await new Promise(r => setTimeout(r, 2000))\n    return retweet(tweetUrl)\n  }\n  if (/login|session/i.test(msg)) throw new Error('Re-auth required')\n  throw err\n}","preventionTips":["Keep selectors in a single constants file and update them when Twitter ships UI changes.","Refresh the session cookie on a schedule rather than waiting for a failure.","Wrap each automation step in a timeout and surface a typed error so callers can retry vs re-auth."],"tags":["browser-automation","twitter","dom-selector","timeout","session"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}