Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 46 additions & 11 deletions shortcuts/task/task_complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/larksuite/cli/shortcuts/common"
)

// CompleteTask marks a task as complete and skips the PATCH call if already completed.
var CompleteTask = common.Shortcut{
Service: "task",
Command: "+complete",
Expand All @@ -34,35 +35,69 @@ var CompleteTask = common.Shortcut{
body := buildCompleteBody()
taskId := url.PathEscape(runtime.Str("task-id"))
return common.NewDryRunAPI().
GET("/open-apis/task/v2/tasks/" + taskId).
Desc("get current task status").
Params(map[string]interface{}{"user_id_type": "open_id"}).
PATCH("/open-apis/task/v2/tasks/" + taskId).
Desc("complete task if not completed").
Params(map[string]interface{}{"user_id_type": "open_id"}).
Body(body)
},

Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
taskId := url.PathEscape(runtime.Str("task-id"))
body := buildCompleteBody()

queryParams := make(larkcore.QueryParams)
queryParams.Set("user_id_type", "open_id")

apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
HttpMethod: http.MethodPatch,
var data map[string]interface{}

// 1. Get current task status
getResp, getErr := runtime.DoAPI(&larkcore.ApiReq{
HttpMethod: http.MethodGet,
ApiPath: "/open-apis/task/v2/tasks/" + taskId,
QueryParams: queryParams,
Body: body,
})

var result map[string]interface{}
if err == nil {
if parseErr := json.Unmarshal(apiResp.RawBody, &result); parseErr != nil {
return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse complete response")
var getResult map[string]interface{}
if getErr == nil {
if parseErr := json.Unmarshal(getResp.RawBody, &getResult); parseErr != nil {
return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse get response: %v", parseErr), "parse get response")
}
}

data, err := HandleTaskApiResult(result, err, "complete task")
if err != nil {
return err
getData, getErr := HandleTaskApiResult(getResult, getErr, "get task")
if getErr != nil {
return getErr
}

taskData, _ := getData["task"].(map[string]interface{})
completedAtStr, _ := taskData["completed_at"].(string)

// 2. If already completed, directly return success
if completedAtStr != "" && completedAtStr != "0" {
data = getData
} else {
// 3. Complete the task
body := buildCompleteBody()
apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
HttpMethod: http.MethodPatch,
ApiPath: "/open-apis/task/v2/tasks/" + taskId,
QueryParams: queryParams,
Body: body,
})

var result map[string]interface{}
if err == nil {
if parseErr := json.Unmarshal(apiResp.RawBody, &result); parseErr != nil {
return WrapTaskError(ErrCodeTaskInternalError, fmt.Sprintf("failed to parse response: %v", parseErr), "parse complete response")
}
}

data, err = HandleTaskApiResult(result, err, "complete task")
if err != nil {
return err
}
}

task, _ := data["task"].(map[string]interface{})
Expand Down
111 changes: 111 additions & 0 deletions shortcuts/task/task_complete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package task

import (
"strings"
"testing"

"github.com/larksuite/cli/internal/httpmock"
)

func TestCompleteTask(t *testing.T) {
tests := []struct {
name string
taskId string
isCompleted bool
formatFlag string
expectedOutput []string
}{
{
name: "task already completed",
taskId: "task-123",
isCompleted: true,
formatFlag: "pretty",
expectedOutput: []string{
"✅ Task completed successfully!",
"Task ID: task-123",
},
},
{
name: "task not completed",
taskId: "task-456",
isCompleted: false,
formatFlag: "pretty",
expectedOutput: []string{
"✅ Task completed successfully!",
"Task ID: task-456",
},
},
{
name: "task not completed json format",
taskId: "task-789",
isCompleted: false,
formatFlag: "json",
expectedOutput: []string{
`"guid": "task-789"`,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, stdout, _, reg := taskShortcutTestFactory(t)
warmTenantToken(t, f, reg)

completedAt := "0"
if tt.isCompleted {
completedAt = "1775174400000"
}

reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/task/v2/tasks/" + tt.taskId,
Body: map[string]interface{}{
"code": 0, "msg": "success",
"data": map[string]interface{}{
"task": map[string]interface{}{
"guid": tt.taskId,
"summary": "Test Task " + tt.taskId,
"completed_at": completedAt,
"url": "https://example.com/" + tt.taskId,
},
},
},
})

if !tt.isCompleted {
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/task/v2/tasks/" + tt.taskId,
Body: map[string]interface{}{
"code": 0, "msg": "success",
"data": map[string]interface{}{
"task": map[string]interface{}{
"guid": tt.taskId,
"summary": "Test Task " + tt.taskId,
"completed_at": "1775174400000",
"url": "https://example.com/" + tt.taskId,
},
},
},
})
}

err := runMountedTaskShortcut(t, CompleteTask, []string{"+complete", "--task-id", tt.taskId, "--format", tt.formatFlag, "--as", "bot"}, f, stdout)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

out := stdout.String()
outNorm := strings.ReplaceAll(out, `":"`, `": "`)

for _, expected := range tt.expectedOutput {
if !strings.Contains(outNorm, expected) && !strings.Contains(out, expected) {
t.Errorf("output missing expected string (%s), got: %s", expected, out)
}
}
Comment thread
ILUO marked this conversation as resolved.
})
}
}
9 changes: 5 additions & 4 deletions shortcuts/task/task_get_my_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/larksuite/cli/shortcuts/common"
)

