Documentation
¶
Overview ¶
Package cw is the scenario API of module chatwright.dev/runtime: a framework- and language-agnostic testing harness for conversational applications.
A scenario is written once against platform-neutral verbs — send a text, expect a message, expect an action — and Chatwright maps them onto a concrete platform (Telegram today, WhatsApp next). It emulates that platform's API server, which owns delivering updates to the bot-under-test (over a real HTTP webhook, or via getUpdates long-polling on platforms that support it) and captures the API calls the bot makes back. The bot under test may be written in any language or framework — Chatwright only speaks HTTP.
Typical use:
w := cw.New(t) // defaults to Telegram
// Configure the bot-under-test to use w.BotAPIURL() as its platform API,
// then hand Chatwright its webhook handler (any http.Handler):
w.ServeWebhook(myBot.WebhookHandler())
chat := w.PrivateChat(cw.User{ID: "alice", FirstName: "Alice"})
chat.SendText("/start")
chat.ExpectBotMessage().Within(time.Second).Text("Howdy stranger")
Index ¶
- type Action
- type BotMessage
- func (m *BotMessage) ExpectAction(row, col int) *Action
- func (m *BotMessage) ExpectEdited() *BotMessage
- func (m *BotMessage) IsTextMessage() *BotMessage
- func (m *BotMessage) Metrics() Metrics
- func (m *BotMessage) Snapshot() platform.Message
- func (m *BotMessage) Text(want string) *BotMessage
- func (m *BotMessage) TextContains(substr string) *BotMessage
- func (m *BotMessage) TextMatches(pattern string) *BotMessage
- func (m *BotMessage) Within(d time.Duration) *BotMessage
- type BranchEvidence
- type Chat
- type Chatwright
- type CheckpointEvidence
- type CheckpointID
- type Definition
- type EffectiveInputs
- type ExecutionContext
- func (c *ExecutionContext) Checkpoint(name string, source SourceReference) (CheckpointEvidence, error)
- func (c *ExecutionContext) Evidence() ExecutionEvidence
- func (c *ExecutionContext) Path() InvocationPath
- func (c *ExecutionContext) RecordBranch(name string, source SourceReference) BranchEvidence
- func (c *ExecutionContext) RecordFailure(err error, source SourceReference) FailureEvidence
- func (c *ExecutionContext) RecordStep(name string, source SourceReference) StepEvidence
- type ExecutionEvidence
- type FailureEvidence
- type Fragment
- type FragmentInvocation
- type InputSource
- type InvocationPath
- type Metrics
- type Option
- type SourceReference
- type StepEvidence
- type User
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Action ¶
type Action struct {
// contains filtered or unexported fields
}
Action is a platform-neutral assertion handle for an interactive action.
func (*Action) Click ¶
Click activates the action, sending the appropriate event back to the bot: a callback for actions with an ID (Telegram callback query / WhatsApp interactive reply), or the action's label as text otherwise. Returns the chat so a reply can be asserted next.
type BotMessage ¶
type BotMessage struct {
// contains filtered or unexported fields
}
BotMessage is a fluent handle to a message the bot sent. Assertion methods (Text, IsTextMessage, ExpectAction, ...) block until the message arrives — up to the harness's safety timeout (see WithSafetyTimeout), regardless of any Within budget — and fail the test if it never does. They are platform-neutral.
func (*BotMessage) ExpectAction ¶
func (m *BotMessage) ExpectAction(row, col int) *Action
ExpectAction returns a platform-neutral handle to the interactive action (button) at (row, col). Use Label and ID — these map onto each platform's native representation (Telegram button text/callback_data, WhatsApp reply title/id).
func (*BotMessage) ExpectEdited ¶
func (m *BotMessage) ExpectEdited() *BotMessage
ExpectEdited returns a fluent handle that waits for this message to be edited in place (e.g. a Telegram editMessageText call) and asserts on its new content — the same assertion methods as ExpectBotMessage, but bound to this message's identity rather than to the next outbound message. Like ExpectBotMessage, it waits up to the harness's safety timeout and starts with no latency budget of its own; add one with Within.
func (*BotMessage) IsTextMessage ¶
func (m *BotMessage) IsTextMessage() *BotMessage
IsTextMessage asserts the bot's message carries text, returning the handle for further assertions.
func (*BotMessage) Metrics ¶
func (m *BotMessage) Metrics() Metrics
Metrics returns the metrics captured for this message.
func (*BotMessage) Snapshot ¶
func (m *BotMessage) Snapshot() platform.Message
Snapshot returns an immutable observation of the resolved bot message. Nested action rows are detached from the emulator's mutable message state, so callers can inspect transport output without changing later assertions.
func (*BotMessage) Text ¶
func (m *BotMessage) Text(want string) *BotMessage
Text asserts the bot's message text equals want.
func (*BotMessage) TextContains ¶
func (m *BotMessage) TextContains(substr string) *BotMessage
TextContains asserts the bot's message text contains substr.
func (*BotMessage) TextMatches ¶
func (m *BotMessage) TextMatches(pattern string) *BotMessage
TextMatches asserts the bot's message text matches the given regular expression (as accepted by the regexp package). An invalid pattern fails the test immediately rather than silently matching nothing.
func (*BotMessage) Within ¶
func (m *BotMessage) Within(d time.Duration) *BotMessage
Within sets the latency budget a reply is judged against: once a reply arrives, if it took longer than d, the test fails showing the observed latency and the reply's actual text. Within does NOT shorten how long Chatwright waits for that reply to arrive in the first place — that ceiling is the harness's safety timeout (default 5s; see WithSafetyTimeout), independent of d, so a late-but-arrived reply is a diagnostic failure (expected/actual text, observed latency) rather than an opaque "none arrived" timeout. If d exceeds the configured safety timeout, the wait is extended to d so a generous budget is never undercut by it.
Calling Within after the message has already resolved (e.g. after Text or IsTextMessage) is a usage error: the wait already happened, so a budget set now can no longer be honored. It fails the test immediately with a clear message instead of silently doing nothing.
type BranchEvidence ¶
type BranchEvidence struct {
Name string
InvocationPath InvocationPath
Source SourceReference
}
BranchEvidence records a branch declared directly by one invocation.
type Chat ¶
type Chat struct {
// contains filtered or unexported fields
}
Chat is a conversation between a user and the bot-under-test. Obtain one via Chatwright.PrivateChat, then drive it with SendText and assert with ExpectBotMessage. Its methods are platform-neutral.
func (*Chat) ExpectBotMessage ¶
func (c *Chat) ExpectBotMessage() *BotMessage
ExpectBotMessage asserts that the bot sends a message to this chat, returning a fluent handle to assert on its content. Chatwright waits up to the harness's safety timeout (default 5s; see WithSafetyTimeout) for it to arrive; narrow the latency this is judged against — without shortening that wait — with Within.
func (*Chat) ExpectNoMessage ¶
ExpectNoMessage asserts that the bot does not send a new message to this chat within the given window. It fails the test if one arrives, reporting its text. Unlike ExpectBotMessage, it does not consume a slot in the chat's message cursor: a subsequent ExpectBotMessage still waits for the next unconsumed message.
type Chatwright ¶
type Chatwright struct {
// contains filtered or unexported fields
}
Chatwright is a single test's conversational world: an emulated platform API server plus the wiring to attach the bot-under-test's webhook (when it has one). The emulator — not Chatwright — owns building updates, assigning them identity, and delivering them; Chatwright only submits neutral actions.
func New ¶
func New(t testing.TB, opts ...Option) *Chatwright
New starts a Chatwright harness. It selects a platform (Telegram by default, override with OnPlatform), boots that platform's emulated API server, and registers cleanup with the test. Configure the bot-under-test with BotAPIURL, then attach its webhook via ServeWebhook or WebhookAt — or, on platforms that support it (Telegram), leave neither set and run the bot's own getUpdates polling loop against BotAPIURL instead.
func (*Chatwright) BotAPIURL ¶
func (cw *Chatwright) BotAPIURL() string
BotAPIURL is the base URL the bot-under-test must use as its platform API host, in place of the real one. Every call the bot makes there is captured.
func (*Chatwright) Platform ¶
func (cw *Chatwright) Platform() string
Platform is the name of the active platform, e.g. "telegram".
func (*Chatwright) PrivateChat ¶
func (cw *Chatwright) PrivateChat(u User) *Chat
PrivateChat returns the private chat between the given user and the bot. Calling it again for the same user returns the same *Chat handle, not a fresh one: the consumption cursor (which bot messages have already been asserted on) and lastSent latency baseline are shared across every call site that asks for that user's chat, matching Telegram's one chat per user.
func (*Chatwright) ServeWebhook ¶
func (cw *Chatwright) ServeWebhook(h http.Handler)
ServeWebhook runs the given handler as the bot-under-test's webhook on a local HTTP server, so updates are delivered over real HTTP. Use this for in-process bots. The server is shut down when the test ends.
func (*Chatwright) WebhookAt ¶
func (cw *Chatwright) WebhookAt(url string)
WebhookAt points Chatwright at an already-running bot webhook (a bot process started separately, in any language). The emulator POSTs updates to url.
type CheckpointEvidence ¶
type CheckpointEvidence struct {
ID CheckpointID
Name string
InvocationPath InvocationPath
ParentID CheckpointID
Lineage []CheckpointID
Source SourceReference
}
CheckpointEvidence records a named checkpoint and its qualified lineage. Lineage contains ancestor checkpoint IDs in root-to-parent order.
type CheckpointID ¶
type CheckpointID string
CheckpointID is a checkpoint's qualified machine identity.
type Definition ¶
type Definition struct {
Name string
Source SourceReference
}
Definition identifies a scenario or reusable fragment and its source.
type EffectiveInputs ¶
type EffectiveInputs[T any] struct { Value T Sources map[string]InputSource }
EffectiveInputs holds a fragment's typed input value and the provenance of its named fields. Sources is copied for each invocation.
type ExecutionContext ¶
type ExecutionContext struct {
// contains filtered or unexported fields
}
ExecutionContext is the small, invocation-local context used by scenarios and fragments to record source-linked evidence and checkpoint lineage.
func NewExecutionContext ¶
func NewExecutionContext(definition Definition, path ...string) (*ExecutionContext, error)
NewExecutionContext creates a root scenario execution. Each path argument is one machine-path segment, for example "listus" and "new-user".
func (*ExecutionContext) Checkpoint ¶
func (c *ExecutionContext) Checkpoint(name string, source SourceReference) (CheckpointEvidence, error)
Checkpoint records a named checkpoint qualified by the current invocation path. The previously active checkpoint, including one inherited from a parent invocation, becomes its parent and is appended to its lineage.
func (*ExecutionContext) Evidence ¶
func (c *ExecutionContext) Evidence() ExecutionEvidence
Evidence returns a detached snapshot of the evidence produced directly by this execution context.
func (*ExecutionContext) Path ¶
func (c *ExecutionContext) Path() InvocationPath
Path returns the qualified path for this execution context.
func (*ExecutionContext) RecordBranch ¶
func (c *ExecutionContext) RecordBranch(name string, source SourceReference) BranchEvidence
RecordBranch adds source-linked evidence for a locally declared branch.
func (*ExecutionContext) RecordFailure ¶
func (c *ExecutionContext) RecordFailure(err error, source SourceReference) FailureEvidence
RecordFailure adds source-linked evidence for a locally observed failure.
func (*ExecutionContext) RecordStep ¶
func (c *ExecutionContext) RecordStep(name string, source SourceReference) StepEvidence
RecordStep adds source-linked evidence for a locally produced step.
type ExecutionEvidence ¶
type ExecutionEvidence struct {
Path InvocationPath
Definition Definition
Steps []StepEvidence
Checkpoints []CheckpointEvidence
Branches []BranchEvidence
Failures []FailureEvidence
}
ExecutionEvidence is a snapshot of evidence produced directly by an execution context. Evidence from nested fragment invocations remains attached to those invocations rather than being flattened into their caller.
type FailureEvidence ¶
type FailureEvidence struct {
Message string
InvocationPath InvocationPath
Source SourceReference
}
FailureEvidence records a failure attributed to one invocation and source.
type Fragment ¶
type Fragment[T any] struct { Definition Definition CloneInputs func(T) T Execute func(*ExecutionContext, T) error }
Fragment is a reusable typed scenario definition. CloneInputs must return a detached copy: Chatwright calls it separately for execution and evidence so a fragment cannot mutate its caller's inputs or its recorded effective inputs.
type FragmentInvocation ¶
type FragmentInvocation[T any] struct { Path InvocationPath ParentPath InvocationPath Definition Definition Inputs EffectiveInputs[T] Steps []StepEvidence Checkpoints []CheckpointEvidence Branches []BranchEvidence Failures []FailureEvidence }
FragmentInvocation is the source-linked evidence for one fragment call. Its locally produced records do not include evidence from nested fragments.
func InvokeFragment ¶
func InvokeFragment[T any]( parent *ExecutionContext, invocationName string, fragment Fragment[T], inputs EffectiveInputs[T], ) (FragmentInvocation[T], error)
InvokeFragment executes a reusable fragment beneath parent. invocationName is part of the machine path, allowing the same definition and checkpoint labels to be used more than once without identity collisions.
type InputSource ¶
type InputSource struct {
Kind string
Reference string
Source SourceReference
}
InputSource describes where one named effective input came from. Kind is an application-defined category such as "fixture", "default", or "override".
type InvocationPath ¶
type InvocationPath string
InvocationPath is the qualified machine path of a scenario or fragment invocation. Paths are built from escaped, non-empty segments.
func (InvocationPath) String ¶
func (p InvocationPath) String() string
type Option ¶
type Option func(*Chatwright)
Option configures a Chatwright harness at construction time.
func OnPlatform ¶
OnPlatform selects the platform a scenario runs against, e.g. cw.OnPlatform(whatsapp.Platform()). Defaults to Telegram.
func WithHTTPClient ¶
WithHTTPClient overrides the HTTP client used to deliver updates to the bot's webhook. Rarely needed; useful for custom timeouts or transports.
func WithListenAddr ¶
WithListenAddr binds the emulated platform API server to a caller-chosen local address (e.g. "127.0.0.1:54321") instead of a random port.
The common case — ServeWebhook driving an in-process bot — never needs this: cw.BotAPIURL() is available as soon as New returns, before the bot is even constructed. It matters for a bot-under-test started as a separate process, in any language, since Chatwright only speaks HTTP: that process reads its API base URL from its own configuration (e.g. an environment variable) at start-up, so the address must be decided before New runs, not read back from it afterwards. Pick a free address once (e.g. bind to "127.0.0.1:0", read the assigned port, then close it), configure the process with it, and pass the same address here — the emulator then binds exactly where the process already expects it, regardless of which of the two is started first. See examples/pybot for a complete non-Go example using this seam.
Only platforms that implement platform.AddrPlatform support this (Telegram does); New fails the test via t.Fatalf if a non-empty address is set for a platform that doesn't, and if the address itself cannot be bound (e.g. already in use).
func WithSafetyTimeout ¶
WithSafetyTimeout overrides the wall-clock ceiling Chatwright waits for a bot reply before failing a test (default 5s). It applies to every BotMessage wait regardless of any per-assertion Within budget: Within records a latency budget asserted once a reply arrives, but never shortens how long Chatwright is willing to keep listening. Lower it in fast, tight test suites to fail sooner when a bot never replies at all; raise it if the bot-under-test is intrinsically slow (e.g. it calls a real, unmocked external service).
func WithWebhookHandler ¶
WithWebhookHandler attaches the bot-under-test's webhook handler at construction time, equivalent to calling ServeWebhook after New.
type SourceReference ¶
SourceReference links scenario evidence to the definition that produced it. URI should identify the source location (usually a VCS blob URL), while Revision identifies the exact version that was executed.
type StepEvidence ¶
type StepEvidence struct {
Name string
InvocationPath InvocationPath
Source SourceReference
}
StepEvidence records a step produced directly by one invocation.