diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index bc1eac4a..81ccf124 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -97,6 +97,11 @@ var simulateCommand = &cli.Command{ Name: "view", Usage: "Open a pre-existing simulation", }, + &cli.BoolFlag{ + Name: "list", + Usage: "List the project's simulation runs, newest first. Nothing is run", + }, + jsonFlag, &cli.StringFlag{ Name: "export", Usage: "Print the run with run `ID` and its exact per-job chat contexts as JSON. Nothing is run or polled: the run must already be finished", @@ -318,6 +323,9 @@ func runSimulate(ctx context.Context, cmd *cli.Command, simulationMode livekit.S } return exportSimulationRunJSON(ctx, pc, exportRunID) } + if cmd.Bool("list") { + return listSimulationRuns(ctx, pc, cmd.Bool("json")) + } numSimulations := int32(cmd.Int("num-simulations")) concurrency := int32(cmd.Int("concurrency")) @@ -647,6 +655,60 @@ func getSimulationRun(ctx context.Context, client *lksdk.AgentSimulationClient, return resp.Run, nil } +// listSimulationRuns prints the first page the API returns, which is already +// newest-first; older runs live in the dashboard. +func listSimulationRuns(ctx context.Context, pc *config.ProjectConfig, asJSON bool) error { + client := lksdk.NewAgentSimulationClient(serverURL, pc.APIKey, pc.APISecret) + + fetchCtx, cancel := context.WithTimeout(ctx, simulationAPITimeout) + defer cancel() + resp, err := client.ListSimulationRuns(fetchCtx, &livekit.SimulationRun_List_Request{ + ProjectId: pc.ProjectId, + }) + if err != nil { + return err + } + if asJSON { + util.PrintJSON(resp) + return nil + } + + table := util.CreateTable(). + Headers("ID", "Created", "Agent", "Mode", "Status", "Passed", "Failed", "Jobs") + for _, run := range resp.Runs { + table.Row(simulationRunRow(run)...) + } + out.Result(table) + if len(resp.Runs) == 0 { + out.Status("No simulation runs yet.") + return nil + } + out.Statusf("To open a run: %s", viewCommandHint("")) + return nil +} + +// SimulationRun.mode defines UNSPECIFIED as TEXT. +func simulationRunRow(run *livekit.SimulationRun) []string { + mode := run.GetMode() + if mode == livekit.SimulationMode_SIMULATION_MODE_UNSPECIFIED { + mode = livekit.SimulationMode_SIMULATION_MODE_TEXT + } + created := "--" + if run.CreatedAt != nil { + created = formatTime(run.CreatedAt.AsTime()) + } + return []string{ + run.GetId(), + created, + run.GetAgentName(), + strings.TrimPrefix(mode.String(), "SIMULATION_MODE_"), + strings.TrimPrefix(run.GetStatus().String(), "STATUS_"), + fmt.Sprint(run.GetPassedCount()), + fmt.Sprint(run.GetFailedCount()), + fmt.Sprint(run.GetJobCount()), + } +} + func isTerminalRunStatus(status livekit.SimulationRun_Status) bool { return status == livekit.SimulationRun_STATUS_COMPLETED || status == livekit.SimulationRun_STATUS_FAILED || diff --git a/cmd/lk/simulate_list_test.go b/cmd/lk/simulate_list_test.go new file mode 100644 index 00000000..5f53db37 --- /dev/null +++ b/cmd/lk/simulate_list_test.go @@ -0,0 +1,45 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "testing" + "time" + + "github.com/livekit/protocol/livekit" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestSimulationRunRow(t *testing.T) { + created := time.Date(2026, 9, 8, 17, 0, 0, 0, time.UTC) + row := simulationRunRow(&livekit.SimulationRun{ + Id: "run_123", + CreatedAt: timestamppb.New(created), + AgentName: "my-agent", + Mode: livekit.SimulationMode_SIMULATION_MODE_AUDIO, + Status: livekit.SimulationRun_STATUS_COMPLETED, + JobCount: 5, + PassedCount: 4, + FailedCount: 1, + }) + require.Equal(t, []string{"run_123", "2026-09-08T17:00:00Z", "my-agent", "AUDIO", "COMPLETED", "4", "1", "5"}, row) +} + +func TestSimulationRunRowUnspecifiedModeIsText(t *testing.T) { + row := simulationRunRow(&livekit.SimulationRun{Id: "run_1"}) + require.Equal(t, "TEXT", row[3]) + require.Equal(t, "--", row[1]) +}