// Command mockapi is a fake Messages API for exercising the TUI by hand, // without an API key and without spending anything. // // It lives under testdata so it is invisible to ./... — it is a manual test // fixture, not part of the build. // // go run ./internal/tui/testdata/mockapi & // ./harness -C /tmp/demo -base-url http://127.0.0.1:8899 -api-key mock // // The script is fixed: the first turn streams a sentence and asks to run a // shell command, which makes the TUI raise an approval prompt; once a // tool_result comes back it streams a closing sentence and stops. That covers // streaming, tool rendering, approve, deny, and interrupt. // // Flags: -addr to move it, -fail to make the command exit non-zero, -slow to // stretch the stream out for watching the spinner and ctrl-c. package main import ( "encoding/json" "flag" "fmt" "io" "log" "net/http" "strings" "time" ) var ( addr = flag.String("addr", "127.0.0.1:8899", "listen address") slow = flag.Duration("slow", 0, "pause between streamed words, e.g. 150ms") cmd = flag.String("cmd", "git status --short && go build ./...", "command the model asks to run") ) func main() { flag.Parse() http.HandleFunc("/v1/messages", handle) log.Printf("mock Messages API on http://%s", *addr) log.Fatal(http.ListenAndServe(*addr, nil)) } func handle(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) // A tool_result in the history means the harness ran (or refused) our // command and fed the outcome back, so the next turn is the last one. final := strings.Contains(string(body), "tool_result") w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) send(w, "message_start", map[string]any{ "type": "message_start", "message": map[string]any{ "id": "msg_mock", "model": "claude-opus-5", "usage": map[string]any{"input_tokens": 1200, "cache_read_input_tokens": 8000}, }, }) text := "Checking the repository state first." if final { text = "That is everything I can tell from here." } send(w, "content_block_start", map[string]any{ "type": "content_block_start", "index": 0, "content_block": map[string]any{"type": "text", "text": ""}, }) for _, word := range strings.SplitAfter(text, " ") { send(w, "content_block_delta", map[string]any{ "type": "content_block_delta", "index": 0, "delta": map[string]any{"type": "text_delta", "text": word}, }) time.Sleep(*slow) } send(w, "content_block_stop", map[string]any{"type": "content_block_stop", "index": 0}) stop := "end_turn" if !final { stop = "tool_use" send(w, "content_block_start", map[string]any{ "type": "content_block_start", "index": 1, "content_block": map[string]any{"type": "tool_use", "id": "toolu_1", "name": "bash"}, }) args, _ := json.Marshal(map[string]string{"command": *cmd}) send(w, "content_block_delta", map[string]any{ "type": "content_block_delta", "index": 1, "delta": map[string]any{"type": "input_json_delta", "partial_json": string(args)}, }) send(w, "content_block_stop", map[string]any{"type": "content_block_stop", "index": 1}) } send(w, "message_delta", map[string]any{ "type": "message_delta", "delta": map[string]any{"stop_reason": stop}, "usage": map[string]any{"output_tokens": 95}, }) send(w, "message_stop", map[string]any{"type": "message_stop"}) } func send(w http.ResponseWriter, event string, data any) { b, _ := json.Marshal(data) fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, b) w.(http.Flusher).Flush() }