Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions autocomplete/fish_autocomplete
Original file line number Diff line number Diff line change
Expand Up @@ -229,21 +229,24 @@ complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcomma
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate' -f -l agent-name -r -d 'Run against an already-running agent instead of spawning one locally. Pass the registered `NAME`, or "" to target the project\'s default agent (the one that auto-joins every room). Requires --scenarios.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and not __fish_seen_subcommand_from init create dockerfile config deploy promote status update restart rollback logs tail delete destroy versions list secrets update-secrets private-link start dev console daemon simulate help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_lk_no_subcommand' -a 'analytics' -d 'List and inspect LiveKit Cloud analytics sessions'
complete -c lk -n '__fish_seen_subcommand_from analytics' -f -l experimental -d 'Enable experimental features'
complete -c lk -n '__fish_seen_subcommand_from analytics' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and not __fish_seen_subcommand_from list get help h' -a 'list' -d 'List analytics sessions'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from list' -f -l json -s j -d 'Output as JSON'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from list' -f -l limit -r -d 'Maximum number of sessions to return'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from list' -f -l page -r -d 'Page number (starts at 0)'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from list' -f -l start -r -d 'Start date in `YYYY-MM-DD` format'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from list' -f -l end -r -d 'End date in `YYYY-MM-DD` format'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from list' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and not __fish_seen_subcommand_from list get help h' -a 'get' -d 'Get analytics session details by session ID'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from get' -f -l json -s j -d 'Output as JSON'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from get' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from get; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and not __fish_seen_subcommand_from list get help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and not __fish_seen_subcommand_from session help h' -a 'session' -d 'List and inspect sessions'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and not __fish_seen_subcommand_from list get help h' -a 'list' -d 'List analytics sessions'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and __fish_seen_subcommand_from list' -f -l json -s j -d 'Output as JSON'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and __fish_seen_subcommand_from list' -f -l limit -r -d 'Maximum number of sessions to return'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and __fish_seen_subcommand_from list' -f -l page -r -d 'Page number (starts at 0)'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and __fish_seen_subcommand_from list' -f -l start -r -d 'Start date in `YYYY-MM-DD` format'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and __fish_seen_subcommand_from list' -f -l end -r -d 'End date in `YYYY-MM-DD` format'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and __fish_seen_subcommand_from list' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and not __fish_seen_subcommand_from list get help h' -a 'get' -d 'Get analytics session details by session ID'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and __fish_seen_subcommand_from get' -f -l json -s j -d 'Output as JSON'
complete -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and __fish_seen_subcommand_from get' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and __fish_seen_subcommand_from get; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and __fish_seen_subcommand_from session; and not __fish_seen_subcommand_from list get help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_seen_subcommand_from analytics; and not __fish_seen_subcommand_from session help h' -a 'help' -d 'Shows a list of commands or help for one command'
complete -x -c lk -n '__fish_lk_no_subcommand' -a 'cloud' -d 'Interact with LiveKit Cloud services'
complete -c lk -n '__fish_seen_subcommand_from cloud' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from cloud; and not __fish_seen_subcommand_from auth help h' -a 'auth' -d 'Authenticate LiveKit Cloud account to link your projects'
Expand Down
10 changes: 0 additions & 10 deletions cmd/lk/agent_private_link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,9 @@ import (
lkproto "github.com/livekit/protocol/livekit"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"google.golang.org/protobuf/types/known/timestamppb"
)

func findCommandByName(commands []*cli.Command, name string) *cli.Command {
for _, cmd := range commands {
if cmd != nil && cmd.Name == name {
return cmd
}
}
return nil
}

