Skip to content

fix(issue-90): restore observability and input validation in Telegram menu - #148

Merged
georgyia merged 3 commits into
mainfrom
fix/issue-90-telegram-menu-observability
May 13, 2026
Merged

georgyia merged 3 commits into
mainfrom
fix/issue-90-telegram-menu-observability

Conversation

@georgyia

@georgyia georgyia commented May 3, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #90.

Summary

The Telegram menu system shipped with three silent-failure patterns that made production debugging impossible and let forged callback payloads bypass input validation. This PR restores observability and tightens the input boundary.

Bug 1 — Bare catch { } blocks discarded every error

doCancelIntent, doCancelCard, and the outer handleMenuCallback each used catch { } with no bound variable and no log line, hiding IntentNotFoundError, IllegalTransitionError, Stripe errors, DB errors, and everything else. In particular, doCancelIntent could leave the user thinking their intent was cancelled when it had actually failed — with zero trace.

Each is now catch (err) with a structured pino log line at error level (including chatId, intentId/action/payload, userId, and the error itself) before showing the user-facing fallback. The user-visible behaviour is unchanged.

Bug 2 — editMenu swallowed every Telegram API error

The previous .catch(() => {}) masked 403 (bot blocked), 429 (rate limit), and network failures together with the benign 400 "message is not modified" response. Real Telegram failures became completely invisible.

editMenu now distinguishes the two: the benign "message is not modified" 400 is silently ignored (matched by checking err.error_code === 400 and description.includes('message is not modified')), while every other failure is logged and propagated. The outer error-recovery path uses a small editMenuSafe wrapper so a failing recovery edit cannot crash the handler — the secondary failure is logged but not rethrown.

Bug 3 — Unvalidated TTL/policy payloads from callback data

parseInt(payload, 10) was applied to the menu_pref_ttl callback payload with no range check, so a crafted callback like menu_pref_ttl:-1, menu_pref_ttl:99999, or menu_pref_ttl:12.5 bypassed the API-level 1..10080 bound enforced by the Zod schema in src/api/routes/users.ts. The fix validates the payload as an integer-only string within the bound (regex ^[1-9]\\d*$ + numeric range) before calling savePrefTtl. Out-of-range values are logged at `warn` and surface a "Must be between 1 and 10080 minutes" message to the user.

The same class of bug in menu_pref_policy — an unchecked payload as CardCancelPolicy cast — is fixed at the same time by checking the payload against Object.values(CardCancelPolicy) and rejecting unknown values.

Unknown menu_* actions, previously silently ignored, are now logged at `warn` so a UI that ships a button the backend doesn't handle is visible in logs.

Tests

tests/unit/telegram/menuHandler.test.ts gains 16 new unit tests in four describe blocks:

  • editMenu error handling — benign "message is not modified" is swallowed; non-benign 429 triggers the fallback edit; double-failure (primary + fallback) doesn't crash.
  • TTL payload validation — table-driven coverage of negative, zero, too-large, non-numeric, empty, and fractional rejections, plus the three boundary acceptances (1, 60, 10080).
  • Cancel-policy payload validation — unknown policy rejection.
  • Error logging in cancel handlers — expireIntent failure, cancelCard failure, and an inner DB failure all surface the user-facing fallback without rethrowing.

