Move cmdcore to corecmd, declare attendance ContractFinal in helpers, keep Sheet destructive commands --yes-only, and align catalog/policy provenance. Co-authored-by: Cursor <cursoragent@cursor.com>
284 lines
10 KiB
Go
284 lines
10 KiB
Go
package helpers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func executeTodoEdge(t *testing.T, caller *scriptedToolCaller, args ...string) error {
|
|
t.Helper()
|
|
previous := deps
|
|
previousArgs := os.Args
|
|
os.Args = append([]string{"dws", "todo"}, os.Args[1:]...)
|
|
InitDeps(caller)
|
|
deps.Out.w = io.Discard
|
|
deps.Out.errW = io.Discard
|
|
defer func() {
|
|
deps = previous
|
|
os.Args = previousArgs
|
|
}()
|
|
|
|
cmd := newTodoCommand()
|
|
if cmd.PersistentFlags().Lookup("yes") == nil {
|
|
cmd.PersistentFlags().Bool("yes", false, "confirm execution")
|
|
}
|
|
if cmd.PersistentFlags().Lookup("dry-run") == nil {
|
|
cmd.PersistentFlags().Bool("dry-run", false, "preview without executing")
|
|
}
|
|
cmd.SilenceErrors = true
|
|
cmd.SilenceUsage = true
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
cmd.SetIn(os.Stdin)
|
|
cmd.SetArgs(args)
|
|
return cmd.Execute()
|
|
}
|
|
|
|
func TestCrossPlatformCoverageTodoCreateAndListCommandEdges(t *testing.T) {
|
|
validDate := "2026-03-10T18:00:00+08:00"
|
|
errorCases := [][]string{
|
|
{"task", "create", "--title", "x", "--executors", "u", "--remind-at", validDate},
|
|
{"task", "create", "--title", "x", "--executors", "u", "--due", "bad"},
|
|
{"task", "create-sub", "--title", "x", "--executors", "u", "--parent-id", "abc"},
|
|
{"task", "create-sub", "--title", "x", "--executors", "u", "--parent-id", "1", "--remind-at", validDate},
|
|
{"task", "create-sub", "--title", "x", "--executors", "u", "--parent-id", "1", "--due", "bad"},
|
|
{"task", "list", "--role-types", "invalid"},
|
|
{"task", "list", "--plan-finish-date-start", "bad"},
|
|
{"task", "list", "--plan-finish-date-end", "bad"},
|
|
}
|
|
for _, args := range errorCases {
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{}, args...); err == nil {
|
|
t.Fatalf("expected error for %v", args)
|
|
}
|
|
}
|
|
|
|
validCases := [][]string{
|
|
{"task", "create", "--title", "x", "--executors", "u1,u2", "--due", validDate, "--priority", "40", "--recurrence", "daily"},
|
|
{"task", "create", "--subject", "x", "--executors", "u", "--priority", "bad"},
|
|
{"task", "create-sub", "--content", "x", "--executors", "u", "--parent-id", "1", "--due", validDate, "--priority", "40", "--recurrence", "daily"},
|
|
{"task", "create-sub", "--title", "x", "--executors", "u", "--parent-id", "1", "--priority", "bad"},
|
|
{"task", "list", "--size", "bad"},
|
|
{"task", "list", "--status", "true", "--priority", "40,bad", "--role-types", "creator,executor", "--plan-finish-date-start", validDate, "--plan-finish-date-end", validDate},
|
|
}
|
|
for _, args := range validCases {
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{}, args...); err != nil {
|
|
t.Fatalf("execute %v: %v", args, err)
|
|
}
|
|
}
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{dry: true}, "task", "list", "--size", "21"); err != nil {
|
|
t.Fatalf("dry auto-page: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageTodoSimpleCommandValidationAndSuccessEdges(t *testing.T) {
|
|
errorCases := [][]string{
|
|
{"task", "create"},
|
|
{"task", "create", "--title", "x"},
|
|
{"task", "create-sub"},
|
|
{"task", "create-sub", "--title", "x"},
|
|
{"task", "list-sub"},
|
|
{"task", "update"},
|
|
{"task", "done"},
|
|
{"task", "get"},
|
|
{"task", "delete"},
|
|
{"task", "add-executor"},
|
|
{"task", "remove-executor"},
|
|
{"task", "add-participant"},
|
|
{"task", "remove-participant"},
|
|
{"task", "add-reminder"},
|
|
{"task", "reset-reminder"},
|
|
{"task", "add-attachment"},
|
|
{"task", "list-attachment"},
|
|
{"task", "remove-attachment"},
|
|
{"comment", "add"},
|
|
{"comment", "list"},
|
|
{"comment", "delete"},
|
|
}
|
|
for _, args := range errorCases {
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{}, args...); err == nil {
|
|
t.Fatalf("expected validation error for %v", args)
|
|
}
|
|
}
|
|
|
|
validCases := []struct {
|
|
args []string
|
|
steps []scriptedToolStep
|
|
}{
|
|
{args: []string{"task", "list-sub", "--task-id", "1"}},
|
|
{
|
|
args: []string{"task", "done", "--task-id", "1", "--status", "true"},
|
|
steps: []scriptedToolStep{
|
|
{text: `{"result":{"todoDetailModel":{"taskId":"1"}}}`},
|
|
{text: `{}`},
|
|
},
|
|
},
|
|
{args: []string{"task", "add-executor", "--task-id", "1", "--executors", "u1,u2"}},
|
|
{args: []string{"task", "remove-executor", "--task-id", "1", "--executors", "u1,u2"}},
|
|
{args: []string{"task", "add-participant", "--task-id", "1", "--participants", "u1,u2"}},
|
|
{args: []string{"task", "remove-participant", "--task-id", "1", "--participants", "u1,u2"}},
|
|
{
|
|
args: []string{"task", "list-attachment", "--task-id", "1"},
|
|
steps: []scriptedToolStep{
|
|
{text: `{"result":{"todoDetailModel":{"taskId":"1"}}}`},
|
|
{text: `{}`},
|
|
},
|
|
},
|
|
{args: []string{"comment", "add", "--task-id", "1", "--content", "hello"}},
|
|
{args: []string{"comment", "list", "--task-id", "1"}},
|
|
}
|
|
for _, tc := range validCases {
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{steps: tc.steps}, tc.args...); err != nil {
|
|
t.Fatalf("execute %v: %v", tc.args, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageTodoUpdateReminderAndDetailEdges(t *testing.T) {
|
|
validDate := "2026-03-10T18:00:00+08:00"
|
|
errorCases := [][]string{
|
|
{"task", "update", "--task-id", "1", "--remind-at", validDate},
|
|
{"task", "update", "--task-id", "1", "--due", "bad"},
|
|
{"task", "add-reminder", "--task-id", "1", "--base-time", "customTime", "--reminder-time-stamp", "bad"},
|
|
{"task", "reset-reminder", "--task-id", "1", "--reminder-rules", `[{"baseTime":"customTime","reminderTimeStamp":"bad"}]`},
|
|
{"task", "reset-reminder", "--task-id", "1", "--reminder-rules", `not-json`},
|
|
{"task", "reset-reminder", "--task-id", "1", "--reminder-rules", `[1,{"baseTime":"dueTime"},{"baseTime":"customTime","reminderTimeStamp":"` + validDate + `"}]`},
|
|
}
|
|
for _, args := range errorCases {
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{}, args...); err == nil {
|
|
t.Fatalf("expected error for %v", args)
|
|
}
|
|
}
|
|
validCases := [][]string{
|
|
{"task", "update", "--task-id", "1", "--title", "new", "--due", validDate, "--priority", "40", "--done", "true"},
|
|
{"task", "update", "--task-id", "1", "--priority", "bad"},
|
|
{"task", "add-reminder", "--task-id", "1", "--base-time", "dueTime", "--due-date-offset", "-10"},
|
|
{"task", "add-reminder", "--task-id", "1", "--base-time", "customTime", "--reminder-time-stamp", validDate},
|
|
}
|
|
for _, args := range validCases {
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{}, args...); err != nil {
|
|
t.Fatalf("execute %v: %v", args, err)
|
|
}
|
|
}
|
|
|
|
details := []scriptedToolStep{
|
|
{text: ""},
|
|
{text: "not-json"},
|
|
{text: `{"result":{"todoDetailModel":{"detailUrl":{"appUrl":"https://app.example/a b","pcUrl":"https://pc.example/a b"}}}}`},
|
|
}
|
|
for _, step := range details {
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{steps: []scriptedToolStep{step}}, "task", "get", "--task-id", "1"); err != nil {
|
|
t.Fatalf("detail %q: %v", step.text, err)
|
|
}
|
|
}
|
|
boom := errors.New("boom")
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{steps: []scriptedToolStep{{err: boom}}}, "task", "get", "--task-id", "1"); err == nil {
|
|
t.Fatal("detail tool error returned nil")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageTodoAttachmentCommandEdges(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "attachment.txt")
|
|
if err := os.WriteFile(file, []byte("data"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{dry: true}, "task", "add-attachment", "--task-id", "1", "--file-path", file); err != nil {
|
|
t.Fatalf("dry attachment: %v", err)
|
|
}
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{}, "task", "add-attachment", "--task-id", "1", "--file-path", file+".missing"); err == nil {
|
|
t.Fatal("missing attachment returned nil")
|
|
}
|
|
|
|
originalPut := httpPutFile
|
|
defer func() { httpPutFile = originalPut }()
|
|
httpPutFile = func(context.Context, string, map[string]string, string, int64) error { return nil }
|
|
credential := `{"resourceUrl":"https://upload.invalid","uploadKey":"key"}`
|
|
for _, tc := range []struct {
|
|
name string
|
|
steps []scriptedToolStep
|
|
ok bool
|
|
}{
|
|
{"upload-error", []scriptedToolStep{{err: errors.New("upload")}}, false},
|
|
{"commit-parse-error", []scriptedToolStep{{text: credential}, {text: `{}`}}, false},
|
|
{"success", []scriptedToolStep{{text: credential}, {text: `{"dentryId":1,"spaceId":2}`}, {text: `{}`}}, true},
|
|
} {
|
|
err := executeTodoEdge(t, &scriptedToolCaller{steps: tc.steps}, "task", "add-attachment", "--task-id", "1", "--file-path", file)
|
|
if tc.ok && err != nil {
|
|
t.Fatalf("%s: %v", tc.name, err)
|
|
}
|
|
if !tc.ok && err == nil {
|
|
t.Fatalf("%s returned nil", tc.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageTodoDeleteCancellationAndConfirmationEdges(t *testing.T) {
|
|
originalArgs, originalStdin := os.Args, os.Stdin
|
|
defer func() {
|
|
os.Args = originalArgs
|
|
os.Stdin = originalStdin
|
|
}()
|
|
|
|
os.Args = []string{"dws"}
|
|
for _, args := range [][]string{
|
|
{"task", "delete", "--task-id", "1"},
|
|
// remove-attachment is schema-excluded and still uses confirmDelete (nil cancel).
|
|
{"task", "remove-attachment", "--task-id", "1", "--attachment-id", "2"},
|
|
{"comment", "delete", "--task-id", "1", "--comment-id", "2"},
|
|
} {
|
|
readEnd, writeEnd, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := writeEnd.WriteString("no\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = writeEnd.Close()
|
|
os.Stdin = readEnd
|
|
err = executeTodoEdge(t, &scriptedToolCaller{}, args...)
|
|
_ = readEnd.Close()
|
|
switch args[1] {
|
|
case "remove-attachment":
|
|
if err != nil {
|
|
t.Fatalf("cancel %v: %v", args, err)
|
|
}
|
|
default:
|
|
// Contract ConfirmSafety returns a typed cancel error (not silent nil).
|
|
if err == nil || !strings.Contains(err.Error(), "用户取消了操作") {
|
|
t.Fatalf("cancel %v: error = %v, want 用户取消了操作", args, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
os.Args = []string{"dws", "--yes"}
|
|
for _, args := range [][]string{
|
|
{"task", "delete", "--task-id", "1", "--yes"},
|
|
{"task", "remove-attachment", "--task-id", "1", "--attachment-id", "2", "--yes"},
|
|
{"comment", "delete", "--task-id", "1", "--comment-id", "2", "--yes"},
|
|
} {
|
|
if err := executeTodoEdge(t, &scriptedToolCaller{}, args...); err != nil {
|
|
t.Fatalf("confirm %v: %v", args, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageTodoMD5AndEmptyListEdges(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "file.txt")
|
|
if err := os.WriteFile(file, []byte("data"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
original := todoFileMD5Hex
|
|
todoFileMD5Hex = func(string) (string, error) { return "", errors.New("md5") }
|
|
defer func() { todoFileMD5Hex = original }()
|
|
if _, err := buildTodoLocalFileMeta(file, "", ""); err == nil {
|
|
t.Fatal("md5 error returned nil")
|
|
}
|
|
if got := parseStringList(", ,"); got != nil {
|
|
t.Fatalf("empty list=%v", got)
|
|
}
|
|
}
|