diff --git a/shortcuts/calendar/calendar_search_event.go b/shortcuts/calendar/calendar_search_event.go index 1200230abe..db2723c4a1 100644 --- a/shortcuts/calendar/calendar_search_event.go +++ b/shortcuts/calendar/calendar_search_event.go @@ -174,7 +174,7 @@ var CalendarSearchEvent = common.Shortcut{ Description: "Search calendar events by keyword, time range, and attendees", Risk: "read", Scopes: []string{"calendar:calendar.event:read"}, - AuthTypes: []string{"user"}, + AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "calendar-id", Desc: "calendar ID (default: primary)"}, diff --git a/skills/lark-calendar/SKILL.md b/skills/lark-calendar/SKILL.md index 1974360181..1d201fd2d3 100644 --- a/skills/lark-calendar/SKILL.md +++ b/skills/lark-calendar/SKILL.md @@ -52,6 +52,8 @@ lark-cli calendar +get --calendar-id --event-id 仅返回基础字段(`event_id`/`summary`/`start`/`end` 等),需要详情请走 `+get`。 +身份范围:用户身份搜索当前用户有权访问的日历;应用身份搜索应用自身有权访问的日历(例如应用主日历及应用创建的日程),不会自动获得用户个人日历的访问权限。 + ```bash # query 按关键词 可选 # start/end 按时间范围(ISO 8601 或 YYYY-MM-DD)可选 diff --git a/tests/cli_e2e/calendar/calendar_search_event_workflow_test.go b/tests/cli_e2e/calendar/calendar_search_event_workflow_test.go new file mode 100644 index 0000000000..1dd98d335d --- /dev/null +++ b/tests/cli_e2e/calendar/calendar_search_event_workflow_test.go @@ -0,0 +1,130 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package calendar + +import ( + "context" + "fmt" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestCalendar_SearchEventDryRunAsBot(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + t.Setenv("LARKSUITE_CLI_APP_ID", "app") + t.Setenv("LARKSUITE_CLI_APP_SECRET", "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{ + "calendar", "+search-event", + "--calendar-id", "cal_dry", + "--query", "calendar-e2e-search", + "--start", "2026-07-22T10:00:00+08:00", + "--end", "2026-07-22T11:00:00+08:00", + "--dry-run", + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + require.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.0.method").String(), "stdout:\n%s", result.Stdout) + require.Equal(t, "/open-apis/calendar/v4/calendars/cal_dry/events/search_event", clie2e.DryRunGet(result.Stdout, "api.0.url").String(), "stdout:\n%s", result.Stdout) + require.Equal(t, "cal_dry", clie2e.DryRunGet(result.Stdout, "calendar_id").String(), "stdout:\n%s", result.Stdout) +} + +func TestCalendar_SearchEventWorkflowAsBot(t *testing.T) { + clie2e.SkipWithoutTenantAccessToken(t) + parentT := t + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + calendarID := getPrimaryCalendarID(t, ctx) + eventSummary := "calendar-e2e-search-" + clie2e.GenerateSuffix() + startAt := time.Now().UTC().Add(time.Hour).Truncate(time.Minute) + endAt := startAt.Add(30 * time.Minute) + + var eventID string + t.Run("create searchable event as bot", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "calendar", "+create", + "--calendar-id", calendarID, + "--summary", eventSummary, + "--start", startAt.Format(time.RFC3339), + "--end", endAt.Format(time.RFC3339), + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + eventID = gjson.Get(result.Stdout, "data.event_id").String() + require.NotEmpty(t, eventID, "stdout:\n%s", result.Stdout) + + parentT.Cleanup(func() { + if eventID == "" { + return + } + + cleanupCtx, cleanupCancel := clie2e.CleanupContext() + defer cleanupCancel() + deleteResult, deleteErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{ + Args: []string{"calendar", "events", "delete"}, + DefaultAs: "bot", + Params: map[string]any{ + "calendar_id": calendarID, + "event_id": eventID, + }, + }) + clie2e.ReportCleanupFailure(parentT, "delete event "+eventID, deleteResult, deleteErr) + }) + }) + + t.Run("find created event with shortcut as bot", func(t *testing.T) { + require.NotEmpty(t, eventID) + + var lastResult *clie2e.Result + err := clie2e.WaitForCondition(ctx, clie2e.WaitOptions{ + Timeout: 30 * time.Second, + Interval: 2 * time.Second, + TimeoutError: func() error { + if lastResult == nil { + return fmt.Errorf("created event was not searchable before timeout") + } + return fmt.Errorf("created event was not searchable before timeout; stdout=%s stderr=%s", lastResult.Stdout, lastResult.Stderr) + }, + }, func() (bool, error) { + result, runErr := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "calendar", "+search-event", + "--calendar-id", calendarID, + "--query", eventSummary, + "--start", startAt.Add(-time.Minute).Format(time.RFC3339), + "--end", endAt.Add(time.Minute).Format(time.RFC3339), + }, + DefaultAs: "bot", + }) + if runErr != nil { + return false, runErr + } + lastResult = result + if result.ExitCode != 0 { + return false, fmt.Errorf("search command failed: exit=%d stderr=%s", result.ExitCode, result.Stderr) + } + return gjson.Get(result.Stdout, `data.items.#(event_id=="`+eventID+`").event_id`).String() == eventID, nil + }) + require.NoError(t, err) + }) +} diff --git a/tests/cli_e2e/calendar/coverage.md b/tests/cli_e2e/calendar/coverage.md index 551c49ef5e..f5507a5c89 100644 --- a/tests/cli_e2e/calendar/coverage.md +++ b/tests/cli_e2e/calendar/coverage.md @@ -1,18 +1,21 @@ # Calendar CLI E2E Coverage ## Metrics -- Denominator: 23 leaf commands -- Covered: 11 -- Coverage: 47.8% +- Denominator: 28 leaf commands +- Covered: 13 +- Coverage: 46.4% ## Summary - TestCalendar_ViewAgenda: proves the user shortcut `calendar +agenda`; key `t.Run(...)` proof points are `view today agenda as user`, `view agenda with date range as user`, and `view agenda with pretty format as user`. - TestCalendar_PersonalEventWorkflowAsUser: proves a self-contained user event workflow across `calendar calendars primary`, `calendar +create`, `calendar events get`, and `calendar +agenda`; key `t.Run(...)` proof points are `get primary calendar as user`, `create personal event with shortcut as user`, `get created event as user`, and `find created event in agenda as user`. - TestCalendar_RSVPWorkflowAsUser: proves the user shortcuts `calendar +freebusy` and `calendar +rsvp`; key `t.Run(...)` proof points are `query freebusy as user`, `reply tentative as user`, `verify tentative freebusy as user`, `reply accept as user`, and `verify accepted freebusy as user`. +- TestCalendar_SearchEventDryRunAsBot: proves `calendar +search-event` accepts bot identity and produces the expected `search_event` request path without live credentials. +- TestCalendar_SearchEventWorkflowAsBot: proves a self-contained bot workflow across `calendar +create`, `calendar +search-event`, and `calendar events delete`; key `t.Run(...)` proof points are `create searchable event as bot` and `find created event with shortcut as bot`. +- TestCalendar_UpdateDryRun: proves `calendar +update` request construction as bot, including event mutation and attendee add/remove calls. - TestCalendar_CreateEvent: proves `calendar +create`, `calendar events get`, and `calendar events delete`; key `t.Run(...)` proof points are `create event with shortcut as bot`, `verify event created as bot`, and `delete event as bot`. - TestCalendar_ManageCalendar: proves `calendar calendars primary`, `calendar calendars create`, `calendar calendars get`, and `calendar calendars patch`; key `t.Run(...)` proof points are `get primary calendar as bot`, `create calendar as bot`, `get created calendar as bot`, and `update calendar as bot`. - Cleanup note: `calendar calendars delete` is part of the calendar lifecycle workflow and is counted as covered because the workflow proves the full shared-calendar lifecycle. -- Blocked area: direct `event.attendees *` APIs, `calendar calendars search`, `calendar events create|instance_view|patch|search`, `calendar freebusys list`, and planning shortcuts `calendar +room-find` / `calendar +suggestion` still need deterministic workflows; the planning shortcuts currently depend on live tenant availability and room inventory, so they remain uncovered. +- Blocked area: direct `event.attendees *` APIs, `calendar calendars search`, `calendar events create|instance_view|patch|search_event|share_info`, `calendar freebusys list`, and shortcuts `calendar +get` / `calendar +meeting` / `calendar +room-find` / `calendar +suggestion` still need deterministic workflows; the planning shortcuts currently depend on live tenant availability and room inventory, so they remain uncovered. ## Command Table @@ -21,9 +24,13 @@ | ✓ | calendar +agenda | shortcut | calendar_view_agenda_test.go::TestCalendar_ViewAgenda; calendar_personal_event_workflow_test.go::TestCalendar_PersonalEventWorkflowAsUser/find created event in agenda as user | default today; `--start`; `--end`; `--format pretty` | user identity readback plus general agenda view | | ✓ | calendar +create | shortcut | calendar_create_event_test.go::TestCalendar_CreateEvent/create event with shortcut as bot; calendar_personal_event_workflow_test.go::TestCalendar_PersonalEventWorkflowAsUser/create personal event with shortcut as user | `--summary`; `--start`; `--end`; `--calendar-id`; `--description` | bot and user workflow coverage | | ✓ | calendar +freebusy | shortcut | calendar_rsvp_workflow_test.go::TestCalendar_RSVPWorkflowAsUser/query freebusy as user; calendar_rsvp_workflow_test.go::TestCalendar_RSVPWorkflowAsUser/verify tentative freebusy as user; calendar_rsvp_workflow_test.go::TestCalendar_RSVPWorkflowAsUser/verify accepted freebusy as user | default current user; `--start`; `--end` | user identity flow | +| ✕ | calendar +get | shortcut | | none | no direct shortcut workflow yet | +| ✕ | calendar +meeting | shortcut | | none | requires an event with linked meeting artifacts | | ✕ | calendar +room-find | shortcut | | none | no deterministic self-contained workflow yet; output depends on live room inventory | | ✓ | calendar +rsvp | shortcut | calendar_rsvp_workflow_test.go::TestCalendar_RSVPWorkflowAsUser/reply tentative as user; calendar_rsvp_workflow_test.go::TestCalendar_RSVPWorkflowAsUser/reply accept as user | `--calendar-id`; `--event-id`; `--rsvp-status` | user reply flow | +| ✓ | calendar +search-event | shortcut | calendar_search_event_workflow_test.go::TestCalendar_SearchEventDryRunAsBot; calendar_search_event_workflow_test.go::TestCalendar_SearchEventWorkflowAsBot/find created event with shortcut as bot | bot identity; `--calendar-id`; `--query`; `--start`; `--end`; `--dry-run` | bot-owned event search plus dry-run request contract | | ✕ | calendar +suggestion | shortcut | | none | no deterministic self-contained workflow yet; output depends on live availability suggestions | +| ✓ | calendar +update | shortcut | calendar_update_dryrun_test.go::TestCalendar_UpdateDryRun | event fields plus attendee add/remove under bot identity | dry-run request contract | | ✓ | calendar calendars create | api | calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/create calendar as bot | `summary`; `description` in `--data` | | | ✓ | calendar calendars delete | api | calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/delete calendar as bot | `calendar_id` in `--params` | | | ✓ | calendar calendars get | api | calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/get created calendar as bot; calendar_manage_calendar_test.go::TestCalendar_ManageCalendar/verify updated calendar as bot | `calendar_id` in `--params` | | @@ -36,7 +43,8 @@ | ✓ | calendar events get | api | calendar_create_event_test.go::TestCalendar_CreateEvent/verify event created as bot; calendar_personal_event_workflow_test.go::TestCalendar_PersonalEventWorkflowAsUser/get created event as user | `calendar_id`; `event_id` in `--params` | bot and user read-after-write coverage | | ✕ | calendar events instance_view | api | | none | `+agenda` is indirect orchestration, not direct API coverage | | ✕ | calendar events patch | api | | none | no direct event-update workflow yet | -| ✕ | calendar events search | api | | none | no search workflow yet | +| ✕ | calendar events search_event | api | | none | only covered indirectly through `calendar +search-event` | +| ✕ | calendar events share_info | api | | none | no event sharing workflow yet | | ✕ | calendar freebusys list | api | | none | no direct freebusy API workflow yet | | ✕ | calendar event.attendees batch_delete | api | | none | requires an isolated attendee lifecycle workflow | | ✕ | calendar event.attendees create | api | | none | requires an isolated attendee lifecycle workflow |