func TestAgentPrivateLinkCommandTree(t *testing.T) {
agentCmd := findCommandByName(AgentCommands, "agent")
require.NotNil(t, agentCmd, "top-level 'agent' command must exist")
Expand Down
71 changes: 40 additions & 31 deletions cmd/lk/analytics.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,43 +41,52 @@ const (
var (
AnalyticsCommands = []*cli.Command{
{
Name: "analytics",
Usage: "List and inspect LiveKit Cloud analytics sessions",
Name: "analytics",
Usage: "List and inspect LiveKit Cloud analytics",
Hidden: true,
Flags: []cli.Flag{
experimentalFlag,
},
Commands: []*cli.Command{
{
Name: "list",
Usage: "List analytics sessions",
Action: listAnalyticsSessions,
Flags: []cli.Flag{
jsonFlag,
&cli.IntFlag{
Name: "limit",
Usage: "Maximum number of sessions to return",
Value: defaultAnalyticsLimit,
},
&cli.IntFlag{
Name: "page",
Usage: "Page number (starts at 0)",
},
&cli.StringFlag{
Name: "start",
Usage: "Start date in `YYYY-MM-DD` format",
Name: "session",
Usage: "List and inspect sessions", Commands: []*cli.Command{
{
Name: "list",
Usage: "List analytics sessions",
Action: listAnalyticsSessions,
Flags: []cli.Flag{
jsonFlag,
&cli.IntFlag{
Name: "limit",
Usage: "Maximum number of sessions to return",
Value: defaultAnalyticsLimit,
},
&cli.IntFlag{
Name: "page",
Usage: "Page number (starts at 0)",
},
&cli.StringFlag{
Name: "start",
Usage: "Start date in `YYYY-MM-DD` format",
},
&cli.StringFlag{
Name: "end",
Usage: "End date in `YYYY-MM-DD` format",
},
},
},
&cli.StringFlag{
Name: "end",
Usage: "End date in `YYYY-MM-DD` format",
{
Name: "get",
Usage: "Get analytics session details by session ID",
ArgsUsage: "SESSION_ID",
Action: getAnalyticsSession,
Flags: []cli.Flag{
jsonFlag,
},
},
},
},
{
Name: "get",
Usage: "Get analytics session details by session ID",
ArgsUsage: "SESSION_ID",
Action: getAnalyticsSession,
Flags: []cli.Flag{
jsonFlag,
},
},
},
},
}
Expand Down
63 changes: 57 additions & 6 deletions cmd/lk/analytics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,64 @@ func TestAnalyticsCommandTree(t *testing.T) {
analyticsCmd := findCommandByName(AnalyticsCommands, "analytics")
require.NotNil(t, analyticsCmd, "top-level 'analytics' command must exist")

listCmd := findCommandByName(analyticsCmd.Commands, "list")
require.NotNil(t, listCmd, "'analytics list' command must exist")
require.NotNil(t, listCmd.Action, "'analytics list' must have an action")
sessionCmd := findCommandByName(analyticsCmd.Commands, "session")
require.NotNil(t, sessionCmd, "'analytics session' command must exist")

getCmd := findCommandByName(analyticsCmd.Commands, "get")
require.NotNil(t, getCmd, "'analytics get' command must exist")
require.NotNil(t, getCmd.Action, "'analytics get' must have an action")
listCmd := findCommandByName(sessionCmd.Commands, "list")
require.NotNil(t, listCmd, "'analytics session list' command must exist")
require.NotNil(t, listCmd.Action, "'analytics session list' must have an action")

getCmd := findCommandByName(sessionCmd.Commands, "get")
require.NotNil(t, getCmd, "'analytics session get' command must exist")
require.NotNil(t, getCmd.Action, "'analytics session get' must have an action")
}

func TestAnalyticsCommandRequiresExperimentalFlag(t *testing.T) {
analyticsCmd := findCommandByName(AnalyticsCommands, "analytics")
require.NotNil(t, analyticsCmd, "top-level 'analytics' command must exist")

experimental := findFlagByName(analyticsCmd.Flags, "experimental")
require.NotNil(t, experimental, "'analytics' command must declare an --experimental flag")

boolFlag, ok := experimental.(*cli.BoolFlag)
require.True(t, ok, "--experimental must be a bool flag")
assert.True(t, boolFlag.Required, "--experimental flag must be required")
}

// runAnalytics runs the analytics command tree in isolation (as a subcommand of
// a bare root) so flag validation is exercised without touching global CLI state.
func runAnalytics(args ...string) error {
app := &cli.Command{Name: "lk", Commands: AnalyticsCommands}
return app.Run(context.Background(), append([]string{"lk", "analytics"}, args...))
}

func TestAnalyticsFailsWithoutExperimentalFlag(t *testing.T) {
// Every leaf must reject invocation when --experimental is omitted, before
// any action (and its network calls) runs.
err := runAnalytics("session", "list")
require.Error(t, err, "'analytics session list' must fail without --experimental")
assert.Contains(t, err.Error(), `Required flag "experimental" not set`)

err = runAnalytics("session", "get", "sess_123")
require.Error(t, err, "'analytics session get' must fail without --experimental")
assert.Contains(t, err.Error(), `Required flag "experimental" not set`)
}

func TestAnalyticsFailsWithExperimentalFlagDisabled(t *testing.T) {
// Required only checks presence; an explicit --experimental=false must still
// be rejected by the flag's Validator rather than running the command.
err := runAnalytics("--experimental=false", "session", "list")
require.Error(t, err, "'analytics session list' must fail when --experimental is disabled")
assert.Contains(t, err.Error(), "--experimental must be enabled")
}

func TestAnalyticsPassesFlagValidationWithExperimentalFlag(t *testing.T) {
// With --experimental set, flag validation passes; any resulting error comes
// from the action itself (e.g. project resolution), not the required flag.
err := runAnalytics("--experimental", "session", "list")
if err != nil {
assert.NotContains(t, err.Error(), `Required flag "experimental" not set`)
}
}

func TestValidateAnalyticsDateRange(t *testing.T) {
Expand Down
13 changes: 13 additions & 0 deletions cmd/lk/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ var (
Name: "install",
Usage: "Run installation after creating the application",
}
experimentalFlag = &cli.BoolFlag{
Name: "experimental",
Usage: "Enable experimental features",
Required: true,
// Required only asserts the flag was provided; the Validator rejects an
// explicit --experimental=false so the value must actually be true.
Validator: func(enabled bool) error {
if !enabled {
return errors.New("--experimental must be enabled to use this command")
}
return nil
},
}

// out is the process-wide sink for human-facing CLI output. It is initialized
// in main.go's root Before hook from the parsed root command, so all status,
Expand Down
24 changes: 24 additions & 0 deletions cmd/lk/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"bytes"
"context"
"io"
"slices"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -27,6 +28,29 @@ import (
"github.com/livekit/livekit-cli/v2/pkg/util"
)

// findCommandByName returns the command with the given name from commands, or nil.
func findCommandByName(commands []*cli.Command, name string) *cli.Command {
for _, cmd := range commands {
if cmd != nil && cmd.Name == name {
return cmd
}
}
return nil
}

// findFlagByName returns the flag matching name (by any of its names) from flags, or nil.
func findFlagByName(flags []cli.Flag, name string) cli.Flag {
for _, flag := range flags {
if flag == nil {
continue
}
if slices.Contains(flag.Names(), name) {
return flag
}
}
return nil
}

// withCapturedAnnounce swaps the package-level Printer for a buffer-backed one for the
// duration of the test, returning the buffer that captures status output. The Printer's
// nil-safety means we don't have to worry about state from other tests.
Expand Down
2 changes: 1 addition & 1 deletion version.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
package livekitcli

const (
Version = "2.18.1"
Version = "2.18.2"
)
Loading