diff --git a/CHANGELOG.md b/CHANGELOG.md index e77b44dd..de3f43ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,32 +2,45 @@ All notable changes to this project will be documented in this file. -The format is inspired by Keep a Changelog and this project follows -Semantic Versioning once versioned releases are published. +The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and this project follows [Semantic Versioning](https://semver.org/). -## [Unreleased] +## [1.0.0] - 2026-03-27 -### Added +First public release of DingTalk Workspace CLI. -- Open-source governance baseline: - - `LICENSE` (Apache License 2.0) - - `CODE_OF_CONDUCT.md` - - `SECURITY.md` - - `.github/PULL_REQUEST_TEMPLATE.md` - - `.github/workflows/ci.yml` - - `.env.example` - - `docs/architecture.md` -- Open-source policy checks: - - `scripts/policy/check-open-source-assets.sh` - - `scripts/policy/open-source-audit.sh` - - CI integration through `.github/workflows/ci.yml` -- Release hardening: - - SHA256 checksum generation for packaged artifacts - - Homebrew tap publish helper +### Core -### Changed +- Discovery-driven CLI pipeline: Market → Discovery → IR → CLI → Transport +- MCP JSON-RPC transport with retries, auth injection, and response size limits +- Disk-based discovery cache with TTL and stale-fallback for offline resilience +- OAuth device flow authentication with PBKDF2 + AES-256-GCM encrypted token storage +- Structured output formats: JSON, table, raw +- Global flags: `--format`, `--verbose`, `--debug`, `--dry-run`, `--yes`, `--timeout` +- Exit codes with structured error payloads (category, reason, hint, actions) -- Project license migrated from MIT to Apache License 2.0. -- `README.md` and `CONTRIBUTING.md` expanded for open-source contribution and - governance guidance. -- `scripts/README.md` now includes open-source policy checks. +### Supported Services + +- **aitable** — AI table: bases, tables, fields, records, templates +- **approval** — Approval processes, forms, instances +- **attendance** — Attendance records, shifts, statistics +- **calendar** — Events, participants, meeting rooms, free-busy +- **chat** — Bot messaging (group/batch), webhook, bot management +- **contact** — Users, departments, org structure +- **devdoc** — Open platform docs search +- **ding** — DING messages: send, recall +- **report** — Reports, templates, statistics +- **todo** — Task management: create, update, complete, delete +- **workbench** — Workbench app query + +### Agent Skills + +- Bundled `SKILL.md` with product reference docs, intent routing guide, error codes, and batch scripts +- One-line installer for macOS / Linux / Windows +- Skills installed to `~/.agents/skills/dws` (home) or `./.agents/skills/dws` (project) + +### Packaging + +- Pre-built binaries for macOS (arm64/amd64), Linux (arm64/amd64), Windows (amd64) +- One-line install scripts (`install.sh`, `install.ps1`) +- Project-level skill installer (`install-skills.sh`) +- Shell completion: Bash, Zsh, Fish diff --git a/test/.DS_Store b/test/.DS_Store deleted file mode 100644 index 250699f3..00000000 Binary files a/test/.DS_Store and /dev/null differ diff --git a/test/cli_compat/aiapp_test.go b/test/cli_compat/aiapp_test.go deleted file mode 100644 index a4b05eda..00000000 --- a/test/cli_compat/aiapp_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package cli_compat_test - -import ( - "testing" -) - -// ── create ────────────────────────────────────────────────── - -func TestAiappCreate_should_call_create_ai_app(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - err := execCmd(t, root, []string{"aiapp", "create"}, map[string]string{ - "prompt": "创建一个天气查询应用", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "create_ai_app") - assertToolArg(t, cap, "prompt", "创建一个天气查询应用") -} - -func TestAiappCreate_should_include_skills_when_provided(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "create"}, map[string]string{ - "prompt": "翻译应用", "skills": "s1,s2", - }) - assertToolArg(t, cap, "officialSkillUids", []string{"s1", "s2"}) -} - -func TestAiappCreate_should_not_include_skills_when_empty(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "create"}, map[string]string{ - "prompt": "P", "skills": "", - }) - assertArgNotPresent(t, cap, "officialSkillUids") -} - -func TestAiappCreate_should_handle_long_prompt(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - longPrompt := "创建一个能够处理多语言翻译、支持语音输入、并且具备历史记录功能的AI翻译助手应用" - _ = execCmd(t, root, []string{"aiapp", "create"}, map[string]string{"prompt": longPrompt}) - assertToolArg(t, cap, "prompt", longPrompt) -} - -func TestAiappCreate_should_make_single_call(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "create"}, map[string]string{"prompt": "P"}) - assertCallCount(t, cap, 1) -} - -func TestAiappCreate_should_handle_single_skill(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "create"}, map[string]string{ - "prompt": "P", "skills": "only_skill", - }) - assertToolArg(t, cap, "officialSkillUids", []string{"only_skill"}) -} - -func TestAiappCreate_should_not_call_mcp_in_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "create"}, map[string]string{"prompt": "P"}) - assertCallCount(t, cap, 0) -} - -// ── query ─────────────────────────────────────────────────── - -func TestAiappQuery_should_call_query_ai_app(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - err := execCmd(t, root, []string{"aiapp", "query"}, map[string]string{ - "task-id": "TASK001", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "query_ai_app") - assertToolArg(t, cap, "taskId", "TASK001") -} - -func TestAiappQuery_should_handle_different_task_ids(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "query"}, map[string]string{"task-id": "T_XYZ"}) - assertToolArg(t, cap, "taskId", "T_XYZ") -} - -func TestAiappQuery_should_make_single_call(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "query"}, map[string]string{"task-id": "T1"}) - assertCallCount(t, cap, 1) -} - -func TestAiappQuery_should_return_no_error(t *testing.T) { - _ = setupTestDeps(t, "aiapp") - root := buildRoot() - err := execCmd(t, root, []string{"aiapp", "query"}, map[string]string{"task-id": "T1"}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestAiappQuery_should_not_call_mcp_in_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "query"}, map[string]string{"task-id": "T1"}) - assertCallCount(t, cap, 0) -} - -// ── modify ────────────────────────────────────────────────── - -func TestAiappModify_should_call_modify_ai_app(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - err := execCmd(t, root, []string{"aiapp", "modify"}, map[string]string{ - "prompt": "改为翻译应用", - "thread-id": "THREAD001", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "modify_ai_app") - assertToolArg(t, cap, "prompt", "改为翻译应用") - assertToolArg(t, cap, "threadId", "THREAD001") -} - -func TestAiappModify_should_include_skills_when_provided(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "modify"}, map[string]string{ - "prompt": "P", "thread-id": "TH1", "skills": "s1,s2,s3", - }) - assertToolArg(t, cap, "officialSkillUids", []string{"s1", "s2", "s3"}) -} - -func TestAiappModify_should_not_include_skills_when_empty(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "modify"}, map[string]string{ - "prompt": "P", "thread-id": "TH1", "skills": "", - }) - assertArgNotPresent(t, cap, "officialSkillUids") -} - -func TestAiappModify_should_pass_thread_id(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "modify"}, map[string]string{ - "prompt": "P", "thread-id": "TH_XYZ", - }) - assertToolArg(t, cap, "threadId", "TH_XYZ") -} - -func TestAiappModify_should_make_single_call(t *testing.T) { - cap := setupTestDeps(t, "aiapp") - root := buildRoot() - _ = execCmd(t, root, []string{"aiapp", "modify"}, map[string]string{ - "prompt": "P", "thread-id": "TH1", - }) - assertCallCount(t, cap, 1) -} diff --git a/test/cli_compat/conference_test.go b/test/cli_compat/conference_test.go deleted file mode 100644 index 6f2f9ca5..00000000 --- a/test/cli_compat/conference_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package cli_compat_test - -import "testing" - -// ── conference meeting create ────────────────────────────── - -func TestConferenceMeetingCreate_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "conference") - root := buildRoot() - err := execCmd(t, root, []string{"conference", "meeting", "create"}, map[string]string{ - "title": "产品评审会", "start": "2026-03-11T14:00:00+08:00", "end": "2026-03-11T15:00:00+08:00", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "create_meeting_reservation") -} - -func TestConferenceMeetingCreate_should_pass_title_start_end(t *testing.T) { - cap := setupTestDeps(t, "conference") - root := buildRoot() - _ = execCmd(t, root, []string{"conference", "meeting", "create"}, map[string]string{ - "title": "产品评审会", "start": "2026-03-11T14:00:00+08:00", "end": "2026-03-11T15:00:00+08:00", - }) - assertToolArg(t, cap, "title", "产品评审会") - // Verify startTime and endTime are numeric (may be int64 or float64) - last := cap.last() - switch last.Args["startTime"].(type) { - case int64, float64: - // ok - default: - t.Errorf("expected startTime to be numeric, got %T", last.Args["startTime"]) - } - switch last.Args["endTime"].(type) { - case int64, float64: - // ok - default: - t.Errorf("expected endTime to be numeric, got %T", last.Args["endTime"]) - } -} - -func TestConferenceMeetingCreate_should_handle_long_title(t *testing.T) { - cap := setupTestDeps(t, "conference") - root := buildRoot() - longTitle := "这是一个很长很长的会议标题用来测试标题长度处理能力的极限情况" - _ = execCmd(t, root, []string{"conference", "meeting", "create"}, map[string]string{ - "title": longTitle, "start": "2026-03-11T14:00:00+08:00", "end": "2026-03-11T15:00:00+08:00", - }) - assertToolArg(t, cap, "title", longTitle) -} - -func TestConferenceMeetingCreate_should_handle_empty_title(t *testing.T) { - // execCmd skips empty values, so we use root.SetArgs directly - cap := setupTestDeps(t, "conference") - root := buildRoot() - _ = execCmd(t, root, []string{"conference", "meeting", "create"}, map[string]string{ - "title": "x", "start": "2026-03-11T14:00:00+08:00", "end": "2026-03-11T15:00:00+08:00", - }) - // Just verify the call was made; empty title testing requires direct SetArgs - assertCallCount(t, cap, 1) -} - -func TestConferenceMeetingCreate_should_make_exactly_one_call(t *testing.T) { - cap := setupTestDeps(t, "conference") - root := buildRoot() - _ = execCmd(t, root, []string{"conference", "meeting", "create"}, map[string]string{ - "title": "会议", "start": "2026-01-01T00:00:00Z", "end": "2026-01-01T01:00:00Z", - }) - assertCallCount(t, cap, 1) -} - -func TestConferenceMeetingCreate_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "conference") - root := buildRoot() - _ = execCmd(t, root, []string{"conference", "meeting", "create"}, map[string]string{ - "title": "会议", "start": "2026-01-01T00:00:00Z", "end": "2026-01-01T01:00:00Z", - }) - assertCallCount(t, cap, 0) -} - -func TestConferenceMeetingCreate_should_reject_invalid_start(t *testing.T) { - _ = setupTestDeps(t, "conference") - root := buildRoot() - err := execCmd(t, root, []string{"conference", "meeting", "create"}, map[string]string{ - "title": "测试", "start": "not-a-date", "end": "2026-01-01T01:00:00Z", - }) - if err == nil { - t.Fatal("expected error for invalid start time") - } -} diff --git a/test/cli_compat/law_test.go b/test/cli_compat/law_test.go deleted file mode 100644 index 5caee3d8..00000000 --- a/test/cli_compat/law_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package cli_compat_test - -import ( - "testing" -) - -// ── law consult ───────────────────────────────────────────── - -func TestLawConsult_should_pass_deep_think(t *testing.T) { - t.Skip("bool flag --deep-think=true not reliably passed through execCmd") -} - -func TestLawConsult_should_pass_online_search(t *testing.T) { - t.Skip("bool flag --online-search=true not reliably passed through execCmd") -} - -func TestLawConsult_should_not_pass_bool_when_false(t *testing.T) { - t.Skip("bool flag default handling not reliably testable through execCmd") -} - -func TestLawConsult_should_make_single_call(t *testing.T) { - cap := setupTestDeps(t, "law") - root := buildRoot() - _ = execCmd(t, root, []string{"law", "consult"}, map[string]string{ - "query": "test", - }) - assertCallCount(t, cap, 1) -} - -func TestLawConsult_should_not_call_mcp_in_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "law") - root := buildRoot() - _ = execCmd(t, root, []string{"law", "consult"}, map[string]string{ - "query": "test", - }) - assertCallCount(t, cap, 0) -} - -// ── law search ────────────────────────────────────────────── diff --git a/test/cli_compat/live_test.go b/test/cli_compat/live_test.go deleted file mode 100644 index c0fc0220..00000000 --- a/test/cli_compat/live_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package cli_compat_test - -import "testing" - -// ── live stream list ─────────────────────────────────────── - -func TestLiveStreamList_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "live") - root := buildRoot() - err := execCmd(t, root, []string{"live", "stream", "list"}, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "get_my_lives") -} - -func TestLiveStreamList_should_pass_nil_args(t *testing.T) { - cap := setupTestDeps(t, "live") - root := buildRoot() - _ = execCmd(t, root, []string{"live", "stream", "list"}, nil) - assertNilArgs(t, cap) -} - -func TestLiveStreamList_should_make_exactly_one_call(t *testing.T) { - cap := setupTestDeps(t, "live") - root := buildRoot() - _ = execCmd(t, root, []string{"live", "stream", "list"}, nil) - assertCallCount(t, cap, 1) -} - -func TestLiveStreamList_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "live") - root := buildRoot() - err := execCmd(t, root, []string{"live", "stream", "list"}, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertCallCount(t, cap, 0) -} - -func TestLiveStreamList_should_succeed_without_errors(t *testing.T) { - _ = setupTestDeps(t, "live") - root := buildRoot() - err := execCmd(t, root, []string{"live", "stream", "list"}, nil) - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } -} diff --git a/test/cli_compat/oa_test.go b/test/cli_compat/oa_test.go deleted file mode 100644 index 96421f1f..00000000 --- a/test/cli_compat/oa_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package cli_compat_test - -import "testing" - -// ── oa approval list-pending ─────────────────────────────── - -func TestOaApprovalListPending_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "oa") - root := buildRoot() - err := execCmd(t, root, []string{"oa", "approval", "list-pending"}, map[string]string{ - "start": "2026-03-10T00:00:00+08:00", "end": "2026-03-10T23:59:59+08:00", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "list_pending_approvals") -} - -func TestOaApprovalListPending_should_pass_timestamps(t *testing.T) { - cap := setupTestDeps(t, "oa") - root := buildRoot() - _ = execCmd(t, root, []string{"oa", "approval", "list-pending"}, map[string]string{ - "start": "2026-03-10T00:00:00+08:00", "end": "2026-03-10T23:59:59+08:00", - }) - // Source converts ISO-8601 to float64 millis; MCP uses starTime (not startTime) - last := cap.last() - if last == nil { - t.Fatal("no MCP call captured") - } - if _, ok := last.Args["starTime"].(float64); !ok { - t.Errorf("expected starTime as float64, got %T", last.Args["starTime"]) - } - if _, ok := last.Args["endTime"].(float64); !ok { - t.Errorf("expected endTime as float64, got %T", last.Args["endTime"]) - } -} - -func TestOaApprovalListPending_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "oa") - root := buildRoot() - _ = execCmd(t, root, []string{"oa", "approval", "list-pending"}, map[string]string{ - "start": "2026-03-10T00:00:00+08:00", "end": "2026-03-10T23:59:59+08:00", - }) - assertCallCount(t, cap, 0) -} - -func TestOaApprovalListPending_should_reject_invalid_iso_start(t *testing.T) { - _ = setupTestDeps(t, "oa") - root := buildRoot() - err := execCmd(t, root, []string{"oa", "approval", "list-pending"}, map[string]string{ - "start": "not-a-date", "end": "2026-03-10T23:59:59+08:00", - }) - if err == nil { - t.Fatal("expected error for invalid start time") - } -} diff --git a/test/cli_compat/tb_test.go b/test/cli_compat/tb_test.go deleted file mode 100644 index bbd0ffd5..00000000 --- a/test/cli_compat/tb_test.go +++ /dev/null @@ -1,1250 +0,0 @@ -package cli_compat_test - -// 暂未上线,保留测试源码但不执行 -// 对应 register.go 中注释掉的 tbCmd - -/* - -import "testing" - -// ── tb project list ──────────────────────────────────────── - -func TestTbProjectList_should_call_list_projects(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "list"}, map[string]string{"name": ""}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "list_projects") -} - -func TestTbProjectList_should_include_name_when_provided(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list"}, map[string]string{"name": "Q1"}) - assertToolArg(t, cap, "name", "Q1") -} - -func TestTbProjectList_should_omit_name_when_empty(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list"}, map[string]string{"name": ""}) - last := cap.last() - if _, ok := last.Args["name"]; ok { - t.Error("expected name to be omitted when empty") - } -} - -func TestTbProjectList_should_handle_chinese_name(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list"}, map[string]string{"name": "产品迭代"}) - assertToolArg(t, cap, "name", "产品迭代") -} - -func TestTbProjectList_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list"}, map[string]string{"name": ""}) - assertCallCount(t, cap, 0) -} - -// ── tb project list-mine ─────────────────────────────────── - -func TestTbProjectListMine_should_call_get_user_projects(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "list-mine"}, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "get_user_projects") -} - -func TestTbProjectListMine_should_pass_nil_args(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-mine"}, nil) - assertNilArgs(t, cap) -} - -func TestTbProjectListMine_should_make_exactly_one_call(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-mine"}, nil) - assertCallCount(t, cap, 1) -} - -func TestTbProjectListMine_should_succeed(t *testing.T) { - _ = setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "list-mine"}, nil) - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } -} - -func TestTbProjectListMine_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-mine"}, nil) - assertCallCount(t, cap, 0) -} - -// ── tb project create ────────────────────────────────────── - -func TestTbProjectCreate_should_call_create_project(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "create"}, map[string]string{"name": "新项目"}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "create_project") -} - -func TestTbProjectCreate_should_pass_name(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "create"}, map[string]string{"name": "Q1 产品迭代"}) - assertToolArg(t, cap, "name", "Q1 产品迭代") -} - -func TestTbProjectCreate_should_handle_chinese_name(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "create"}, map[string]string{"name": "钉钉开发项目"}) - assertToolArg(t, cap, "name", "钉钉开发项目") -} - -func TestTbProjectCreate_should_handle_empty_name(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "create"}, map[string]string{"name": ""}) - assertToolArg(t, cap, "name", "") -} - -func TestTbProjectCreate_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "create"}, map[string]string{"name": "test"}) - assertCallCount(t, cap, 0) -} - -// ── tb project update ────────────────────────────────────── - -func TestTbProjectUpdate_should_call_update_project_info(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "update"}, map[string]string{"id": "PID1", "name": "新名称"}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "update_project_info") -} - -func TestTbProjectUpdate_should_pass_project_id(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "update"}, map[string]string{"id": "PID123", "name": ""}) - assertToolArg(t, cap, "projectId", "PID123") -} - -func TestTbProjectUpdate_should_include_name_when_provided(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "update"}, map[string]string{"id": "PID1", "name": "Updated"}) - assertToolArg(t, cap, "name", "Updated") -} - -func TestTbProjectUpdate_should_omit_name_when_empty(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "update"}, map[string]string{"id": "PID1", "name": ""}) - last := cap.last() - if _, ok := last.Args["name"]; ok { - t.Error("expected name to be omitted when empty") - } -} - -func TestTbProjectUpdate_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "update"}, map[string]string{"id": "PID1", "name": ""}) - assertCallCount(t, cap, 0) -} - -// ── tb project list-members ──────────────────────────────── - -func TestTbProjectListMembers_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "list-members"}, map[string]string{"id": "PID1"}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "get_project_member_list") -} - -func TestTbProjectListMembers_should_pass_project_id(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-members"}, map[string]string{"id": "PID999"}) - assertToolArg(t, cap, "projectId", "PID999") -} - -func TestTbProjectListMembers_should_make_one_call(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-members"}, map[string]string{"id": "PID1"}) - assertCallCount(t, cap, 1) -} - -func TestTbProjectListMembers_should_handle_empty_id(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-members"}, map[string]string{"id": ""}) - assertToolArg(t, cap, "projectId", "") -} - -func TestTbProjectListMembers_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-members"}, map[string]string{"id": "PID1"}) - assertCallCount(t, cap, 0) -} - -// ── tb project add-member ────────────────────────────────── - -func TestTbProjectAddMember_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "add-member"}, map[string]string{"id": "PID1", "users": "userId1,userId2"}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "add_project_members") -} - -func TestTbProjectAddMember_should_pass_project_id_and_users(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "add-member"}, map[string]string{"id": "PID1", "users": "userId1,userId2"}) - assertToolArg(t, cap, "projectId", "PID1") - assertToolArg(t, cap, "userIds", "userId1,userId2") -} - -func TestTbProjectAddMember_should_handle_single_user(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "add-member"}, map[string]string{"id": "PID1", "users": "userId1"}) - assertToolArg(t, cap, "userIds", "userId1") -} - -func TestTbProjectAddMember_should_handle_empty_users(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "add-member"}, map[string]string{"id": "PID1", "users": ""}) - assertToolArg(t, cap, "userIds", "") -} - -func TestTbProjectAddMember_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "add-member"}, map[string]string{"id": "PID1", "users": "userId1"}) - assertCallCount(t, cap, 0) -} - -// ── tb project list-task-types ───────────────────────────── - -func TestTbProjectListTaskTypes_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "list-task-types"}, map[string]string{"id": "PID1"}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "get_project_task_types") - assertToolArg(t, cap, "projectId", "PID1") -} - -func TestTbProjectListTaskTypes_should_handle_empty_id(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-task-types"}, map[string]string{"id": ""}) - assertToolArg(t, cap, "projectId", "") -} - -func TestTbProjectListTaskTypes_should_make_one_call(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-task-types"}, map[string]string{"id": "PID1"}) - assertCallCount(t, cap, 1) -} - -func TestTbProjectListTaskTypes_should_succeed(t *testing.T) { - _ = setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "list-task-types"}, map[string]string{"id": "PID1"}) - if err != nil { - t.Fatalf("expected no error: %v", err) - } -} - -func TestTbProjectListTaskTypes_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-task-types"}, map[string]string{"id": "PID1"}) - assertCallCount(t, cap, 0) -} - -// ── tb project list-workflow ─────────────────────────────── - -func TestTbProjectListWorkflow_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "list-workflow"}, map[string]string{"id": "PID1", "keyword": ""}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "search_project_workflow_status") -} - -func TestTbProjectListWorkflow_should_include_keyword_when_provided(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-workflow"}, map[string]string{"id": "PID1", "keyword": "进行中"}) - assertToolArg(t, cap, "keyword", "进行中") -} - -func TestTbProjectListWorkflow_should_omit_keyword_when_empty(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-workflow"}, map[string]string{"id": "PID1", "keyword": ""}) - last := cap.last() - if _, ok := last.Args["keyword"]; ok { - t.Error("expected keyword to be omitted when empty") - } -} - -func TestTbProjectListWorkflow_should_pass_project_id(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-workflow"}, map[string]string{"id": "PID1", "keyword": ""}) - assertToolArg(t, cap, "projectId", "PID1") -} - -func TestTbProjectListWorkflow_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-workflow"}, map[string]string{"id": "PID1", "keyword": ""}) - assertCallCount(t, cap, 0) -} - -// ── tb project list-priorities ───────────────────────────── - -func TestTbProjectListPriorities_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "list-priorities"}, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "list_org_task_priorities") -} - -func TestTbProjectListPriorities_should_pass_nil_args(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-priorities"}, nil) - assertNilArgs(t, cap) -} - -func TestTbProjectListPriorities_should_make_one_call(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-priorities"}, nil) - assertCallCount(t, cap, 1) -} - -func TestTbProjectListPriorities_should_succeed(t *testing.T) { - _ = setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "project", "list-priorities"}, nil) - if err != nil { - t.Fatalf("expected no error: %v", err) - } -} - -func TestTbProjectListPriorities_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "project", "list-priorities"}, nil) - assertCallCount(t, cap, 0) -} - -// ── tb task create ───────────────────────────────────────── - -func TestTbTaskCreate_should_call_create_task(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "task", "create"}, map[string]string{ - "project": "PID1", "title": "开发登录模块", "content": "实现 OAuth 登录", "executor": "", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "create_task") -} - -func TestTbTaskCreate_should_return_error_when_content_empty(t *testing.T) { - _ = setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "task", "create"}, map[string]string{ - "project": "PID1", "title": "test", "content": "", "executor": "", - }) - if err == nil { - t.Fatal("expected error for empty content") - } -} - -func TestTbTaskCreate_should_include_executor_when_provided(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "task", "create"}, map[string]string{ - "project": "PID1", "title": "test", "content": "desc", "executor": "exec001", - }) - assertToolArg(t, cap, "executorId", "exec001") -} - -func TestTbTaskCreate_should_omit_executor_when_empty(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "task", "create"}, map[string]string{ - "project": "PID1", "title": "test", "content": "desc", "executor": "", - }) - last := cap.last() - if _, ok := last.Args["executorId"]; ok { - t.Error("expected executorId to be omitted when empty") - } -} - -func TestTbTaskCreate_should_pass_all_required_fields(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "task", "create"}, map[string]string{ - "project": "PID1", "title": "任务标题", "content": "任务描述", "executor": "", - }) - assertToolArg(t, cap, "projectId", "PID1") - assertToolArg(t, cap, "title", "任务标题") - assertToolArg(t, cap, "content", "任务描述") -} - -// ── tb task get ──────────────────────────────────────────── - -func TestTbTaskGet_should_call_get_task_detail(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "get") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - err := cmd.RunE(cmd, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "get_task_detail") - assertToolArg(t, cap, "taskId", "TID1") -} - -func TestTbTaskGet_should_handle_different_task_ids(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "get") - setParentFlags(cmd, map[string]string{"id": "TID999"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "taskId", "TID999") -} - -func TestTbTaskGet_should_make_one_call(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "get") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - _ = cmd.RunE(cmd, nil) - assertCallCount(t, cap, 1) -} - -func TestTbTaskGet_should_handle_empty_id(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "get") - setParentFlags(cmd, map[string]string{"id": ""}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "taskId", "") -} - -func TestTbTaskGet_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "get") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - _ = cmd.RunE(cmd, nil) - assertCallCount(t, cap, 0) -} - -// ── tb task search ───────────────────────────────────────── - -func TestTbTaskSearch_should_call_search_task_ids_by_tql(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "task", "search"}, map[string]string{ - "tql": "isDone = false ORDER BY priority DESC", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "search_task_ids_by_tql") -} - -func TestTbTaskSearch_should_pass_tql_query(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "task", "search"}, map[string]string{"tql": "isDone = false"}) - assertToolArg(t, cap, "tql", "isDone = false") -} - -func TestTbTaskSearch_should_handle_complex_tql(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - tql := `isDone = false AND priority = "high" ORDER BY created DESC LIMIT 50` - _ = execCmd(t, root, []string{"tb", "task", "search"}, map[string]string{"tql": tql}) - assertToolArg(t, cap, "tql", tql) -} - -func TestTbTaskSearch_should_handle_tql_with_chinese_chars(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - tql := `statusName = "进行中"` - _ = execCmd(t, root, []string{"tb", "task", "search"}, map[string]string{"tql": tql}) - assertToolArg(t, cap, "tql", tql) -} - -func TestTbTaskSearch_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "task", "search"}, map[string]string{"tql": "isDone = false"}) - assertCallCount(t, cap, 0) -} - -// ── tb task update-title ─────────────────────────────────── - -func TestTbTaskUpdateTitle_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-title") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"title": "新标题"}) - err := cmd.RunE(cmd, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "update_task_title") -} - -func TestTbTaskUpdateTitle_should_return_error_when_title_empty(t *testing.T) { - _ = setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-title") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"title": ""}) - err := cmd.RunE(cmd, nil) - if err == nil { - t.Fatal("expected error for empty title") - } -} - -func TestTbTaskUpdateTitle_should_pass_task_id_and_content(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-title") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"title": "Updated"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "taskId", "TID1") - assertToolArg(t, cap, "content", "Updated") -} - -func TestTbTaskUpdateTitle_should_handle_chinese_title(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-title") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"title": "完成开发"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "content", "完成开发") -} - -func TestTbTaskUpdateTitle_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-title") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"title": "test"}) - _ = cmd.RunE(cmd, nil) - assertCallCount(t, cap, 0) -} - -// ── tb task update-status ────────────────────────────────── - -func TestTbTaskUpdateStatus_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-status") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"status": "已完成"}) - err := cmd.RunE(cmd, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "update_task_status") -} - -func TestTbTaskUpdateStatus_should_pass_task_id_and_status(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-status") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"status": "进行中"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "taskId", "TID1") - assertToolArg(t, cap, "statusName", "进行中") -} - -func TestTbTaskUpdateStatus_should_handle_english_status(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-status") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"status": "Done"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "statusName", "Done") -} - -func TestTbTaskUpdateStatus_should_handle_empty_status(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-status") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"status": ""}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "statusName", "") -} - -func TestTbTaskUpdateStatus_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-status") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"status": "Done"}) - _ = cmd.RunE(cmd, nil) - assertCallCount(t, cap, 0) -} - -// ── tb task update-priority ──────────────────────────────── - -func TestTbTaskUpdatePriority_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-priority") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"priority": "高"}) - err := cmd.RunE(cmd, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "update_task_priority") -} - -func TestTbTaskUpdatePriority_should_pass_priority_name(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-priority") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"priority": "紧急"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "priorityName", "紧急") -} - -func TestTbTaskUpdatePriority_should_handle_english_priority(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-priority") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"priority": "High"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "priorityName", "High") -} - -func TestTbTaskUpdatePriority_should_handle_empty_priority(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-priority") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"priority": ""}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "priorityName", "") -} - -func TestTbTaskUpdatePriority_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-priority") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"priority": "High"}) - _ = cmd.RunE(cmd, nil) - assertCallCount(t, cap, 0) -} - -// ── tb task assign ───────────────────────────────────────── - -func TestTbTaskAssign_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "assign") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"executor": "exec001"}) - err := cmd.RunE(cmd, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "assign_task_assignees") -} - -func TestTbTaskAssign_should_pass_task_id_and_executor(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "assign") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"executor": "exec001"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "taskId", "TID1") - assertToolArg(t, cap, "executorId", "exec001") -} - -func TestTbTaskAssign_should_handle_different_executor(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "assign") - setParentFlags(cmd, map[string]string{"id": "TID2"}) - setFlags(cmd, map[string]string{"executor": "exec999"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "executorId", "exec999") -} - -func TestTbTaskAssign_should_handle_empty_executor(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "assign") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"executor": ""}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "executorId", "") -} - -func TestTbTaskAssign_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "assign") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"executor": "exec001"}) - _ = cmd.RunE(cmd, nil) - assertCallCount(t, cap, 0) -} - -// ── tb task update-due ───────────────────────────────────── - -func TestTbTaskUpdateDue_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-due") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"date": "2026-03-31T00:00:00Z"}) - err := cmd.RunE(cmd, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "set_task_due_date") -} - -func TestTbTaskUpdateDue_should_pass_date(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-due") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"date": "2026-12-31"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "dueDate", "2026-12-31") -} - -func TestTbTaskUpdateDue_should_pass_iso_date(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-due") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"date": "2026-03-31T23:59:59+08:00"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "dueDate", "2026-03-31T23:59:59+08:00") -} - -func TestTbTaskUpdateDue_should_handle_empty_date(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-due") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"date": ""}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "dueDate", "") -} - -func TestTbTaskUpdateDue_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "update-due") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"date": "2026-03-31"}) - _ = cmd.RunE(cmd, nil) - assertCallCount(t, cap, 0) -} - -// ── tb task comment ──────────────────────────────────────── - -func TestTbTaskComment_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "comment") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"content": "做得好"}) - err := cmd.RunE(cmd, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "add_task_comment") -} - -func TestTbTaskComment_should_pass_task_id_and_content(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "comment") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"content": "评论内容"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "taskId", "TID1") - assertToolArg(t, cap, "content", "评论内容") -} - -func TestTbTaskComment_should_handle_long_content(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "comment") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - long := "这是一段很长的评论内容用于测试长文本处理能力的边界情况请注意观察" - setFlags(cmd, map[string]string{"content": long}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "content", long) -} - -func TestTbTaskComment_should_handle_empty_content(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "comment") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"content": ""}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "content", "") -} - -func TestTbTaskComment_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "comment") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"content": "test"}) - _ = cmd.RunE(cmd, nil) - assertCallCount(t, cap, 0) -} - -// ── tb task add-progress ─────────────────────────────────── - -func TestTbTaskAddProgress_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "add-progress") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"title": "进展1", "content": "完成50%", "status": ""}) - err := cmd.RunE(cmd, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "create_task_progress") -} - -func TestTbTaskAddProgress_should_include_status_when_provided(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "add-progress") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"title": "进展1", "content": "有风险", "status": "2"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "status", "2") -} - -func TestTbTaskAddProgress_should_omit_status_when_empty(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "add-progress") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"title": "进展1", "content": "desc", "status": ""}) - _ = cmd.RunE(cmd, nil) - last := cap.last() - if _, ok := last.Args["status"]; ok { - t.Error("expected status to be omitted when empty") - } -} - -func TestTbTaskAddProgress_should_pass_all_fields(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "add-progress") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"title": "周报", "content": "完成核心功能", "status": "1"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "taskId", "TID1") - assertToolArg(t, cap, "title", "周报") - assertToolArg(t, cap, "content", "完成核心功能") - assertToolArg(t, cap, "status", "1") -} - -func TestTbTaskAddProgress_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "add-progress") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - setFlags(cmd, map[string]string{"title": "t", "content": "c", "status": ""}) - _ = cmd.RunE(cmd, nil) - assertCallCount(t, cap, 0) -} - -// ── tb task get-progress ─────────────────────────────────── - -func TestTbTaskGetProgress_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "get-progress") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - err := cmd.RunE(cmd, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "get_task_progress") - assertToolArg(t, cap, "taskId", "TID1") -} - -func TestTbTaskGetProgress_should_handle_different_task_id(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "get-progress") - setParentFlags(cmd, map[string]string{"id": "TID999"}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "taskId", "TID999") -} - -func TestTbTaskGetProgress_should_make_one_call(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "get-progress") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - _ = cmd.RunE(cmd, nil) - assertCallCount(t, cap, 1) -} - -func TestTbTaskGetProgress_should_handle_empty_id(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "get-progress") - setParentFlags(cmd, map[string]string{"id": ""}) - _ = cmd.RunE(cmd, nil) - assertToolArg(t, cap, "taskId", "") -} - -func TestTbTaskGetProgress_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - cmd := findCmd(root, "tb", "task", "get-progress") - setParentFlags(cmd, map[string]string{"id": "TID1"}) - _ = cmd.RunE(cmd, nil) - assertCallCount(t, cap, 0) -} - -// ── tb worktime list ─────────────────────────────────────── - -func TestTbWorktimeList_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "worktime", "list"}, map[string]string{"task": "TID1"}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "get_actual_work_hours_by_task_id") -} - -func TestTbWorktimeList_should_pass_task_id(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "list"}, map[string]string{"task": "TID123"}) - assertToolArg(t, cap, "taskId", "TID123") -} - -func TestTbWorktimeList_should_handle_empty_task_id(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "list"}, map[string]string{"task": ""}) - assertToolArg(t, cap, "taskId", "") -} - -func TestTbWorktimeList_should_make_one_call(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "list"}, map[string]string{"task": "TID1"}) - assertCallCount(t, cap, 1) -} - -func TestTbWorktimeList_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "list"}, map[string]string{"task": "TID1"}) - assertCallCount(t, cap, 0) -} - -// ── tb worktime create ───────────────────────────────────── - -func TestTbWorktimeCreate_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "worktime", "create"}, map[string]string{ - "task": "TID1", "executor": "exec001", "start": "2026-03-01", "end": "2026-03-02", "hours": "3600000", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "create_actual_work_hour_record") -} - -func TestTbWorktimeCreate_should_pass_all_required_fields(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "create"}, map[string]string{ - "task": "TID1", "executor": "exec001", "start": "2026-03-01", "end": "2026-03-02", "hours": "3600000", - }) - assertToolArg(t, cap, "taskId", "TID1") - assertToolArg(t, cap, "executorId", "exec001") - assertToolArg(t, cap, "startDate", "2026-03-01") - assertToolArg(t, cap, "endDate", "2026-03-02") - assertToolArg(t, cap, "actualHour", "3600000") -} - -func TestTbWorktimeCreate_should_handle_large_hours(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "create"}, map[string]string{ - "task": "TID1", "executor": "exec001", "start": "2026-03-01", "end": "2026-03-02", "hours": "999999999", - }) - assertToolArg(t, cap, "actualHour", "999999999") -} - -func TestTbWorktimeCreate_should_handle_empty_fields(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "create"}, map[string]string{ - "task": "", "executor": "", "start": "", "end": "", "hours": "", - }) - assertToolArg(t, cap, "taskId", "") -} - -func TestTbWorktimeCreate_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "create"}, map[string]string{ - "task": "TID1", "executor": "exec001", "start": "2026-03-01", "end": "2026-03-02", "hours": "100", - }) - assertCallCount(t, cap, 0) -} - -// ── tb worktime update ───────────────────────────────────── - -func TestTbWorktimeUpdate_should_call_correct_tool(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - err := execCmd(t, root, []string{"tb", "worktime", "update"}, map[string]string{ - "id": "WH001", "executor": "exec001", "date": "2026-03-01", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertToolName(t, cap, "update_actual_work_hour_record") -} - -func TestTbWorktimeUpdate_should_pass_all_fields(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "update"}, map[string]string{ - "id": "WH001", "executor": "exec001", "date": "2026-03-15", - }) - assertToolArg(t, cap, "workHourId", "WH001") - assertToolArg(t, cap, "executorId", "exec001") - assertToolArg(t, cap, "date", "2026-03-15") -} - -func TestTbWorktimeUpdate_should_handle_different_ids(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "update"}, map[string]string{ - "id": "WH999", "executor": "exec999", "date": "2026-12-31", - }) - assertToolArg(t, cap, "workHourId", "WH999") - assertToolArg(t, cap, "executorId", "exec999") -} - -func TestTbWorktimeUpdate_should_handle_empty_fields(t *testing.T) { - cap := setupTestDeps(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "update"}, map[string]string{ - "id": "", "executor": "", "date": "", - }) - assertToolArg(t, cap, "workHourId", "") -} - -func TestTbWorktimeUpdate_should_not_call_when_dry_run(t *testing.T) { - cap := setupTestDepsWithDryRun(t, "tb") - root := buildRoot() - skipIfNotRegistered(t, root, "tb") - _ = execCmd(t, root, []string{"tb", "worktime", "update"}, map[string]string{ - "id": "WH001", "executor": "exec001", "date": "2026-03-01", - }) - assertCallCount(t, cap, 0) -} - -*/