All 39 menuHandler unit tests pass (23 pre-existing + 16 new), all 371 unit tests pass, and the existing menu integration tests pass. The two pre-existing failures in tests/integration/stripe/reconciliation.test.ts are unrelated (tracked in #88).

Test plan

docker compose up -d
npx jest --testPathPattern=unit/telegram/menuHandler --forceExit
# → 39/39 pass

npx jest --testPathPattern=integration/telegram/menuHandler.test.ts --runInBand --forceExit
# → 26/26 pass (17 .live tests skipped without a real bot token)

npx jest --testPathPattern=unit --forceExit
# → 371/371 pass

PORT=3300 npm run dev
curl http://localhost:3300/health
# → {\"status\":\"ok\",...}

Out of scope

  • Replacing user-facing copy with provider-specific failure reasons. The fallback messages are intentionally generic; the new structured logs give operators the detail they need.
  • Restructuring the menu router. Validation lives at the action boundary; a future router refactor can centralise it.

Summary by CodeRabbit

  • Bug Fixes

    • Menu edits are more resilient: harmless "message is not modified" is ignored; other edit failures show a safe fallback without surfacing Telegram edit errors. Cancellation reliably shows confirmation when successful and avoids misleading errors; saving cancel-policy clears any existing TTL. Preference inputs are validated (integer TTL within allowed range; known policies); unknown menu actions now log a warning.
  • Tests

    • Expanded tests for edit-error handling, cancellation flows, validation, and recovery/logging.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 3, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 49ff3510-078d-4fd3-8ace-16ee74566d9a

📥 Commits

Reviewing files that changed from the base of the PR and between 0c22d2a and c8524c8.

📒 Files selected for processing (2)
  • src/telegram/menuHandler.ts
  • tests/unit/telegram/menuHandler.test.ts

📝 Walkthrough

Walkthrough

editMenu now only ignores Telegram’s benign "message is not modified" error and logs other edit failures; tryEditMenu was added to swallow edits. Cancellation mutations are separated from confirmation edits with logging. handleMenuCallback runs inside one recovery boundary, validates menu_pref_policy and integer menu_pref_ttl (with bounds), and logs unknown menu_* actions.

Changes

Telegram Menu Error Handling & Input Validation

Layer / File(s) Summary
Constants & Logger
src/telegram/menuHandler.ts
Adds TTL_MIN_MINUTES, TTL_MAX_MINUTES and a module-scoped child logger.
Error Detection Utility
src/telegram/menuHandler.ts
Adds isMessageNotModifiedError(err) to recognize Telegram 400 "message is not modified".
Edit Wrapper & Logging
src/telegram/menuHandler.ts
Refactors editMenu to swallow only the benign "not modified" error, log and rethrow other edit errors; adds tryEditMenu that never throws.
Cancellation Mutation & UI Split
src/telegram/menuHandler.ts
Refactors doCancelIntent and doCancelCard so mutations (expireIntent/cancelCard) run in their own try/catch with error logging; on mutation failure a generic warning is rendered via tryEditMenu, and on success the confirmation edit is performed via tryEditMenu.
Preference Persistence
src/telegram/menuHandler.ts
savePrefPolicy now persists cancelPolicy: policy and clears cardTtlMinutes (sets to null) when switching policies.
Callback Dispatch & Validation
src/telegram/menuHandler.ts
Wraps entire callback dispatch (including user lookup) in one try/catch with detailed logging and a generic tryEditMenu fallback. Validates menu_pref_policy against CardCancelPolicy and validates menu_pref_ttl with integer-only regex and TTL_MIN_MINUTES/TTL_MAX_MINUTES bounds; invalid inputs log a warning and render an "invalid ..." edit without writing to DB. Unknown menu_* actions are logged.
Tests / Observability
tests/unit/telegram/menuHandler.test.ts
Adds a mocked logger.child() and tests for benign vs non-benign edit errors, swallowed vs fallback edits, cancel-handler mutation/UI separation, outer recovery on user lookup failures, menu_pref_ttl integer/range validation and accepted boundaries, menu_pref_policy validation, unknown action warnings, and structured error logging assertions.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant BotHandler as MenuHandler
  participant DB
  participant TelegramAPI as Telegram
  participant Logger

  User->>BotHandler: callback (menu_pref_ttl/menu_pref_policy/menu_cancel_*)
  BotHandler->>Logger: child(logger context)
  BotHandler->>DB: lookup user
  alt validation error
    BotHandler->>Telegram: tryEditMenu("invalid ...")
    BotHandler->>Logger: warn(payload ...)
  else mutation path
    BotHandler->>DB: expireIntent / cancelCard / savePref...
    DB-->>BotHandler: success / error
    alt DB error
      BotHandler->>Logger: error(mutation failure ...)
      BotHandler->>Telegram: tryEditMenu("Something went wrong")
    else DB success
      BotHandler->>Telegram: tryEditMenu("confirmation / updated prefs")
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through callbacks in the quiet night,

Logged the whispers that hid from sight,
Bounded TTLs and checked each plea,
Edits now only hush what’s meant to be,
The little rabbit made menus right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing observability and input validation issues in the Telegram menu handler for issue #90.
Linked Issues check ✅ Passed All three objectives from issue #90 are addressed: empty catch blocks now log errors with context, editMenu distinguishes benign 400 errors from genuine failures and logs them, and TTL/policy validation prevents unvalidated callback payloads.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the three silent-failure patterns and adding input validation in the Telegram menu handler; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-90-telegram-menu-observability

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/telegram/menuHandler.ts (2)

216-241: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Separate the irreversible cancel from the Telegram confirmation edit.

Both handlers treat a post-success editMessageText failure as if the cancel itself failed. If the card/intent is already cancelled and Telegram then returns 403/429/network errors, the catch logs the operation as failed and shows “Something went wrong”, which invites a retry against already-mutated state.

Suggested split for the success side effect vs. notification path
 async function doCancelIntent(
   bot: ReturnType<typeof getTelegramBot>,
   chatId: number | string,
   messageId: number,
   intentId: string,
 ): Promise<void> {
   try {
     await expireIntent(intentId);
-    const keyboard = new InlineKeyboard().text('⬅️ Back', 'menu_main:_');
-    await editMenu(
-      bot,
-      chatId,
-      messageId,
-      '✅ Intent cancelled. Your budget has been returned.',
-      keyboard,
-    );
   } catch (err) {
     log.error({ chatId, intentId, err }, 'doCancelIntent failed');
     const keyboard = new InlineKeyboard().text('⬅️ Back', 'menu_main:_');
     await editMenuSafe(
       bot,
       chatId,
       messageId,
       '⚠️ Something went wrong. Please try again.',
       keyboard,
     );
+    return;
+  }
+
+  const keyboard = new InlineKeyboard().text('⬅️ Back', 'menu_main:_');
+  try {
+    await editMenu(
+      bot,
+      chatId,
+      messageId,
+      '✅ Intent cancelled. Your budget has been returned.',
+      keyboard,
+    );
+  } catch (err) {
+    log.error({ chatId, intentId, err }, 'Intent cancelled, but confirmation edit failed');
   }
 }

Apply the same split to doCancelCard.

Also applies to: 373-392

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/telegram/menuHandler.ts` around lines 216 - 241, The current
doCancelIntent conflates the irreversible state change (expireIntent) with the
Telegram UI update so any editMessageText failure is treated as a cancellation
failure; change it to first call expireIntent and handle its errors (log and
return/fail) and then in a separate try/catch attempt editMenu (and on error log
only and optionally call editMenuSafe or notify user without reverting the
cancellation), ensuring you do not treat edit failures as cancel failures; apply
the same pattern to doCancelCard (separate the state mutation from the
Telegram-side notification, use distinct try/catch blocks, and log errors
accordingly).

453-455: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include the user lookup in the recovery boundary.

Line 454 still runs outside the outer try, so a rejected getUserByChatId(chatId) bypasses the new logging and fallback message entirely. That leaves an unhandled DB-read failure on every action that requires a user record.

Suggested change
-  const user = await getUserByChatId(chatId);
-
-  if (!user) {
-    await editMenu(
-      bot,
-      chatId,
-      messageId,
-      '⚠️ You need to sign up first. Send /start &lt;code&gt; to get started.',
-    );
-    return;
-  }
-
-  try {
+  let userId: string | undefined;
+  try {
+    const user = await getUserByChatId(chatId);
+    if (!user) {
+      await editMenu(
+        bot,
+        chatId,
+        messageId,
+        '⚠️ You need to sign up first. Send /start &lt;code&gt; to get started.',
+      );
+      return;
+    }
+    userId = user.id;
     if (action === 'menu_balance') {
       await showBalance(bot, chatId, messageId, user);
     } else if (action === 'menu_history') {
       await showHistory(bot, chatId, messageId, user);
@@
   } catch (err) {
     log.error(
-      { chatId, messageId, action, payload, userId: user.id, err },
+      { chatId, messageId, action, payload, userId, err },
       'menu callback handler failed',
     );

Also applies to: 524-534

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/telegram/menuHandler.ts` around lines 453 - 455, The user lookup "const
user = await getUserByChatId(chatId)" is executed outside the outer recovery
try/catch so a rejected promise bypasses your error logging and fallback reply;
move this await into the same outer try block (or wrap it in the same recovery
try/catch) so failures are caught and handled by the existing recovery logging
and fallback message code path, and apply the same change to the second
occurrence around the getUserByChatId usage (the block covering the other action
at the 524–534 area) so both user reads follow the same error-handling boundary.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/telegram/menuHandler.test.ts`:
- Around line 588-773: Mock the '@/config/logger' export in these unit tests and
add assertions that the new observability calls are made: for failure paths in
handleMenuCallback (reference handleMenuCallback, mockExpireIntent,
mockCancelCard, and the show* handlers) assert logger.error was called with an
error for cancel/inner failures; for invalid TTL/policy and unknown menu_*
payloads assert logger.warn (or logger.error per implementation) is called with
a message indicating invalid payload/action; ensure the mocked logger is reset
between tests and add expect(...) checks immediately after invoking
handleMenuCallback in the existing "rejects ... without writing to the DB" and
cancel-failure tests to verify the correct logger method and that it received an
informative message.

---

Outside diff comments:
In `@src/telegram/menuHandler.ts`:
- Around line 216-241: The current doCancelIntent conflates the irreversible
state change (expireIntent) with the Telegram UI update so any editMessageText
failure is treated as a cancellation failure; change it to first call
expireIntent and handle its errors (log and return/fail) and then in a separate
try/catch attempt editMenu (and on error log only and optionally call
editMenuSafe or notify user without reverting the cancellation), ensuring you do
not treat edit failures as cancel failures; apply the same pattern to
doCancelCard (separate the state mutation from the Telegram-side notification,
use distinct try/catch blocks, and log errors accordingly).
- Around line 453-455: The user lookup "const user = await
getUserByChatId(chatId)" is executed outside the outer recovery try/catch so a
rejected promise bypasses your error logging and fallback reply; move this await
into the same outer try block (or wrap it in the same recovery try/catch) so
failures are caught and handled by the existing recovery logging and fallback
message code path, and apply the same change to the second occurrence around the
getUserByChatId usage (the block covering the other action at the 524–534 area)
so both user reads follow the same error-handling boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 73365b41-04da-45aa-9206-62e6b3808964

📥 Commits

Reviewing files that changed from the base of the PR and between 2c92df5 and deb3fdb.

📒 Files selected for processing (2)
  • src/telegram/menuHandler.ts
  • tests/unit/telegram/menuHandler.test.ts

Comment thread tests/unit/telegram/menuHandler.test.ts
georgyia added a commit that referenced this pull request May 3, 2026
recovery boundary, assert observability calls in tests

CodeRabbit review on PR #148 surfaced three real issues:

1. doCancelIntent / doCancelCard conflated the irreversible state change
   with the Telegram confirmation edit. With bug 2's stricter editMenu
   that now propagates real failures, a 403/429/network error after the
   cancel succeeded would fall into the same catch and show
   "Something went wrong" — inviting the user to retry an
   already-cancelled intent or card. Both handlers are now structured
   as: mutation (try/catch → log + show error + return), then a
   separate fire-and-forget tryEditMenu for the confirmation. A
   Telegram failure on the success edit is logged but never surfaces a
   misleading error to the user. Renamed editMenuSafe → tryEditMenu
   for honesty (it does not throw; it does not "secure" anything).

2. getUserByChatId was awaited outside the outer recovery try/catch,
   so a Prisma failure on the user lookup bypassed the structured
   error log and the user-facing fallback. The whole dispatch — early
   shortcuts plus user lookup plus per-action handling — now lives
   inside one recovery boundary.

3. The unit tests asserted only the user-visible fallback string. The
   main contract of this PR is restored observability, so a regression
   to a bare catch could still pass. The tests now mock @/config/logger
   and assert log.warn / log.error were called with the expected
   payload and message for every new path: invalid TTL/policy/unknown
   action (warn), cancel-handler mutation failures (error), outer
   recovery (error), and the benign "message is not modified" case
   (no error log).

Plus two new tests pinning the review's headline concern: when
expireIntent / cancelCard succeed but the confirmation editMessageText
fails, the user must NOT see "Something went wrong" and the handler must
NOT log the operation as failed.

Also fixes the `npm run format:check` CI failure on the previous commit.

Tests: 43/43 menuHandler unit, 26/26 menuHandler integration, 375/375
total unit, 0 lint errors, prettier --check passes, dev server boots
cleanly under docker.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/telegram/menuHandler.ts`:
- Around line 487-503: In the menu_pref_policy branch ensure we clear any stale
TTL when switching away from AFTER_TTL: after validating payload as
CardCancelPolicy, if policy === CardCancelPolicy.AFTER_TTL call showTtlPicker as
before, otherwise call savePrefPolicy in a way that also clears cardTtlMinutes
(e.g. pass null/undefined for cardTtlMinutes or update savePrefPolicy to set
cardTtlMinutes = NULL when policy !== AFTER_TTL). Update savePrefPolicy (or its
DB update logic) so it accepts/handles an explicit cardTtlMinutes value and
writes NULL to the users row when requested, preserving the invariant that
cardTtlMinutes is only set for AFTER_TTL.
- Around line 444-456: The cancel/card callback handlers currently accept an
untrusted intentId payload before resolving the Telegram user; resolve the
Telegram user first, validate ownership, and pass user.id into
showCancelConfirm, doCancelIntent, and doCancelCard so downstream
lookups/mutations are constrained by both intentId and userId. Update the
implementations of showCancelConfirm/ doCancelIntent/ doCancelCard (and any
called helpers like expireIntent or cancelCard) to accept a userId parameter and
use it in their DB queries/updates (e.g., WHERE intentId = ? AND userId = ?) to
ensure only the owner can view or modify the intent/card.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6098a07e-2428-466b-941a-729d74a74e9c

📥 Commits

Reviewing files that changed from the base of the PR and between deb3fdb and 14e2259.

📒 Files selected for processing (2)
  • src/telegram/menuHandler.ts
  • tests/unit/telegram/menuHandler.test.ts

Comment thread src/telegram/menuHandler.ts
Comment thread src/telegram/menuHandler.ts

@JonasBaeumer JonasBaeumer left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, please address the open PR change and then proceed with merge. Well done @georgyia :)