// GetMyTasks lists tasks assigned to the current user.
var GetMyTasks = common.Shortcut{
Service: "task",
Command: "+get-my-tasks",
Expand Down Expand Up @@ -214,13 +215,13 @@ var GetMyTasks = common.Shortcut{
}
if createdAtStr, ok := item["created_at"].(string); ok {
if ts, err := strconv.ParseInt(createdAtStr, 10, 64); err == nil {
outputItem["created_at"] = time.UnixMilli(ts).UTC().Format(time.RFC3339)
outputItem["created_at"] = time.UnixMilli(ts).Local().Format(time.RFC3339)
}
}
if dueObj, ok := item["due"].(map[string]interface{}); ok {
if tsStr, ok := dueObj["timestamp"].(string); ok {
if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil {
outputItem["due_at"] = time.UnixMilli(ts).UTC().Format(time.RFC3339)
outputItem["due_at"] = time.UnixMilli(ts).Local().Format(time.RFC3339)
}
}
}
Expand Down Expand Up @@ -249,15 +250,15 @@ var GetMyTasks = common.Shortcut{
if dueObj, ok := item["due"].(map[string]interface{}); ok {
if tsStr, ok := dueObj["timestamp"].(string); ok {
if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil {
dueTimeStr = time.UnixMilli(ts).Format("2006-01-02 15:04")
dueTimeStr = time.UnixMilli(ts).Local().Format("2006-01-02 15:04")
}
}
}

var createdDateStr string
if createdStr, ok := item["created_at"].(string); ok {
if ts, err := strconv.ParseInt(createdStr, 10, 64); err == nil {
createdDateStr = time.UnixMilli(ts).Format("2006-01-02")
createdDateStr = time.UnixMilli(ts).Local().Format("2006-01-02")
}
}

Expand Down
91 changes: 91 additions & 0 deletions shortcuts/task/task_get_my_tasks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package task

import (
"strconv"
"strings"
"testing"
"time"

"github.com/larksuite/cli/internal/httpmock"
)

func TestGetMyTasks_LocalTimeFormatting(t *testing.T) {
tsMs := int64(1775174400000)
tsStr := strconv.FormatInt(tsMs, 10)
expectedDueTimeStr := time.UnixMilli(tsMs).Local().Format("2006-01-02 15:04")
expectedCreatedDateStr := time.UnixMilli(tsMs).Local().Format("2006-01-02")
expectedRFC3339 := time.UnixMilli(tsMs).Local().Format(time.RFC3339)

tests := []struct {
name string
formatFlag string
expectedOutput []string
}{
{
name: "pretty format",
formatFlag: "pretty",
expectedOutput: []string{
"Due: " + expectedDueTimeStr,
"Created: " + expectedCreatedDateStr,
},
},
{
name: "json format",
formatFlag: "json",
expectedOutput: []string{
`"due_at": "` + expectedRFC3339 + `"`,
`"created_at": "` + expectedRFC3339 + `"`,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, stdout, _, reg := taskShortcutTestFactory(t)
warmTenantToken(t, f, reg)

reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/task/v2/tasks",
Body: map[string]interface{}{
"code": 0, "msg": "success",
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{
"guid": "task-123",
"summary": "Test Task",
"created_at": tsStr,
"due": map[string]interface{}{
"timestamp": tsStr,
},
"url": "https://example.com/task-123",
},
},
"has_more": false,
"page_token": "",
},
},
})

s := GetMyTasks
s.AuthTypes = []string{"bot", "user"}

err := runMountedTaskShortcut(t, s, []string{"+get-my-tasks", "--format", tt.formatFlag, "--as", "bot"}, f, stdout)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

out := stdout.String()
outNorm := strings.ReplaceAll(out, `":"`, `": "`)

for _, expected := range tt.expectedOutput {
if !strings.Contains(outNorm, expected) && !strings.Contains(out, expected) {
t.Errorf("output missing expected string (%s), got: %s", expected, out)
}
}
})
}
}
Loading
Loading