Skip to content

Commit 12f637d

Browse files
isaacrowntreeclaude
andcommitted
fix: review findings — add missing tests, fix consistency issues
- Add 9 missing tests: chat reply/react/delete, goal view/delete, view get, template use, task move error path - Normalize PreRunE → PersistentPreRunE across 12 new command files - Fix view get help text (always outputs JSON — documented) - Wire up view list --team flag (was a no-op) - Show created goal ID in goal create output - All 25 test packages pass Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5bf9ad3 commit 12f637d

17 files changed

Lines changed: 269 additions & 26 deletions

File tree

pkg/cmd/chat/chat_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,108 @@ func TestChatMessages_RequiresChannelArg(t *testing.T) {
251251
assert.Error(t, cmd.Args(cmd, []string{}))
252252
assert.NoError(t, cmd.Args(cmd, []string{"chan-id"}))
253253
}
254+
255+
// ── chat reply ──────────────────────────────────────────────────────
256+
257+
func TestChatReply(t *testing.T) {
258+
tf := testutil.NewTestFactory(t)
259+
260+
var capturedBody map[string]interface{}
261+
var capturedMethod string
262+
var capturedPath string
263+
264+
tf.HandleFuncV3("workspaces/12345/chat/messages/msg-abc/replies", func(w http.ResponseWriter, r *http.Request) {
265+
capturedMethod = r.Method
266+
capturedPath = r.URL.Path
267+
body, _ := io.ReadAll(r.Body)
268+
json.Unmarshal(body, &capturedBody)
269+
w.Header().Set("Content-Type", "application/json")
270+
w.Header().Set("X-RateLimit-Remaining", "99")
271+
w.WriteHeader(200)
272+
w.Write([]byte(`{
273+
"content": "Got it!",
274+
"type": "message",
275+
"date": 1700000000000,
276+
"user_id": "user1",
277+
"resolved": false,
278+
"links": {"reactions": "", "replies": "", "tagged_users": ""},
279+
"replies_count": 0
280+
}`))
281+
})
282+
283+
cmd := NewCmdReply(tf.Factory)
284+
err := testutil.RunCommand(t, cmd, "msg-abc", "Got it!")
285+
require.NoError(t, err)
286+
287+
assert.Equal(t, "POST", capturedMethod)
288+
assert.Contains(t, capturedPath, "/workspaces/12345/chat/messages/msg-abc/replies")
289+
assert.Equal(t, "message", capturedBody["type"])
290+
assert.Equal(t, "Got it!", capturedBody["content"])
291+
292+
out := tf.OutBuf.String()
293+
assert.Contains(t, out, "Reply sent")
294+
assert.Contains(t, out, "msg-abc")
295+
}
296+
297+
// ── chat react ──────────────────────────────────────────────────────
298+
299+
func TestChatReact(t *testing.T) {
300+
tf := testutil.NewTestFactory(t)
301+
302+
var capturedBody map[string]interface{}
303+
var capturedMethod string
304+
var capturedPath string
305+
306+
tf.HandleFuncV3("workspaces/12345/chat/messages/msg-abc/reactions", func(w http.ResponseWriter, r *http.Request) {
307+
capturedMethod = r.Method
308+
capturedPath = r.URL.Path
309+
body, _ := io.ReadAll(r.Body)
310+
json.Unmarshal(body, &capturedBody)
311+
w.Header().Set("Content-Type", "application/json")
312+
w.Header().Set("X-RateLimit-Remaining", "99")
313+
w.WriteHeader(200)
314+
w.Write([]byte(`{}`))
315+
})
316+
317+
cmd := NewCmdReact(tf.Factory)
318+
err := testutil.RunCommand(t, cmd, "msg-abc", "rocket")
319+
require.NoError(t, err)
320+
321+
assert.Equal(t, "POST", capturedMethod)
322+
assert.Contains(t, capturedPath, "/workspaces/12345/chat/messages/msg-abc/reactions")
323+
assert.Equal(t, "rocket", capturedBody["reaction"])
324+
325+
out := tf.OutBuf.String()
326+
assert.Contains(t, out, "Reaction")
327+
assert.Contains(t, out, "rocket")
328+
assert.Contains(t, out, "msg-abc")
329+
}
330+
331+
// ── chat delete ─────────────────────────────────────────────────────
332+
333+
func TestChatDelete(t *testing.T) {
334+
tf := testutil.NewTestFactory(t)
335+
336+
var capturedMethod string
337+
var capturedPath string
338+
339+
tf.HandleFuncV3("workspaces/12345/chat/messages/msg-del", func(w http.ResponseWriter, r *http.Request) {
340+
capturedMethod = r.Method
341+
capturedPath = r.URL.Path
342+
w.Header().Set("Content-Type", "application/json")
343+
w.Header().Set("X-RateLimit-Remaining", "99")
344+
w.WriteHeader(200)
345+
w.Write([]byte(`{}`))
346+
})
347+
348+
cmd := NewCmdDelete(tf.Factory)
349+
err := testutil.RunCommand(t, cmd, "msg-del", "--yes")
350+
require.NoError(t, err)
351+
352+
assert.Equal(t, "DELETE", capturedMethod)
353+
assert.Contains(t, capturedPath, "/workspaces/12345/chat/messages/msg-del")
354+
355+
out := tf.OutBuf.String()
356+
assert.Contains(t, out, "deleted")
357+
assert.Contains(t, out, "msg-del")
358+
}

