okami.mom/albedo

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

nekomimi.pettest(e2e): run session-scoped Go scenarios in parallel5b3f8be

main
10.8 KiB258 linesraw
1//go:build unix
2
3// Slash shortcuts must change the same real resources as their page actions,
4// retaining workspace boundaries, observed validators, and delete confirmation.
5package e2e
6
7import (
8	"encoding/json"
9	"errors"
10	"net/http"
11	"slices"
12	"strings"
13	"testing"
14
15	"albedo/cli/internal/daemon"
16	"albedo/cli/internal/daemon/protocol"
17	"albedo/cli/internal/tui"
18
19	tea "charm.land/bubbletea/v2"
20)
21
22func executeShortcut(t *testing.T, session, command, arguments string) daemon.CommandResult {
23	t.Helper()
24	prepared, err := daemon.PreparePageShortcut(t.Context(), conn(t), session, command, arguments)
25	if err != nil {
26		t.Fatalf("prepare %s %s: %v", command, arguments, err)
27	}
28	result, err := daemon.ExecutePageAction(t.Context(), conn(t), prepared.Request)
29	if err != nil {
30		t.Fatalf("execute %s %s: %v", command, arguments, err)
31	}
32	return result
33}
34
35func shortcutWorkItem(t *testing.T, result daemon.CommandResult) protocol.WorkItem {
36	t.Helper()
37	var changed protocol.WorkChange
38	if err := json.Unmarshal(result.Result, &changed); err != nil || changed.Resource.Value.ID == "" {
39		t.Fatalf("work change: %s, %v", result.Result, err)
40	}
41	return changed.Resource.Value
42}
43
44func shortcutPaperclip(t *testing.T, result daemon.CommandResult) protocol.Paperclip {
45	t.Helper()
46	var changed protocol.PaperclipChange
47	if err := json.Unmarshal(result.Result, &changed); err != nil || changed.Resource.Value.ID == "" {
48		t.Fatalf("paperclip change: %s, %v", result.Result, err)
49	}
50	return changed.Resource.Value
51}
52
53func createShortcutPaperclip(t *testing.T, session string) protocol.Paperclip {
54	t.Helper()
55	page, err := daemon.LoadPage(t.Context(), conn(t), session, "/paperclips")
56	if err != nil {
57		t.Fatal(err)
58	}
59	index := slices.IndexFunc(page.Actions, func(action daemon.PageAction) bool { return action.ID == "create" })
60	if index < 0 {
61		t.Fatal("paperclips page has no create action")
62	}
63	result, err := daemon.ExecutePageAction(t.Context(), conn(t), daemon.PageActionRequest{
64		Action: page.Actions[index], Session: page.Session,
65		Form: map[string]json.RawMessage{"message": json.RawMessage(`"shortcut fixture"`)},
66	})
67	if err != nil {
68		t.Fatal(err)
69	}
70	return shortcutPaperclip(t, result)
71}
72
73func TestWorkShortcutsPreserveOtherFieldsAndWorkspaceScope(t *testing.T) {
74	providerRoute(t, echoReply)
75	t.Parallel()
76	session := newSession(t, t.TempDir())
77	item := shortcutWorkItem(t, executeShortcut(t, session, "/work", "add a multiword title"))
78	if item.Title != "a multiword title" || item.Status != "open" {
79		t.Fatalf("add shortcut changed the wrong fields: %+v", item)
80	}
81	prepared, err := daemon.PreparePageShortcut(t.Context(), conn(t), session, "/work", "edit "+item.ID+" revised title")
82	if err != nil {
83		t.Fatal(err)
84	}
85	prepared.Request.Form["notes"] = json.RawMessage(`"keep these notes"`)
86	result, err := daemon.ExecutePageAction(t.Context(), conn(t), prepared.Request)
87	if err != nil {
88		t.Fatal(err)
89	}
90	item = shortcutWorkItem(t, result)
91	if item.Title != "revised title" || item.Notes != "keep these notes" {
92		t.Fatalf("edit shortcut: %+v", item)
93	}
94	item = shortcutWorkItem(t, executeShortcut(t, session, "/work", "status "+item.ID+" blocked"))
95	if item.Status != "blocked" || item.Title != "revised title" || item.Notes != "keep these notes" {
96		t.Fatalf("status shortcut lost the other fields: %+v", item)
97	}
98	item = shortcutWorkItem(t, executeShortcut(t, session, "/work", "edit "+item.ID+" final title"))
99	if item.Title != "final title" || item.Status != "blocked" || item.Notes != "keep these notes" {
100		t.Fatalf("title shortcut lost the other fields: %+v", item)
101	}
102	other := newSession(t, t.TempDir())
103	_, err = daemon.PreparePageShortcut(t.Context(), conn(t), other, "/work", "edit "+item.ID+" crossed workspace")
104	problem, ok := errors.AsType[*daemon.APIError](err)
105	if !ok || problem.StatusCode != http.StatusNotFound {
106		t.Fatalf("shortcut reached another workspace's item: %v", err)
107	}
108	unchanged, err := daemon.PreparePageShortcut(t.Context(), conn(t), session, "/work", "edit "+item.ID+" untouched")
109	if err != nil || unchanged.Request.Row.Text != "final title" {
110		t.Fatalf("foreign workspace shortcut changed the item: %+v, %v", unchanged, err)
111	}
112	if status := daemonSession(t, session).Status; status.KernelInstanceID != nil {
113		t.Fatalf("HTTP page shortcuts opened a kernel: %+v", status)
114	}
115}
116
117func TestPaperclipShortcutsApplyDeclaredStatusAndReplyActions(t *testing.T) {
118	providerRoute(t, echoReply)
119	t.Parallel()
120	session := newSession(t, t.TempDir())
121	item := createShortcutPaperclip(t, session)
122	for _, scenario := range []struct {
123		arguments, status, reply, resolution string
124	}{
125		{"acknowledge " + item.ID, "acknowledged", "", ""},
126		{"reply " + item.ID + " a multiword reply", "acknowledged", "a multiword reply", ""},
127		{"resolve " + item.ID + " a multiword resolution", "resolved", "a multiword reply", "a multiword resolution"},
128		{"dismiss " + item.ID, "dismissed", "a multiword reply", "a multiword resolution"},
129		{"resolve " + item.ID, "resolved", "a multiword reply", "a multiword resolution"},
130	} {
131		item = shortcutPaperclip(t, executeShortcut(t, session, "/paperclips", scenario.arguments))
132		if item.Status != scenario.status || item.Reply != scenario.reply || item.Resolution != scenario.resolution {
133			t.Fatalf("%s changed the wrong fields: %+v", scenario.arguments, item)
134		}
135	}
136}
137
138func TestPageShortcutsRejectInvalidArgumentsWithoutChangingResources(t *testing.T) {
139	providerRoute(t, echoReply)
140	t.Parallel()
141	session := newSession(t, t.TempDir())
142	work := shortcutWorkItem(t, executeShortcut(t, session, "/work", "add unchanged work"))
143	clip := createShortcutPaperclip(t, session)
144	for _, scenario := range []struct{ command, arguments string }{
145		{"/work", "add"},
146		{"/work", "edit"},
147		{"/work", "edit 0 title"},
148		{"/work", "edit invalid title"},
149		{"/work", "status " + work.ID},
150		{"/work", "status " + work.ID + " unsupported"},
151		{"/work", "remove " + work.ID + " extra"},
152		{"/work", "unknown " + work.ID},
153		{"/paperclips", "reply " + clip.ID},
154		{"/paperclips", "acknowledge"},
155		{"/paperclips", "dismiss " + clip.ID + " extra"},
156		{"/paperclips", "remove -1"},
157	} {
158		if _, err := daemon.PreparePageShortcut(t.Context(), conn(t), session, scenario.command, scenario.arguments); err == nil {
159			t.Fatalf("accepted invalid shortcut %s %s", scenario.command, scenario.arguments)
160		}
161	}
162	for _, scenario := range []struct{ command, arguments, title, badge string }{
163		{"/work", "edit " + work.ID + " unsubmitted", "unchanged work", "open"},
164		{"/paperclips", "acknowledge " + clip.ID, "shortcut fixture", "open"},
165	} {
166		prepared, err := daemon.PreparePageShortcut(t.Context(), conn(t), session, scenario.command, scenario.arguments)
167		if err != nil || prepared.Request.Row.Text != scenario.title || prepared.Request.Row.Badge != scenario.badge {
168			t.Fatalf("invalid arguments changed a resource: %+v, %v", prepared, err)
169		}
170	}
171}
172
173func TestWorkShortcutFindsOffPageTargetAndRejectsStaleValidator(t *testing.T) {
174	providerRoute(t, echoReply)
175	t.Parallel()
176	session := newSession(t, t.TempDir())
177	creation, err := daemon.PreparePageShortcut(t.Context(), conn(t), session, "/work", "add paginated fixture")
178	if err != nil {
179		t.Fatal(err)
180	}
181	var target protocol.WorkItem
182	for range 51 {
183		result, err := daemon.ExecutePageAction(t.Context(), conn(t), creation.Request)
184		if err != nil {
185			t.Fatal(err)
186		}
187		target = shortcutWorkItem(t, result)
188	}
189	page, err := daemon.LoadPage(t.Context(), conn(t), session, "/work")
190	if err != nil {
191		t.Fatal(err)
192	}
193	if slices.ContainsFunc(page.Rows, func(row daemon.PageRow) bool { return row.ID == target.ID }) {
194		t.Fatal("fixture did not place its target beyond the first page")
195	}
196	stale, err := daemon.PreparePageShortcut(t.Context(), conn(t), session, "/work", "edit "+target.ID+" stale title")
197	if err != nil || stale.Request.Row.ID != target.ID || !slices.ContainsFunc(stale.Page.Rows, func(row daemon.PageRow) bool { return row.ID == target.ID }) {
198		t.Fatalf("off-page target was not captured: %+v, %v", stale, err)
199	}
200	target = shortcutWorkItem(t, executeShortcut(t, session, "/work", "edit "+target.ID+" newer title"))
201	_, err = daemon.ExecutePageAction(t.Context(), conn(t), stale.Request)
202	problem, ok := errors.AsType[*daemon.APIError](err)
203	if !ok || problem.StatusCode != http.StatusPreconditionFailed {
204		t.Fatalf("stale shortcut overwrote a newer edit: %v", err)
205	}
206	current, err := daemon.PreparePageShortcut(t.Context(), conn(t), session, "/work", "edit "+target.ID+" unsubmitted")
207	if err != nil || current.Request.Row.Text != "newer title" {
208		t.Fatalf("stale edit changed the item: %+v, %v", current, err)
209	}
210}
211
212func TestTUIPageRemoveShortcutConfirmsOrCancels(t *testing.T) {
213	providerRoute(t, echoReply)
214	t.Parallel()
215	session := daemonSession(t, newSession(t, t.TempDir()))
216	driver := driveTUI(t, &session)
217	t.Cleanup(func() { driver.App.Chat.Close() })
218	driver.connected()
219	commands, err := daemon.ListSessionCommands(t.Context(), conn(t), session.ID)
220	if err != nil {
221		t.Fatal(err)
222	}
223	driver.App.Chat.CommandMenu.Catalog = commands
224	driver.App.Chat.TextArea.SetValue("/work add confirmed work removal")
225	driver.Dispatch(tea.KeyPressMsg{Code: tea.KeyEnter})
226	if driver.App.State != tui.AppStatePageView || driver.App.PageView.Error != "" || driver.App.PageView.Doc == nil || len(driver.App.PageView.Doc.Rows) != 1 {
227		t.Fatalf("composer shortcut did not create a work item:\n%s", driver.View())
228	}
229	workID := driver.App.PageView.Doc.Rows[0].ID
230	clip := createShortcutPaperclip(t, session.ID)
231	for _, scenario := range []struct{ command, id string }{{"/work", workID}, {"/paperclips", clip.ID}} {
232		remove := tui.ChatExecuteCommandMsg{Name: scenario.command, Args: "remove " + scenario.id}
233		driver.Dispatch(remove)
234		if driver.App.State != tui.AppStatePageView || driver.App.PageView.CurrentAction == nil || !driver.App.PageView.CurrentAction.Confirm || !strings.Contains(driver.View(), "Delete this item?") {
235			t.Fatalf("remove skipped confirmation for %s:\n%s", scenario.command, driver.View())
236		}
237		if _, err := daemon.PreparePageShortcut(t.Context(), conn(t), session.ID, scenario.command, "remove "+scenario.id); err != nil {
238			t.Fatalf("item disappeared before confirmation: %v", err)
239		}
240		driver.Dispatch(tea.KeyPressMsg{Code: tea.KeyEscape})
241		if driver.App.PageView.CurrentAction != nil {
242			t.Fatal("cancelled removal retained its action")
243		}
244		if _, err := daemon.PreparePageShortcut(t.Context(), conn(t), session.ID, scenario.command, "remove "+scenario.id); err != nil {
245			t.Fatalf("Escape removed the item: %v", err)
246		}
247		driver.Dispatch(remove)
248		driver.Dispatch(tea.KeyPressMsg{Code: tea.KeyEnter})
249		if driver.App.PageView.Error != "" {
250			t.Fatalf("confirmed removal failed: %s", driver.App.PageView.Error)
251		}
252		_, err := daemon.PreparePageShortcut(t.Context(), conn(t), session.ID, scenario.command, "remove "+scenario.id)
253		problem, ok := errors.AsType[*daemon.APIError](err)
254		if !ok || problem.StatusCode != http.StatusNotFound {
255			t.Fatalf("confirmed %s item %s still exists: %v", scenario.command, scenario.id, err)
256		}
257	}
258}