From d6b4d0377167a4bb4a05e8caaa0dd4bc277036d4 Mon Sep 17 00:00:00 2001 From: houzhicong Date: Fri, 17 Apr 2026 13:28:25 +0800 Subject: [PATCH] feat(vc): add meeting events shortcut Add vc +meeting-events for bot meeting activity queries with page-all pagination support and tested pretty/json output. --- shortcuts/vc/shortcuts.go | 1 + shortcuts/vc/vc_meeting_events.go | 503 +++++++++++++++++++++++++ shortcuts/vc/vc_meeting_events_test.go | 340 +++++++++++++++++ 3 files changed, 844 insertions(+) create mode 100644 shortcuts/vc/vc_meeting_events_test.go diff --git a/shortcuts/vc/shortcuts.go b/shortcuts/vc/shortcuts.go index 1fbaee36d..34b9ec246 100644 --- a/shortcuts/vc/shortcuts.go +++ b/shortcuts/vc/shortcuts.go @@ -13,5 +13,6 @@ func Shortcuts() []common.Shortcut { VCRecording, VCMeetingJoin, VCMeetingLeave, + VCMeetingEvents, } } diff --git a/shortcuts/vc/vc_meeting_events.go b/shortcuts/vc/vc_meeting_events.go index ae78c3156..813010753 100644 --- a/shortcuts/vc/vc_meeting_events.go +++ b/shortcuts/vc/vc_meeting_events.go @@ -2,3 +2,506 @@ // SPDX-License-Identifier: MIT package vc + +import ( + "context" + "fmt" + "io" + "net/http" + "strconv" + "strings" + + larkcore "github.com/larksuite/oapi-sdk-go/v3/core" + + "github.com/larksuite/cli/internal/output" + "github.com/larksuite/cli/shortcuts/common" +) + +const ( + vcMeetingEventsAPIPath = "/open-apis/vc/v1/bots/events" + defaultVCMeetingEventsSize = 20 + minVCMeetingEventsPageSize = 20 + maxVCMeetingEventsPageSize = 100 + maxVCMeetingEventSummarySize = 80 + defaultVCMeetingPageLimit = 20 + maxVCMeetingPageLimit = 40 +) + +// toUnixSeconds converts a supported CLI time input into a Unix seconds string. +func toUnixSeconds(input string, hint ...string) (string, error) { + ts, err := common.ParseTime(input, hint...) + if err != nil { + return "", err + } + if _, err := strconv.ParseInt(ts, 10, 64); err != nil { + return "", fmt.Errorf("invalid timestamp %q: %w", ts, err) + } + return ts, nil +} + +// VCMeetingEvents lists bot meeting events for a meeting. +var VCMeetingEvents = common.Shortcut{ + Service: "vc", + Command: "+meeting-events", + Description: "List bot meeting events by meeting ID", + Risk: "read", + Scopes: []string{"vc:meeting.meetingevent:read"}, + AuthTypes: []string{"user"}, + HasFormat: true, + Flags: []common.Flag{ + {Name: "meeting-id", Required: true, Desc: "meeting ID to query"}, + {Name: "start", Desc: "time lower bound (ISO 8601, YYYY-MM-DD, or Unix seconds)"}, + {Name: "end", Desc: "time upper bound (ISO 8601, YYYY-MM-DD, or Unix seconds)"}, + {Name: "page-token", Desc: "page token for the next page"}, + {Name: "page-size", Default: "20", Desc: "page size, 20-100 (default 20)"}, + {Name: "page-all", Type: "bool", Desc: "automatically paginate through all pages (max 40)"}, + {Name: "page-limit", Type: "int", Default: "20", Desc: "max page limit when --page-all is set (default 20, max 40)"}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if err := validateMeetingEventsMeetingID(runtime.Str("meeting-id")); err != nil { + return err + } + if _, _, err := parseMeetingEventsTimeRange(runtime); err != nil { + return err + } + if _, err := common.ValidatePageSize(runtime, "page-size", defaultVCMeetingEventsSize, minVCMeetingEventsPageSize, maxVCMeetingEventsPageSize); err != nil { + return err + } + if _, err := meetingEventsPageLimit(runtime); err != nil { + return err + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + startTime, endTime, err := parseMeetingEventsTimeRange(runtime) + if err != nil { + return common.NewDryRunAPI().Set("error", err.Error()) + } + params, err := buildMeetingEventsParams(runtime, startTime, endTime) + if err != nil { + return common.NewDryRunAPI().Set("error", err.Error()) + } + dryRun := common.NewDryRunAPI().GET(vcMeetingEventsAPIPath) + if flat := flattenQueryParams(params); len(flat) > 0 { + dryRun.Params(flat) + } + return dryRun + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + startTime, endTime, err := parseMeetingEventsTimeRange(runtime) + if err != nil { + return err + } + data, events, hasMore, pageToken, err := fetchMeetingEvents(ctx, runtime, startTime, endTime) + if err != nil { + return err + } + rows := buildMeetingEventRows(events) + outData := map[string]interface{}{ + "events": events, + "total": data["total"], + "has_more": data["has_more"], + "page_token": data["page_token"], + } + + runtime.OutFormat(outData, &output.Meta{Count: len(rows)}, func(w io.Writer) { + if len(rows) == 0 { + fmt.Fprintln(w, "No meeting events.") + return + } + output.PrintTable(w, rows) + }) + if hasMore && runtime.Format != "json" && runtime.Format != "" { + fmt.Fprintf(runtime.IO().Out, "\n(more available, page_token: %s)\n", pageToken) + } + return nil + }, +} + +func meetingEventsPageLimit(runtime *common.RuntimeContext) (int, error) { + limit := runtime.Int("page-limit") + if limit == 0 && !runtime.Cmd.Flags().Changed("page-limit") { + return defaultVCMeetingPageLimit, nil + } + if limit < 1 || limit > maxVCMeetingPageLimit { + return 0, common.FlagErrorf("invalid --page-limit %d: must be between 1 and %d", limit, maxVCMeetingPageLimit) + } + return limit, nil +} + +func validateMeetingEventsMeetingID(meetingID string) error { + meetingID = strings.TrimSpace(meetingID) + if meetingID == "" { + return common.FlagErrorf("--meeting-id is required") + } + value, err := strconv.ParseInt(meetingID, 10, 64) + if err != nil || value <= 0 { + return common.FlagErrorf("--meeting-id must be a positive integer, got %q", meetingID) + } + return nil +} + +// parseMeetingEventsTimeRange validates --start/--end and returns Unix seconds strings. +func parseMeetingEventsTimeRange(runtime *common.RuntimeContext) (string, string, error) { + start := strings.TrimSpace(runtime.Str("start")) + end := strings.TrimSpace(runtime.Str("end")) + if start == "" && end == "" { + return "", "", nil + } + + var startTime, endTime string + if start != "" { + parsed, err := toUnixSeconds(start) + if err != nil { + return "", "", output.ErrValidation("--start: %v", err) + } + startTime = parsed + } + if end != "" { + parsed, err := toUnixSeconds(end, "end") + if err != nil { + return "", "", output.ErrValidation("--end: %v", err) + } + endTime = parsed + } + if startTime != "" && endTime != "" { + startValue, _ := strconv.ParseInt(startTime, 10, 64) + endValue, _ := strconv.ParseInt(endTime, 10, 64) + if startValue > endValue { + return "", "", output.ErrValidation("--start (%s) is after --end (%s)", start, end) + } + } + return startTime, endTime, nil +} + +func buildMeetingEventsParams(runtime *common.RuntimeContext, startTime, endTime string) (larkcore.QueryParams, error) { + pageSize, err := common.ValidatePageSize(runtime, "page-size", defaultVCMeetingEventsSize, minVCMeetingEventsPageSize, maxVCMeetingEventsPageSize) + if err != nil { + return nil, err + } + + params := make(larkcore.QueryParams) + params.Set("meeting_id", strings.TrimSpace(runtime.Str("meeting-id"))) + params.Set("page_size", strconv.Itoa(pageSize)) + if pageToken := strings.TrimSpace(runtime.Str("page-token")); pageToken != "" { + params.Set("page_token", pageToken) + } + if startTime != "" { + params.Set("start_time", startTime) + } + if endTime != "" { + params.Set("end_time", endTime) + } + return params, nil +} + +func fetchMeetingEvents(ctx context.Context, runtime *common.RuntimeContext, startTime, endTime string) (map[string]interface{}, []interface{}, bool, string, error) { + params, err := buildMeetingEventsParams(runtime, startTime, endTime) + if err != nil { + return nil, nil, false, "", err + } + if !runtime.Bool("page-all") { + data, err := runtime.DoAPIJSON(http.MethodGet, vcMeetingEventsAPIPath, params, nil) + if err != nil { + return nil, nil, false, "", err + } + if data == nil { + data = map[string]interface{}{} + } + events := common.GetSlice(data, "events") + hasMore, _ := data["has_more"].(bool) + pageToken, _ := data["page_token"].(string) + return data, events, hasMore, pageToken, nil + } + + pageLimit, err := meetingEventsPageLimit(runtime) + if err != nil { + return nil, nil, false, "", err + } + var ( + allEvents []interface{} + lastData map[string]interface{} + lastPageToken string + lastHasMore bool + ) + for page := 0; page < pageLimit; page++ { + data, err := runtime.DoAPIJSON(http.MethodGet, vcMeetingEventsAPIPath, params, nil) + if err != nil { + return nil, nil, false, "", err + } + if data == nil { + data = map[string]interface{}{} + } + lastData = data + events := common.GetSlice(data, "events") + allEvents = append(allEvents, events...) + lastHasMore, _ = data["has_more"].(bool) + lastPageToken, _ = data["page_token"].(string) + if !lastHasMore || lastPageToken == "" { + break + } + params.Set("page_token", lastPageToken) + } + if lastData == nil { + lastData = map[string]interface{}{} + } + lastData["events"] = allEvents + lastData["total"] = len(allEvents) + lastData["has_more"] = lastHasMore + lastData["page_token"] = lastPageToken + return lastData, allEvents, lastHasMore, lastPageToken, nil +} + +func flattenQueryParams(params larkcore.QueryParams) map[string]interface{} { + if len(params) == 0 { + return nil + } + flat := make(map[string]interface{}, len(params)) + for key, values := range params { + switch len(values) { + case 0: + continue + case 1: + flat[key] = values[0] + default: + copied := make([]string, len(values)) + copy(copied, values) + flat[key] = copied + } + } + return flat +} + +func buildMeetingEventRows(events []interface{}) []map[string]interface{} { + rows := make([]map[string]interface{}, 0, len(events)) + for _, raw := range events { + event, _ := raw.(map[string]interface{}) + if event == nil { + continue + } + rows = append(rows, map[string]interface{}{ + "event_id": common.TruncateStr(common.GetString(event, "event_id"), 24), + "event_type": common.TruncateStr(meetingEventType(event), 24), + "event_time": common.TruncateStr(common.GetString(event, "event_time"), 25), + "summary": common.TruncateStr(meetingEventSummary(event), maxVCMeetingEventSummarySize), + }) + } + return rows +} + +func meetingEventType(event map[string]interface{}) string { + if eventType := common.GetString(event, "event_type"); eventType != "" { + return eventType + } + return common.GetString(common.GetMap(event, "payload"), "activity_event_type") +} + +func meetingEventSummary(event map[string]interface{}) string { + payload := common.GetMap(event, "payload") + eventType := meetingEventType(event) + switch eventType { + case "participant_joined": + return participantJoinedSummary(payload) + case "participant_left": + return participantLeftSummary(payload) + case "transcript_received": + return transcriptReceivedSummary(payload) + case "chat_received": + return chatReceivedSummary(payload) + case "magic_share_started": + return magicShareStartedSummary(payload) + case "magic_share_ended": + return magicShareEndedSummary(payload) + default: + return fallbackMeetingEventSummary(payload, eventType) + } +} + +func participantJoinedSummary(payload map[string]interface{}) string { + items := common.GetSlice(payload, "participant_joined_items") + switch len(items) { + case 0: + return "participant joined" + case 1: + user := common.GetMap(firstSliceMap(payload, "participant_joined_items"), "participant") + if label := meetingEventUserLabel(user); label != "" { + return fmt.Sprintf("participant %s joined", label) + } + return "participant joined" + default: + return fmt.Sprintf("%d participants joined", len(items)) + } +} + +func participantLeftSummary(payload map[string]interface{}) string { + items := common.GetSlice(payload, "participant_left_items") + switch len(items) { + case 0: + return "participant left" + case 1: + user := common.GetMap(firstSliceMap(payload, "participant_left_items"), "participant") + if label := meetingEventUserLabel(user); label != "" { + return fmt.Sprintf("participant %s left", label) + } + return "participant left" + default: + return fmt.Sprintf("%d participants left", len(items)) + } +} + +func transcriptReceivedSummary(payload map[string]interface{}) string { + items := common.GetSlice(payload, "transcript_received_items") + if len(items) > 1 { + return fmt.Sprintf("%d transcript items", len(items)) + } + item := firstSliceMap(payload, "transcript_received_items") + text := common.GetString(item, "text") + speaker := meetingEventUserLabel(common.GetMap(item, "speaker")) + switch { + case speaker != "" && text != "": + return fmt.Sprintf("speaker %s: %s", speaker, text) + case speaker != "": + return fmt.Sprintf("speaker %s transcript received", speaker) + case text != "": + return fmt.Sprintf("transcript: %s", text) + default: + return "transcript received" + } +} + +func chatReceivedSummary(payload map[string]interface{}) string { + items := common.GetSlice(payload, "chat_received_items") + switch len(items) { + case 0: + return "chat received" + case 1: + item := firstSliceMap(payload, "chat_received_items") + content := common.GetString(item, "content") + operator := meetingEventUserDisplayName(common.GetMap(item, "operator")) + switch { + case operator != "" && content != "": + return fmt.Sprintf("%s: %s", operator, content) + case operator != "": + return fmt.Sprintf("message by %s", operator) + case content != "": + return fmt.Sprintf("message: %s", content) + default: + return "chat received" + } + default: + count, operator := summarizeChatOperators(items) + switch { + case count == 1 && operator != "": + return fmt.Sprintf("%d messages by %s", len(items), operator) + case count > 1: + return fmt.Sprintf("%d messages by %d users", len(items), count) + default: + return fmt.Sprintf("%d messages", len(items)) + } + } +} + +func magicShareStartedSummary(payload map[string]interface{}) string { + items := common.GetSlice(payload, "magic_share_started_items") + if len(items) > 1 { + return fmt.Sprintf("%d share start events", len(items)) + } + item := firstSliceMap(payload, "magic_share_started_items") + shareID := common.GetString(item, "share_id") + title := common.GetString(common.GetMap(item, "share_doc"), "title") + switch { + case shareID != "" && title != "": + return fmt.Sprintf("share %s started: %s", shareID, title) + case shareID != "": + return fmt.Sprintf("share %s started", shareID) + case title != "": + return fmt.Sprintf("share started: %s", title) + default: + return "share started" + } +} + +func magicShareEndedSummary(payload map[string]interface{}) string { + items := common.GetSlice(payload, "magic_share_ended_items") + if len(items) > 1 { + return fmt.Sprintf("%d share end events", len(items)) + } + item := firstSliceMap(payload, "magic_share_ended_items") + if shareID := common.GetString(item, "share_id"); shareID != "" { + return fmt.Sprintf("share %s ended", shareID) + } + return "share ended" +} + +func fallbackMeetingEventSummary(payload map[string]interface{}, eventType string) string { + meeting := common.GetMap(payload, "meeting") + if topic := common.GetString(meeting, "topic"); topic != "" { + if eventType != "" { + return fmt.Sprintf("%s: %s", eventType, topic) + } + return topic + } + if eventType != "" { + return eventType + } + return "meeting event" +} + +func firstSliceMap(payload map[string]interface{}, key string) map[string]interface{} { + items := common.GetSlice(payload, key) + if len(items) == 0 { + return nil + } + first, _ := items[0].(map[string]interface{}) + return first +} + +func meetingEventUserLabel(user map[string]interface{}) string { + if user == nil { + return "" + } + userID := common.GetString(user, "id") + userName := common.GetString(user, "user_name") + switch { + case userID != "" && userName != "": + return fmt.Sprintf("%s (%s)", userID, userName) + case userID != "": + return userID + case userName != "": + return userName + default: + return "" + } +} + +func meetingEventUserDisplayName(user map[string]interface{}) string { + if user == nil { + return "" + } + if userName := common.GetString(user, "user_name"); userName != "" { + return userName + } + return common.GetString(user, "id") +} + +func summarizeChatOperators(items []interface{}) (int, string) { + seen := make(map[string]struct{}, len(items)) + for _, raw := range items { + item, _ := raw.(map[string]interface{}) + if item == nil { + continue + } + operator := meetingEventUserDisplayName(common.GetMap(item, "operator")) + if operator == "" { + continue + } + seen[operator] = struct{}{} + } + if len(seen) != 1 { + return len(seen), "" + } + for operator := range seen { + return 1, operator + } + return 0, "" +} diff --git a/shortcuts/vc/vc_meeting_events_test.go b/shortcuts/vc/vc_meeting_events_test.go new file mode 100644 index 000000000..8c89f11fb --- /dev/null +++ b/shortcuts/vc/vc_meeting_events_test.go @@ -0,0 +1,340 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package vc + +import ( + "context" + "strings" + "testing" + + "github.com/spf13/cobra" + + "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/internal/httpmock" + "github.com/larksuite/cli/shortcuts/common" +) + +func newMeetingEventsRuntime() *common.RuntimeContext { + cmd := &cobra.Command{Use: "test"} + cmd.Flags().String("meeting-id", "", "") + cmd.Flags().String("start", "", "") + cmd.Flags().String("end", "", "") + cmd.Flags().String("page-token", "", "") + cmd.Flags().String("page-size", "", "") + cmd.Flags().Bool("page-all", false, "") + cmd.Flags().Int("page-limit", 20, "") + return common.TestNewRuntimeContext(cmd, defaultConfig()) +} + +func meetingEventsStub(events []interface{}, hasMore bool, pageToken string) *httpmock.Stub { + return &httpmock.Stub{ + Method: "GET", + URL: vcMeetingEventsAPIPath, + Body: map[string]interface{}{ + "code": 0, + "msg": "ok", + "data": map[string]interface{}{ + "total": len(events), + "has_more": hasMore, + "page_token": pageToken, + "events": events, + }, + }, + } +} + +func participantJoinedEvent() map[string]interface{} { + return map[string]interface{}{ + "event_id": "event-1", + "event_type": "participant_joined", + "event_time": "2026-04-17T08:00:00Z", + "payload": map[string]interface{}{ + "activity_event_type": "participant_joined", + "participant_joined_items": []interface{}{ + map[string]interface{}{ + "participant": map[string]interface{}{ + "id": "bot_001", + "user_name": "Demo Bot", + }, + "join_time": "2026-04-17T08:00:00Z", + }, + }, + }, + } +} + +func TestChatReceivedSummary_MultipleItems(t *testing.T) { + payload := map[string]interface{}{ + "chat_received_items": []interface{}{ + map[string]interface{}{"content": "hello"}, + map[string]interface{}{"content": "world"}, + }, + } + + got := chatReceivedSummary(payload) + if got != "2 messages" { + t.Fatalf("chatReceivedSummary() = %q, want %q", got, "2 messages") + } +} + +func TestChatReceivedSummary_MultipleItemsSameOperator(t *testing.T) { + payload := map[string]interface{}{ + "chat_received_items": []interface{}{ + map[string]interface{}{"content": "hello", "operator": map[string]interface{}{"id": "u1", "user_name": "Alice"}}, + map[string]interface{}{"content": "world", "operator": map[string]interface{}{"id": "u1", "user_name": "Alice"}}, + }, + } + + got := chatReceivedSummary(payload) + if got != "2 messages by Alice" { + t.Fatalf("chatReceivedSummary() = %q, want %q", got, "2 messages by Alice") + } +} + +func TestChatReceivedSummary_MultipleItemsMultipleOperators(t *testing.T) { + payload := map[string]interface{}{ + "chat_received_items": []interface{}{ + map[string]interface{}{"content": "hello", "operator": map[string]interface{}{"id": "u1", "user_name": "Alice"}}, + map[string]interface{}{"content": "world", "operator": map[string]interface{}{"id": "u2", "user_name": "Bob"}}, + map[string]interface{}{"content": "again", "operator": map[string]interface{}{"id": "u3", "user_name": "Carol"}}, + }, + } + + got := chatReceivedSummary(payload) + if got != "3 messages by 3 users" { + t.Fatalf("chatReceivedSummary() = %q, want %q", got, "3 messages by 3 users") + } +} + +func TestParticipantJoinedSummary_MultipleItems(t *testing.T) { + payload := map[string]interface{}{ + "participant_joined_items": []interface{}{ + map[string]interface{}{"participant": map[string]interface{}{"id": "u1", "user_name": "User 1"}}, + map[string]interface{}{"participant": map[string]interface{}{"id": "u2", "user_name": "User 2"}}, + }, + } + + got := participantJoinedSummary(payload) + if got != "2 participants joined" { + t.Fatalf("participantJoinedSummary() = %q, want %q", got, "2 participants joined") + } +} + +func TestMeetingEvents_Validation_InvalidMeetingID(t *testing.T) { + runtime := newMeetingEventsRuntime() + _ = runtime.Cmd.Flags().Set("meeting-id", "not-a-number") + + err := VCMeetingEvents.Validate(context.Background(), runtime) + if err == nil { + t.Fatal("expected validation error for invalid meeting ID") + } + if !strings.Contains(err.Error(), "positive integer") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestMeetingEvents_Validation_InvalidTimeRange(t *testing.T) { + runtime := newMeetingEventsRuntime() + _ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381") + _ = runtime.Cmd.Flags().Set("start", "200") + _ = runtime.Cmd.Flags().Set("end", "100") + + err := VCMeetingEvents.Validate(context.Background(), runtime) + if err == nil { + t.Fatal("expected validation error for invalid time range") + } + if !strings.Contains(err.Error(), "after --end") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestMeetingEvents_Validation_InvalidPageSize(t *testing.T) { + runtime := newMeetingEventsRuntime() + _ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381") + _ = runtime.Cmd.Flags().Set("page-size", "10") + + err := VCMeetingEvents.Validate(context.Background(), runtime) + if err == nil { + t.Fatal("expected validation error for invalid page size") + } + if !strings.Contains(err.Error(), "must be between 20 and 100") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestMeetingEvents_Validation_InvalidPageLimit(t *testing.T) { + runtime := newMeetingEventsRuntime() + _ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381") + _ = runtime.Cmd.Flags().Set("page-limit", "41") + + err := VCMeetingEvents.Validate(context.Background(), runtime) + if err == nil { + t.Fatal("expected validation error for invalid page limit") + } + if !strings.Contains(err.Error(), "must be between 1 and 40") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestBuildMeetingEventsParams(t *testing.T) { + runtime := newMeetingEventsRuntime() + _ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381") + _ = runtime.Cmd.Flags().Set("page-size", "40") + _ = runtime.Cmd.Flags().Set("page-token", "1710000000000000000") + + params, err := buildMeetingEventsParams(runtime, "1710000000", "1710003600") + if err != nil { + t.Fatalf("buildMeetingEventsParams() error = %v", err) + } + if got := params["meeting_id"][0]; got != "7628568141510692381" { + t.Fatalf("meeting_id = %q, want %q", got, "7628568141510692381") + } + if got := params["page_size"][0]; got != "40" { + t.Fatalf("page_size = %q, want %q", got, "40") + } + if got := params["page_token"][0]; got != "1710000000000000000" { + t.Fatalf("page_token = %q, want %q", got, "1710000000000000000") + } + if got := params["start_time"][0]; got != "1710000000" { + t.Fatalf("start_time = %q, want %q", got, "1710000000") + } + if got := params["end_time"][0]; got != "1710003600" { + t.Fatalf("end_time = %q, want %q", got, "1710003600") + } +} + +func TestMeetingEvents_DryRun(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, VCMeetingEvents, []string{ + "+meeting-events", + "--meeting-id", "7628568141510692381", + "--page-token", "1710000000000000000", + "--page-size", "40", + "--start", "1710000000", + "--end", "1710003600", + "--dry-run", + "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := stdout.String() + for _, want := range []string{ + vcMeetingEventsAPIPath, + `"meeting_id": "7628568141510692381"`, + `"page_token": "1710000000000000000"`, + `"page_size": "40"`, + `"start_time": "1710000000"`, + `"end_time": "1710003600"`, + } { + if !strings.Contains(out, want) { + t.Fatalf("dry-run output missing %q: %s", want, out) + } + } +} + +func TestMeetingEvents_ExecuteJSON_PageAll(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "pt_2")) + reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, false, "")) + + err := mountAndRun(t, VCMeetingEvents, []string{ + "+meeting-events", + "--meeting-id", "7628568141510692381", + "--format", "json", + "--page-all", + "--page-limit", "2", + "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + reg.Verify(t) + + out := strings.ReplaceAll(stdout.String(), " ", "") + out = strings.ReplaceAll(out, "\n", "") + if count := strings.Count(out, `"event_type":"participant_joined"`); count != 2 { + t.Fatalf("expected 2 aggregated events, got %d: %s", count, stdout.String()) + } + if !strings.Contains(out, `"has_more":false`) { + t.Fatalf("expected final has_more=false: %s", stdout.String()) + } +} + +func TestMeetingEvents_ExecuteJSON(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "1710000000000000000")) + + err := mountAndRun(t, VCMeetingEvents, []string{ + "+meeting-events", + "--meeting-id", "7628568141510692381", + "--format", "json", + "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + reg.Verify(t) + + out := strings.ReplaceAll(stdout.String(), " ", "") + out = strings.ReplaceAll(out, "\n", "") + for _, want := range []string{ + `"event_type":"participant_joined"`, + `"has_more":true`, + `"page_token":"1710000000000000000"`, + `"events":[`, + } { + if !strings.Contains(out, want) { + t.Fatalf("json output missing %q: %s", want, stdout.String()) + } + } +} + +func TestMeetingEvents_ExecutePretty(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "1710000000000000000")) + + err := mountAndRun(t, VCMeetingEvents, []string{ + "+meeting-events", + "--meeting-id", "7628568141510692381", + "--format", "pretty", + "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + reg.Verify(t) + + out := stdout.String() + for _, want := range []string{ + "participant_joined", + "participant bot_001 (Demo Bot) joined", + "page_token: 1710000000000000000", + } { + if !strings.Contains(out, want) { + t.Fatalf("pretty output missing %q: %s", want, out) + } + } +} + +func TestMeetingEvents_ExecuteEmpty(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(meetingEventsStub(nil, false, "")) + + err := mountAndRun(t, VCMeetingEvents, []string{ + "+meeting-events", + "--meeting-id", "7628568141510692381", + "--format", "pretty", + "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + reg.Verify(t) + + if !strings.Contains(stdout.String(), "No meeting events.") { + t.Fatalf("unexpected output: %s", stdout.String()) + } +}