@georgyia
georgyia requested a review from JonasBaeumer May 4, 2026 15:28

@JonasBaeumer JonasBaeumer left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, please resolve the conflicts and you should be good to merge :)

georgyia added 3 commits May 13, 2026 20:16
… menu

The menu system shipped with three silent-failure patterns that made
production debugging impossible and let forged callback payloads bypass
input validation:

1. Three bare `catch { }` blocks in `doCancelIntent`, `doCancelCard`, and
   the outer `handleMenuCallback` discarded every error (IntentNotFound,
   IllegalTransition, Stripe, DB, etc.) with no log line. Each is now a
   bound `catch (err)` that logs at error level via the project pino
   logger before showing the user-facing fallback.

2. `editMenu` swallowed every Telegram API error with `.catch(() => {})`,
   masking 403 (bot blocked), 429 (rate limit), and network failures
   alongside the benign 400 "message is not modified" response. It now
   distinguishes the two: the benign case is silently ignored, real
   failures are logged and propagated. A small `editMenuSafe` wrapper is
   used in the outer error-recovery path so a failing fallback edit
   doesn't crash the handler.

3. The TTL value parsed from `menu_pref_ttl` callback payloads was
   written to the DB with no range check, so a forged
   `menu_pref_ttl:-1` or `menu_pref_ttl:99999` bypassed the API-level
   1..10080 bound enforced by Zod. The Telegram path now validates the
   payload (integer-only string, within bounds) before calling
   `savePrefTtl`. The same class of bug in `menu_pref_policy` (an
   unchecked `payload as CardCancelPolicy` cast) is fixed at the same
   time by validating against the enum.

