rickub/clipublic Fork
⑂ fe71cc6
Commits
⬇ Clone ▾
git clone https://git.rickub.com/rickub/cli.git
git clone ssh://git@rickub.com/rickub/cli.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

tests: cover clone URL derivation, draft flow, labels, and version outputUnverified

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgkcT3UPxwxukBKojB5eg4
Olivier Girardot committed 2026-09-03T07:26:56+02:00 Browse files
fe71cc6 parent: 3daaa75
added cmd/release_smoke_test.go +236 -0
new file mode 100644
@@ -0,0 +1,236 @@
1+package cmd
2+
3+import (
4+ "encoding/json"
5+ "net/http"
6+ "net/http/httptest"
7+ "runtime"
8+ "strings"
9+ "testing"
10+)
11+
12+// The web host does not speak smart HTTP; git lives on ssh://git@<host> and on
13+// https://git.<host>. Both forms must be derived from the API host, never the
14+// API host itself (issue #1).
15+func TestCloneURLFor(t *testing.T) {
16+ cases := []struct{ host, protocol, want string }{
17+ {"https://rickub.com", "", "ssh://git@rickub.com/rickub/cli.git"},
18+ {"https://rickub.com", "ssh", "ssh://git@rickub.com/rickub/cli.git"},
19+ {"https://rickub.com/", "ssh", "ssh://git@rickub.com/rickub/cli.git"},
20+ {"https://dev.rickub.com", "https", "https://git.dev.rickub.com/rickub/cli.git"},
21+ {"http://localhost:3000", "https", "http://git.localhost:3000/rickub/cli.git"},
22+ {"http://localhost:3000", "ssh", "ssh://git@localhost/rickub/cli.git"},
23+ }
24+ for _, tc := range cases {
25+ got, err := cloneURLFor(tc.host, "rickub", "cli", tc.protocol)
26+ if err != nil {
27+ t.Fatalf("cloneURLFor(%q, %q): %v", tc.host, tc.protocol, err)
28+ }
29+ if got != tc.want {
30+ t.Errorf("cloneURLFor(%q, %q) = %q, want %q", tc.host, tc.protocol, got, tc.want)
31+ }
32+ }
33+ if _, err := cloneURLFor("https://rickub.com", "rickub", "cli", "ftp"); err == nil {
34+ t.Error("unknown protocol accepted")
35+ }
36+ if _, err := cloneURLFor("", "rickub", "cli", "ssh"); err == nil {
37+ t.Error("empty host accepted")
38+ }
39+}
40+
41+// The token reaches git as an extraHeader through GIT_CONFIG_*, appended after
42+// any entries the caller's environment already carries, so nothing of the
43+// user's own git config is clobbered and the token never touches argv.
44+func TestGitAuthEnv(t *testing.T) {
45+ t.Setenv("GIT_CONFIG_COUNT", "")
46+ got := gitAuthEnv("rickub_pat_x")
47+ want := []string{"GIT_CONFIG_COUNT=1", "GIT_CONFIG_KEY_0=http.extraHeader", "GIT_CONFIG_VALUE_0=Authorization: Bearer rickub_pat_x"}
48+ if strings.Join(got, "\n") != strings.Join(want, "\n") {
49+ t.Errorf("gitAuthEnv = %q, want %q", got, want)
50+ }
51+ t.Setenv("GIT_CONFIG_COUNT", "2")
52+ got = gitAuthEnv("rickub_pat_x")
53+ if got[0] != "GIT_CONFIG_COUNT=3" || !strings.HasPrefix(got[1], "GIT_CONFIG_KEY_2=") {
54+ t.Errorf("existing entries not preserved: %q", got)
55+ }
56+}
57+
58+// pr create must say "draft": false out loud — the API's default is a draft,
59+// which `pr merge` then refuses (issue #2) — and --draft flips it.
60+func TestPRCreateSendsDraftExplicitly(t *testing.T) {
61+ var bodies []map[string]any
62+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
63+ switch {
64+ case r.Method == "POST" && r.URL.Path == "/api/v1/repos/rick/portal/merge-requests":
65+ var b map[string]any
66+ _ = json.NewDecoder(r.Body).Decode(&b)
67+ bodies = append(bodies, b)
68+ draft, _ := b["draft"].(bool)
69+ _ = json.NewEncoder(w).Encode(map[string]any{"number": len(bodies), "title": b["title"], "state": "open", "draft": draft})
70+ case r.Method == "POST" && r.URL.Path == "/api/v1/repos/rick/portal/merge-requests/7/draft":
71+ var b map[string]bool
72+ _ = json.NewDecoder(r.Body).Decode(&b)
73+ _ = json.NewEncoder(w).Encode(map[string]any{"number": 7, "state": "open", "draft": b["draft"]})
74+ default:
75+ w.WriteHeader(http.StatusNotFound)
76+ _, _ = w.Write([]byte(`{"error":{"code":"not_found","message":"no"}}`))
77+ }
78+ }))
79+ defer srv.Close()
80+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
81+ t.Setenv("RICKUB_HOST", srv.URL)
82+ t.Setenv("RICKUB_TOKEN", "rickub_pat_test")
83+
84+ out, _, err := execute(t, "pr", "create", "-R", "rick/portal", "--base", "main", "--head", "f", "--title", "T")
85+ if err != nil {
86+ t.Fatalf("pr create: %v", err)
87+ }
88+ if v, ok := bodies[0]["draft"]; !ok || v != false {
89+ t.Fatalf("create body must carry draft=false explicitly, got %v", bodies[0])
90+ }
91+ if !strings.Contains(out, "Opened merge request #1") || strings.Contains(out, "draft") {
92+ t.Errorf("unexpected output for a ready MR:\n%s", out)
93+ }
94+
95+ prDraft = false
96+ out, _, err = execute(t, "pr", "create", "-R", "rick/portal", "--base", "main", "--head", "f", "--title", "T", "--draft")
97+ if err != nil {
98+ t.Fatalf("pr create --draft: %v", err)
99+ }
100+ if v := bodies[1]["draft"]; v != true {
101+ t.Fatalf("--draft must send draft=true, got %v", bodies[1])
102+ }
103+ if !strings.Contains(out, "draft merge request #2") || !strings.Contains(out, "pr ready 2") {
104+ t.Errorf("draft output should say so and point at pr ready:\n%s", out)
105+ }
106+ prDraft = false
107+
108+ prReadyUndo = false
109+ out, _, err = execute(t, "pr", "ready", "7", "-R", "rick/portal")
110+ if err != nil {
111+ t.Fatalf("pr ready: %v", err)
112+ }
113+ if !strings.Contains(out, "#7 is ready for review") {
114+ t.Errorf("pr ready output:\n%s", out)
115+ }
116+ out, _, err = execute(t, "pr", "ready", "7", "-R", "rick/portal", "--undo")
117+ if err != nil {
118+ t.Fatalf("pr ready --undo: %v", err)
119+ }
120+ if !strings.Contains(out, "#7 is a draft again") {
121+ t.Errorf("pr ready --undo output:\n%s", out)
122+ }
123+ prReadyUndo = false
124+}
125+
126+func TestPRListShowsDraft(t *testing.T) {
127+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
128+ _ = json.NewEncoder(w).Encode(map[string]any{
129+ "page": 1, "per_page": 30, "has_next": false,
130+ "items": []map[string]any{
131+ {"number": 1, "title": "ready one", "state": "open", "draft": false, "head_branch": "a", "base_branch": "main", "author": "rick"},
132+ {"number": 2, "title": "draft one", "state": "open", "draft": true, "head_branch": "b", "base_branch": "main", "author": "rick"},
133+ },
134+ })
135+ }))
136+ defer srv.Close()
137+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
138+ t.Setenv("RICKUB_HOST", srv.URL)
139+ t.Setenv("RICKUB_TOKEN", "rickub_pat_test")
140+ out, _, err := execute(t, "pr", "list", "-R", "rick/portal")
141+ if err != nil {
142+ t.Fatalf("pr list: %v", err)
143+ }
144+ lines := strings.Split(strings.TrimSpace(out), "\n")
145+ if len(lines) < 3 || !strings.Contains(lines[1], "open") || !strings.Contains(lines[2], "draft") {
146+ t.Errorf("draft state not rendered:\n%s", out)
147+ }
148+}
149+
150+// Labels are managed by name; create/edit/delete resolve the id through the
151+// list endpoint (issue #3).
152+func TestLabelCommands(t *testing.T) {
153+ var created, patched map[string]any
154+ deleted := ""
155+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
156+ switch {
157+ case r.Method == "GET" && r.URL.Path == "/api/v1/repos/rick/portal/labels":
158+ _ = json.NewEncoder(w).Encode([]map[string]any{{"id": "11111111-1111-4111-8111-111111111111", "name": "Bug", "color": "d73a4a"}})
159+ case r.Method == "POST" && r.URL.Path == "/api/v1/repos/rick/portal/labels":
160+ _ = json.NewDecoder(r.Body).Decode(&created)
161+ w.WriteHeader(http.StatusCreated)
162+ _ = json.NewEncoder(w).Encode(map[string]any{"id": "2", "name": created["name"], "color": "0075ca"})
163+ case r.Method == "PATCH" && r.URL.Path == "/api/v1/repos/rick/portal/labels/11111111-1111-4111-8111-111111111111":
164+ _ = json.NewDecoder(r.Body).Decode(&patched)
165+ _ = json.NewEncoder(w).Encode(map[string]any{"id": "11111111-1111-4111-8111-111111111111", "name": "defect", "color": "ffffff"})
166+ case r.Method == "DELETE" && r.URL.Path == "/api/v1/repos/rick/portal/labels/11111111-1111-4111-8111-111111111111":
167+ deleted = r.URL.Path
168+ w.WriteHeader(http.StatusNoContent)
169+ default:
170+ w.WriteHeader(http.StatusNotFound)
171+ _, _ = w.Write([]byte(`{"error":{"code":"not_found","message":"no"}}`))
172+ }
173+ }))
174+ defer srv.Close()
175+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
176+ t.Setenv("RICKUB_HOST", srv.URL)
177+ t.Setenv("RICKUB_TOKEN", "rickub_pat_test")
178+
179+ labelColor, labelNewName = "", ""
180+ out, _, err := execute(t, "label", "create", "docs", "-R", "rick/portal", "--color", "#0075ca")
181+ if err != nil {
182+ t.Fatalf("label create: %v", err)
183+ }
184+ if created["name"] != "docs" || created["color"] != "0075ca" {
185+ t.Errorf("create body = %v (color must lose its #)", created)
186+ }
187+ if !strings.Contains(out, "Created label docs") {
188+ t.Errorf("create output:\n%s", out)
189+ }
190+
191+ labelColor, labelNewName = "", ""
192+ if _, _, err := execute(t, "label", "edit", "bug", "-R", "rick/portal", "--name", "defect", "--color", "ffffff"); err != nil {
193+ t.Fatalf("label edit (case-insensitive lookup): %v", err)
194+ }
195+ if patched["name"] != "defect" || patched["color"] != "ffffff" {
196+ t.Errorf("patch body = %v", patched)
197+ }
198+
199+ labelColor, labelNewName = "", ""
200+ if _, _, err := execute(t, "label", "edit", "bug", "-R", "rick/portal"); err == nil {
201+ t.Error("edit with nothing to change should fail")
202+ }
203+
204+ if _, _, err := execute(t, "label", "delete", "Bug", "-R", "rick/portal"); err != nil {
205+ t.Fatalf("label delete: %v", err)
206+ }
207+ if deleted == "" {
208+ t.Error("delete never reached the API")
209+ }
210+ if _, _, err := execute(t, "label", "delete", "nope", "-R", "rick/portal"); err == nil || !strings.Contains(err.Error(), "no label named") {
211+ t.Errorf("unknown label should fail by name, got %v", err)
212+ }
213+
214+ out, _, err = execute(t, "label", "list", "-R", "rick/portal")
215+ if err != nil || !strings.Contains(out, "Bug") || !strings.Contains(out, "d73a4a") {
216+ t.Errorf("label list: %v\n%s", err, out)
217+ }
218+}
219+
220+// `rickub --version` and `rickub version` print the same line (issue #6).
221+func TestVersionFlagMatchesSubcommand(t *testing.T) {
222+ sub, _, err := execute(t, "version")
223+ if err != nil {
224+ t.Fatal(err)
225+ }
226+ flag, _, err := execute(t, "--version")
227+ if err != nil {
228+ t.Fatal(err)
229+ }
230+ if sub != flag {
231+ t.Errorf("version outputs differ:\n%q\n%q", sub, flag)
232+ }
233+ if !strings.Contains(sub, runtime.GOOS+"/"+runtime.GOARCH) {
234+ t.Errorf("version line lacks os/arch: %q", sub)
235+ }
236+}
new file mode 100644
@@ -0,0 +1,236 @@
1+package cmd
2+
3+import (
4+ "encoding/json"
5+ "net/http"
6+ "net/http/httptest"
7+ "runtime"
8+ "strings"
9+ "testing"
10+)
11+
12+// The web host does not speak smart HTTP; git lives on ssh://git@<host> and on
13+// https://git.<host>. Both forms must be derived from the API host, never the
14+// API host itself (issue #1).
15+func TestCloneURLFor(t *testing.T) {
16+ cases := []struct{ host, protocol, want string }{
17+ {"https://rickub.com", "", "ssh://git@rickub.com/rickub/cli.git"},
18+ {"https://rickub.com", "ssh", "ssh://git@rickub.com/rickub/cli.git"},
19+ {"https://rickub.com/", "ssh", "ssh://git@rickub.com/rickub/cli.git"},
20+ {"https://dev.rickub.com", "https", "https://git.dev.rickub.com/rickub/cli.git"},
21+ {"http://localhost:3000", "https", "http://git.localhost:3000/rickub/cli.git"},
22+ {"http://localhost:3000", "ssh", "ssh://git@localhost/rickub/cli.git"},
23+ }
24+ for _, tc := range cases {
25+ got, err := cloneURLFor(tc.host, "rickub", "cli", tc.protocol)
26+ if err != nil {
27+ t.Fatalf("cloneURLFor(%q, %q): %v", tc.host, tc.protocol, err)
28+ }
29+ if got != tc.want {
30+ t.Errorf("cloneURLFor(%q, %q) = %q, want %q", tc.host, tc.protocol, got, tc.want)
31+ }
32+ }
33+ if _, err := cloneURLFor("https://rickub.com", "rickub", "cli", "ftp"); err == nil {
34+ t.Error("unknown protocol accepted")
35+ }
36+ if _, err := cloneURLFor("", "rickub", "cli", "ssh"); err == nil {
37+ t.Error("empty host accepted")
38+ }
39+}
40+
41+// The token reaches git as an extraHeader through GIT_CONFIG_*, appended after
42+// any entries the caller's environment already carries, so nothing of the
43+// user's own git config is clobbered and the token never touches argv.
44+func TestGitAuthEnv(t *testing.T) {
45+ t.Setenv("GIT_CONFIG_COUNT", "")
46+ got := gitAuthEnv("rickub_pat_x")
47+ want := []string{"GIT_CONFIG_COUNT=1", "GIT_CONFIG_KEY_0=http.extraHeader", "GIT_CONFIG_VALUE_0=Authorization: Bearer rickub_pat_x"}
48+ if strings.Join(got, "\n") != strings.Join(want, "\n") {
49+ t.Errorf("gitAuthEnv = %q, want %q", got, want)
50+ }
51+ t.Setenv("GIT_CONFIG_COUNT", "2")
52+ got = gitAuthEnv("rickub_pat_x")
53+ if got[0] != "GIT_CONFIG_COUNT=3" || !strings.HasPrefix(got[1], "GIT_CONFIG_KEY_2=") {
54+ t.Errorf("existing entries not preserved: %q", got)
55+ }
56+}
57+
58+// pr create must say "draft": false out loud — the API's default is a draft,
59+// which `pr merge` then refuses (issue #2) — and --draft flips it.
60+func TestPRCreateSendsDraftExplicitly(t *testing.T) {
61+ var bodies []map[string]any
62+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
63+ switch {
64+ case r.Method == "POST" && r.URL.Path == "/api/v1/repos/rick/portal/merge-requests":
65+ var b map[string]any
66+ _ = json.NewDecoder(r.Body).Decode(&b)
67+ bodies = append(bodies, b)
68+ draft, _ := b["draft"].(bool)
69+ _ = json.NewEncoder(w).Encode(map[string]any{"number": len(bodies), "title": b["title"], "state": "open", "draft": draft})
70+ case r.Method == "POST" && r.URL.Path == "/api/v1/repos/rick/portal/merge-requests/7/draft":
71+ var b map[string]bool
72+ _ = json.NewDecoder(r.Body).Decode(&b)
73+ _ = json.NewEncoder(w).Encode(map[string]any{"number": 7, "state": "open", "draft": b["draft"]})
74+ default:
75+ w.WriteHeader(http.StatusNotFound)
76+ _, _ = w.Write([]byte(`{"error":{"code":"not_found","message":"no"}}`))
77+ }
78+ }))
79+ defer srv.Close()
80+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
81+ t.Setenv("RICKUB_HOST", srv.URL)
82+ t.Setenv("RICKUB_TOKEN", "rickub_pat_test")
83+
84+ out, _, err := execute(t, "pr", "create", "-R", "rick/portal", "--base", "main", "--head", "f", "--title", "T")
85+ if err != nil {
86+ t.Fatalf("pr create: %v", err)
87+ }
88+ if v, ok := bodies[0]["draft"]; !ok || v != false {
89+ t.Fatalf("create body must carry draft=false explicitly, got %v", bodies[0])
90+ }
91+ if !strings.Contains(out, "Opened merge request #1") || strings.Contains(out, "draft") {
92+ t.Errorf("unexpected output for a ready MR:\n%s", out)
93+ }
94+
95+ prDraft = false
96+ out, _, err = execute(t, "pr", "create", "-R", "rick/portal", "--base", "main", "--head", "f", "--title", "T", "--draft")
97+ if err != nil {
98+ t.Fatalf("pr create --draft: %v", err)
99+ }
100+ if v := bodies[1]["draft"]; v != true {
101+ t.Fatalf("--draft must send draft=true, got %v", bodies[1])
102+ }
103+ if !strings.Contains(out, "draft merge request #2") || !strings.Contains(out, "pr ready 2") {
104+ t.Errorf("draft output should say so and point at pr ready:\n%s", out)
105+ }
106+ prDraft = false
107+
108+ prReadyUndo = false
109+ out, _, err = execute(t, "pr", "ready", "7", "-R", "rick/portal")
110+ if err != nil {
111+ t.Fatalf("pr ready: %v", err)
112+ }
113+ if !strings.Contains(out, "#7 is ready for review") {
114+ t.Errorf("pr ready output:\n%s", out)
115+ }
116+ out, _, err = execute(t, "pr", "ready", "7", "-R", "rick/portal", "--undo")
117+ if err != nil {
118+ t.Fatalf("pr ready --undo: %v", err)
119+ }
120+ if !strings.Contains(out, "#7 is a draft again") {
121+ t.Errorf("pr ready --undo output:\n%s", out)
122+ }
123+ prReadyUndo = false
124+}
125+
126+func TestPRListShowsDraft(t *testing.T) {
127+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
128+ _ = json.NewEncoder(w).Encode(map[string]any{
129+ "page": 1, "per_page": 30, "has_next": false,
130+ "items": []map[string]any{
131+ {"number": 1, "title": "ready one", "state": "open", "draft": false, "head_branch": "a", "base_branch": "main", "author": "rick"},
132+ {"number": 2, "title": "draft one", "state": "open", "draft": true, "head_branch": "b", "base_branch": "main", "author": "rick"},
133+ },
134+ })
135+ }))
136+ defer srv.Close()
137+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
138+ t.Setenv("RICKUB_HOST", srv.URL)
139+ t.Setenv("RICKUB_TOKEN", "rickub_pat_test")
140+ out, _, err := execute(t, "pr", "list", "-R", "rick/portal")
141+ if err != nil {
142+ t.Fatalf("pr list: %v", err)
143+ }
144+ lines := strings.Split(strings.TrimSpace(out), "\n")
145+ if len(lines) < 3 || !strings.Contains(lines[1], "open") || !strings.Contains(lines[2], "draft") {
146+ t.Errorf("draft state not rendered:\n%s", out)
147+ }
148+}
149+
150+// Labels are managed by name; create/edit/delete resolve the id through the
151+// list endpoint (issue #3).
152+func TestLabelCommands(t *testing.T) {
153+ var created, patched map[string]any
154+ deleted := ""
155+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
156+ switch {
157+ case r.Method == "GET" && r.URL.Path == "/api/v1/repos/rick/portal/labels":
158+ _ = json.NewEncoder(w).Encode([]map[string]any{{"id": "11111111-1111-4111-8111-111111111111", "name": "Bug", "color": "d73a4a"}})
159+ case r.Method == "POST" && r.URL.Path == "/api/v1/repos/rick/portal/labels":
160+ _ = json.NewDecoder(r.Body).Decode(&created)
161+ w.WriteHeader(http.StatusCreated)
162+ _ = json.NewEncoder(w).Encode(map[string]any{"id": "2", "name": created["name"], "color": "0075ca"})
163+ case r.Method == "PATCH" && r.URL.Path == "/api/v1/repos/rick/portal/labels/11111111-1111-4111-8111-111111111111":
164+ _ = json.NewDecoder(r.Body).Decode(&patched)
165+ _ = json.NewEncoder(w).Encode(map[string]any{"id": "11111111-1111-4111-8111-111111111111", "name": "defect", "color": "ffffff"})
166+ case r.Method == "DELETE" && r.URL.Path == "/api/v1/repos/rick/portal/labels/11111111-1111-4111-8111-111111111111":
167+ deleted = r.URL.Path
168+ w.WriteHeader(http.StatusNoContent)
169+ default:
170+ w.WriteHeader(http.StatusNotFound)
171+ _, _ = w.Write([]byte(`{"error":{"code":"not_found","message":"no"}}`))
172+ }
173+ }))
174+ defer srv.Close()
175+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
176+ t.Setenv("RICKUB_HOST", srv.URL)
177+ t.Setenv("RICKUB_TOKEN", "rickub_pat_test")
178+
179+ labelColor, labelNewName = "", ""
180+ out, _, err := execute(t, "label", "create", "docs", "-R", "rick/portal", "--color", "#0075ca")
181+ if err != nil {
182+ t.Fatalf("label create: %v", err)
183+ }
184+ if created["name"] != "docs" || created["color"] != "0075ca" {
185+ t.Errorf("create body = %v (color must lose its #)", created)
186+ }
187+ if !strings.Contains(out, "Created label docs") {
188+ t.Errorf("create output:\n%s", out)
189+ }
190+
191+ labelColor, labelNewName = "", ""
192+ if _, _, err := execute(t, "label", "edit", "bug", "-R", "rick/portal", "--name", "defect", "--color", "ffffff"); err != nil {
193+ t.Fatalf("label edit (case-insensitive lookup): %v", err)
194+ }
195+ if patched["name"] != "defect" || patched["color"] != "ffffff" {
196+ t.Errorf("patch body = %v", patched)
197+ }
198+
199+ labelColor, labelNewName = "", ""
200+ if _, _, err := execute(t, "label", "edit", "bug", "-R", "rick/portal"); err == nil {
201+ t.Error("edit with nothing to change should fail")
202+ }
203+
204+ if _, _, err := execute(t, "label", "delete", "Bug", "-R", "rick/portal"); err != nil {
205+ t.Fatalf("label delete: %v", err)
206+ }
207+ if deleted == "" {
208+ t.Error("delete never reached the API")
209+ }
210+ if _, _, err := execute(t, "label", "delete", "nope", "-R", "rick/portal"); err == nil || !strings.Contains(err.Error(), "no label named") {
211+ t.Errorf("unknown label should fail by name, got %v", err)
212+ }
213+
214+ out, _, err = execute(t, "label", "list", "-R", "rick/portal")
215+ if err != nil || !strings.Contains(out, "Bug") || !strings.Contains(out, "d73a4a") {
216+ t.Errorf("label list: %v\n%s", err, out)
217+ }
218+}
219+
220+// `rickub --version` and `rickub version` print the same line (issue #6).
221+func TestVersionFlagMatchesSubcommand(t *testing.T) {
222+ sub, _, err := execute(t, "version")
223+ if err != nil {
224+ t.Fatal(err)
225+ }
226+ flag, _, err := execute(t, "--version")
227+ if err != nil {
228+ t.Fatal(err)
229+ }
230+ if sub != flag {
231+ t.Errorf("version outputs differ:\n%q\n%q", sub, flag)
232+ }
233+ if !strings.Contains(sub, runtime.GOOS+"/"+runtime.GOARCH) {
234+ t.Errorf("version line lacks os/arch: %q", sub)
235+ }
236+}