diff --git a/acceptance/experimental/air/get/output.txt b/acceptance/experimental/air/get/output.txt index 4bc127d79b..c0e361b2ca 100644 --- a/acceptance/experimental/air/get/output.txt +++ b/acceptance/experimental/air/get/output.txt @@ -1,6 +1,6 @@ === get (text) ->>> [CLI] experimental air get run 123 +>>> [CLI] experimental air get 123 ╭─ Configuration ────────────────────────────────────────────────╮ │ │ @@ -31,11 +31,9 @@ │ │ ╰────────────────────────────────────────────────────────────────╯ -Run URL: [DATABRICKS_URL]/jobs/runs/123?o=[NUMID] -MLflow URL: [DATABRICKS_URL]/ml/experiments/exp1/runs/run1 === get (json) ->>> [CLI] experimental air get run 123 -o json +>>> [CLI] experimental air get 123 -o json { "v": 1, "ts": "[TIMESTAMP]", @@ -52,13 +50,13 @@ MLflow URL: [DATABRICKS_URL]/ml/experiments/exp1/runs/run1 } === invalid run id ->>> [CLI] experimental air get run notanumber +>>> [CLI] experimental air get notanumber Error: invalid JOB_RUN_ID "notanumber": must be a positive integer Exit code: 1 === invalid run id (json) ->>> [CLI] experimental air get run notanumber -o json +>>> [CLI] experimental air get notanumber -o json { "v": 1, "ts": "[TIMESTAMP]", diff --git a/acceptance/experimental/air/get/script b/acceptance/experimental/air/get/script index b775d06a48..ee66b4aff0 100644 --- a/acceptance/experimental/air/get/script +++ b/acceptance/experimental/air/get/script @@ -1,11 +1,11 @@ title "get (text)" -trace $CLI experimental air get run 123 +trace $CLI experimental air get 123 title "get (json)" -trace $CLI experimental air get run 123 -o json +trace $CLI experimental air get 123 -o json title "invalid run id" -errcode trace $CLI experimental air get run notanumber +errcode trace $CLI experimental air get notanumber title "invalid run id (json)" -errcode trace $CLI experimental air get run notanumber -o json +errcode trace $CLI experimental air get notanumber -o json diff --git a/acceptance/experimental/air/help/output.txt b/acceptance/experimental/air/help/output.txt index 9348686ce4..1690e7878f 100644 --- a/acceptance/experimental/air/help/output.txt +++ b/acceptance/experimental/air/help/output.txt @@ -11,8 +11,9 @@ Usage: Available Commands: cancel Cancel one or more runs - get Show details for a specific resource + get Show status, configuration, and timing details for a specific run list List your recent runs (active and completed) for the current profile + logs Stream or fetch logs for a run register-image Mirror a Docker image into the workspace registry run Submit a training workload from a YAML config diff --git a/experimental/air/cmd/get.go b/experimental/air/cmd/get.go index 98581fd5dd..0da1f082e1 100644 --- a/experimental/air/cmd/get.go +++ b/experimental/air/cmd/get.go @@ -13,7 +13,7 @@ import ( "github.com/spf13/cobra" ) -// getData is the payload printed by `air get run`. The json-tagged fields form +// getData is the payload printed by `air get`. The json-tagged fields form // the machine-readable output; fields tagged `json:"-"` are shown only in the // human-readable text view. type getData struct { @@ -27,7 +27,7 @@ type getData struct { MLflowURL *string `json:"mlflow_url"` // The fields below are pre-rendered text-view cells, excluded from JSON - // (matching `air get run --json`). Each shows "N/A" when its value is + // (matching `air get --json`). Each shows "N/A" when its value is // missing. The styled single-run renderer (render.go) consumes them; the // Run ID, Status, and MLflow Run cells it draws are styled and hyperlinked // there rather than stored here. @@ -63,20 +63,11 @@ Sweep Tasks: {{- end}} ` -// newGetCommand is the `get` parent group. Subcommands name the resource to -// describe, e.g. `air get run JOB_RUN_ID`, mirroring the Python CLI. +// newGetCommand returns the `air get JOB_RUN_ID` command, which shows status, +// configuration, and timing details for a specific run. func newGetCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "get", - Short: "Show details for a specific resource", - } - cmd.AddCommand(newGetRunCommand()) - return cmd -} - -func newGetRunCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "run JOB_RUN_ID", + Use: "get JOB_RUN_ID", Args: root.ExactArgs(1), Short: "Show status, configuration, and timing details for a specific run", Annotations: map[string]string{ diff --git a/experimental/air/cmd/get_test.go b/experimental/air/cmd/get_test.go index 7f4d784807..8ff864e387 100644 --- a/experimental/air/cmd/get_test.go +++ b/experimental/air/cmd/get_test.go @@ -29,10 +29,23 @@ func renderGet(t *testing.T, data getData) string { return buf.String() } +// TestGetCommandShape locks in that `get` takes the run id directly as +// `air get JOB_RUN_ID` and has no `run` subcommand (it was collapsed back into +// `get`). The acceptance test exercises the happy path end to end. +func TestGetCommandShape(t *testing.T) { + cmd := newGetCommand() + assert.Equal(t, "get JOB_RUN_ID", cmd.Use) + assert.Empty(t, cmd.Commands(), "get must not register subcommands") + // ExactArgs(1): exactly one run id is required. + assert.NoError(t, cmd.Args(cmd, []string{"123"})) + assert.Error(t, cmd.Args(cmd, []string{})) + assert.Error(t, cmd.Args(cmd, []string{"1", "2"})) +} + func TestGetRunInvalidID(t *testing.T) { m := mocks.NewMockWorkspaceClient(t) ctx := cmdctx.SetWorkspaceClient(cmdio.MockDiscard(t.Context()), m.WorkspaceClient) - cmd := withOutput(newGetRunCommand(), flags.OutputText) + cmd := withOutput(newGetCommand(), flags.OutputText) cmd.SetContext(ctx) err := cmd.RunE(cmd, []string{"abc"}) @@ -45,7 +58,7 @@ func TestGetRunNotFound(t *testing.T) { m.GetMockJobsAPI().EXPECT().GetRun(mock.Anything, jobs.GetRunRequest{RunId: 5}).Return( nil, apierr.ErrResourceDoesNotExist) ctx := cmdctx.SetWorkspaceClient(cmdio.MockDiscard(t.Context()), m.WorkspaceClient) - cmd := withOutput(newGetRunCommand(), flags.OutputText) + cmd := withOutput(newGetCommand(), flags.OutputText) cmd.SetContext(ctx) err := cmd.RunE(cmd, []string{"5"}) @@ -60,7 +73,7 @@ func TestGetRunNotFoundJSON(t *testing.T) { nil, apierr.ErrResourceDoesNotExist) ctx := cmdctx.SetWorkspaceClient(t.Context(), m.WorkspaceClient) ctx = cmdio.InContext(ctx, cmdio.NewIO(ctx, flags.OutputJSON, nil, &buf, &buf, "", "")) - cmd := withOutput(newGetRunCommand(), flags.OutputJSON) + cmd := withOutput(newGetCommand(), flags.OutputJSON) cmd.SetContext(ctx) // In JSON mode the not-found error is a structured envelope, not a bare error. diff --git a/experimental/air/cmd/render.go b/experimental/air/cmd/render.go index d6c2e6a25b..a7a14a8235 100644 --- a/experimental/air/cmd/render.go +++ b/experimental/air/cmd/render.go @@ -125,19 +125,6 @@ func renderRunText(ctx context.Context, out io.Writer, w *databricks.WorkspaceCl // A single write: a blank line before the first box and after the last, and // one between each box. fmt.Fprintf(out, "\n%s\n\n", strings.Join(sections, "\n\n")) - - // Bare-URL footer so the job run / MLflow links remain reachable when - // stdout is not a hyperlink-capable terminal (piped, redirected, NO_COLOR). - // In that case the OSC 8 hyperlinks on the Run ID / MLflow Run cells - // degrade to plain labels and the URLs would otherwise disappear from text - // output, breaking workflows like `air get run X > out.txt` or - // `NO_COLOR=1 air get run X` that the previous `Job Link:` line supported. - if view.dashboardURL != "" { - fmt.Fprintf(out, "Run URL: %s\n", view.dashboardURL) - } - if view.mlflowURL != "" { - fmt.Fprintf(out, "MLflow URL: %s\n", view.mlflowURL) - } } // genAIComputeTask returns the run's first GenAI-compute task, or nil.