Tests: 16 new unit tests in `tests/unit/telegram/menuHandler.test.ts`
covering the benign-vs-real Telegram error split, fallback safety when
the recovery edit also fails, the full TTL validation matrix (negative,
zero, too large, non-numeric, empty, fractional, and the three valid
boundary values), unknown policy rejection, and the bound-error
surfacing path for each cancel handler. All 39 menuHandler unit tests
and the existing menu integration tests pass.

Closes #90
recovery boundary, assert observability calls in tests

CodeRabbit review on PR #148 surfaced three real issues:

1. doCancelIntent / doCancelCard conflated the irreversible state change
   with the Telegram confirmation edit. With bug 2's stricter editMenu
   that now propagates real failures, a 403/429/network error after the
   cancel succeeded would fall into the same catch and show
   "Something went wrong" — inviting the user to retry an
   already-cancelled intent or card. Both handlers are now structured
   as: mutation (try/catch → log + show error + return), then a
   separate fire-and-forget tryEditMenu for the confirmation. A
   Telegram failure on the success edit is logged but never surfaces a
   misleading error to the user. Renamed editMenuSafe → tryEditMenu
   for honesty (it does not throw; it does not "secure" anything).

2. getUserByChatId was awaited outside the outer recovery try/catch,
   so a Prisma failure on the user lookup bypassed the structured
   error log and the user-facing fallback. The whole dispatch — early
   shortcuts plus user lookup plus per-action handling — now lives
   inside one recovery boundary.

