Skip to content

Real-Telegram test suite: 11 -> 32 tests, every command and button covered#272

Merged
Szer merged 12 commits into
mainfrom
feat/real-tests-expansion
Jul 26, 2026
Merged

Real-Telegram test suite: 11 -> 32 tests, every command and button covered#272
Szer merged 12 commits into
mainfrom
feat/real-tests-expansion

Conversation

@Szer

@Szer Szer commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Expands the real-Telegram (MTProto) suite from 11 to 32 tests, covering every user-facing
command and every inline button the bot builds — happy paths only, since the hermetic suite
(192 tests) owns the edge cases.

Real-run evidence: https://github.com/Szer/bots/actions/runs/30194699791Passed: 32, 9m56s.
Hermetic suite unchanged at 192/192. No production code changed.

Coverage added

Before this PR, the real suite drove 4 inline buttons. It now drives every callback the bot
emits, and every command including aliases.

New file Tests Covers
AddWizardRealTests 6 the whole interactive /add wizard — `addflow:ocr:yes
MyAndAddedRealTests 7 /added + void:<id>, myAdded, the bare used:<id>/return:<id> buttons, and text /used, /return, /void
ReportButtonFlowRealTests 4 /myreportreport:<id>report:<id>:confirm, reportCancel at both steps, reportedUsed:<id>
AdminAndFeedbackRealTests 4 /feedback end-to-end, admin /debug and /undo, and all six command aliases

Two gaps worth naming: /report previously had coverage only as a text command, while the button
chain — the path announced to the community — had none. The /add wizard, the most-used and most
stateful add path in production, had none at all.

Shared plumbing

  • RealTestHelpers.addCouponViaCaptionAsync — the idempotent add: clears the fixture's barcode,
    sends the photo, returns the new coupon id.
  • DbSeed gains deleteCouponsByBarcodeAsync, deleteCouponByIdAsync, releaseTakenCouponAsync,
    getCouponStatusAsync, getLatestOwnedCouponIdAsync, setCouponExpiryAsync,
    deletePendingAddFlowAsync, deletePendingBatchesAsync.
  • TgUserClient gains PressCallbackButtonMatching and AwaitAndPressButton; a missing button now
    reports which callback datas were actually present instead of failing opaquely.
  • tests/CouponHubBot.RealTests/README.md documents the skeleton, the helpers, and the idempotence
    rule below.
  • CI job timeout-minutes 30 → 50; 32 tests plus the ~7-minute deploy chain no longer fit in 30.

The one rule this suite lives by

Every coupon add must be idempotent. 32 tests share one database, one Telegram account, and one
bot, with no per-test cleanup and no guaranteed class order. Since #269 made manual /add extract
barcodes, adding a fixture whose barcode already exists is rejected with «Купон с таким штрихкодом
уже есть в базе…» rather than «Добавлен купон» — so a leftover row from any other test silently
breaks an unrelated one.

"The other test deletes first, so we're fine" is not a safety argument: if the deleting test runs
first, it leaves the row behind and the non-deleting test is the one that fails. Every add site now
clears its own barcode immediately before sending. This is stated at the top of the suite README.

Three CI dispatches were needed to get here (27/32 → 28/32 → 32/32). Every failure across both red
runs came from shared mutable state, not from bot bugs and not from latency:

  • non-idempotent adds in the pre-existing tests colliding with the new ones;
  • MAX_TAKEN_COUPONS=6 exhausted by tests that left coupons taken forever, after which /take
    replies with a text limit message that no photo-caption await can ever match;
  • ReminderRealTests inserting a coupon with a synthetic photo_file_id, which made /my fail its
    whole media-group send with wrong remote file identifier;
  • bulk-cancel confirming by editing its message, while the MTProto client deliberately drops edit
    updates — so the awaited text could never arrive;
  • Dapper 2.1.72 silently returning default(DateOnly) for a bare scalar DateOnly query.

Notes for future readers

  • addflow:ocr:yes needs a SQL bump of the staged expiry, permanently. Fixtures must stay
    expired coupons because this repo is public, so OCR always reads a past date, and TryAddCoupon
    rejects expires_at < today before any DB write. There is a doc comment at the site; it is not a
    temporary hack to be cleaned up.
  • BulkAddRealTests still overrides expires_at via direct SQL for the same reason, so it does not
    exercise the batch path's expiry validation.
  • The suite remains sensitive to real-Telegram latency. TestRetry retries once, on timeout-class
    failures only — never on assertion failures.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU

Szer and others added 12 commits July 26, 2026 01:16
…hors

Adds DB/Telegram/cross-cutting helpers to CouponHubBot.RealTests so four agents
can each author one expansion test file without touching a shared file:

- DbSeed.fs: deleteCouponsByBarcodeAsync, deletePendingAddFlowAsync,
  deletePendingBatchesAsync, getCouponStatusAsync, getLatestOwnedCouponIdAsync,
  setCouponExpiryAsync.
- TgUserClient.fs: PressCallbackButtonMatching (self-explanatory button-not-found
  errors) and AwaitAndPressButton, kept assertion-class vs timeout-class distinct
  so TestRetry's single-retry-on-AwaitTimeoutException semantics are preserved.
- RealTestHelpers.fs (new): fixtureBarcodes map (read off each OCR fixture's own
  filename) and addCouponViaCaptionAsync, the retry-safe/fixture-reusable /add
  helper that closes the coupon_barcode_active_uniq flake introduced by merge #269.
- Four compiling stub files (AddWizardRealTests, ReportButtonFlowRealTests,
  MyAndAddedRealTests, AdminAndFeedbackRealTests) registered in the .fsproj, each
  with one placeholder /help-round-trip [<Fact>] for the four authors to replace.
- README.md: authoring guide (skeleton, helper inventory, retry contract, fixture
  reuse rule, CI-dispatch-only rule).

No production code changes; existing 11 real tests left untouched (see report for
why the DB-helper substitution was judged more than mechanical and skipped).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU
…used /return /void

Adds 7 happy-path tests to MyAndAddedRealTests.fs covering the slice not
already exercised by CouponLifecycleRealTests.fs: /added listing + its bare
void:<id> button, the myAdded button (renders the same as /added), the bare
used:<id>/return:<id> buttons under /my (distinct from the :del-suffixed
take-confirmation-card buttons), and the text commands /used, /return, /void.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU
…gement

Adds ReportButtonFlowRealTests.fs (real-Telegram happy paths, this agent's
exclusive file): /my -> report -> report:<id> -> report:<id>:confirm marks a
coupon reported; reportCancel at both the select and confirm steps leaves the
coupon untouched; and the owner-side reportedUsed:<id> button marks a
'reported' coupon 'used'. Complements ReportFlowRealTests.fs's TEXT /report
coverage with the button-driven chain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU
Replaces AdminAndFeedbackRealTests.fs's placeholder with four real tests:

- /feedback happy path: arm the flag, send a uniquely-marked message, assert
  the thank-you reply, the admin forward (lands in the same DM since
  FEEDBACK_ADMINS is seeded with the test account's own id), and the DB row
  via a private user_feedback query (no DbSeed.fs helper exists for it).
- /debug <id>: add + take a coupon, assert its event-history dump contains
  both event types.
- /undo <id>: add + take a coupon, undo, assert the confirmation text and
  the DB rollback (taken -> available, taken_by cleared) via
  DbSeed.getCouponStatusAsync.
- Short aliases (/l, /coupons, bare /take, /m, /ad, /s, /a, /f) against
  CommandHandler.fs's Dispatch, with explicit cleanup of the two stateful
  aliases (/a's add-wizard row, /f's pending-feedback flag) in a finally
  block so this test can't corrupt a later test class in the same serial run.

No GitHub issue creation is exercised or asserted on (GITHUB_TOKEN is
deliberately absent from the CI test pod, gating GitHubService.IsConfigured
false). No other file touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU
AddWizardRealTests.fs: full wizard happy path (ocr:no -> disc:10:50 ->
date:today -> confirm, DB-verified), date:tomorrow confirm-screen check,
addflow:cancel, the other three discount buttons (cheap: confirm-screen
assert + cancel, no DB writes), and addflow:bulk:cancel for an album.
addflow:ocr:yes is left untested — unreachable as a happy path for these
(deliberately expired) fixtures, since TryAddCoupon rejects any
expires_at < today before ocr:yes's direct-add path can succeed
(DbService.fs:198-200); addflow:ocr:no explicitly clears the OCR-derived
expiry so every other route reaches a genuine, DB-verified happy path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU
…-dependent assertion, add ocr:yes coverage, raise CI timeout

Review fixes on top of the four merged real-test branches:
- AddWizardRealTests.fs: date:tomorrow no longer leaves a pending_add row for
  another test to sweep; rewritten to drive both date:today/date:tomorrow to
  addflow:confirm and assert the DB expiries are exactly one day apart,
  independent of the test process's own clock.
- AddWizardRealTests.fs: added the previously-untestable addflow:ocr:yes
  happy path, reachable by bumping the pending_add row's own expires_at via
  direct SQL (documented why this must never be "cleaned up" — fixtures must
  stay expired since the repo is public).
- AdminAndFeedbackRealTests.fs: alias test's pending-feedback cleanup no
  longer blocks on .GetAwaiter().GetResult() from inside a task body;
  replaced with a DB-level DELETE FROM pending_feedback + try/with re-raise
  so the flag is disarmed on every exit path without a blocking wait.
- .github/workflows/coupon-real-test.yml: timeout-minutes 30 -> 50 for the
  ~21 additional real tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU
Root-caused each from bot pod logs + CI test-runner logs, not guessed:

1. ReportFlowRealTests "report command..." — hand-rolled, non-deleting /add
   collided with a leftover VOIDED row for the same fixture barcode left by
   MyAndAddedRealTests (which ran first), because DbService.TryAddCoupon's
   primary duplicate-barcode check doesn't filter by status (production bug,
   reported separately, not fixed here). Switched to the idempotent
   RealTestHelpers.addCouponViaCaptionAsync helper.

2. AdminAndFeedbackRealTests "short aliases..." — its /m step crashed
   handleMy's sendMediaGroupPhotos on a coupon with a synthetic, non-Telegram
   photo_file_id that ReminderRealTests inserts directly via SQL and never
   cleaned up (unbarcode-tied, so deleteCouponsByBarcodeAsync could never
   find it). Confirmed via the two "wrong remote file identifier ... Wrong
   padding" bot-log errors landing exactly on this test's two TestRetry
   attempts. Fixed by deleting the coupon after ReminderRealTests' assertion.

3. AdminAndFeedbackRealTests "undo rewinds..." — its own /take hit
   DbService.TryTakeCoupon's LimitReached (MAX_TAKEN_COUPONS=6) because
   several earlier tests leave a coupon 'taken' with no further use and never
   release it. Added DbSeed.releaseTakenCouponAsync and called it after each
   such test's own assertions (CouponLifecycleRealTests x2,
   ReportButtonFlowRealTests x2, MyAndAddedRealTests x1), plus the
   ReminderRealTests cleanup from (2), eliminating the permanent leaks.

4. AddWizardRealTests "addflow:bulk:cancel..." — awaited a NEW Telegram
   message for the cancel confirmation, but BulkBatchCancel delivers it via
   EditBulkOrSend, which EDITS the existing message in place; TgUserClient's
   `record` explicitly drops all edits, so the await could never succeed by
   construction. Switched to the file's own waitForBatchCleared DB poll,
   matching BulkAddRealTests' sibling confirm test's existing pattern.

5. AddWizardRealTests "date:today and date:tomorrow..." — QuerySingleAsync
   against a bare DateOnly scalar silently returned default(DateOnly)
   (Dapper doesn't treat DateOnly as a scalar-mappable type). Mapped into
   CouponRow instead, matching the working pattern used elsewhere in the
   same file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU
…less of order

CI run 30193635437 (28/32) failed 4 tests via duplicate-barcode rejection because
AddFlowRealTests, CouponLifecycleRealTests's local addCoupon helper, and
BulkAddRealTests's album confirm all sent /add without deleting any pre-existing
row for their fixture's barcode first. The previous pass's "the later user always
deletes first" reasoning is wrong: test class order is not guaranteed, so whichever
add runs second collides. Route CouponLifecycleRealTests through the shared
RealTestHelpers.addCouponViaCaptionAsync helper, delete-by-barcode-first in
AddFlowRealTests (kept inline — it tests the manual-add path itself) and
BulkAddRealTests (all 3 album fixtures). Also close a taken-coupon leak the prior
MAX_TAKEN_COUPONS audit missed: AdminAndFeedbackRealTests's "debug shows..." test
took a coupon and never released it. Document the idempotence rule prominently in
README.md so a future test author can't reintroduce this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU
@Szer
Szer merged commit 1b315f3 into main Jul 26, 2026
5 checks passed
@Szer
Szer deleted the feat/real-tests-expansion branch July 26, 2026 09:37
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.

1 participant