Changelog
0.26.0 — 2026-06-16
First public release as grammy-testing
This is the first public npm release. The package is published under the third-party name grammy-testing (previously scaffolded as @grammyjs/testing), following the grammY third-party plugin convention (grammy-<name>). Install with npm install --save-dev grammy-testing and import from grammy-testing / grammy-testing/low-level. JSR publishing is deferred for now; the @grammyjs/testing name is reserved for a future official grammY release.
grammY 1.44 / Bot API 10.0 + 10.1 support
Bumps the grammy peer and dev dependency to ^1.44.0 (Bot API 10.1, @grammyjs/types@3.28.0) and adds first-class support for the new surfaces. All shapes were verified against the published types.
- New message-sending methods:
sendRichMessageandsendLivePhotonow return a syntheticMessageand are routed intochat.messages/user.replies, like other senders. - Rich message accessor:
reply.richMessageexposes the sentInputRichMessage(html/markdown/is_rtl/skip_entity_detection) with aplainTextconvenience. - Message drafts:
sendMessageDraft/sendRichMessageDraft(both returntrue) are captured into a drafts projection —user.draftsandchats.draftsFor(user)— for streaming-sequence assertions. - Guest mode:
user.sendGuestMessage(chat, text?)dispatches aguest_messageupdate and returns the generatedguest_query_id.answerGuestQueryresolves with a syntheticSentGuestMessage({ inline_message_id }) and is correlated to the originating user viachats.guestQueryUser(queryId); it is not routed intochat.messages. - Join-request queries:
user.requestJoin(group)now emits and returns thechat_join_request.query_id. - Reaction removal:
deleteMessageReaction/deleteAllMessageReactionsare captured intochats.reactionRemovals. - Managed-bot defaults: synthetic defaults for
getManagedBotAccessSettings,setManagedBotAccessSettings,getManagedBotToken,replaceManagedBotToken, andgetUserPersonalChatMessages;getChatAdministratorshonorsreturn_bots: false. - Examples & docs: new
24-guest-mode-bot,25-rich-message-bot, and26-reaction-removal-botexamples plus a "Bot API 10 Features" documentation page.
0.25.0 — 2026-05-07
Transformer infrastructure
TerminalTransformertype:createTransformerinsrc/low-level/transformer.tsnow returns an internalTerminalTransformertype whose signature omits_previous. A newasTransformeradapter inprepare-bot.tsconverts it forbot.api.config.use. The 4-line prose comment explaining why_previouswas never called is replaced by this compile-time invariant.respondNextRaw(method, response): New method onOutgoingRequests(accessible aschats.outgoing.respondNextRaw) that injects a verbatim raw response for the next matching API call — bypassing the{ ok: true, result }wrapper. Use it to simulate rate-limit responses ({ ok: false, error_code: 429, parameters: { retry_after: 0 } }) that outer transformers such as@grammyjs/auto-retrycan observe and act on.- Auto-retry retry-on-429 test:
tests/plugins/auto-retry.spec.tsnow includes a test that verifies autoRetry retries asendMessagecall whenrespondNextRawinjects a 429 raw response. TwosendMessageentries appear inchats.outgoing.requests(original + retry).
0.24.1 — 2026-05-07
Code quality
createTransformerterminal-intent comment:_previousinsrc/low-level/transformer.tsis now annotated with an inline comment explaining it is intentionally never called. This documents the invariant that the snapshot-and-reinstall pattern inprepareBotrelies on.- Plugin example context flavor types:
examples/21-files-botnow usesFileFlavor<Context>andexamples/22-hydrate-botusesHydrateFlavor<Context>, replacingas unknown ascasts with proper plugin-exported flavor types. - Test helper cleanup: Empty
/** */JSDoc blocks removed from the four private helper functions intests/plugins/chat-members.spec.ts. - ESLint examples alignment:
examples/**/*.tsJSDoc rules are no longer silenced — examples are now held to the same JSDoc standards assrc/.
0.24.0 — 2026-05-07
Plugin interop: grammy-media-groups
mediaGroupTransformer(adapter) installed via bot.api.config.use() before prepareBot now runs correctly during tests and stores outgoing media group messages in the adapter.
syntheticMediaGroupresponse shape: every message in the defaultsendMediaGroupresponse now includes achatfield (required bystoreMessagesfor deduplication bychat.id) and amedia_group_idstring shared across all messages in the same call (required for adapter grouping). This is a minor additive change — bots readingsendMediaGroupreturn values in tests will see the additional fields.grammy-media-groupsadded to plugin interop table: installmediaGroupTransformer(adapter)beforeprepareBot, assert onadapter.read(media_group_id).
See tests/plugins/media-groups.spec.ts and site/plugins/media-groups.md.
0.23.0 — 2026-05-07
Plugin transformer support
Bot-level transformers installed via bot.api.config.use() are now correctly chained during tests. Previously the library's mock transformer was installed last (outermost), silently skipping all user-installed transformers. With this fix, the library transformer is always innermost — every transformer you install runs normally and can process synthetic responses.
- Transformer chain fix:
prepareBotnow snapshots existing transformers, installs the library transformer first, then reinstalls user transformers on top. Uses only the publicinstalledTransformers()/use()API — no private access. - Realistic
getFiledefault:buildDefaultResponsesnow returns a validFileshape forgetFile(file_id,file_unique_id,file_size,file_path) instead oftrue, so@grammyjs/filescan hydrate it without a customresponsesoverride. - Synthetic message includes
chat: DefaultsendMessage/sendPhoto/ etc. responses now include achatfield so@grammyjs/hydratecan attachdelete()/edit()methods.
New plugin interop: @grammyjs/files
hydrateFiles(bot.token) installed before prepareBot runs correctly. ctx.getFile() returns a hydrated File with getUrl() and download() methods.
See tests/plugins/files.spec.ts and examples/21-files-bot/.
New plugin interop: @grammyjs/hydrate
hydrateApi() (bot-level transformer) and hydrate() (context middleware) both work. Bot API call results include delete() / edit() / pin() convenience methods. Context objects get ctx.message.delete() and similar shortcuts.
See tests/plugins/hydrate.spec.ts and examples/22-hydrate-bot/.
New plugin interop: @grammyjs/auto-retry
autoRetry(options) installed before prepareBot is now part of the transformer chain for every API call. Normal bot operation is unaffected. failNext errors propagate through autoRetry to the handler catch block (autoRetry does not retry thrown GrammyErrors).
See tests/plugins/auto-retry.spec.ts and examples/23-auto-retry-bot/.
New plugin interop: @grammyjs/chat-members — hydrateChatMember()
hydrateChatMember() API transformer is now supported. Install via bot.api.config.use(hydrateChatMember()) before prepareBot. getChatMember and getChatAdministrators results are augmented with an .is(query) method at test time, matching production behaviour.
See tests/plugins/chat-members.spec.ts (new hydrateChatMember() describe block).
Custom transformer chain support
Request-mutating and response-augmenting transformers installed via bot.api.config.use() before prepareBot now run normally. Payload mutations are visible in chats.outgoing.requests; response augmentations are visible to handlers.
See tests/plugins/custom-transformer.spec.ts.
VitePress: new Plugins section
A dedicated Plugins sidebar group replaces the mixed plugin/recipe content in Recipes:
conversationsandmenupages moved fromsite/recipes/tosite/plugins/- Five new pages: Chat Members, Files, Hydrate, Auto-Retry, Transformer Throttler
- Recipes retains only general-pattern pages: Sessions, Keyboards, Error Simulation, Multi-Chat Scenarios, Fire & Forget
0.22.0 — 2026-05-05
VitePress documentation site
Standalone documentation site under site/ — 42 pages covering every public export.
- Infrastructure:
site/.vitepress/config.tswith dynamic GitHub Pages base URL, full nav/sidebar, local search, social links, and version badge frompackage.json. - Logo:
docs/Y.svg(the grammY Y mark) copied tosite/public/logo.svg. - Brand theme: blue palette —
#0057b7in light mode (6.9:1 on white, WCAG AA) /#4d9effin dark mode (6.3:1 on dark background, WCAG AA). - Guide section (6 pages): Introduction, Getting Started, How It Works, With Vitest, With Jest, With Deno.
- High-Level API section (9 pages): Chats, User, Group & Supergroup, Channel, PrivateChat, BusinessAccount, Reply, Logs, Overview.
- Low-Level API section (5 pages): Outgoing Requests, Session Mocking, Update Builders, Response Mocking, Overview.
- Recipes section (7 pages): Sessions & State, Keyboards & Buttons, Error Simulation, Multi-Chat Scenarios, Conversations Plugin, Menu Plugin, Fire & Forget.
- API Reference section (14 pages): full typed signatures for every exported symbol.
- Reference: Changelog page linked to
docs/CHANGELOG.md. - GitHub Actions workflow (
.github/workflows/docs.yml): builds and deploys to GitHub Pages athttps://drsmile444.github.io/grammy-testing/on every push tomain.
0.21.0 — 2026-05-05
README rewrite
README.md has been completely replaced. The previous file was a TypeScript Boilerplate placeholder with no relation to the library. The new README is production-grade and client-facing:
- Logo (
docs/Y.svg— the grammY Y mark) with yellow#ffd700brand color across all badges - Why section with bold hook sentence and problem/solution statement
- Quick Start with npm + JSR install commands and a
/startcommand bot example - Features section covering high-level actors, dispatch verbs, assertions, session injection, and isolation utilities
- Examples table linking all 20 examples in
examples/ - Documentation placeholder pointing to the planned VitePress site
- Credits section acknowledging
grammy_testsandua-anti-spam-bot
0.20.0 — 2026-05-03
20 runnable examples added under examples/
A new examples/ directory ships 20 self-contained bots with matching test files, covering a wide range of testing patterns:
| # | Scenario |
|---|---|
| 01 | Echo bot — simplest possible text-echo handler |
| 02 | Start command — /start reply |
| 03 | Greeting bot — per-user name with fallback |
| 04 | Chat-type filter — private vs. group routing |
| 05 | Regex handler — pattern-matched messages |
| 06 | Callback query — inline keyboard responses |
| 07 | Session counter — persistent per-user state |
| 08 | Chat settings — mockChatSession usage |
| 09 | Photo bot — caption extraction |
| 10 | Document bot — file-ID and MIME type reply |
| 11 | Poll bot — quiz creation and answer scoring |
| 12 | Group welcome — new_chat_members service event |
| 13 | Admin guard — getChatMember status check |
| 14 | Moderation bot — banChatMember / restrictChatMember |
| 15 | Channel post bot — channel_post handler |
| 16 | Reactions bot — message_reaction handler |
| 17 | Dice game — incoming dice value evaluation |
| 18 | Middleware test — prepareMiddleware isolation |
| 19 | Composer test — prepareComposer isolation |
| 20 | Multi-chat scenario — cross-chat summary posting |
All examples are included in the test run and pass the full quality gate.
0.19.0 — 2026-05-03
user.sendCallbackQuery and clickButton reply_markup fix
user.sendCallbackQuery(data, options?) is now available. Dispatches a callback_query update from the user without requiring a prior captured reply. Useful for cross-feature tests where the keyboard lives in a different composer.
await user.sendCallbackQuery('button-data');
// With explicit message context (for chatType filters or handlers that read keyboard state):
const msg = await user.sendCommand('/menu');
await user.sendCallbackQuery('language:en', { message: msg });When options.message is omitted, a minimal private-chat stub is synthesized automatically so grammY filters like chatType('private') evaluate correctly without boilerplate.
clickButton now populates callback_query.message.reply_markup. Reply.toCapturedMessage() previously omitted reply_markup, leaving ctx.callbackQuery.message.reply_markup as undefined in the handler. Handlers that read keyboard state (e.g. ctx.callbackQuery.message.reply_markup.inline_keyboard) now receive the full keyboard.
0.18.0 — 2026-05-03
Internal fixes: update_id counter independence, sendMediaGroup response shape, GROUP_ANONYMOUS_BOT documentation
update_id counter is now independent from message IDs. All User dispatch methods (sendPhoto, sendDocument, sendVideo, sendAudio, sendVoice, sendVideoNote, sendAnimation, sendSticker, sendLocation, sendContact, sendVenue, sendPoll, sendDice, sendWebAppData, sendSuccessfulPayment, sendInlineQuery, sendChosenInlineResult, sendPreCheckoutQuery, sendShippingQuery, reactTo, answerPoll, requestJoin, boostChat, removeBoost, manageBot, purchasePaidMedia, sendMediaGroup) and Reply.clickButton now draw update_id from IdGenerator.nextUpdateId() rather than nextMessageId() + offset. Message IDs and update IDs no longer share a counter.
sendMediaGroup auto-response returns N messages. The default buildDefaultResponses resolver for sendMediaGroup now returns an array whose length matches the number of items in the bot's media payload. Previously it always returned a single-element array regardless of how many media items were sent.
GROUP_ANONYMOUS_BOT.is_bot: false is intentional. An inline comment documents that Telegram sends is_bot: false for this identity in real update payloads, consistent with Channel_Bot (id: 136 817 688).
0.17.0 — 2026-05-03
channel.postMessageTo returns Message and accepts reply_to_message
channel.postMessageTo(target, text, options?) now returns Promise<Message> (previously Promise<void>), consistent with all other send verbs since v0.15.0. The returned value can be used immediately as reply_to_message in a follow-up user.sendText:
const post = await channel.postMessageTo(group, 'announcement');
await user.sendText('nice post', { chat: group, reply_to_message: post });A new reply_to_message option is also accepted. It follows the same partial-shape semantics as user.sendText: date and chat are auto-filled when absent, and all explicitly supplied fields are preserved:
await channel.postMessageTo(group, 'reply', {
reply_to_message: { message_id: 10 },
// date and chat are auto-filled from context
});0.16.0 — 2026-05-03
Relay message support (group.postRelayMessage, TELEGRAM_RELAY)
Groups and supergroups now have a dedicated postRelayMessage verb that dispatches the synthetic message update Telegram produces when a channel post is forwarded into a linked group (from.id === 777_000). The returned Message can be passed directly as reply_to_message in a follow-up user.sendText:
const relay = await group.postRelayMessage('channel post');
await user.sendText('my comment', { chat: group, reply_to_message: relay });To simulate a relayed post with channel attribution, pass options.channel:
const channel = chats.newChannel('My Channel');
const relay = await group.postRelayMessage('post text', { channel });
// ctx.message.forward_origin.type === 'channel'A TELEGRAM_RELAY constant is exported for assertions:
import { TELEGRAM_RELAY } from 'grammy-testing';
expect(ctx.message.from).toMatchObject(TELEGRAM_RELAY);Partial reply_to_message in SendTextOptions
SendTextOptions.reply_to_message now accepts Partial<Message> & { message_id: number } — only message_id is required. date and chat are auto-filled when absent:
// No more `as any` casts or manual date/chat construction
await user.sendText('reply', { chat: group, reply_to_message: { message_id: 42 } });Callers that already pass a full Message are unaffected.
0.15.0 — 2026-05-03
Actor send verbs return the dispatched Message
All User send verbs that produce a message update now return Promise<Message> instead of Promise<void>. The returned object is the exact synthetic Message dispatched to bot.handleUpdate, giving tests direct access to message_id, chat, from, and content fields without magic numbers or private state access:
const msg = await user.sendText('not a card');
await user.editMessage(msg.message_id, '4111 1111 1111 1111');
// or chain with reply_to_message:
await user.sendText('reply', { chat: group, reply_to_message: msg });user.sendMediaGroup(items) returns Promise<Message[]> — one Message per dispatched item in order, all sharing the same media_group_id:
const [first, second] = await user.sendMediaGroup([{ photo: 'a' }, { photo: 'b' }]);
expect(first.media_group_id).toBe(second.media_group_id);Existing callers that ignore the return value are unaffected — the change is fully backward-compatible.
Affected verbs: sendText, sendMessage, sendCommand, sendForwarded, sendPhoto, sendDocument, sendVideo, sendAudio, sendVoice, sendVideoNote, sendAnimation, sendSticker, sendLocation, sendContact, sendVenue, sendPoll, sendDice, sendWebAppData, sendSuccessfulPayment, sendMediaGroup.
0.14.0 — 2026-05-03
ChatProfile — chat factory methods accept a caller-supplied ID
chats.newGroup, chats.newSupergroup, and chats.newChannel now accept an optional object profile { id?, title? } in addition to the existing string/undefined forms, mirroring the UserProfile pattern used by chats.newUser. Any integer ID is accepted without validation — use this to register chats whose IDs are fixed at configuration time (log channels, training chats, etc.):
const logsGroup = chats.newSupergroup({ id: 1_234_567, title: 'Logs' });
// title defaults to 'Supergroup1234567' when omitted
const alerts = chats.newChannel({ id: -500, title: 'Alerts' });getChat and getChatAdministrators auto-derivation work normally for specific-ID chats, eliminating the need for respondNext('getChat', ...) workarounds.
anonymous option — sendText / sendCommand dispatch as GroupAnonymousBot
SendTextOptions gains anonymous?: boolean. When true, the dispatched message's from is replaced with the GroupAnonymousBot identity and sender_chat is set to the target group, matching Telegram's wire format for the "Send as Group" admin feature:
await user.sendText('/role user', { chat: group, anonymous: true });
await user.sendCommand('/role', 'user', { chat: group, anonymous: true });Requires options.chat to be a Group or Supergroup; throws a descriptive error otherwise.
GROUP_ANONYMOUS_BOT — exported constant for assertions
import { GROUP_ANONYMOUS_BOT } from 'grammy-testing';
// { id: 1_087_968_824, username: 'GroupAnonymousBot', is_bot: false, first_name: 'Group' }
expect(ctx.message.from).toMatchObject(GROUP_ANONYMOUS_BOT);sendSystemMessage — dispatch a from-absent message update
Group, Supergroup, and Channel each gain sendSystemMessage(text, options?) which dispatches a message update with the from field intentionally absent. Tests the common if (!ctx.from) return next() guard path without raw handleUpdate calls:
await group.sendSystemMessage('no sender text');
await channel.sendSystemMessage('notice', { messageId: 42 });0.13.0 — 2026-05-02
Channel.changeMemberStatus — dispatch my_chat_member for channels
- Added
channel.changeMemberStatus(fromUser, transition)toChannel. Dispatches amy_chat_memberupdate withchat.type === 'channel', updates the bot's membership inchannel.members, and enablesgetChatAdministratorsauto-derivation for channels. This closes the last remaining rawhandleUpdategap — allmy_chat_memberscenarios acrossGroup,Supergroup, andChannelnow use the same actor-verb API. - Added
CHANNEL_ADMIN_RIGHTSconstant providing channel-appropriate defaults (can_post_messages: true; excludescan_manage_video_chatsandcan_manage_topics). Permissions supplied in the transition override the defaults.
Fix: changeMemberStatus now correctly tracks the bot's membership
Breaking (minor): group.changeMemberStatus(user, transition) and supergroup.changeMemberStatus(user, transition) previously stored the trigger actor (user) in the chat's members map and used that same user for old/new_chat_member.user in the dispatched update. Both were wrong — my_chat_member always describes the bot's status change, and from is the actor who triggered it.
After this fix:
old/new_chat_member.userin the dispatched update isbot.botInfo(the bot), not the trigger user.- The bot's membership is stored in the members map, keyed by
bot.botInfo.id. getChatAdministratorsauto-derivation now returns the bot after a promotion transition, not the trigger actor.- The trigger user's own membership entry is not affected by
changeMemberStatus.
Tests that called user.in(group) after changeMemberStatus to verify the new status should switch to group.members.get(bot.botInfo.id)?.status.
0.12.0 — 2026-05-01
group.own() and group.join() — pure-state membership setters
- Added
group.own(user)andsupergroup.own(user)— setsstatus: 'creator'in the members map with no Telegram update dispatched. Mirrors the existingpromote()/restrict()pattern. - Added
group.join(user)andsupergroup.join(user)— setsstatus: 'member'in the members map with no Telegram update dispatched. - Added
chats.newOwner(profile?)— convenience factory that creates a new user and callsdefaultGroup.own(user), mirroringchats.newAdmin().
Auto-derived getChatMember, getChatAdministrators, getChat
getChatMembernow resolves from the registered chat's members map via aMembership → ChatMemberconverter. Returns the appropriate discriminated union shape ('creator','administrator','member','restricted','left','kicked'). Falls back to{ status: 'left', user }for users not in the map, and totruefor unregistered chats.getChatAdministratorsnow resolves by filteringchat.membersfor'creator'and'administrator'entries. Returns[]for unregistered chats.getChatnow resolves fromchat.toTelegramChat()enriched withinvite_link: ''. Returnstruefor unregistered chats.- All three are populated automatically in
buildDefaultResponses(). User-suppliedresponsesentries always take precedence — existing overrides are unaffected.
0.11.0 — 2026-05-01
chats.clear() — single-call state reset
- Added
chats.clear()method that atomically resets all captured state:outgoingrequests, per-userreplies,actions, andeditslogs, per-chatmessagesanddeletionslogs, and internal routing registries (messageIdToReply,clickers). User/chat registries and membership state are preserved, so existinguserandgroupreferences remain valid. Replaces the previous 4–5 individualclear()calls required inbeforeEachblocks.
warnOnUnregisteredChats — developer warning for silent misses
- Bot calls to
sendMessage,sendPhoto, and other message-sending methods,sendChatAction, anddeleteMessagetargeting a chat ID not registered with theChatsorchestrator now emit aconsole.warnby default. The warning includes the method name, the unregistered chat ID, and guidance on how to register the chat or suppress the warning. - Pass
{ warnOnUnregisteredChats: false }toprepareBot,prepareComposer, orprepareMiddlewareto suppress the warning (useful for bots that intentionally fan out to external log channels).
Fix: postinstall script no longer breaks consumer installs
- Removed the
postinstallentry frompackage.json. The./scripts/link-codex-skills.shhook is a local development convenience and is not present in the published package. Consumers no longer neednpm install --ignore-scriptsto work around the missing script error.
0.10.0 — 2026-05-01
Deletion tracking
- Added
DeletionsLogper-chat log ofdeleteMessagecalls, accessible viachats.deletionsFor(chat) - Each deletion entry carries the synthetic
message_idand a back-reference to the originalReplyobject (if the message was sent during the test) - Exported new
Deletiontype
copyMessage and forwardMessage fixes
copyMessagenow returns a syntheticMessageId({ message_id }) instead oftrueforwardMessagenow returns a syntheticMessage({ message_id, date }) instead oftrue- Both methods are now tracked by the
Chatspipeline and produceReplyobjects that appear inchat.messagesanduser.replies
0.9.0 — 2026-05-01
Synthetic Message responses
- All message-sending methods (
sendMessage,sendPhoto,sendDocument,sendVideo,sendAudio,sendVoice,sendVideoNote,sendAnimation,sendSticker,sendLocation,sendContact,sendVenue,sendPoll,sendDice,sendMediaGroup) now return a realMessage(orMessage[]forsendMediaGroup) by default, using the syntheticmessage_idalready assigned to the capturedReply - User-supplied
responsesentries continue to override the defaults
State injection
PrepareOptionsgains an optionalstatefield- When provided, a
mockStatemiddleware is automatically inserted before the bot/composer under test soctx.stateis pre-populated for every dispatched update - Compatible with
prepareBot,prepareComposer, andprepareMiddleware
0.8.0 — 2026-05-01
User DX enhancements
- Added
user.repliesgetter returning the user'sRepliesInboxdirectly — no morechats.repliesFor(user)at every assertion site - Added
RepliesInbox.lastOrThrow()returningReply<TContext>(non-nullable), throwing with a descriptive message when the inbox is empty - Added
chats.actionsFor(user)returning anActionsLogthat capturessendChatActionpayloads for that user - Added
chats.editsFor(user)returning anEditsLogthat captureseditMessageText,editMessageCaption, andeditMessageMediacalls resolved to that user
ID and counter fixes
- Fixed
joinChat/leaveChatusing hardcoded constants forupdate_id; all user actor dispatches now usenextUpdateId() - Fixed
sendText,sendForwarded, andeditMessagederivingupdateIdfromnextMessageId()instead ofnextUpdateId() - Implemented
IdGenerator-scoped message IDs; removed all module-level counters to eliminate counter bleed between test runs in the same process
JSDoc coverage
- Added JSDoc to all public and non-trivial internal methods and constructors across
src/ - Enabled
jsdoc/require-jsdocfor class methods and constructors in ESLint config
CI improvements
- Dropped Node 18 from the test matrix; raised minimum engine to
>=20.0.0 - Fixed
verify-cjs.cjsrequire paths to resolve from project root - Fixed npm/corepack CI workflow issues; switched to direct
npm install
0.7.2 — 2026-04-30
Fixes and internal improvements
- Fixed
recordClicknot includingchat_idin the callback routing record, causing button clicks in one chat to route replies globally - Added
nextUpdateId()method toIdGenerator - Refactored
OutgoingRequests.requestsfrom a public mutable field to a read-only getter backed by a private array - Extracted CJS verification to
scripts/verify-cjs.cjs; lowered Node.js engine requirement to>=18.0.0 - Raised
OutgoingRequests.getAll()typed overloads from 6 to 10 type parameters
0.7.1 — 2026-04-30
Type safety improvements
- Replaced hand-copied
ParseModeunion with a re-export from grammy, keeping it in sync with upstream automatically - Converted
MediaTypeunion to a derived(typeof MEDIA_FIELDS)[number]type so adding a new media field is a compile-time error if the union is not updated - Added exhaustiveness guard to
makeChatMemberswitch so new grammyChatMemberStatusvariants produce a TypeScript error rather than silently falling through to'kicked'
ESLint compliance
- Removed
Plugin source overridesandTest overridesblocks fromeslint.config.mjs; all violations insrc/andtests/have been fixed instead - Zero ESLint overrides in source and test code
0.7.0 — 2026-04-30
Business account API
- Added
BusinessAccounthigh-level actor with verbs for all Telegram Business API update types:connect(options?)→business_connection(is_enabled: true)disconnect(options?)→business_connection(is_enabled: false)sendMessage(text, options?)→business_messageeditMessage(messageId, newText, options?)→edited_business_messagedeleteMessages(messageIds, options?)→deleted_business_messages
- Added
user.manageBot(botUser, options?)→managed_bot
Previously excluded update types
- Added
user.purchasePaidMedia(payload, options?)→purchased_paid_media - Added
chat.dispatchReactionCount(messageId, reactions, options?)onGroup,Supergroup, andChannel→message_reaction_count - Added
chats.dispatchPollState(poll, options?)→poll - Removed all newly-covered types from the "Not covered" section in README
0.6.0 — 2026-04-30
Modern update types
- Added
user.reactTo(reply, reaction)→message_reaction - Added
user.answerPoll(reply, optionIndices)→poll_answer - Added
user.requestJoin(group)→chat_join_request - Added
group.dispatchMemberUpdate(adminUser, targetUser, newStatus, options?)onGroupandSupergroup→chat_member - Added
channel.editPost(messageId, newText, options?)→edited_channel_post - Added
user.boostChat(chat)→chat_boost - Added
user.removeBoost(chat, boostId)→removed_chat_boost
0.5.1 — 2026-04-29
Reply accessors
- Added
reply.replyingTo— the earlierReplyobject that this message is replying to - Added
reply.replyMarkup— rawreply_markupescape hatch for inspecting non-inline-keyboard markup
Private chat message log
PrivateChatnow exposes amessageslog consistent withGroupandSupergroup, capturing every bot message sent to a private DM
Context constructor option
prepareComposerandprepareMiddlewarenow accept aContextConstructoroption inPrepareOptions, enabling bots with class-based custom context types to instantiate the correct runtime class
0.4.0 — 2026-04-29
Special message verbs
- Added
user.sendInlineQuery(query, options?)→inline_query - Added
user.chooseInlineResult(resultId, options?)→chosen_inline_result - Added
user.sendWebAppData(data, buttonText, options?)→web_app_data - Added
user.completePurchase(options?)→successful_payment - Added
user.sendPreCheckoutQuery(options?)→pre_checkout_query - Added
user.sendShippingQuery(options?)→shipping_query
0.3.0 — 2026-04-28
Remaining dispatch verbs
- Added
user.sendAudio(options?)→audiomessage - Added
user.sendVoice(options?)→voicemessage - Added
user.sendVideoNote(options?)→video_notemessage - Added
user.sendAnimation(options?)→animationmessage - Added
user.sendSticker(options?)→stickermessage - Added
user.sendLocation(options?)→locationmessage - Added
user.sendContact(options?)→contactmessage - Added
user.sendVenue(options?)→venuemessage - Added
user.sendPoll(question, options?, options2?)→pollmessage - Added
user.sendDice(options?)→dicemessage
0.2.0 — 2026-04-28
Media send verbs
- Added
user.sendPhoto(options?)→ singlephotomessage - Added
user.sendDocument(options?)→documentmessage - Added
user.sendVideo(options?)→videomessage - Added
user.sendMediaGroup(items)→ dispatches N updates sharing amedia_group_id, each with realisticfile_idfields
0.1.1 — 2026-04-28
Plugin interop
- Added
tests/plugins/reference suite demonstratinggrammy-testingusage alongside:@grammyjs/conversations— multi-step conversation flows@grammyjs/menu— inline menu navigation and callback routing@grammyjs/parse-mode—ctx.replyWithHTML()/ctx.replyFmt()andparseModeassertions@grammyjs/hydrate— hydrated message objects@grammyjs/chat-members— member status tracking
0.1.0 — 2026-04-27
Low-level testing primitives
- Added
prepareBot(bot, options?)entry point — sets up an in-process grammY bot with a captured transformer and returns{ chats, bot } - Added
prepareComposer(composer, options?)andprepareMiddleware(middleware, options?)entry points for testing composers and middleware in isolation - Added error simulation via
options.responses— supply per-method canned responses or a function to produce them - Added
OutgoingRequestscapture surface (outgoing.requests,outgoing.getAll(),outgoing.getLast(),outgoing.idle()) - Added low-level update builders:
buildTextMessage,buildCallbackQuery,buildInlineQuery,buildMyChatMember, and others
High-level Chats/User/Admin API
- Added
Chats<TContext>orchestrator withnewUser(profile?),newGroup(name?),newSupergroup(name?),newChannel(name?),newPrivateChat(user)factory methods - Added
User<TContext>actor as the primary test subject driver - Added
Adminrole viagroup.promote(user, perms?)andgroup.restrict(user, perms?) - Added
Reply<TContext>normalized object withtext,parseMode,entities,buttons,replyMarkup,chat,replyingTo,raw, andclickButton(textOrCallbackData)for synthesizing callback queries - Added
user.repliesandchat.messagesinbox/log as the primary assertion surfaces - Added
chats.outgoingfor raw outgoing request inspection
User dispatch verbs
- Added
user.sendText(text, options?)anduser.sendMessage(text, options?)(alias) - Added
user.sendCommand(command, args?, options?)with auto-emittedbot_commandentity and optionalchattarget for group commands - Added
user.sendForwarded(reply, options?)anduser.editMessage(reply, newText, options?) - Added
user.joinChat(chat, options?)anduser.leaveChat(chat, options?)service-message verbs - Added
channel.postMessageTo(group, text, options?)for channel-as-author (sender_chat) scenarios
Build and CI
- Configured dual ESM + CJS exports with subpath export map (
./,./low-level,./high-level) - Added GitHub Actions CI matrix covering Node 20 and Node 22, CJS verification, and Bun
- Added
jsr.jsonscaffold for future Deno/JSR publishing