Documentation
¶
Overview ¶
Package platform defines the neutral contracts that let a scenario be written once and executed against any chat platform.
A scenario drives Chatwright with platform-agnostic verbs (send a text, expect a message, expect an action). Each Platform maps those neutral operations onto a concrete wire protocol (Telegram, WhatsApp, ...) and normalizes the bot's outbound calls back into the neutral Message/Action types below.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Action ¶
type Action struct {
Label string `json:"label"` // user-visible text (Telegram button text / WhatsApp reply title)
ID string `json:"id"` // stable identifier (Telegram callback_data / WhatsApp reply id)
URL string `json:"url"` // set for link actions
CopyText string `json:"copyText,omitempty"` // text copied by a platform-native copy action
InlineQuery string `json:"inlineQuery,omitempty"` // query prefilled by a platform-native inline-share action
OpensInlineQuery bool `json:"opensInlineQuery,omitempty"` // true for a platform-native action that opens inline mode
}
Action is a neutral interactive action (a button) captured from a bot message. Telegram inline buttons and WhatsApp interactive replies both normalize to it.
type AddrPlatform ¶
type AddrPlatform interface {
Platform
// StartAt boots this platform's emulated API server bound to addr (e.g.
// "127.0.0.1:54321"). It returns an error if addr cannot be bound (e.g.
// already in use) instead of panicking, so the caller can surface it
// through the test.
StartAt(addr string) (Emulator, error)
}
AddrPlatform is optionally implemented by a Platform whose emulated API server can bind a caller-chosen local address instead of a random port. It exists for a bot-under-test started as a separate process — in any language, since Chatwright only speaks HTTP — that must be configured with the emulator's API base URL in its environment before that process starts, which means the address has to be decided up front rather than read back from the emulator afterwards. cw.WithListenAddr uses this interface; New fails the test if a listen address is configured for a platform that doesn't implement it.
type ChosenInlineResultEmulator ¶ added in v0.6.0
type ChosenInlineResultEmulator interface {
SelectInlineQueryResult(user User, queryID, resultID string) (inlineMessageID string, err error)
WaitForInlineResultEdit(
inlineMessageID string,
afterVersion int,
timeout time.Duration,
) (*InlineQueryResult, int, bool)
}
ChosenInlineResultEmulator is the optional follow-up capability for selecting an answered inline result and observing later inline-message edits. Telegram supplies an opaque inline_message_id, never a destination chat ID.
type Direction ¶
type Direction string
Direction identifies which side of a conversation produced a JournalEntry. It is a string type, not an int enum, so a JournalEntry marshals to human-readable JSON (see AGENTS.md's "JSON artefacts carry human-readable string constants" convention) rather than a bare, meaningless integer.
type Emulator ¶
type Emulator interface {
// BotAPIURL is the base URL the bot-under-test must use for this platform's
// API, in place of the real one (api.telegram.org, graph.facebook.com, ...).
BotAPIURL() string
// Close shuts the emulator's HTTP server down.
Close()
// SetWebhook registers the URL (and the HTTP client used to reach it) the
// emulator pushes updates to, as the bot-under-test's webhook. Passing an
// empty url clears it. On platforms that support polling (Telegram),
// Submit* calls queue updates for getUpdates instead of failing while no
// webhook is set; platforms that don't (WhatsApp) return an error from
// Submit* until one is set — mirroring how a real bot token is either
// webhook- or polling-driven, never both, and WhatsApp's Cloud API has no
// polling mode at all.
SetWebhook(url string, client *http.Client)
// SubmitText delivers a user's text message from user in chatID to the
// bot-under-test: it reserves the message's ID from chatID's shared
// per-chat sequence, journals the inbound event, builds the
// platform-native update, and delivers it via the configured strategy.
// The returned error reports only a delivery failure (e.g. the webhook
// responded with an error status, or no delivery strategy is available);
// it is not a scenario assertion.
SubmitText(chatID int64, user User, text string) error
// SubmitClick delivers a user activating an interactive action (button
// click) with the given callback data on the bot message identified by
// targetMessageID. A callback query does not occupy its own slot in the
// per-chat message-ID sequence.
SubmitClick(chatID int64, user User, data string, targetMessageID int) error
// WaitForMessage waits up to timeout for the (consumed+1)-th outbound bot
// message to chatID, returning it normalized, or false on timeout.
WaitForMessage(chatID int64, consumed int, timeout time.Duration) (*Message, bool)
// WaitForEdit waits up to timeout for the message identified by
// (chatID, messageID) to be edited past afterVersion (i.e. Version >
// afterVersion), returning its new content normalized, or false on timeout.
// Platforms with no message-edit capability may return false immediately.
WaitForEdit(chatID int64, messageID int, afterVersion int, timeout time.Duration) (*Message, bool)
// Transcript renders a chronological, human-readable dump of everything
// recorded for chatID — inbound and outbound alike, edits shown as their
// current version — for inclusion in assertion failure messages.
Transcript(chatID int64) string
// Journal returns chatID's chronological, structured journal entries —
// the same events Transcript renders as prose, given directly to callers
// (the observe package's Engine, diagnostics) that need to reason about
// them structurally. An implementation that cannot produce a structured
// journal returns a clear not-supported error and a nil slice.
Journal(chatID int64) ([]JournalEntry, error)
}
Emulator is a running fake platform API server. It owns everything about getting a user action to the bot-under-test and capturing what comes back: building the platform-native update, assigning it identity (message and update IDs) from its own state, delivering it (webhook push, or queuing it for the bot to retrieve via getUpdates, on platforms that support polling), and recording every inbound and outbound event in an append-only per-chat journal. Current message state is derived from the journal; nothing is mutated in place, so edit history survives and a transcript can always be reconstructed. The harness (Chatwright) never builds wire bytes or POSTs them itself — it only submits neutral actions and waits for results.
type InlineQueryAnswer ¶ added in v0.6.0
type InlineQueryAnswer struct {
QueryID string `json:"queryId"`
Results []InlineQueryResult `json:"results"`
CacheTime int `json:"cacheTime,omitempty"`
IsPersonal bool `json:"isPersonal,omitempty"`
NextOffset string `json:"nextOffset,omitempty"`
}
InlineQueryAnswer is the normalized observable value of answerInlineQuery.
type InlineQueryEmulator ¶ added in v0.6.0
type InlineQueryEmulator interface {
// SubmitInlineQuery delivers an inline query and returns its platform query
// identifier. The result is observed separately via WaitForInlineQueryAnswer.
SubmitInlineQuery(user User, query, offset string) (queryID string, err error)
// WaitForInlineQueryAnswer waits for the bot's answer to queryID.
WaitForInlineQueryAnswer(queryID string, timeout time.Duration) (*InlineQueryAnswer, bool)
}
InlineQueryEmulator is the optional platform capability used by inline-mode scenarios. It is separate from Emulator because most chat platforms have no Telegram-style inline-query surface.
type InlineQueryResult ¶ added in v0.6.0
type InlineQueryResult struct {
Type string `json:"type"`
ID string `json:"id"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Text string `json:"text,omitempty"`
PhotoURL string `json:"photoUrl,omitempty"`
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
Caption string `json:"caption,omitempty"`
Actions [][]Action `json:"actions,omitempty"`
}
InlineQueryResult is one normalized result returned by a bot for an inline query. Media remains a result property rather than a chat message: selecting a result is a separate client action, and answerInlineQuery itself does not send a message.
type JournalEntry ¶
type JournalEntry struct {
Direction Direction `json:"direction"`
Kind JournalEntryKind `json:"kind"`
MessageID int `json:"messageId"` // logical message identity, shared by inbound/outbound entries in this chat; 0 when Kind has no message identity of its own
RefMessageID int `json:"refMessageId"` // JournalEntryAction only: the message the action targeted
Version int `json:"version"` // JournalEntryMessage only: 0 = original send/inbound, N = the Nth edit
Text string `json:"text"`
Actions [][]Action `json:"actions"` // JournalEntryMessage only: actions attached to this entry, in platform row/col layout
Method string `json:"method"` // JournalEntryUncaptured only: the Bot API method name that was called
At time.Time `json:"at"`
// FromID is the platform-native identity of this entry's originator:
// the Telegram user id of the client actor for a client-originated
// entry, or the bot's own id for a bot-originated entry. It is 0 when
// no identity is available (e.g. a pure method-call record with no
// resolvable sender) — a Platform never invents an identity it does not
// actually know. This is what lets a run-bundle roster (see
// chatwright.dev/sdk's Actor.PlatformIdentities) attribute every journal
// entry to whoever produced it.
FromID int64 `json:"fromId"`
}
JournalEntry is one chronological, structured record from a chat's append-only journal — the same events an Emulator's Transcript renders as human-readable prose, given directly to callers (the observe package's Engine, diagnostics) that need to reason about them structurally instead of parsing rendered text. It carries the emulator's full internal record, including platform-native identifiers and action data (e.g. Telegram callback_data via Actions[*][*].ID) — this is the developer/trace-level seam, not the actor-facing observation surface; the observe package is where raw platform payloads are dropped before an actor ever sees them.
type JournalEntryKind ¶
type JournalEntryKind string
JournalEntryKind distinguishes what a JournalEntry records. It is a string type for the same reason as Direction — see Direction's doc comment.
const ( // JournalEntryMessage is an inbound user message or an outbound bot // send/edit; MessageID, Version, Text and Actions apply. JournalEntryMessage JournalEntryKind = "message" // JournalEntryAction is an inbound action activation (a button click or // equivalent interactive reply); RefMessageID names the message it // targeted, Text carries the platform action identifier that was // activated. JournalEntryAction JournalEntryKind = "action" // JournalEntryUncaptured records a bot API call the emulator does not // simulate — it produced no observable chat content; Method names the // call. JournalEntryUncaptured JournalEntryKind = "uncaptured" )
type Message ¶
type Message struct {
Platform string
ChatID int64
MessageID int // id the platform assigned to this outbound message
Text string
Actions [][]Action
ReceivedAt time.Time
Version int // 0 for the original send; incremented on each in-place edit
}
Message is a neutral bot message captured from a platform's outbound API call.