3. The unit tests asserted only the user-visible fallback string. The
   main contract of this PR is restored observability, so a regression
   to a bare catch could still pass. The tests now mock @/config/logger
   and assert log.warn / log.error were called with the expected
   payload and message for every new path: invalid TTL/policy/unknown
   action (warn), cancel-handler mutation failures (error), outer
   recovery (error), and the benign "message is not modified" case
   (no error log).

Plus two new tests pinning the review's headline concern: when
expireIntent / cancelCard succeed but the confirmation editMessageText
fails, the user must NOT see "Something went wrong" and the handler must
NOT log the operation as failed.

Also fixes the `npm run format:check` CI failure on the previous commit.

Tests: 43/43 menuHandler unit, 26/26 menuHandler integration, 375/375
total unit, 0 lint errors, prettier --check passes, dev server boots
cleanly under docker.
When users switch away from AFTER_TTL, we now null out cardTtlMinutes to keep the DB state consistent with API validation and avoid stale TTL values.
@georgyia
georgyia force-pushed the fix/issue-90-telegram-menu-observability branch from 0c22d2a to c8524c8 Compare May 13, 2026 18:18
@georgyia

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main and resolved the conflict in src/telegram/menuHandler.ts.

Conflict resolution kept both sides:

  • current main's provider-aware cancellation via getProviderForIntent(intentId), and
  • this PR's review fix that separates the card-cancel mutation from the Telegram confirmation edit, so a post-cancel Telegram edit failure doesn't look like the cancel itself failed.

Local verification:

  • npm ci
  • npx prisma generate
  • npx tsc --noEmit -p tsconfig.json
  • npm test -- tests/unit/telegram/menuHandler.test.ts tests/unit/telegram/callbackHandler.test.ts tests/unit/telegram/notificationService.test.ts (69/69 passed)
  • npx eslint src/telegram/menuHandler.ts tests/unit/telegram/menuHandler.test.ts (warnings only, pre-existing any usage in tests)

npm test -- --testPathPattern=telegram also ran; the unit suites passed, but DB-backed integration suites failed locally due the existing host Postgres / local DB access issue (postgres denied on agentpay.public), not due this rebase.

@georgyia
georgyia merged commit 4e0e788 into main May 13, 2026
7 checks passed
@georgyia
georgyia deleted the fix/issue-90-telegram-menu-observability branch May 13, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Telegram menu errors are silently swallowed — zero observability

2 participants