Real-Telegram test suite: 11 -> 32 tests, every command and button covered#272
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/30194699791 —
Passed: 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.
AddWizardRealTests/addwizard — `addflow:ocr:yesMyAndAddedRealTests/added+void:<id>,myAdded, the bareused:<id>/return:<id>buttons, and text/used,/return,/voidReportButtonFlowRealTests/my→report→report:<id>→report:<id>:confirm,reportCancelat both steps,reportedUsed:<id>AdminAndFeedbackRealTests/feedbackend-to-end, admin/debugand/undo, and all six command aliasesTwo gaps worth naming:
/reportpreviously had coverage only as a text command, while the buttonchain — the path announced to the community — had none. The
/addwizard, the most-used and moststateful 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.
DbSeedgainsdeleteCouponsByBarcodeAsync,deleteCouponByIdAsync,releaseTakenCouponAsync,getCouponStatusAsync,getLatestOwnedCouponIdAsync,setCouponExpiryAsync,deletePendingAddFlowAsync,deletePendingBatchesAsync.TgUserClientgainsPressCallbackButtonMatchingandAwaitAndPressButton; a missing button nowreports which callback datas were actually present instead of failing opaquely.
tests/CouponHubBot.RealTests/README.mddocuments the skeleton, the helpers, and the idempotencerule below.
timeout-minutes30 → 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
/addextractbarcodes, 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:
MAX_TAKEN_COUPONS=6exhausted by tests that left couponstakenforever, after which/takereplies with a text limit message that no photo-caption await can ever match;
ReminderRealTestsinserting a coupon with a syntheticphoto_file_id, which made/myfail itswhole media-group send with
wrong remote file identifier;updates — so the awaited text could never arrive;
default(DateOnly)for a bare scalarDateOnlyquery.Notes for future readers
addflow:ocr:yesneeds a SQL bump of the staged expiry, permanently. Fixtures must stayexpired coupons because this repo is public, so OCR always reads a past date, and
TryAddCouponrejects
expires_at < todaybefore any DB write. There is a doc comment at the site; it is not atemporary hack to be cleaned up.
BulkAddRealTestsstill overridesexpires_atvia direct SQL for the same reason, so it does notexercise the batch path's expiry validation.
TestRetryretries once, on timeout-classfailures only — never on assertion failures.
🤖 Generated with Claude Code
https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU