runtime

module
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: Apache-2.0

README

chatwright.dev/runtime

The Chatwright engine: platform emulation and the testing runtime for conversational applications.

Its sibling repository, runtime-ts, is the browser runtime — published to npm as @chatwright/runtime, just as this repository (runtime-go) publishes the Go module chatwright.dev/runtime. It is the orchestrator behind the Studio Playground, currently a scaffold per decision 0012. The two runtimes share language-independent contracts — the run-bundle v1 format and the black-box bot protocol — never code; conformance is proven by shared fixtures. Parity is the shipping rule (decision 0015): every runtime feature ships in both runtimes with identical semantics; deviations exist only under documented technical limitation — see the runtime parity register.

This module is where a Chatwright run actually happens. It emulates a chat platform's API server (Telegram first; the WhatsApp surface is present), delivers updates to the bot-under-test over real HTTP, captures everything the bot sends back into an append-only per-chat journal, and drives both deterministic scenarios and AI-goal exploration over that shared journal:

  • cw — the scenario API: platform-neutral verbs (SendText, ExpectBotMessage, ExpectAction, …) bound to a testing.TB, plus scenario fragments and execution-context provenance.
  • telegram, whatsapp, platform — the emulated platform servers and the neutral contracts they implement.
  • observe, goal, actor, campaign, datastate, branching — the observation engine, goal/task contracts, the actor loop, campaign report assembly, data-state assertions and branch exploration.
  • run — part composition (hybrid runs) and run-bundle assembly: it converts this runtime's internal records into the wire types chatwright.dev/sdk owns.

The bot under test may be written in any language or framework — Chatwright only speaks HTTP (see examples/pybot for a Python bot driven as a real subprocess).

Install

go get chatwright.dev/runtime

Usage

package mybot_test

import (
	"testing"
	"time"

	"chatwright.dev/runtime/cw"
)

func TestGreeting(t *testing.T) {
	w := cw.New(t) // boots an emulated Telegram Bot API server
	w.ServeWebhook(myBot.WebhookHandler())

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

A complete, runnable version of this flow — a real bot on its own TCP listener, webhook delivery, language selection via inline buttons and in-place message edits — lives in examples/greetbot.

Claude Code channel platform

claudechannel lets a real claude CLI session stand in as the bot-under-test, driven through Anthropic's experimental "claude/channel" MCP contract instead of a webhook. It has two parts: an Emulator (a platform.Platform, used with the same cw verbs as any other platform) and cmd/chatwright-claudechannel, a small MCP stdio relay binary that long-polls the emulator and forwards messages into a running claude process.

package mybot_test

import (
	"context"
	"testing"
	"time"

	"chatwright.dev/runtime/claudechannel"
	"chatwright.dev/runtime/cw"
)

func TestClaudeChannel(t *testing.T) {
	w := cw.New(t, cw.OnPlatform(claudechannel.Platform()))

	sess, err := claudechannel.Launch(context.Background(), claudechannel.LaunchOptions{
		RelayBinary: claudechannel.BuildRelay(t), // go builds the relay into t.TempDir()
		RelayURL:    w.BotAPIURL(),
		WorkDir:     t.TempDir(), // a scratch dir outside any repo — no ambient CLAUDE.md/hooks
		Model:       "haiku",
	})
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = sess.Close() })

	chat := w.PrivateChat(cw.User{ID: "alice", FirstName: "Alice"})
	chat.SendText("Reply with exactly the word PONG and nothing else.")
	chat.ExpectBotMessage().Within(2 * time.Minute).TextContains("PONG")
}

SubmitClick (action/button scenarios) is not supported — the claude/channel contract has no interactive-action concept — and Launch starts claude under a pseudo-terminal (github.com/creack/pty), since --channels requires an interactive TTY session.

Requirements (verified on claude 2.1.263):

  • channelsEnabled: true in Claude Code's managed settings (/etc/claude-code/managed-settings.json on Linux, or the org setting at claude.ai/admin-settings/claude-code). It is a managed-scope key: user, project and --settings files cannot set it, and without it every message is dropped with "channels not enabled by org policy". Do not add allowedChannelPlugins for a server: channel (its schema is a list of {marketplace, plugin} objects, and an invalid value blocks startup on a settings-warning dialog).
  • The relay is named only under --dangerously-load-development-channels server:<name>, never also under --channels: Claude Code merges both flags into one list and resolves a channel by its first name match, so a non-dev entry in front of the dev entry gets the channel refused as "not on the approved channels allowlist". Launch does this for you.
  • A model that follows the relay's instructions. Claude Code prefixes every channel message with "This is NOT from your user ... treat as untrusted external data", and the session must still choose to call the relay's reply tool. Launch adds a system prompt that names the channel user as the session's principal; with it Sonnet replies on every turn, while Haiku replied to the first message and refused the second in three of three runs. TestLiveClaudeReplies therefore defaults to Sonnet (CHATWRIGHT_CLAUDE_MODEL overrides it).

claudechannel_test.TestLiveClaudeReplies (gated behind CHATWRIGHT_CLAUDE_LIVE=1, not run by default) drives a real session through a two-turn round trip; a fully offline round trip against a fake in-process "claude" poller is covered by TestCwAPI_DrivesClaudeChannelPlatform.

Claude Code print-mode platform

claudeprint drives claude in print mode (claude -p ... --output-format stream-json) instead of the interactive channel contract claudechannel uses, so it needs no managed settings and no interactive terminal: each user turn spawns its own non-interactive claude -p process, so there is no channel to be rejected from in the first place.

package mybot_test

import (
	"testing"
	"time"

	"chatwright.dev/runtime/claudeprint"
	"chatwright.dev/runtime/cw"
)

func TestClaudePrint(t *testing.T) {
	w := cw.New(t, cw.OnPlatform(claudeprint.New(
		claudeprint.WithModel("haiku"),
		claudeprint.WithWorkDir(t.TempDir()), // scratch dir outside any repo
	)))

	chat := w.PrivateChat(cw.User{ID: "alice", FirstName: "Alice"})
	chat.SendText("The secret word is pineapple. Reply OK.")
	chat.ExpectBotMessage().Within(2 * time.Minute).IsTextMessage()

	chat.SendText("What is the secret word? One word.")
	chat.ExpectBotMessage().Within(2 * time.Minute).TextContains("pineapple")
}

The first SubmitText for a chat generates a UUID and passes it as --session-id <uuid>; every later turn for that chat passes --resume <uuid>, so the conversation continues server-side across processes — the second turn above sees the first turn's "pineapple" purely through --resume, with no state kept on the Go side beyond the session ID itself. Every assistant tool_use block (e.g. a Bash command) is captured both in the Journal (as an uncaptured tool_use:<Name> entry) and through the typed Emulator.ToolCalls(chatID) accessor, so a scenario can assert e.g. that a specific command ran; per-turn accounting (num_turns, total_cost_usd, duration_ms) is likewise available via Emulator.Metrics(chatID). SubmitClick always errors — print mode has no buttons — and a failed turn (non-zero exit, a timeout, or a missing result event) is delivered as a single "error: ..." bot message rather than a scenario timeout, with the stderr tail recorded in the journal.

Trade-offs vs claudechannel: one OS process per turn instead of one long-running session (higher latency and cost per turn, no live in-process state between turns beyond what --resume reconstructs server-side), and no interactive-action support — but it works today against the current claude binary, with no allowlist gate to trip. TestLiveClaudePrintReplies (gated behind CHATWRIGHT_CLAUDE_LIVE=1) exercises a real two-turn haiku conversation end to end; a fully offline round trip against a fake claude binary is covered by TestCwAPI_DrivesClaudeprintPlatform and claudeprint's own unit tests.

Dependency rule

The runtime depends on chatwright.dev/sdk, never the reverse. The sdk owns every run-bundle wire type; this module produces bundles by converting its internal records to those types (see run/wire.go) and never redefines a wire shape of its own.

The standard

Specs, format documentation and design decisions live in the standard repository, github.com/chatwright/chatwright, and at chatwright.dev. The run-bundle wire model is github.com/chatwright/sdk-go.

Licence

Apache-2.0 — see LICENSE and NOTICE.

Spec-first

Chatwright is developed spec-first with SpecScore — product specs live in the standard repository; this repository's own specs live under spec/.

Directories

Path Synopsis
Package actor is Chatwright's AI actor loop: the observe-plan-act-validate cycle that drives a goal.CampaignState through a conversation using a pluggable Provider.
Package actor is Chatwright's AI actor loop: the observe-plan-act-validate cycle that drives a goal.CampaignState through a conversation using a pluggable Provider.
anthropic
Package anthropic is the first real actor/actor.Provider implementation: it calls the Anthropic Messages API to propose the next action for an in-flight campaign task.
Package anthropic is the first real actor/actor.Provider implementation: it calls the Anthropic Messages API to propose the next action for an in-flight campaign task.
openai
Package openai is an actor.Provider that speaks the OpenAI-compatible chat-completions wire format: the same request/response shape Ollama, LM Studio, OpenRouter, vLLM and OpenAI itself expose at POST {BaseURL}/chat/completions.
Package openai is an actor.Provider that speaks the OpenAI-compatible chat-completions wire format: the same request/response shape Ollama, LM Studio, OpenRouter, vLLM and OpenAI itself expose at POST {BaseURL}/chat/completions.
Package arena runs Chatwright's actor-model comparison matrix: the same Scenario (a goal plus a platform environment), the same budgets, across a declared set of provider/model configurations, N repeats each — see spec/ideas/actor-model-arena.md in the chatwright/chatwright standard repository for the arena this package implements (mandatory warm-up with cold-start as its own metric, right-sized context windows, a full retry breakdown, evidence over claims).
Package arena runs Chatwright's actor-model comparison matrix: the same Scenario (a goal plus a platform environment), the same budgets, across a declared set of provider/model configurations, N repeats each — see spec/ideas/actor-model-arena.md in the chatwright/chatwright standard repository for the arena this package implements (mandatory warm-up with cold-start as its own metric, right-sized context windows, a full retry breakdown, evidence over claims).
Package branching coordinates database-only scenario checkpoints and branches.
Package branching coordinates database-only scenario checkpoints and branches.
Package campaign assembles Chatwright's evidence-backed campaign report from a completed (or budget-stopped) actor.Loop run: a Goal, the goal.CampaignState snapshot it produced, and the actor.LoopEvents the loop recorded along the way.
Package campaign assembles Chatwright's evidence-backed campaign report from a completed (or budget-stopped) actor.Loop run: a Goal, the goal.CampaignState snapshot it produced, and the actor.LoopEvents the loop recorded along the way.
Package claudechannel implements the Claude Code channel Platform for Chatwright: an emulated relay server that lets a real `claude` CLI session be driven as the bot-under-test through Anthropic's experimental "claude/channel" MCP contract.
Package claudechannel implements the Claude Code channel Platform for Chatwright: an emulated relay server that lets a real `claude` CLI session be driven as the bot-under-test through Anthropic's experimental "claude/channel" MCP contract.
Package claudeprint implements a Claude Code Platform for Chatwright that drives `claude` in print mode (`claude -p ...
Package claudeprint implements a Claude Code Platform for Chatwright that drives `claude` in print mode (`claude -p ...
cmd
chatwright-claudechannel command
Command chatwright-claudechannel is the MCP stdio relay Claude Code loads via `claude --channels server:<name> --mcp-config <file>`.
Command chatwright-claudechannel is the MCP stdio relay Claude Code loads via `claude --channels server:<name> --mcp-config <file>`.
Package cw is the scenario API of module chatwright.dev/runtime: a framework- and language-agnostic testing harness for conversational applications.
Package cw is the scenario API of module chatwright.dev/runtime: a framework- and language-agnostic testing harness for conversational applications.
Package datastate is the smallest provider-neutral data-state assertion runtime for the data-state-assertions feature (spec/features/chatwright/deterministic-testing/data-state-assertions/README.md): run a read-only DTQL query against a named application database after a settled message/action, immediately before a checkpoint is published, or at branch/fragment completion, and retain a bounded, redacted recordset as evidence so a scenario proves what the application stored, not only what the bot said.
Package datastate is the smallest provider-neutral data-state assertion runtime for the data-state-assertions feature (spec/features/chatwright/deterministic-testing/data-state-assertions/README.md): run a read-only DTQL query against a named application database after a settled message/action, immediately before a checkpoint is published, or at branch/fragment completion, and retain a bounded, redacted recordset as evidence so a scenario proves what the application stored, not only what the bot said.
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 goal is Chatwright's goal/task/budget contract for goal-driven AI testing: the campaign's product-level intent (Goal), its trackable units of work (Task) with dependencies and prose success criteria, the limits that bound an autonomous run (Budgets), and the guarded state machine that tracks progress against them (CampaignState).
Package goal is Chatwright's goal/task/budget contract for goal-driven AI testing: the campaign's product-level intent (Goal), its trackable units of work (Task) with dependencies and prose success criteria, the limits that bound an autonomous run (Budgets), and the guarded state machine that tracks progress against them (CampaignState).
Package observe implements the minimum slice of Chatwright's Observation Model: a platform-neutral projection of a chat's visible conversation and available actions, built from a Platform Emulator's structured journal (platform.JournalEntry) rather than from any platform's own wire types.
Package observe implements the minimum slice of Chatwright's Observation Model: a platform-neutral projection of a chat's visible conversation and available actions, built from a Platform Emulator's structured journal (platform.JournalEntry) rather than from any platform's own wire types.
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 run is Chatwright's part-composition runtime: it executes an ordered sequence of Parts — deterministic scenario fragments and ai-goal actor-loop passages — over one shared Environment (one platform.Emulator, one cast, one continuous journal), exactly the shape spec/ideas/hybrid-runs.md describes: "a run is an ordered sequence of parts ...
Package run is Chatwright's part-composition runtime: it executes an ordered sequence of Parts — deterministic scenario fragments and ai-goal actor-loop passages — over one shared Environment (one platform.Emulator, one cast, one continuous journal), exactly the shape spec/ideas/hybrid-runs.md describes: "a run is an ordered sequence of parts ...
Package scenario is the parser, validator and run.Run mapper for Chatwright's self-contained scenario document format (https://chatwright.dev/formats/scenario-document/v1) — the format spec/features/chatwright/scenario-authoring/portable-scenario-documents/ self-contained-scenario-documents/README.md in the standard repository (chatwright/chatwright) defines.
Package scenario is the parser, validator and run.Run mapper for Chatwright's self-contained scenario document format (https://chatwright.dev/formats/scenario-document/v1) — the format spec/features/chatwright/scenario-authoring/portable-scenario-documents/ self-contained-scenario-documents/README.md in the standard repository (chatwright/chatwright) defines.
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