// Bubble Tea comes from cli/third_party, patched so that a command run with // tea.Exec (the prompt editor, ssh sign-in) hands its result to the model // before any key read once the terminal is back (see // third_party/bubbletea.patch). Upstream sends that result from a goroutine, // so a key typed as the editor closes could reach the chat first and act on // the draft from before the edit: ctrl+g then reopened the editor on the old // text. E2E drives a real terminal but meets this race only now and then. package tui import ( "io" "os" "testing" "time" tea "charm.land/bubbletea/v2" ) // typeAsItExits stands in for an editor the person leaves by typing on: its // last act writes a key to the terminal input. type typeAsItExits struct{ input *os.File } func (c typeAsItExits) Run() error { _, err := c.input.WriteString("x") return err } func (typeAsItExits) SetStdin(io.Reader) {} func (typeAsItExits) SetStdout(io.Writer) {} func (typeAsItExits) SetStderr(io.Writer) {} type execDoneMsg struct{} type execOrderModel struct { input *os.File seen []string } func (m execOrderModel) Init() tea.Cmd { return tea.Exec(typeAsItExits{m.input}, func(error) tea.Msg { return execDoneMsg{} }) } func (m execOrderModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case execDoneMsg: m.seen = append(m.seen, "exec") case tea.KeyPressMsg: m.seen = append(m.seen, msg.String()) } if len(m.seen) == 2 { return m, tea.Quit } return m, nil } func (execOrderModel) View() tea.View { return tea.NewView("") } func TestExecResultArrivesBeforeKeysTypedAsItEnds(t *testing.T) { read, write, err := os.Pipe() if err != nil { t.Fatal(err) } t.Cleanup(func() { read.Close(); write.Close() }) program := tea.NewProgram(execOrderModel{input: write}, tea.WithInput(read), tea.WithOutput(io.Discard)) done := make(chan tea.Model, 1) go func() { final, err := program.Run() if err != nil { t.Error(err) } done <- final }() select { case final := <-done: if seen := final.(execOrderModel).seen; len(seen) != 2 || seen[0] != "exec" || seen[1] != "x" { t.Fatalf("the model saw %v, want the exec result before the key", seen) } case <-time.After(10 * time.Second): program.Kill() t.Fatal("the program never saw both the exec result and the key") } }