chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:54 +08:00
commit bf9395e022
2349 changed files with 588574 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
# Task CLI E2E Coverage
## Metrics
- Denominator: 29 leaf commands
- Covered: 15
- Coverage: 51.7%
## Summary
- TestTask_StatusWorkflow: creates a task via `task +create`, then proves `task +complete`, `task tasks get`, and `task +reopen` through `complete`, `get completed task`, `reopen`, and `get reopened task`; asserts `status` flips between `done` and `todo` and `completed_at` is set then cleared.
- TestTask_ReminderWorkflow: creates a task with a due time via `task +create`, then proves `task +reminder` and `task tasks get` through `set reminder`, `get task with reminder`, `remove reminder`, and `get task without reminder`; asserts `relative_fire_minute=30`, reminder id presence, and reminder removal.
- TestTask_CommentWorkflow: creates a task via `task +create`, runs `comment`, and asserts the returned comment id is non-empty; this is the direct proof for `task +comment`.
- TestTask_UpdateWorkflow: creates a task as `--as user`, proves `task +update` and `task tasks patch` through repeated read-after-write `task tasks get`, then completes it via `task +complete` and verifies the completed task state directly.
- TestTask_TasklistWorkflowAsBot: runs `create tasklist with task`, then `get tasklist`, `list tasklist tasks`, and `get task`; proves `task +tasklist-create`, `task tasklists get`, `task tasklists tasks`, and `task tasks get` with seeded task payload and task-to-tasklist linkage.
- TestTask_TasklistWorkflowAsUser: creates a tasklist as `--as user`, patches its name through `task tasklists patch`, then proves both `task tasklists get` and `task tasklists list` return the patched tasklist.
- TestTask_TasklistAddTaskWorkflow: creates a standalone tasklist and task, runs `add task to tasklist`, then `list tasklist tasks` and `get task with tasklist link`; proves `task +tasklist-task-add`, `task tasklists tasks`, and `task tasks get`, including no failed tasks in the add response.
- TestTask_GetMyTasksDryRun: validates `task +get-my-tasks --dry-run` request shape for `type=my_tasks`, `user_id_type=open_id`, `completed`, `page_token`, and default `page_size` without calling live APIs.
- Cleanup path note: workflow-created tasks and tasklists are deleted through direct `task tasks delete` / `task tasklists delete` cleanup paths in `helpers_test.go::createTask`, `helpers_test.go::createTasklist`, `tasklist_workflow_test.go::TestTask_TasklistWorkflowAsBot`, and `tasklist_workflow_test.go::TestTask_TasklistWorkflowAsUser`, but those cleanup-only executions are not counted as command coverage because no testcase asserts delete behavior as the primary proof surface.
- Blocked area: assignee, follower, and tasklist member mutations still require stable real-user `open_id` fixtures; the current suite is bot-safe only.
- Blocked area: `task +get-my-tasks` live result assertions and `task tasks list` did not return the workflow-created user task deterministically in UAT, so live list visibility remains uncovered instead of being counted from flaky results.
- Blocked area: the remaining user-oriented shortcuts still need deterministic user-owned fixtures or collaborator fixtures beyond the self-owned task created inside the testcase.
- Gap pattern: direct `tasks create/delete/list/patch`, `tasklists create/delete/list/patch`, `members *`, and `subtasks *` APIs still lack deterministic direct-call workflows, so shortcut coverage does not count for those leaf commands.
## Command Table
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
| --- | --- | --- | --- | --- | --- |
| ✕ | task +assign | shortcut | | none | requires real assignee open_id fixtures; shortcut defaults to `--as user` |
| ✓ | task +comment | shortcut | task_comment_workflow_test.go::TestTask_CommentWorkflow/comment | `--task-id`; `--content` | |
| ✓ | task +complete | shortcut | task_status_workflow_test.go::TestTask_StatusWorkflow/complete | `--task-id` | |
| ✓ | task +create | shortcut | task_status_workflow_test.go::TestTask_StatusWorkflow; task_comment_workflow_test.go::TestTask_CommentWorkflow; task_reminder_workflow_test.go::TestTask_ReminderWorkflow; tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow | `summary` + `description`; `due.timestamp` + `due.is_all_day` | |
| ✕ | task +followers | shortcut | | none | requires real follower open_id fixtures; shortcut defaults to `--as user` |
| ✓ | task +get-my-tasks | shortcut | task_get_my_tasks_dryrun_test.go::TestTask_GetMyTasksDryRun | `--complete`; `--page-token`; dry-run only | live UAT did not return the workflow-created user task deterministically in my-tasks views |
| ✓ | task +reminder | shortcut | task_reminder_workflow_test.go::TestTask_ReminderWorkflow/set reminder; task_reminder_workflow_test.go::TestTask_ReminderWorkflow/remove reminder | `--task-id --set 30m`; `--task-id --remove` | |
| ✓ | task +reopen | shortcut | task_status_workflow_test.go::TestTask_StatusWorkflow/reopen | `--task-id` | |
| ✓ | task +tasklist-create | shortcut | tasklist_workflow_test.go::TestTask_TasklistWorkflowAsBot/create tasklist with task as bot; tasklist_workflow_test.go::TestTask_TasklistWorkflowAsUser/create tasklist as user; tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow | `--name` only; `--name` plus task array in `--data` | |
| ✕ | task +tasklist-members | shortcut | | none | requires real member open_id fixtures to add, remove, or set tasklist members |
| ✓ | task +tasklist-task-add | shortcut | tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow/add task to tasklist | `--tasklist-id`; `--task-id` | |
| ✓ | task +update | shortcut | task_update_workflow_test.go::TestTask_UpdateWorkflow/update task with shortcut as user | `--task-id`; `--summary`; `--description` | verified by follow-up `task tasks get` |
| ✕ | task members add | api | | none | requires stable member fixtures and explicit direct API-body assertions |
| ✕ | task members remove | api | | none | requires stable member fixtures and explicit direct API-body assertions |
| ✕ | task subtasks create | api | | none | needs a parent-task workflow plus direct subtask payload assertions |
| ✕ | task subtasks list | api | | none | needs deterministic subtask fixtures created in the same workflow |
| ✕ | task tasklists add_members | api | | none | requires real member open_id fixtures and direct API coverage |
| ✕ | task tasklists create | api | | none | only covered indirectly through `task +tasklist-create`; no direct API invocation yet |
| ✕ | task tasklists delete | api | | none | only exercised in parent cleanup; no testcase asserts delete behavior or post-delete state as the primary proof |
| ✓ | task tasklists get | api | tasklist_workflow_test.go::TestTask_TasklistWorkflowAsBot/get tasklist as bot; tasklist_workflow_test.go::TestTask_TasklistWorkflowAsUser/get patched tasklist as user | `tasklist_guid` in `--params` | |
| ✓ | task tasklists list | api | tasklist_workflow_test.go::TestTask_TasklistWorkflowAsUser/list tasklists and find patched tasklist as user | `page_size` | asserts the workflow-created tasklist appears with patched name |
| ✓ | task tasklists patch | api | tasklist_workflow_test.go::TestTask_TasklistWorkflowAsUser/patch tasklist as user | `tasklist_guid` in `--params`; `name` in `--data` | verified by follow-up `task tasklists get` and `task tasklists list` |
| ✕ | task tasklists remove_members | api | | none | requires real member open_id fixtures and direct API coverage |
| ✓ | task tasklists tasks | api | tasklist_workflow_test.go::TestTask_TasklistWorkflowAsBot/list tasklist tasks as bot; tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow/list tasklist tasks | `tasklist_guid`; `page_size` | |
| ✕ | task tasks create | api | | none | only covered indirectly through `task +create`; no direct API invocation yet |
| ✕ | task tasks delete | api | | none | only exercised in parent cleanup; no testcase asserts delete behavior or post-delete state as the primary proof |
| ✓ | task tasks get | api | task_status_workflow_test.go::TestTask_StatusWorkflow/get completed task; task_status_workflow_test.go::TestTask_StatusWorkflow/get reopened task; task_reminder_workflow_test.go::TestTask_ReminderWorkflow/get task with reminder; task_reminder_workflow_test.go::TestTask_ReminderWorkflow/get task without reminder; tasklist_workflow_test.go::TestTask_TasklistWorkflowAsBot/get task as bot; tasklist_add_task_workflow_test.go::TestTask_TasklistAddTaskWorkflow/get task with tasklist link; task_update_workflow_test.go::TestTask_UpdateWorkflow/get created task as user; task_update_workflow_test.go::TestTask_UpdateWorkflow/get task updated by shortcut as user; task_update_workflow_test.go::TestTask_UpdateWorkflow/get task patched by api as user; task_update_workflow_test.go::TestTask_UpdateWorkflow/get completed task as user | `task_guid` in `--params`; assert status, reminders, summary, description, and tasklist link | |
| ✕ | task tasks list | api | | none | UAT did not return the workflow-created user task deterministically in list views |
| ✓ | task tasks patch | api | task_update_workflow_test.go::TestTask_UpdateWorkflow/patch task with api as user | `task_guid` in `--params`; `summary` + `description` in `--data` | verified by follow-up `task tasks get` |
+118
View File
@@ -0,0 +1,118 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package task
import (
"context"
"testing"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func createTask(t *testing.T, parentT *testing.T, ctx context.Context, req clie2e.Request) string {
t.Helper()
if req.DefaultAs == "" {
req.DefaultAs = "bot"
}
result, err := clie2e.RunCmd(ctx, req)
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
taskGUID := gjson.Get(result.Stdout, "data.guid").String()
require.NotEmpty(t, taskGUID, "stdout:\n%s", result.Stdout)
parentT.Cleanup(func() {
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"task", "tasks", "delete"},
DefaultAs: "bot",
Params: map[string]any{"task_guid": taskGUID},
Yes: true,
})
clie2e.ReportCleanupFailure(parentT, "delete task "+taskGUID, deleteResult, deleteErr)
})
return taskGUID
}
func createTasklist(t *testing.T, parentT *testing.T, ctx context.Context, req clie2e.Request) string {
t.Helper()
if req.DefaultAs == "" {
req.DefaultAs = "bot"
}
result, err := clie2e.RunCmd(ctx, req)
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
tasklistGUID := gjson.Get(result.Stdout, "data.guid").String()
require.NotEmpty(t, tasklistGUID, "stdout:\n%s", result.Stdout)
parentT.Cleanup(func() {
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"task", "tasklists", "delete"},
DefaultAs: "bot",
Params: map[string]any{"tasklist_guid": tasklistGUID},
Yes: true,
})
clie2e.ReportCleanupFailure(parentT, "delete tasklist "+tasklistGUID, deleteResult, deleteErr)
})
return tasklistGUID
}
func findTaskInTasklist(t *testing.T, ctx context.Context, tasklistGUID string, taskGUID string) gjson.Result {
t.Helper()
require.NotEmpty(t, tasklistGUID, "tasklist GUID is required")
require.NotEmpty(t, taskGUID, "task GUID is required")
pageToken := ""
seenPageTokens := map[string]struct{}{}
for {
params := map[string]any{
"tasklist_guid": tasklistGUID,
"page_size": 50,
}
if pageToken != "" {
if _, seen := seenPageTokens[pageToken]; seen {
t.Fatalf("tasklist task pagination loop detected for tasklist %q, repeated page_token %q", tasklistGUID, pageToken)
}
seenPageTokens[pageToken] = struct{}{}
params["page_token"] = pageToken
}
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasklists", "tasks"},
DefaultAs: "bot",
Params: params,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
taskItem := gjson.Get(result.Stdout, `data.items.#(guid=="`+taskGUID+`")`)
if taskItem.Exists() {
return taskItem
}
hasMore := gjson.Get(result.Stdout, "data.has_more").Bool()
pageToken = gjson.Get(result.Stdout, "data.page_token").String()
if !hasMore || pageToken == "" {
t.Fatalf("task %q not found in tasklist %q pages, last stdout:\n%s", taskGUID, tasklistGUID, result.Stdout)
}
}
}
@@ -0,0 +1,44 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package task
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestTask_CommentWorkflow(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
suffix := clie2e.GenerateSuffix()
commentContent := "lark-cli-e2e-comment-" + suffix
taskGUID := createTask(t, parentT, ctx, clie2e.Request{
Args: []string{"task", "+create"},
DefaultAs: "bot",
Data: map[string]any{
"summary": "lark-cli-e2e-comment-task-" + suffix,
"description": "created by tests/cli_e2e/task comment workflow",
},
})
t.Run("comment as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "+comment", "--task-id", taskGUID, "--content", commentContent},
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.id").String(), "stdout:\n%s", result.Stdout)
})
}
@@ -0,0 +1,65 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package task
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
// TestTask_GetMyTasksDryRun validates the request shape emitted by
// task +get-my-tasks under --dry-run. Fake credentials are sufficient because
// dry-run stops before any network call.
func TestTask_GetMyTasksDryRun(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
t.Setenv("LARKSUITE_CLI_APP_ID", "task_dryrun_test")
t.Setenv("LARKSUITE_CLI_APP_SECRET", "task_dryrun_secret")
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"task", "+get-my-tasks",
"--complete",
"--page-token", "pt_001",
"--dry-run",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
out := result.Stdout
if count := gjson.Get(out, "api.#").Int(); count != 1 {
t.Fatalf("expected 1 API call, got %d\nstdout:\n%s", count, out)
}
if method := gjson.Get(out, "api.0.method").String(); method != "GET" {
t.Fatalf("api[0].method = %q, want GET\nstdout:\n%s", method, out)
}
if url := gjson.Get(out, "api.0.url").String(); url != "/open-apis/task/v2/tasks" {
t.Fatalf("api[0].url = %q, want /open-apis/task/v2/tasks\nstdout:\n%s", url, out)
}
if got := gjson.Get(out, "api.0.params.type").String(); got != "my_tasks" {
t.Fatalf("api[0].params.type = %q, want my_tasks\nstdout:\n%s", got, out)
}
if got := gjson.Get(out, "api.0.params.user_id_type").String(); got != "open_id" {
t.Fatalf("api[0].params.user_id_type = %q, want open_id\nstdout:\n%s", got, out)
}
if got := gjson.Get(out, "api.0.params.completed").Bool(); !got {
t.Fatalf("api[0].params.completed = %v, want true\nstdout:\n%s", got, out)
}
if got := gjson.Get(out, "api.0.params.page_token").String(); got != "pt_001" {
t.Fatalf("api[0].params.page_token = %q, want pt_001\nstdout:\n%s", got, out)
}
if got := gjson.Get(out, "api.0.params.page_size").Int(); got != 50 {
t.Fatalf("api[0].params.page_size = %d, want 50\nstdout:\n%s", got, out)
}
}
@@ -0,0 +1,86 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package task
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestTask_ReminderWorkflow(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
suffix := clie2e.GenerateSuffix()
taskGUID := createTask(t, parentT, ctx, clie2e.Request{
Args: []string{"task", "+create"},
DefaultAs: "bot",
Data: map[string]any{
"summary": "lark-cli-e2e-reminder-" + suffix,
"description": "created by tests/cli_e2e/task reminder workflow",
"due": map[string]any{
"timestamp": time.Now().Add(48 * time.Hour).UnixMilli(),
"is_all_day": false,
},
},
})
t.Run("set reminder as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "+reminder", "--task-id", taskGUID, "--set", "30m"},
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.guid").String())
})
t.Run("get task with reminder as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "get"},
DefaultAs: "bot",
Params: map[string]any{"task_guid": taskGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String())
assert.Equal(t, int64(30), gjson.Get(result.Stdout, "data.task.reminders.0.relative_fire_minute").Int())
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.task.reminders.0.id").String(), "stdout:\n%s", result.Stdout)
})
t.Run("remove reminder as bot", func(t *testing.T) {
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
Args: []string{"task", "+reminder", "--task-id", taskGUID, "--remove"},
DefaultAs: "bot",
}, clie2e.RetryOptions{})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.guid").String())
})
t.Run("get task without reminder as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "get"},
DefaultAs: "bot",
Params: map[string]any{"task_guid": taskGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String())
assert.False(t, gjson.Get(result.Stdout, "data.task.reminders.0").Exists(), "stdout:\n%s", result.Stdout)
})
}
@@ -0,0 +1,83 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package task
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestTask_StatusWorkflow(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
suffix := clie2e.GenerateSuffix()
taskGUID := createTask(t, parentT, ctx, clie2e.Request{
Args: []string{"task", "+create"},
DefaultAs: "bot",
Data: map[string]any{
"summary": "lark-cli-e2e-summary-" + suffix,
"description": "created by tests/cli_e2e/task status workflow",
},
})
t.Run("complete as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "+complete", "--task-id", taskGUID},
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.guid").String())
})
t.Run("get completed task as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "get"},
DefaultAs: "bot",
Params: map[string]any{"task_guid": taskGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String())
assert.Equal(t, "done", gjson.Get(result.Stdout, "data.task.status").String())
assert.NotZero(t, gjson.Get(result.Stdout, "data.task.completed_at").Int(), "stdout:\n%s", result.Stdout)
})
t.Run("reopen as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "+reopen", "--task-id", taskGUID},
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.guid").String())
})
t.Run("get reopened task as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "get"},
DefaultAs: "bot",
Params: map[string]any{"task_guid": taskGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String())
assert.Equal(t, "todo", gjson.Get(result.Stdout, "data.task.status").String())
assert.Equal(t, "0", gjson.Get(result.Stdout, "data.task.completed_at").String())
})
}
@@ -0,0 +1,190 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package task
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestTask_UpdateWorkflow(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
suffix := clie2e.GenerateSuffix()
taskSummary := "lark-cli-e2e-user-my-task-" + suffix
taskDescription := "created by tests/cli_e2e/task user workflow"
updatedTaskSummary := "lark-cli-e2e-user-my-task-updated-" + suffix
updatedTaskDescription := "updated by task +update user workflow"
patchedTaskSummary := "lark-cli-e2e-user-my-task-patched-" + suffix
patchedTaskDescription := "patched by task tasks patch user workflow"
taskGUID := ""
clie2e.SkipWithoutUserToken(t)
parentT.Cleanup(func() {
if taskGUID == "" {
return
}
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"task", "tasks", "delete"},
DefaultAs: "user",
Params: map[string]any{"task_guid": taskGUID},
Yes: true,
})
clie2e.ReportCleanupFailure(parentT, "delete user task "+taskGUID, deleteResult, deleteErr)
})
t.Run("create task as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "+create"},
DefaultAs: "user",
Data: map[string]any{
"summary": taskSummary,
"description": taskDescription,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
taskGUID = gjson.Get(result.Stdout, "data.guid").String()
require.NotEmpty(t, taskGUID, "stdout:\n%s", result.Stdout)
assert.Equal(t, "user", gjson.Get(result.Stdout, "identity").String(), "stdout:\n%s", result.Stdout)
})
t.Run("get created task as user", func(t *testing.T) {
require.NotEmpty(t, taskGUID, "task GUID should be created before get")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "get"},
DefaultAs: "user",
Params: map[string]any{"task_guid": taskGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String(), "stdout:\n%s", result.Stdout)
assert.Equal(t, taskSummary, gjson.Get(result.Stdout, "data.task.summary").String(), "stdout:\n%s", result.Stdout)
assert.Equal(t, taskDescription, gjson.Get(result.Stdout, "data.task.description").String(), "stdout:\n%s", result.Stdout)
assert.Equal(t, "todo", gjson.Get(result.Stdout, "data.task.status").String(), "stdout:\n%s", result.Stdout)
})
t.Run("update task with shortcut as user", func(t *testing.T) {
require.NotEmpty(t, taskGUID, "task GUID should be created before update")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"task", "+update",
"--task-id", taskGUID,
"--summary", updatedTaskSummary,
"--description", updatedTaskDescription,
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.tasks.0.guid").String(), "stdout:\n%s", result.Stdout)
})
t.Run("get task updated by shortcut as user", func(t *testing.T) {
require.NotEmpty(t, taskGUID, "task GUID should be updated before get")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "get"},
DefaultAs: "user",
Params: map[string]any{"task_guid": taskGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, updatedTaskSummary, gjson.Get(result.Stdout, "data.task.summary").String(), "stdout:\n%s", result.Stdout)
assert.Equal(t, updatedTaskDescription, gjson.Get(result.Stdout, "data.task.description").String(), "stdout:\n%s", result.Stdout)
})
t.Run("patch task with api as user", func(t *testing.T) {
require.NotEmpty(t, taskGUID, "task GUID should be updated before patch")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "patch"},
DefaultAs: "user",
Params: map[string]any{"task_guid": taskGUID},
Data: map[string]any{
"task": map[string]any{
"summary": patchedTaskSummary,
"description": patchedTaskDescription,
},
"update_fields": []string{"summary", "description"},
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String(), "stdout:\n%s", result.Stdout)
})
t.Run("get task patched by api as user", func(t *testing.T) {
require.NotEmpty(t, taskGUID, "task GUID should be patched before get")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "get"},
DefaultAs: "user",
Params: map[string]any{"task_guid": taskGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String(), "stdout:\n%s", result.Stdout)
assert.Equal(t, patchedTaskSummary, gjson.Get(result.Stdout, "data.task.summary").String(), "stdout:\n%s", result.Stdout)
assert.Equal(t, patchedTaskDescription, gjson.Get(result.Stdout, "data.task.description").String(), "stdout:\n%s", result.Stdout)
})
t.Run("complete task as user", func(t *testing.T) {
require.NotEmpty(t, taskGUID, "task GUID should be created before complete")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "+complete", "--task-id", taskGUID},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.guid").String(), "stdout:\n%s", result.Stdout)
})
t.Run("get completed task as user", func(t *testing.T) {
require.NotEmpty(t, taskGUID, "task GUID should be completed before get")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "get"},
DefaultAs: "user",
Params: map[string]any{"task_guid": taskGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String(), "stdout:\n%s", result.Stdout)
assert.Equal(t, patchedTaskSummary, gjson.Get(result.Stdout, "data.task.summary").String(), "stdout:\n%s", result.Stdout)
assert.Equal(t, "done", gjson.Get(result.Stdout, "data.task.status").String(), "stdout:\n%s", result.Stdout)
assert.NotZero(t, gjson.Get(result.Stdout, "data.task.completed_at").Int(), "stdout:\n%s", result.Stdout)
})
}
@@ -0,0 +1,125 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package task
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
// TestTask_UploadAttachmentDryRun validates the request shape emitted by
// task +upload-attachment under --dry-run: the full CLI binary is invoked
// end-to-end so flag parsing, validation, and the dry-run renderer all
// execute. Fake credentials are sufficient because --dry-run short-circuits
// before any network call.
func TestTask_UploadAttachmentDryRun(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
t.Setenv("LARKSUITE_CLI_APP_ID", "task_dryrun_test")
t.Setenv("LARKSUITE_CLI_APP_SECRET", "task_dryrun_secret")
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
tests := []struct {
name string
args []string
wantResourceType string
wantResourceID string
wantFilePath string
wantFileName string
wantUserIDType string
}{
{
name: "default resource type and user id type",
args: []string{
"task", "+upload-attachment",
"--resource-id", "task-guid-123",
"--file", "./note.pdf",
"--dry-run",
},
wantResourceType: "task",
wantResourceID: "task-guid-123",
wantFilePath: "./note.pdf",
wantFileName: "note.pdf",
wantUserIDType: "open_id",
},
{
name: "explicit resource type and user id type",
args: []string{
"task", "+upload-attachment",
"--resource-id", "task-guid-456",
"--resource-type", "custom_type",
"--file", "./report.txt",
"--user-id-type", "union_id",
"--dry-run",
},
wantResourceType: "custom_type",
wantResourceID: "task-guid-456",
wantFilePath: "./report.txt",
wantFileName: "report.txt",
wantUserIDType: "union_id",
},
{
name: "applink URL resolves to guid",
args: []string{
"task", "+upload-attachment",
"--resource-id", "https://applink.feishu.cn/client/todo/task?guid=task-from-url",
"--file", "./doc.md",
"--dry-run",
},
wantResourceType: "task",
wantResourceID: "task-from-url",
wantFilePath: "./doc.md",
wantFileName: "doc.md",
wantUserIDType: "open_id",
},
}
for _, temp := range tests {
tt := temp
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: tt.args,
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
out := result.Stdout
if count := gjson.Get(out, "api.#").Int(); count != 1 {
t.Fatalf("expected 1 API call, got %d\nstdout:\n%s", count, out)
}
if method := gjson.Get(out, "api.0.method").String(); method != "POST" {
t.Fatalf("api[0].method = %q, want POST\nstdout:\n%s", method, out)
}
if url := gjson.Get(out, "api.0.url").String(); url != "/open-apis/task/v2/attachments/upload" {
t.Fatalf("api[0].url = %q, want /open-apis/task/v2/attachments/upload\nstdout:\n%s", url, out)
}
if got := gjson.Get(out, "api.0.params.user_id_type").String(); got != tt.wantUserIDType {
t.Fatalf("api[0].params.user_id_type = %q, want %q\nstdout:\n%s", got, tt.wantUserIDType, out)
}
if got := gjson.Get(out, "api.0.body.resource_type").String(); got != tt.wantResourceType {
t.Fatalf("api[0].body.resource_type = %q, want %q\nstdout:\n%s", got, tt.wantResourceType, out)
}
if got := gjson.Get(out, "api.0.body.resource_id").String(); got != tt.wantResourceID {
t.Fatalf("api[0].body.resource_id = %q, want %q\nstdout:\n%s", got, tt.wantResourceID, out)
}
if got := gjson.Get(out, "api.0.body.file.field").String(); got != "file" {
t.Fatalf("api[0].body.file.field = %q, want file\nstdout:\n%s", got, out)
}
if got := gjson.Get(out, "api.0.body.file.path").String(); got != tt.wantFilePath {
t.Fatalf("api[0].body.file.path = %q, want %q\nstdout:\n%s", got, tt.wantFilePath, out)
}
if got := gjson.Get(out, "api.0.body.file.name").String(); got != tt.wantFileName {
t.Fatalf("api[0].body.file.name = %q, want %q\nstdout:\n%s", got, tt.wantFileName, out)
}
})
}
}
@@ -0,0 +1,71 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package task
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestTask_TasklistAddTaskWorkflow(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
suffix := clie2e.GenerateSuffix()
tasklistName := "lark-cli-e2e-tasklist-add-" + suffix
taskSummary := "lark-cli-e2e-tasklist-add-task-" + suffix
tasklistGUID := createTasklist(t, parentT, ctx, clie2e.Request{
Args: []string{"task", "+tasklist-create", "--name", tasklistName},
DefaultAs: "bot",
})
taskGUID := createTask(t, parentT, ctx, clie2e.Request{
Args: []string{"task", "+create"},
DefaultAs: "bot",
Data: map[string]any{
"summary": taskSummary,
"description": "created by tests/cli_e2e/task tasklist add workflow",
},
})
t.Run("add task to tasklist as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "+tasklist-task-add", "--tasklist-id", tasklistGUID, "--task-id", taskGUID},
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, tasklistGUID, gjson.Get(result.Stdout, "data.tasklist_guid").String())
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.successful_tasks.0.guid").String())
assert.False(t, gjson.Get(result.Stdout, "data.failed_tasks.0").Exists(), "stdout:\n%s", result.Stdout)
})
t.Run("list tasklist tasks as bot", func(t *testing.T) {
taskItem := findTaskInTasklist(t, ctx, tasklistGUID, taskGUID)
assert.Equal(t, taskSummary, taskItem.Get("summary").String())
})
t.Run("get task with tasklist link as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "get"},
DefaultAs: "bot",
Params: map[string]any{"task_guid": taskGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String())
assert.Equal(t, tasklistGUID, gjson.Get(result.Stdout, "data.task.tasklists.0.tasklist_guid").String())
})
}
@@ -0,0 +1,211 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package task
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestTask_TasklistWorkflowAsBot(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
suffix := clie2e.GenerateSuffix()
tasklistName := "lark-cli-e2e-tasklist-" + suffix
taskSummary := "lark-cli-e2e-task-in-tasklist-" + suffix
taskDescription := "created by tests/cli_e2e/task"
var tasklistGUID string
var taskGUID string
t.Run("create tasklist with task as bot", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "+tasklist-create", "--name", tasklistName},
DefaultAs: "bot",
Data: []map[string]any{
{
"summary": taskSummary,
"description": taskDescription,
},
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
tasklistGUID = gjson.Get(result.Stdout, "data.guid").String()
taskGUID = gjson.Get(result.Stdout, "data.created_tasks.0.guid").String()
require.NotEmpty(t, tasklistGUID, "stdout:\n%s", result.Stdout)
require.NotEmpty(t, taskGUID, "stdout:\n%s", result.Stdout)
parentT.Cleanup(func() {
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"task", "tasks", "delete"},
DefaultAs: "bot",
Params: map[string]any{"task_guid": taskGUID},
Yes: true,
})
clie2e.ReportCleanupFailure(parentT, "delete task "+taskGUID, deleteResult, deleteErr)
})
parentT.Cleanup(func() {
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"task", "tasklists", "delete"},
DefaultAs: "bot",
Params: map[string]any{"tasklist_guid": tasklistGUID},
Yes: true,
})
clie2e.ReportCleanupFailure(parentT, "delete tasklist "+tasklistGUID, deleteResult, deleteErr)
})
})
t.Run("get tasklist as bot", func(t *testing.T) {
require.NotEmpty(t, tasklistGUID, "tasklist GUID should be created before get")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasklists", "get"},
DefaultAs: "bot",
Params: map[string]any{"tasklist_guid": tasklistGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, tasklistGUID, gjson.Get(result.Stdout, "data.tasklist.guid").String())
assert.Equal(t, tasklistName, gjson.Get(result.Stdout, "data.tasklist.name").String())
})
t.Run("list tasklist tasks as bot", func(t *testing.T) {
require.NotEmpty(t, tasklistGUID, "tasklist GUID should be created before listing tasks")
require.NotEmpty(t, taskGUID, "task GUID should be created before listing tasks")
taskItem := findTaskInTasklist(t, ctx, tasklistGUID, taskGUID)
assert.Equal(t, taskSummary, taskItem.Get("summary").String())
})
t.Run("get task as bot", func(t *testing.T) {
require.NotEmpty(t, taskGUID, "task GUID should be created before get")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "get"},
DefaultAs: "bot",
Params: map[string]any{"task_guid": taskGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, taskGUID, gjson.Get(result.Stdout, "data.task.guid").String())
assert.Equal(t, taskSummary, gjson.Get(result.Stdout, "data.task.summary").String())
assert.Equal(t, taskDescription, gjson.Get(result.Stdout, "data.task.description").String())
assert.Equal(t, tasklistGUID, gjson.Get(result.Stdout, "data.task.tasklists.0.tasklist_guid").String())
})
}
func TestTask_TasklistWorkflowAsUser(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
clie2e.SkipWithoutUserToken(t)
suffix := clie2e.GenerateSuffix()
tasklistName := "lark-cli-e2e-user-tasklist-" + suffix
patchedTasklistName := "lark-cli-e2e-user-tasklist-patched-" + suffix
tasklistGUID := ""
parentT.Cleanup(func() {
if tasklistGUID == "" {
return
}
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"task", "tasklists", "delete"},
DefaultAs: "user",
Params: map[string]any{"tasklist_guid": tasklistGUID},
Yes: true,
})
clie2e.ReportCleanupFailure(parentT, "delete user tasklist "+tasklistGUID, deleteResult, deleteErr)
})
t.Run("create tasklist as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "+tasklist-create", "--name", tasklistName},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
tasklistGUID = gjson.Get(result.Stdout, "data.guid").String()
require.NotEmpty(t, tasklistGUID, "stdout:\n%s", result.Stdout)
assert.Equal(t, "user", gjson.Get(result.Stdout, "identity").String(), "stdout:\n%s", result.Stdout)
})
t.Run("patch tasklist as user", func(t *testing.T) {
require.NotEmpty(t, tasklistGUID, "tasklist GUID should be created before patch")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasklists", "patch"},
DefaultAs: "user",
Params: map[string]any{"tasklist_guid": tasklistGUID},
Data: map[string]any{
"tasklist": map[string]any{"name": patchedTasklistName},
"update_fields": []string{"name"},
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, tasklistGUID, gjson.Get(result.Stdout, "data.tasklist.guid").String(), "stdout:\n%s", result.Stdout)
})
t.Run("get patched tasklist as user", func(t *testing.T) {
require.NotEmpty(t, tasklistGUID, "tasklist GUID should be patched before get")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasklists", "get"},
DefaultAs: "user",
Params: map[string]any{"tasklist_guid": tasklistGUID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, tasklistGUID, gjson.Get(result.Stdout, "data.tasklist.guid").String(), "stdout:\n%s", result.Stdout)
assert.Equal(t, patchedTasklistName, gjson.Get(result.Stdout, "data.tasklist.name").String(), "stdout:\n%s", result.Stdout)
})
t.Run("list tasklists and find patched tasklist as user", func(t *testing.T) {
require.NotEmpty(t, tasklistGUID, "tasklist GUID should be patched before list")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasklists", "list"},
DefaultAs: "user",
Params: map[string]any{"page_size": 50},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
tasklistItem := gjson.Get(result.Stdout, `data.items.#(guid=="`+tasklistGUID+`")`)
require.True(t, tasklistItem.Exists(), "stdout:\n%s", result.Stdout)
assert.Equal(t, patchedTasklistName, tasklistItem.Get("name").String(), "stdout:\n%s", result.Stdout)
})
}