okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.pettui: ctrl-click a path in the transcript to open it in an editorcc6ab84

main
3.3 KiB97 linesraw
1//go:build unix
2
3// Ctrl-click opens the file a transcript row names in the editor, at its
4// line. The rows come from the real renderer and a real files.write diff,
5// and the click lands on screen coordinates, so a change to how rows are
6// drawn or clicks are mapped breaks this test rather than the feature.
7package e2e
8
9import (
10	"os"
11	"path/filepath"
12	"strings"
13	"testing"
14	"time"
15
16	"albedo/cli/internal/daemon"
17	"albedo/cli/internal/tui"
18
19	tea "charm.land/bubbletea/v2"
20)
21
22func TestTUICtrlClickOpensTheNamedFileInTheEditor(t *testing.T) {
23	profile := providerRoute(t, func(request map[string]any) string {
24		if toolAnswered(request) {
25			return "I fixed main.go:3:6 for you."
26		}
27		return pythonCall(`files.write("main.go", "package main\n\nfunc main() {}\n")`)
28	})
29	workspace := t.TempDir()
30	session := daemonSession(t, newSession(t, workspace))
31	if _, err := daemon.NewChatClient(conn(t), session.ID).Send(t.Context(), "fix main", nil); err != nil {
32		t.Fatal(err)
33	}
34	waitIdle(t, session.ID, profile, 2)
35
36	// albedo runs in Zed's terminal, so the file opens through zed, which
37	// here only writes down what it was asked to open.
38	bin, opened := t.TempDir(), filepath.Join(t.TempDir(), "opened")
39	script := "#!/bin/sh\nprintf '%s\\n' \"$@\" > " + opened + ".tmp && mv " + opened + ".tmp " + opened + "\n"
40	if err := os.WriteFile(filepath.Join(bin, "zed"), []byte(script), 0o755); err != nil {
41		t.Fatal(err)
42	}
43	t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
44	t.Setenv("TERM_PROGRAM", "zed")
45	t.Setenv("EDITOR", "false")
46
47	d := driveTUI(t, &session)
48	t.Cleanup(d.App.Chat.Close)
49	d.Update(tea.WindowSizeMsg{Width: 100, Height: 50})
50	pumpStreamUntil(t, d, func(msg tea.Msg) bool {
51		event, ok := msg.(tui.ChatStreamEventMsg)
52		return ok && strings.Contains(event.Event.Text, "I fixed main.go")
53	})
54	file := filepath.Join(workspace, "main.go")
55
56	// at is the screen cell of text's third character.
57	at := func(text string) (x, y int) {
58		t.Helper()
59		view := d.View()
60		for y, row := range strings.Split(view, "\n") {
61			if i := strings.Index(row, text); i >= 0 {
62				return len([]rune(row[:i])) + 2, y
63			}
64		}
65		t.Fatalf("no row shows %q:\n%s", text, view)
66		return 0, 0
67	}
68	ctrlClick := func(text string) string {
69		t.Helper()
70		x, y := at(text)
71		_ = os.Remove(opened)
72		d.Update(tea.MouseClickMsg{X: x, Y: y, Button: tea.MouseLeft, Mod: tea.ModCtrl})
73		result := d.Send(tea.MouseReleaseMsg{X: x, Y: y, Button: tea.MouseLeft, Mod: tea.ModCtrl})
74		if done, ok := result.(tui.FileOpenResultMsg); !ok || done.Err != nil {
75			t.Fatalf("ctrl-click on %q did not open a file: %#v\n%s", text, result, d.View())
76		}
77		for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
78			if got, err := os.ReadFile(opened); err == nil {
79				return strings.TrimSpace(string(got))
80			}
81		}
82		t.Fatalf("the editor never ran for %q", text)
83		return ""
84	}
85
86	if got, want := ctrlClick("main.go:3:6"), file+":3:6"; got != want {
87		t.Fatalf("a reply's path opened %q, want %q", got, want)
88	}
89	if got := ctrlClick("edited main.go"); got != file {
90		t.Fatalf("the folded edit row opened %q, want %q", got, file)
91	}
92	// ctrl+j shows diffs, and a diff row names its line
93	d.Dispatch(tea.KeyPressMsg{Code: 'j', Mod: tea.ModCtrl})
94	if got, want := ctrlClick("func main() {}"), file+":3"; got != want {
95		t.Fatalf("a diff row opened %q, want %q", got, want)
96	}
97}