v0.28.1 cc7ba64

cli_issue_e2e_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6	"strings"
7	"testing"
8
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/require"
11
12	"bnema.dev/ermite-api/gen/ermite"
13)
14
15type issueOut struct {
16	Repo         string  `json:"repo"`
17	Number       int64   `json:"number"`
18	Title        string  `json:"title"`
19	Body         string  `json:"body"`
20	State        string  `json:"state"`
21	Author       string  `json:"author"`
22	CreatedAt    *string `json:"created_at"`
23	UpdatedAt    *string `json:"updated_at"`
24	CommentCount int     `json:"comment_count"`
25}
26
27var issueKeys = []string{"author", "body", "comment_count", "created_at", "number", "repo", "state", "title", "updated_at"}
28
29type issueCommentOut struct {
30	ID     string `json:"id"`
31	Author string `json:"author"`
32	Text   string `json:"text"`
33}
34
35// The issue commands against a real forge: issues share the numbering of the
36// merge requests.
37func TestIssuesCLIEndToEnd(t *testing.T) {
38	c := newCLIHarness(t)
39	agent := strings.TrimSpace(func() string {
40		out, errOut, code := c.bin(t, adminToken, "token", "create", "agent-7", "--preset", "agent", "--scope", "issue:write", "--repo", "proj")
41		require.Equal(t, 0, code, errOut)
42		return out
43	}())
44	c.cli(t, adminToken, "repo", "create", "proj").ok(t)
45
46	// Issues are off by default.
47	res := c.cli(t, adminToken, "repo", "show", "proj", "--json").ok(t)
48	assert.Equal(t, "off", decode[struct {
49		Issues string `json:"issues"`
50	}](t, res.stdout).Issues)
51	res = c.cli(t, adminToken, "issue", "create", "proj", "--title", "too early")
52	require.Error(t, res.err)
53
54	// repo edit --issues.
55	res = c.cli(t, adminToken, "repo", "edit", "proj", "--issues", "owner", "--json").ok(t)
56	assert.Equal(t, "owner", decode[struct {
57		Issues string `json:"issues"`
58	}](t, res.stdout).Issues)
59	res = c.cli(t, adminToken, "repo", "show", "proj").ok(t)
60	assert.Regexp(t, `issues\s+owner`, res.stdout)
61	require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--issues", "everyone").err)
62	res = c.cli(t, adminToken, "repo", "edit", "proj", "--issues", "public", "--json").ok(t)
63	assert.Equal(t, "public", decode[struct {
64		Issues string `json:"issues"`
65	}](t, res.stdout).Issues)
66	c.cli(t, adminToken, "repo", "edit", "proj", "--issues", "owner").ok(t)
67
68	// A merge request first: the issue takes the next number.
69	wt := c.workTree("main", "README.md")
70	url := c.urlAuth(adminToken, "/proj.git")
71	c.mustGit(wt, "push", "-q", url, "main")
72	c.mustGit(wt, "checkout", "-q", "-b", "feat/x")
73	c.writeFile(wt, "x.txt", []byte("x\n"))
74	c.mustGit(wt, "push", "-q", url, "feat/x")
75	c.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/x", "--title", "First").ok(t)
76
77	res = c.cli(t, agent, "issue", "create", "proj", "--title", "Crash on start", "--body", "It crashes.\nSecond line.", "--json").ok(t)
78	assert.Equal(t, issueKeys, keys(t, res.stdout))
79	issue := decode[issueOut](t, res.stdout)
80	assert.EqualValues(t, 2, issue.Number, "shared numbering with merge requests")
81	assert.Equal(t, "open", issue.State)
82	assert.Equal(t, "agent-7", issue.Author)
83	assert.Equal(t, "It crashes.\nSecond line.", issue.Body)
84
85	res = runCLI(t, c.vars(agent), "from stdin\n", "issue", "create", "proj", "--title", "Second", "--body-file", "-").ok(t)
86	assert.Equal(t, "opened proj#3 (Second)\n", res.stdout)
87
88	// list.
89	res = c.cli(t, adminToken, "issue", "list", "proj", "--json").ok(t)
90	list := decode[[]issueOut](t, res.stdout)
91	require.Len(t, list, 2)
92	assert.EqualValues(t, 3, list[0].Number, "highest number first")
93	res = c.cli(t, adminToken, "issue", "list", "proj").ok(t)
94	lines := strings.Split(strings.TrimSpace(res.stdout), "\n")
95	require.Len(t, lines, 3)
96	assert.Regexp(t, `^NUMBER\s+STATE\s+AUTHOR\s+COMMENTS\s+UPDATED\s+TITLE$`, lines[0])
97	assert.Regexp(t, `^#3\s+open\s+agent-7\s+0\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}\s+Second$`, lines[1])
98
99	// comment, then show with it.
100	res = c.cli(t, agent, "issue", "comment", "proj", "2", "-m", "Reproduced.", "--json").ok(t)
101	cm := decode[issueCommentOut](t, res.stdout)
102	assert.Equal(t, "agent-7", cm.Author)
103	assert.Equal(t, "Reproduced.", cm.Text)
104	res = c.cli(t, adminToken, "issue", "comment", "proj", "#2", "-m", "Thanks.").ok(t)
105	assert.Regexp(t, `^comment \S+ added to proj#2\n$`, res.stdout)
106	res = c.cli(t, adminToken, "issue", "show", "proj", "2", "--json").ok(t)
107	type showOut struct {
108		issueOut
109		Comments []issueCommentOut `json:"comments"`
110	}
111	show := decode[showOut](t, res.stdout)
112	assert.Equal(t, 2, show.CommentCount)
113	require.Len(t, show.Comments, 2)
114	assert.Equal(t, "Reproduced.", show.Comments[0].Text, "oldest first")
115	res = c.cli(t, adminToken, "issue", "show", "proj", "2").ok(t)
116	assert.Contains(t, res.stdout, "Crash on start")
117	assert.Contains(t, res.stdout, "comments (2)")
118
119	// close and reopen.
120	res = c.cli(t, agent, "issue", "close", "proj", "2", "--json").ok(t)
121	assert.Equal(t, "closed", decode[issueOut](t, res.stdout).State)
122	res = c.cli(t, adminToken, "issue", "list", "proj", "--json").ok(t)
123	assert.Len(t, decode[[]issueOut](t, res.stdout), 1)
124	res = c.cli(t, adminToken, "issue", "list", "proj", "--state", "closed", "--json").ok(t)
125	assert.Len(t, decode[[]issueOut](t, res.stdout), 1)
126	res = c.cli(t, adminToken, "issue", "list", "proj", "--state", "all", "--json").ok(t)
127	assert.Len(t, decode[[]issueOut](t, res.stdout), 2)
128	res = c.cli(t, agent, "issue", "reopen", "proj", "2").ok(t)
129	assert.Equal(t, "reopened proj#2 (Crash on start)\n", res.stdout)
130
131	// An issue is not a merge request and the other way round.
132	_, stderr, code := c.bin(t, adminToken, "issue", "show", "proj", "1")
133	requireOneLineError(t, stderr, code, "not found")
134	_, stderr, code = c.bin(t, adminToken, "issue", "show", "proj", "9")
135	requireOneLineError(t, stderr, code, "not found")
136}
137
138// A server without the issues feature is refused before anything is created.
139func TestIssueWriteTokenRefusedWithoutTheFeature(t *testing.T) {
140	f := &fakeAPI{
141		getVersion: func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) {
142			return &ermite.GetVersionResponse{Version: "v0.4.0", Features: []string{"merge_requests"}}, nil
143		},
144		createToken: func(*ermite.CreateTokenRequest) (*ermite.CreateTokenResponse, error) {
145			return &ermite.CreateTokenResponse{Token: &ermite.Token{Id: "t"}, Secret: "ermite_S"}, nil
146		},
147	}
148	res := runCLI(t, envFor(f.serve(t)), "", "token", "create", "bot", "--preset", "agent", "--scope", "issue:write")
149	require.Error(t, res.err)
150	assert.Contains(t, res.err.Error(), "does not support issues")
151	assert.Empty(t, res.stdout)
152	calls, _ := f.snapshot()
153	assert.NotContains(t, calls, "CreateToken")
154}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 44

↑↓ moveenter openesc close