pkg/cmd/goal/create.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func NewCmdGoalCreate(f *cmdutil.Factory) *cobra.Command {
2828
2929
# Create with a due date (Unix timestamp in ms)
3030
clickup goal create --name "Ship v2" --due-date 1704067200000`,
31-
PreRunE: cmdutil.NeedsAuth(f),
31+
PersistentPreRunE: cmdutil.NeedsAuth(f),
3232
RunE: func(cmd *cobra.Command, args []string) error {
3333
if name == "" {
3434
return fmt.Errorf("--name is required")
@@ -62,8 +62,17 @@ func NewCmdGoalCreate(f *cmdutil.Factory) *cobra.Command {
6262
}
6363

6464
cs := f.IOStreams.ColorScheme()
65-
fmt.Fprintf(f.IOStreams.Out, "%s Goal %s created\n", cs.Green("!"), cs.Bold(name))
66-
_ = resp
65+
goalID := ""
66+
if m, ok := resp.Goal.(map[string]interface{}); ok {
67+
if id, ok := m["id"].(string); ok {
68+
goalID = id
69+
}
70+
}
71+
if goalID != "" {
72+
fmt.Fprintf(f.IOStreams.Out, "%s Goal %s created (id: %s)\n", cs.Green("!"), cs.Bold(name), goalID)
73+
} else {
74+
fmt.Fprintf(f.IOStreams.Out, "%s Goal %s created\n", cs.Green("!"), cs.Bold(name))
75+
}
6776

6877
return nil
6978
},

pkg/cmd/goal/delete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This action cannot be undone. A confirmation prompt is shown unless --yes is pas
2626
# Delete without confirmation
2727
clickup goal delete e53a33d0-2eb2-4664-a4b3-5e1b0df0e912 --yes`,
2828
Args: cobra.ExactArgs(1),
29-
PreRunE: cmdutil.NeedsAuth(f),
29+
PersistentPreRunE: cmdutil.NeedsAuth(f),
3030
RunE: func(cmd *cobra.Command, args []string) error {
3131
goalID := args[0]
3232
ios := f.IOStreams

pkg/cmd/goal/goal_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,69 @@ func TestGoalCreate(t *testing.T) {
143143
assert.Contains(t, out, "Goal")
144144
assert.Contains(t, out, "New Goal")
145145
assert.Contains(t, out, "created")
146+
assert.Contains(t, out, "goal-new")
147+
}
148+
149+
var sampleGoalJSON = `{
150+
"goal": {
151+
"id": "goal-1",
152+
"pretty_id": "1",
153+
"name": "Q1 Revenue",
154+
"team_id": "12345",
155+
"creator": 1,
156+
"owner": null,
157+
"color": "#ff0000",
158+
"date_created": "1609459200000",
159+
"start_date": null,
160+
"due_date": "1617235200000",
161+
"description": "Hit revenue target",
162+
"private": false,
163+
"archived": false,
164+
"multiple_owners": false,
165+
"folder_id": null,
166+
"pinned": false,
167+
"owners": [],
168+
"key_results": [],
169+
"percent_completed": 45,
170+
"history": [],
171+
"pretty_url": "",
172+
"members": []
173+
}
174+
}`
175+
176+
func TestGoalView(t *testing.T) {
177+
tf := testutil.NewTestFactory(t)
178+
tf.HandleFunc("goal/goal-1", goalsHandler(sampleGoalJSON))
179+
180+
cmd := NewCmdGoalView(tf.Factory)
181+
err := testutil.RunCommand(t, cmd, "goal-1")
182+
require.NoError(t, err)
183+
184+
out := tf.OutBuf.String()
185+
assert.Contains(t, out, "Q1 Revenue")
186+
assert.Contains(t, out, "goal-1")
187+
assert.Contains(t, out, "Hit revenue target")
188+
}
189+
190+
func TestGoalDelete(t *testing.T) {
191+
tf := testutil.NewTestFactory(t)
192+
193+
var capturedMethod string
194+
tf.HandleFunc("goal/goal-1", func(w http.ResponseWriter, r *http.Request) {
195+
capturedMethod = r.Method
196+
w.Header().Set("Content-Type", "application/json")
197+
w.Header().Set("X-RateLimit-Remaining", "99")
198+
w.WriteHeader(200)
199+
w.Write([]byte(`{}`))
200+
})
201+
202+
cmd := NewCmdGoalDelete(tf.Factory)
203+
err := testutil.RunCommand(t, cmd, "goal-1", "--yes")
204+
require.NoError(t, err)
205+
206+
assert.Equal(t, "DELETE", capturedMethod)
207+
208+
out := tf.OutBuf.String()
209+
assert.Contains(t, out, "Goal deleted")
210+
assert.Contains(t, out, "goal-1")
146211
}

pkg/cmd/goal/list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func NewCmdGoalList(f *cmdutil.Factory) *cobra.Command {
2929
3030
# Output as JSON
3131
clickup goal list --json`,
32-
PreRunE: cmdutil.NeedsAuth(f),
32+
PersistentPreRunE: cmdutil.NeedsAuth(f),
3333
RunE: func(cmd *cobra.Command, args []string) error {
3434
client, err := f.ApiClient()
3535
if err != nil {

pkg/cmd/goal/view.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func NewCmdGoalView(f *cmdutil.Factory) *cobra.Command {
2323
# View as JSON
2424
clickup goal view e53a33d0-2eb2-4664-a4b3-5e1b0df0e912 --json`,
2525
Args: cobra.ExactArgs(1),
26-
PreRunE: cmdutil.NeedsAuth(f),
26+
PersistentPreRunE: cmdutil.NeedsAuth(f),
2727
RunE: func(cmd *cobra.Command, args []string) error {
2828
goalID := args[0]
2929

pkg/cmd/task/move_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ func TestMove_JSON(t *testing.T) {
4444
assert.Contains(t, out, "abc123")
4545
}
4646

47+
func TestMoveTask_Error(t *testing.T) {
48+
tf := testutil.NewTestFactory(t)
49+
tf.HandleV3("PUT", "workspaces/12345/tasks/notfound/home_list/list1", 404, `{"err":"Task not found","ECODE":"ITEM_015"}`)
50+
51+
cmd := NewCmdMove(tf.Factory)
52+
err := testutil.RunCommand(t, cmd, "notfound", "--list", "list1")
53+
assert.Error(t, err)
54+
}
55+
4756
func TestMove_RequiresList(t *testing.T) {
4857
cmd := NewCmdMove(nil)
4958
// --list is a required flag; cobra should reject without it.

pkg/cmd/template/list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Use --type to filter by template type: task (default), folder, or list.`,
4141
4242
# Output as JSON
4343
clickup template list --json`,
44-
PreRunE: cmdutil.NeedsAuth(f),
44+
PersistentPreRunE: cmdutil.NeedsAuth(f),
4545
RunE: func(cmd *cobra.Command, args []string) error {
4646
client, err := f.ApiClient()
4747
if err != nil {

pkg/cmd/template/template_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package template
22

33
import (
4+
"encoding/json"
5+
"io"
46
"net/http"
57
"testing"
68

@@ -76,6 +78,31 @@ func TestTemplateList_List(t *testing.T) {
7678
assert.Contains(t, out, "t-300")
7779
}
7880

81+
func TestTemplateUse(t *testing.T) {
82+
tf := testutil.NewTestFactory(t)
83+
84+
var capturedBody map[string]interface{}
85+
tf.HandleFunc("list/list-99/taskTemplate/t-12345", func(w http.ResponseWriter, r *http.Request) {
86+
assert.Equal(t, "POST", r.Method)
87+
body, _ := io.ReadAll(r.Body)
88+
json.Unmarshal(body, &capturedBody)
89+
w.Header().Set("Content-Type", "application/json")
90+
w.Header().Set("X-RateLimit-Remaining", "99")
91+
w.WriteHeader(200)
92+
w.Write([]byte(`{}`))
93+
})
94+
95+
cmd := NewCmdTemplateUse(tf.Factory)
96+
err := testutil.RunCommand(t, cmd, "t-12345", "--list", "list-99", "--name", "New Bug")
97+
require.NoError(t, err)
98+
99+
assert.Equal(t, "New Bug", capturedBody["name"])
100+
101+
out := tf.OutBuf.String()
102+
assert.Contains(t, out, "New Bug")
103+
assert.Contains(t, out, "t-12345")
104+
}
105+
79106
func TestTemplateList_Empty(t *testing.T) {
80107
tf := testutil.NewTestFactory(t)
81108
tf.HandleFunc("team/12345/taskTemplate", templatesHandler(`{"templates": []}`))

pkg/cmd/template/use.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func NewCmdTemplateUse(f *cmdutil.Factory) *cobra.Command {
2424
Example: ` # Create a task from a template
2525
clickup template use t-12345 --list 67890 --name "New Task from Template"`,
2626
Args: cobra.ExactArgs(1),
27-
PreRunE: cmdutil.NeedsAuth(f),
27+
PersistentPreRunE: cmdutil.NeedsAuth(f),
2828
RunE: func(cmd *cobra.Command, args []string) error {
2929
templateID := args[0]
3030

0 commit comments

Comments
 (0)