chatwright

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

README

Chatwright

Emulate messaging platforms locally. Test real conversational applications.

CI

Chatwright is an open, local-first conversation development platform — an independent open-source project developed by Sneat.co. Chatwright emulates the messaging platform; the bot under development is the real software under test. It exercises real bot webhooks through platform-shaped updates and local bot-facing APIs, while keeping scenarios neutral where platforms permit it.

This repository contains the Go runtime and test API at its root (importable as github.com/chatwright/chatwright), the product specification in spec/, the phased roadmap, the product and Cloud strategies, and a connected PrimeNG Studio prototype in prototype/.

Install and write a first test

go get github.com/chatwright/chatwright
// One neutral scenario — no platform-specific calls.
func greetScenario(cw *chatwright.Chatwright) {
	chat := cw.PrivateChat(chatwright.User{ID: "alice", FirstName: "Alice"})
	chat.SendText("Hi")
	chat.ExpectBotMessage().
		Within(time.Second).
		Text("Howdy stranger")
}

func TestGreeting(t *testing.T) {
	t.Run("telegram", func(t *testing.T) {
		cw := chatwright.New(t) // Telegram is the default platform
		cw.ServeWebhook(myTelegramBot(cw.BotAPIURL()))
		greetScenario(cw)
	})
	t.Run("whatsapp", func(t *testing.T) {
		cw := chatwright.New(t, chatwright.OnPlatform(whatsapp.Platform()))
		cw.ServeWebhook(myWhatsAppBot(cw.BotAPIURL()))
		greetScenario(cw)
	})
}

How it works: chatwright.New(t) boots an emulated platform API server (Telegram Bot API or WhatsApp Cloud API) on a local port; you point the bot under test's API base URL at cw.BotAPIURL() and, for webhook-driven bots, hand Chatwright its webhook (an http.Handler, or a URL for an external process in any language — Chatwright only speaks HTTP) with ServeWebhook or WebhookAt. SendText submits a neutral action to the emulator, which builds the platform-native update and owns delivering it — pushed to the webhook, or, for a Telegram bot that never registers one, queued for the bot's own getUpdates long-polling instead. The bot's replies to the emulated API are captured, normalised and asserted — including neutral ExpectAction(...).Click() on inline actions (Telegram today) and ExpectEdited() for in-place message edits. See examples/greetbot for a complete real-bot example and example_test.go for a framework-free HTTP bot.

The small CLI installs with:

go install github.com/chatwright/chatwright/cmd/chatwright@latest
Package Purpose
github.com/chatwright/chatwright The neutral scenario API you write tests against
.../platform Neutral contracts (Platform, Emulator, Message, Action)
.../telegram Telegram platform + emulated Bot API server
.../whatsapp WhatsApp platform + emulated Cloud API server
.../branching All-or-none state checkpoint/branch coordinator
.../cmd/chatwright Command-line entry point

Status: Telegram supports text + inline actions + in-place edits; WhatsApp is text only — interactive replies are not captured yet, so ExpectAction cannot be used against a WhatsApp bot (interactive replies planned); AI actors and Starlark are Phase 2. The roadmap records the supported slice and exclusions honestly.

Explore the connected mock-ups

Run the prototype once, then move between all four views from its shared sidebar:

cd prototype
pnpm install
pnpm start
Mock-up Local route What it demonstrates
Workspace overview http://localhost:4200/workspace Hierarchical scenario coverage, recent runs and the path into an active test
Playground http://localhost:4200/emulator Multiple actors and chats consuming one Telegram Platform Emulator; changing the language edits the existing bot greeting in place
Scenario authoring http://localhost:4200/scenario A readable, hierarchical scenario with platform-neutral steps and assertions
Run inspector http://localhost:4200/run Transcript, HTTP trace, assertions and first-class latency/message metrics

The prototype is intentionally local-only and uses sample state. It is a product conversation aid, not the managed Chatwright Cloud service.

Product direction

  • Now: harden the Telegram Platform Emulator MVP—Client Emulator plus Server/API Emulator—for private text, actions, edits, real HTTP webhook delivery, fake Bot API capture and CI-ready failure reports.
  • Next: make the Playground a first-class consumer for manual offline testing, then add a portable structured scenario model and Starlark.
  • Later: AI actors, fuzz testing and evidence-linked UX evaluation, followed by optional Cloud Run infrastructure, flagship Cloud Intelligence and a Marketplace for reusable assets.

Everything required for local development—including the Runtime, CLI, Platform Emulators, Playground and Studio—is open source and works without an account. Commercial value should come from operated Cloud services, not from closing the local Studio. A free account should earn voluntary sign-in through additive sync, hosted reports, execution, collaboration and AI value.

The roadmap explains the value gates and deliberately excluded scope. The Chatwright idea and feature hierarchy are the specification entry points. The Platform Emulators area defines the Telegram MVP and planned platforms; the Playground is its manual-testing consumer.

Repository map

Path Purpose
*.go, platform/, telegram/, whatsapp/, branching/ The Go runtime, neutral test API and Platform Emulators (module root)
cmd/chatwright/ Command-line entry point
examples/ Runnable example bots and end-to-end tests
spec/ideas/ Product thesis and MVP boundary
spec/features/ Capability hierarchy and acceptance criteria
spec/decisions/ Current product and architecture decisions
spec/research/ Explicit investigation backlog and evidence to collect
spec/plans/ Near-term executable delivery plan
prototype/ Connected Angular + PrimeNG mock-ups
docs/product-strategy.md Platform vision, open-source boundary and adoption strategy
docs/cloud-strategy.md Cloud Run, Cloud Intelligence, free-tier and paid-service direction
AGENTS.md Development principles and working conventions — for humans and AI agents alike
docs/comparison.md Handler unit test vs Chatwright boundary test vs live-account smoke test
docs/glossary.md Canonical vocabulary for every Chatwright surface
docs/compatibility/telegram.md The honest, code-verified Telegram compatibility profile: which Bot API methods, update types and capabilities are supported, partial or unsupported today

The Go runtime previously lived in a chatwrite/ subdirectory (module github.com/chatwright/chatwright/chatwrite). It now lives at the repository root as github.com/chatwright/chatwright; existing consumers of the old nested module path keep resolving their pinned pseudo-versions and should switch import prefixes on their next upgrade.

Status caveat

The original concept brief described a pre-runtime project. The repository has already moved beyond that point: Telegram text/actions/edits and an early WhatsApp text adapter exist. Specifications therefore distinguish observed baseline, approved direction, and future intent rather than labelling implemented code as proposed work. Full WhatsApp fidelity remains deferred.

Open source and what stays open

Chatwright's complete local development stack is Apache-2.0, permanently: the Go runtime, the Chatwright CLI, Platform Emulators (Telegram today, WhatsApp and others as they land), the Playground, Chatwright Studio, and every result and evidence format they produce — transcripts, traces, metrics and assertions. None of it requires an account, a network connection to any Chatwright service, or a Sneat.co account to clone, run, develop against and test a real bot locally.

The only closed layer is the optional, separately operated Chatwright Cloud: managed execution, retained history, collaboration and organisation capabilities. Its portable inputs and exports, and any regression tests it helps produce, remain usable by the open local stack — see decision 0007 for the full boundary and rationale.

Licence

Runtime, CLI, Platform Emulators, Playground and Studio are directed to use the Apache License 2.0. The separately operated Cloud service may remain proprietary. Marketplace assets declare their own licences. Pricing and Cloud packaging remain undecided.

Documentation

Overview

Package chatwright is 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:

cw := chatwright.New(t) // defaults to Telegram
// Configure the bot-under-test to use cw.BotAPIURL() as its platform API,
// then hand Chatwright its webhook handler (any http.Handler):
cw.ServeWebhook(myBot.WebhookHandler())

chat := cw.PrivateChat(chatwright.User{ID: "alice", FirstName: "Alice"})
chat.SendText("/start")
chat.ExpectBotMessage().Within(time.Second).Text("Howdy stranger")

Index

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

func (a *Action) Click() *Chat

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.

func (*Action) ID

func (a *Action) ID(want string) *Action

ID asserts the action's stable identifier.

func (*Action) Label

func (a *Action) Label(want string) *Action

Label asserts the action's user-visible label.

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

func (c *Chat) ExpectNoMessage(within time.Duration)

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.

func (*Chat) SendText

func (c *Chat) SendText(text string) *Chat

SendText delivers a text message from the user to the bot-under-test. The emulator builds the platform-native update and delivers it — over the bot's webhook, or by queuing it for getUpdates on platforms that support polling — Chatwright itself never touches the wire.

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 Metrics

type Metrics struct {
	Latency time.Duration
}

Metrics are first-class measurements captured for a bot message.

type Option

type Option func(*Chatwright)

Option configures a Chatwright harness at construction time.

func OnPlatform

func OnPlatform(p platform.Platform) Option

OnPlatform selects the platform a scenario runs against, e.g. chatwright.OnPlatform(whatsapp.Platform()). Defaults to Telegram.

func WithHTTPClient

func WithHTTPClient(c *http.Client) Option

WithHTTPClient overrides the HTTP client used to deliver updates to the bot's webhook. Rarely needed; useful for custom timeouts or transports.

func WithSafetyTimeout

func WithSafetyTimeout(d time.Duration) Option

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

func WithWebhookHandler(h http.Handler) Option

WithWebhookHandler attaches the bot-under-test's webhook handler at construction time, equivalent to calling ServeWebhook after New.

type SourceReference

type SourceReference struct {
	URI      string
	Revision string
}

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.

type User

type User struct {
	ID        string
	FirstName string
	LastName  string
	Username  string
}

User identifies a participant in a conversation. ID is a stable handle (e.g. "alice"); Chatwright maps it to a deterministic per-platform numeric ID.

Directories

Path Synopsis
Package branching coordinates database-only scenario checkpoints and branches.
Package branching coordinates database-only scenario checkpoints and branches.
chatwrite module
cmd
chatwright command
Command chatwright is the local command-line entry point for the Chatwright conversation execution platform.
Command chatwright is the local command-line entry point for the Chatwright conversation execution platform.
examples
greetbot
Package greetbot is a minimal, real Telegram bot used to exercise Chatwright end-to-end.
Package greetbot is a minimal, real Telegram bot used to exercise Chatwright end-to-end.
Package platform defines the neutral contracts that let a scenario be written once and executed against any chat platform.
Package platform defines the neutral contracts that let a scenario be written once and executed against any chat platform.
Package telegram implements the Telegram Platform for Chatwright: an emulated Telegram Bot API server that delivers updates and captures the bot's outbound calls, normalized to Chatwright's neutral platform types.
Package telegram implements the Telegram Platform for Chatwright: an emulated Telegram Bot API server that delivers updates and captures the bot's outbound calls, normalized to Chatwright's neutral platform types.
Package whatsapp implements the WhatsApp Platform for Chatwright: an emulated WhatsApp Cloud API (Graph) server that delivers inbound webhooks and captures the bot's outbound calls, normalized to Chatwright's neutral platform types.
Package whatsapp implements the WhatsApp Platform for Chatwright: an emulated WhatsApp Cloud API (Graph) server that delivers inbound webhooks and captures the bot's outbound calls, normalized to Chatwright's neutral platform types.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL