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.
This commit is contained in:
houzhicong
2026-04-17 13:28:25 +08:00
parent d5327aa7e9
commit 97ef2bcfb8
3 changed files with 844 additions and 0 deletions

View File

@@ -13,5 +13,6 @@ func Shortcuts() []common.Shortcut {
VCRecording,
VCMeetingJoin,
VCMeetingLeave,
VCMeetingEvents,
}
}

View File

@@ -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, ""
}

View File

@@ -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())
}
}