diff --git a/internal/cli/coverage_test.go b/internal/cli/coverage_test.go index 0150ffad..2939324e 100644 --- a/internal/cli/coverage_test.go +++ b/internal/cli/coverage_test.go @@ -128,14 +128,14 @@ func TestClassifyPushOutcome(t *testing.T) { // JSON contract. (Bugbot #49) func TestRunDatasetPush_OutputJSONEarlyFailureEmitsJSON(t *testing.T) { var jsonBuf, human bytes.Buffer - a := runDatasetPushArgs{ + a := runDataIngestArgs{ LocalPath: "./x", Spec: push.SpecArgs{Table: "../bad", Category: "image_classification", Intent: "train"}, Printer: ui.New(&human, ui.WithColor(false)), OutputJSON: true, JSONOut: &jsonBuf, } - err := runDatasetPush(context.Background(), &human, &human, a) + err := runDataIngest(context.Background(), &human, &human, a) var ee *exitError if !errors.As(err, &ee) || ee.Code() != 2 { @@ -202,7 +202,7 @@ func TestExitError_Methods(t *testing.T) { // and never reaches kubeconfig/cluster resolution. func TestRunDatasetRm_InvalidTableExitsTwo(t *testing.T) { var buf bytes.Buffer - err := runDatasetRm(context.Background(), runDatasetRmArgs{ + err := runDataDelete(context.Background(), runDataDeleteArgs{ Table: "../bad", Printer: ui.New(&buf, ui.WithColor(false)), }) diff --git a/internal/cli/dataset.go b/internal/cli/data.go similarity index 95% rename from internal/cli/dataset.go rename to internal/cli/data.go index 012fd2cf..b91c8bb5 100644 --- a/internal/cli/dataset.go +++ b/internal/cli/data.go @@ -20,34 +20,38 @@ import ( "github.com/tracebloc/cli/internal/ui" ) -// newDatasetCmd wires the `tracebloc dataset` subtree. The dominant -// verb is `push`, completed in Phase 3 (tracebloc/client#151) across +// newDataCmd wires the `tracebloc data` subtree. The dominant +// verb is `ingest`, completed in Phase 3 (tracebloc/client#151) across // PR-a (pre-flight: spec synth, validation, layout walk, cluster // discovery) and PR-b (this one: ephemeral stage Pod + tar-over- -// exec stream + progress bar + SIGINT-safe cleanup). `dataset rm` -// (#30) removes a pushed dataset's in-cluster artifacts; `dataset +// exec stream + progress bar + SIGINT-safe cleanup). `data delete` +// (#30) removes an ingested dataset's in-cluster artifacts; `data // list` lists the ingested datasets. -func newDatasetCmd() *cobra.Command { +// +// Aliases: "dataset" is kept for one deprecation cycle so existing +// scripts continue to work. +func newDataCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "dataset", - Short: "Manage datasets in the parent client release", + Use: "data", + Aliases: []string{"dataset"}, + Short: "Manage datasets in the parent client release", Long: `Commands for staging and managing datasets on the cluster's shared PVC. -` + "`dataset push`" + ` stages a local dataset to the cluster's shared +` + "`data ingest`" + ` stages a local dataset to the cluster's shared PVC, submits the ingestion run to jobs-manager, and watches the ingestor Job to completion (streaming logs + the final summary). ` + "`tracebloc cluster info`" + ` is the pre-flight you'd typically run -before the first push.`, +before the first ingest.`, } - cmd.AddCommand(newDatasetPushCmd()) - cmd.AddCommand(newDatasetListCmd()) - cmd.AddCommand(newDatasetRmCmd()) + cmd.AddCommand(newDataIngestCmd()) + cmd.AddCommand(newDataListCmd()) + cmd.AddCommand(newDataDeleteCmd()) return cmd } -// newDatasetPushCmd implements `tracebloc dataset push `. +// newDataIngestCmd implements `tracebloc data ingest `. // // Phase 3 scope (now complete across PR-a + PR-b): // @@ -64,7 +68,10 @@ before the first push.`, // Phase 4 (`tracebloc/client#152`) hooks the submit-to-jobs-manager // step into the bottom of this command, replacing the "manually // kick off helm ingestor" workaround in the success message. -func newDatasetPushCmd() *cobra.Command { +// +// Aliases: "push" is kept for one deprecation cycle so existing +// scripts continue to work. +func newDataIngestCmd() *cobra.Command { var ( // Kubeconfig flags — same conventions as `cluster info`. // Promoting these to persistent on the root is a v0.2 @@ -117,8 +124,9 @@ func newDatasetPushCmd() *cobra.Command { ) cmd := &cobra.Command{ - Use: "push ", - Short: "Stage a local dataset to the cluster's shared PVC", + Use: "ingest ", + Aliases: []string{"push"}, + Short: "Stage a local dataset to the cluster's shared PVC", Long: `Stages a local dataset to the parent client release's shared PVC, submits an ingestion run to jobs-manager, and watches the ingestor Job to completion. Supports 9 task categories (image classification, @@ -161,7 +169,7 @@ Exit codes: } // Guided mode: on a terminal (and unless --no-input), prompt // for whatever's still missing. Off a TTY / with --no-input, - // prompter stays nil and runDatasetPush keeps flag-only + // prompter stays nil and runDataIngest keeps flag-only // behavior. interactive := !noInput && !outputJSON && isInteractiveTTY() var pr prompter @@ -178,8 +186,8 @@ Exit codes: printer = printerForWriter(cmd, cmd.ErrOrStderr()) jsonOut = cmd.OutOrStdout() } - return runDatasetPush(cmd.Context(), humanOut, cmd.ErrOrStderr(), - runDatasetPushArgs{ + return runDataIngest(cmd.Context(), humanOut, cmd.ErrOrStderr(), + runDataIngestArgs{ LocalPath: localPath, Kubeconfig: kubeconfigPath, Context: contextOverride, @@ -268,11 +276,11 @@ Exit codes: return cmd } -// runDatasetPushArgs collects every parameter runDatasetPush needs, +// runDataIngestArgs collects every parameter runDataIngest needs, // so the body stays testable without going through cobra. The cobra // RunE wrapper above is the ONLY caller in production; tests // construct one of these directly. -type runDatasetPushArgs struct { +type runDataIngestArgs struct { LocalPath string Kubeconfig string Context string @@ -289,7 +297,7 @@ type runDatasetPushArgs struct { Printer *ui.Printer // Interactive guided mode (#28). When Interactive is true, - // runDatasetPush prompts (via Prompter) for any missing core inputs + // runDataIngest prompts (via Prompter) for any missing core inputs // before validation. CategorySet records whether --category was // passed explicitly (its non-empty default would otherwise look // like a deliberate choice). Prompter is nil off a TTY / --no-input. @@ -313,7 +321,7 @@ type runDatasetPushArgs struct { // expandHome expands a leading ~ or ~/… to $HOME, leaving every other // path (relative, absolute, empty) untouched. It mirrors // cluster.expandPath — kept as a small local copy rather than coupling -// the dataset path-handling to the cluster package's internals; if a +// the data path-handling to the cluster package's internals; if a // third caller appears, promote both to a shared pathutil. func expandHome(path string) string { if path == "" || path[0] != '~' { @@ -331,7 +339,7 @@ func expandHome(path string) string { return filepath.Join(home, path[1:]) } -// runDatasetPush is the full Phase 3 implementation: pre-flight +// runDataIngest is the full Phase 3 implementation: pre-flight // checks, then either --dry-run stop or stage Pod + tar stream + // cleanup. Phase 4 (#152) will hook submit-to-jobs-manager after // the staging step. @@ -340,7 +348,7 @@ func expandHome(path string) string { // need the cluster runs before any that does, so a customer with // a bad label-column or oversized dataset gets the diagnostic in // milliseconds without a kubeconfig round-trip. -func runDatasetPush(ctx context.Context, out, errOut io.Writer, a runDatasetPushArgs) (err error) { +func runDataIngest(ctx context.Context, out, errOut io.Writer, a runDataIngestArgs) (err error) { // In --output-json mode, guarantee stdout always carries a JSON // object. The dry-run + post-submit paths emit a result and set // jsonEmitted; this defer covers every early-failure return (bad @@ -358,11 +366,11 @@ func runDatasetPush(ctx context.Context, out, errOut io.Writer, a runDatasetPush } }() - // Intro header: brand + a plain-English explainer of what a push + // Intro header: brand + a plain-English explainer of what an ingest // does, so a first-time user understands it before any prompts. // Routed through a.Printer, so --output-json keeps it on stderr and // --plain/non-TTY degrade cleanly. (#31) - a.Printer.Banner("tracebloc", "dataset push") + a.Printer.Banner("tracebloc", "data ingest") a.Printer.Para(strings.TrimSpace(` This uploads a dataset from your machine into your tracebloc workspace so models can be trained on it. Your files are sent to the Kubernetes cluster your @@ -377,7 +385,7 @@ contributors train against it without ever seeing the raw files.`)) if a.Interactive && a.Prompter != nil { if err := runInteractive(a.Printer, a.Prompter, &a, a.CategorySet); err != nil { if errors.Is(err, errInteractiveCancelled) { - a.Printer.Infof("Cancelled — nothing was pushed.") + a.Printer.Infof("Cancelled — nothing was ingested.") return nil } return &exitError{code: 3, err: fmt.Errorf("interactive setup: %w", err)} @@ -424,7 +432,7 @@ contributors train against it without ever seeing the raw files.`)) case push.IsCLISupported(a.Spec.Category): // supported case push.IsKnown(a.Spec.Category): - // A recognized category dataset push doesn't implement yet — image + // A recognized category data ingest doesn't implement yet — image // (semantic_segmentation / instance_segmentation) or text // (causal_language_modeling). Routed here (not the default branch) so the // user gets the registry's per-category pending-support reason, not a diff --git a/internal/cli/dataset_rm.go b/internal/cli/data_delete.go similarity index 85% rename from internal/cli/dataset_rm.go rename to internal/cli/data_delete.go index ecc83f6a..974630a0 100644 --- a/internal/cli/dataset_rm.go +++ b/internal/cli/data_delete.go @@ -12,10 +12,10 @@ import ( "github.com/tracebloc/cli/internal/ui" ) -// runDatasetRmArgs is the resolved input to runDatasetRm — same shape -// convention as runDatasetPushArgs, so the command's RunE stays a thin +// runDataDeleteArgs is the resolved input to runDataDelete — same shape +// convention as runDataIngestArgs, so the command's RunE stays a thin // flag-to-struct adapter and the logic is unit-testable. -type runDatasetRmArgs struct { +type runDataDeleteArgs struct { Table string Kubeconfig string Context string @@ -26,11 +26,14 @@ type runDatasetRmArgs struct { Prompter prompter // nil off a TTY or when --yes is set } -// newDatasetRmCmd implements `tracebloc dataset rm ` — the -// in-cluster teardown of a previously-pushed dataset. See +// newDataDeleteCmd implements `tracebloc data delete
` — the +// in-cluster teardown of a previously-ingested dataset. See // internal/push.Teardown for the mechanism and the design note on the // approach (CLI-direct vs a server-side delete endpoint). -func newDatasetRmCmd() *cobra.Command { +// +// Aliases: "rm" is kept for one deprecation cycle so existing +// scripts continue to work. +func newDataDeleteCmd() *cobra.Command { var ( kubeconfigPath string contextOverride string @@ -40,9 +43,10 @@ func newDatasetRmCmd() *cobra.Command { ) cmd := &cobra.Command{ - Use: "rm
", - Short: "Delete a pushed dataset's in-cluster artifacts (table + PVC files)", - Long: `Removes the in-cluster artifacts a previous ` + "`dataset push`" + ` created + Use: "delete
", + Aliases: []string{"rm"}, + Short: "Delete an ingested dataset's in-cluster artifacts (table + PVC files)", + Long: `Removes the in-cluster artifacts a previous ` + "`data ingest`" + ` created for a table: the MySQL table in ` + push.IngestionDatabase + ` and the dataset's directories on the shared PVC. Destructive and not undoable. @@ -62,7 +66,7 @@ Exit codes: if !yes && isInteractiveTTY() { pr = surveyPrompter{} } - return runDatasetRm(cmd.Context(), runDatasetRmArgs{ + return runDataDelete(cmd.Context(), runDataDeleteArgs{ Table: args[0], Kubeconfig: kubeconfigPath, Context: contextOverride, @@ -89,16 +93,16 @@ Exit codes: return cmd } -// runDatasetRm discovers the cluster, shows the teardown plan, confirms, -// then removes the in-cluster artifacts. The flow mirrors runDatasetPush +// runDataDelete discovers the cluster, shows the teardown plan, confirms, +// then removes the in-cluster artifacts. The flow mirrors runDataIngest // (validate → discover → plan/pre-flight → act) so the two commands feel // like siblings. -func runDatasetRm(ctx context.Context, a runDatasetRmArgs) error { +func runDataDelete(ctx context.Context, a runDataDeleteArgs) error { p := a.Printer - p.Banner("tracebloc", "delete a pushed dataset") - p.Para(`This permanently removes a dataset you pushed earlier: it drops the table from + p.Banner("tracebloc", "delete an ingested dataset") + p.Para(`This permanently removes a dataset you ingested earlier: it drops the table from the cluster and deletes the dataset's files on the shared storage. It can't be -undone — re-pushing the data is the only way back.`) +undone — re-ingesting the data is the only way back.`) // 1. Validate the name before we build any PVC path from it // (push.PlanTeardown panics on an unsafe name by design). @@ -200,7 +204,7 @@ undone — re-pushing the data is the only way back.`) if res.DroppedTable { return &exitError{code: 7, err: fmt.Errorf( "teardown incomplete — the table %s.%s was dropped, but removing its files failed: %w; "+ - "re-run `tracebloc dataset rm %s`, or delete the leftover staging dirs on the node", + "re-run `tracebloc data delete %s`, or delete the leftover staging dirs on the node", plan.Database, plan.Table, err, a.Table)} } return &exitError{code: 7, err: fmt.Errorf("teardown failed: %w", err)} diff --git a/internal/cli/dataset_list.go b/internal/cli/data_list.go similarity index 73% rename from internal/cli/dataset_list.go rename to internal/cli/data_list.go index a0fb36fb..0d6029a6 100644 --- a/internal/cli/dataset_list.go +++ b/internal/cli/data_list.go @@ -14,10 +14,10 @@ import ( "github.com/tracebloc/cli/internal/ui" ) -// runDatasetListArgs is the resolved input to runDatasetList — same -// shape convention as the other dataset verbs, keeping the RunE a thin +// runDataListArgs is the resolved input to runDataList — same +// shape convention as the other data verbs, keeping the RunE a thin // flag-to-struct adapter. -type runDatasetListArgs struct { +type runDataListArgs struct { Kubeconfig string Context string Namespace string @@ -26,12 +26,12 @@ type runDatasetListArgs struct { JSONOut io.Writer } -// newDatasetListCmd implements `tracebloc dataset list` — a read-only +// newDataListCmd implements `tracebloc data list` — a read-only // listing of the datasets ingested into the cluster. The kubeconfig -// flags are all zero-value-safe, so the minimal `tracebloc dataset list` +// flags are all zero-value-safe, so the minimal `tracebloc data list` // runs against the current context + its namespace; the flags only // override that (same convention as `cluster info`). -func newDatasetListCmd() *cobra.Command { +func newDataListCmd() *cobra.Command { var ( kubeconfigPath string contextOverride string @@ -42,11 +42,11 @@ func newDatasetListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List datasets ingested in the cluster", - Long: `Lists the datasets pushed + ingested into the parent client release — + Long: `Lists the datasets ingested into the parent client release — the tables in ` + push.IngestionDatabase + ` on the cluster. With no flags it uses your current kubeconfig context and its namespace; -the flags below override that, same as ` + "`cluster info`" + ` and ` + "`dataset push`" + `. +the flags below override that, same as ` + "`cluster info`" + ` and ` + "`data ingest`" + `. For the full catalog (with metadata), see the dashboard at https://ai.tracebloc.io/metadata. @@ -58,14 +58,14 @@ Exit codes: Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { // In --output-json mode, human output (the banner) goes to - // stderr so stdout carries only the JSON — same split as push. + // stderr so stdout carries only the JSON — same split as ingest. printer := printerFor(cmd) var jsonOut io.Writer if outputJSON { printer = printerForWriter(cmd, cmd.ErrOrStderr()) jsonOut = cmd.OutOrStdout() } - return runDatasetList(cmd.Context(), runDatasetListArgs{ + return runDataList(cmd.Context(), runDataListArgs{ Kubeconfig: kubeconfigPath, Context: contextOverride, Namespace: nsOverride, @@ -88,14 +88,14 @@ Exit codes: return cmd } -// runDatasetList discovers the cluster, enumerates the ingested tables, -// and renders them. Mirrors the other dataset verbs' discovery so the +// runDataList discovers the cluster, enumerates the ingested tables, +// and renders them. Mirrors the other data verbs' discovery so the // exit-code contract is consistent. -func runDatasetList(ctx context.Context, a runDatasetListArgs) (err error) { +func runDataList(ctx context.Context, a runDataListArgs) (err error) { // In --output-json mode, guarantee stdout always carries JSON: the // success path emits the listing and sets jsonEmitted; this defer // covers the early-failure returns (kubeconfig, no release, query) - // with a JSON error object, mirroring dataset push. (Bugbot #53) + // with a JSON error object, mirroring data ingest. (Bugbot #53) jsonEmitted := false defer func() { if a.OutputJSON && err != nil && !jsonEmitted { @@ -104,7 +104,7 @@ func runDatasetList(ctx context.Context, a runDatasetListArgs) (err error) { if errors.As(err, &ee) { code = ee.Code() } - writeDatasetListErrorJSON(a.JSONOut, err, code) + writeDataListErrorJSON(a.JSONOut, err, code) } }() @@ -134,20 +134,20 @@ func runDatasetList(ctx context.Context, a runDatasetListArgs) (err error) { } if a.OutputJSON { - writeDatasetListJSON(a.JSONOut, resolved.Namespace, release.ReleaseName, tables) + writeDataListJSON(a.JSONOut, resolved.Namespace, release.ReleaseName, tables) jsonEmitted = true return nil } - renderDatasetList(p, resolved.Namespace, tables) + renderDataList(p, resolved.Namespace, tables) return nil } -// renderDatasetList prints the human-facing listing. Split out so it's +// renderDataList prints the human-facing listing. Split out so it's // unit-testable with a buffer-backed Printer. -func renderDatasetList(p *ui.Printer, namespace string, tables []string) { +func renderDataList(p *ui.Printer, namespace string, tables []string) { p.Section(fmt.Sprintf("Datasets in %s (%d)", namespace, len(tables))) if len(tables) == 0 { - p.Infof("No datasets yet — push one with `tracebloc dataset push`.") + p.Infof("No datasets yet — ingest one with `tracebloc data ingest`.") return } for _, t := range tables { @@ -155,19 +155,19 @@ func renderDatasetList(p *ui.Printer, namespace string, tables []string) { } } -// datasetListJSON is the --output-json shape (owned by the CLI layer). -type datasetListJSON struct { +// dataListJSON is the --output-json shape (owned by the CLI layer). +type dataListJSON struct { Namespace string `json:"namespace"` Release string `json:"release"` Count int `json:"count"` Datasets []string `json:"datasets"` } -func writeDatasetListJSON(w io.Writer, namespace, release string, tables []string) { +func writeDataListJSON(w io.Writer, namespace, release string, tables []string) { if tables == nil { tables = []string{} // emit [] not null } - res := datasetListJSON{ + res := dataListJSON{ Namespace: namespace, Release: release, Count: len(tables), @@ -180,10 +180,10 @@ func writeDatasetListJSON(w io.Writer, namespace, release string, tables []strin _, _ = fmt.Fprintln(w, string(b)) } -// writeDatasetListErrorJSON emits a minimal JSON error object for +// writeDataListErrorJSON emits a minimal JSON error object for // --output-json runs that fail before the listing is produced, so -// stdout is never empty on failure (parallels dataset push). (Bugbot #53) -func writeDatasetListErrorJSON(w io.Writer, e error, code int) { +// stdout is never empty on failure (parallels data ingest). (Bugbot #53) +func writeDataListErrorJSON(w io.Writer, e error, code int) { res := struct { Status string `json:"status"` Error string `json:"error"` diff --git a/internal/cli/dataset_list_test.go b/internal/cli/data_list_test.go similarity index 64% rename from internal/cli/dataset_list_test.go rename to internal/cli/data_list_test.go index 7e5d2461..715f8731 100644 --- a/internal/cli/dataset_list_test.go +++ b/internal/cli/data_list_test.go @@ -13,17 +13,17 @@ import ( "github.com/tracebloc/cli/internal/ui" ) -// TestRunDatasetList_OutputJSONEarlyFailureEmitsJSON: with --output-json, +// TestRunDataList_OutputJSONEarlyFailureEmitsJSON: with --output-json, // a failure before the listing (here a broken kubeconfig, exit 3) still // writes a JSON error object to stdout — the stdout-always-JSON contract -// that #49 established for dataset push. (Bugbot #53) -func TestRunDatasetList_OutputJSONEarlyFailureEmitsJSON(t *testing.T) { +// that #49 established for data ingest. (Bugbot #53) +func TestRunDataList_OutputJSONEarlyFailureEmitsJSON(t *testing.T) { bad := filepath.Join(t.TempDir(), "broken.yaml") if err := os.WriteFile(bad, []byte("}{ not valid kubeconfig"), 0o644); err != nil { t.Fatal(err) } var jsonBuf, human bytes.Buffer - err := runDatasetList(context.Background(), runDatasetListArgs{ + err := runDataList(context.Background(), runDataListArgs{ Kubeconfig: bad, OutputJSON: true, Printer: ui.New(&human, ui.WithColor(false)), @@ -43,25 +43,25 @@ func TestRunDatasetList_OutputJSONEarlyFailureEmitsJSON(t *testing.T) { } } -// TestRenderDatasetList_Empty: the empty listing shows the count and -// points the user at `dataset push`. -func TestRenderDatasetList_Empty(t *testing.T) { +// TestRenderDataList_Empty: the empty listing shows the count and +// points the user at `data ingest`. +func TestRenderDataList_Empty(t *testing.T) { var buf bytes.Buffer - renderDatasetList(ui.New(&buf, ui.WithColor(false)), "ap-workspace", nil) + renderDataList(ui.New(&buf, ui.WithColor(false)), "ap-workspace", nil) out := buf.String() if !strings.Contains(out, "Datasets in ap-workspace (0)") { t.Errorf("missing header/count:\n%s", out) } - if !strings.Contains(out, "dataset push") { - t.Errorf("empty state should point at `dataset push`:\n%s", out) + if !strings.Contains(out, "data ingest") { + t.Errorf("empty state should point at `data ingest`:\n%s", out) } } -// TestRenderDatasetList_Items: a populated listing shows the count and +// TestRenderDataList_Items: a populated listing shows the count and // every table name. -func TestRenderDatasetList_Items(t *testing.T) { +func TestRenderDataList_Items(t *testing.T) { var buf bytes.Buffer - renderDatasetList(ui.New(&buf, ui.WithColor(false)), "tracebloc-templates", []string{"reg_train", "churn_test"}) + renderDataList(ui.New(&buf, ui.WithColor(false)), "tracebloc-templates", []string{"reg_train", "churn_test"}) out := buf.String() for _, want := range []string{"Datasets in tracebloc-templates (2)", "reg_train", "churn_test"} { if !strings.Contains(out, want) { @@ -70,13 +70,13 @@ func TestRenderDatasetList_Items(t *testing.T) { } } -// TestWriteDatasetListJSON: valid JSON with the expected fields, and a +// TestWriteDataListJSON: valid JSON with the expected fields, and a // nil dataset slice marshals as [] (not null) so scripts get an array. -func TestWriteDatasetListJSON(t *testing.T) { +func TestWriteDataListJSON(t *testing.T) { var buf bytes.Buffer - writeDatasetListJSON(&buf, "ns1", "tracebloc", []string{"a", "b"}) + writeDataListJSON(&buf, "ns1", "tracebloc", []string{"a", "b"}) - var got datasetListJSON + var got dataListJSON if err := json.Unmarshal(buf.Bytes(), &got); err != nil { t.Fatalf("not JSON: %v\n%s", err, buf.String()) } @@ -88,7 +88,7 @@ func TestWriteDatasetListJSON(t *testing.T) { } buf.Reset() - writeDatasetListJSON(&buf, "ns1", "tracebloc", nil) + writeDataListJSON(&buf, "ns1", "tracebloc", nil) if !strings.Contains(buf.String(), `"datasets": []`) { t.Errorf("nil datasets should marshal as []:\n%s", buf.String()) } diff --git a/internal/cli/dataset_test.go b/internal/cli/data_test.go similarity index 69% rename from internal/cli/dataset_test.go rename to internal/cli/data_test.go index b8415b5c..299849ca 100644 --- a/internal/cli/dataset_test.go +++ b/internal/cli/data_test.go @@ -30,20 +30,21 @@ func imgcLayout(t *testing.T) string { return root } -// execDatasetPush drives the full cobra dispatch for the push -// command and returns the exit code + captured stdout/stderr. +// execDataIngest drives the full cobra dispatch for the ingest +// command using the canonical `data ingest` form and returns the +// exit code + captured stdout/stderr. // Mirrors the execIngestValidate helper from ingest_test.go — same // rationale about not sharing *cobra.Command across cases (cobra // holds flag state on the command tree). // -// kubeconfigPath is required because every push invocation tries +// kubeconfigPath is required because every ingest invocation tries // kubeconfig load before any cluster work; tests that want to // stop EARLIER (at schema validation or layout walk) still need a // kubeconfig path that resolves predictably. We feed in a path // that's guaranteed to fail os.Stat so the kubeconfig branch // errors out consistently when reached — and tests assert on the // EARLIER stage's exit code, which fires before kubeconfig. -func execDatasetPush(t *testing.T, args []string) (exitCode int, stdout, stderr string) { +func execDataIngest(t *testing.T, args []string) (exitCode int, stdout, stderr string) { t.Helper() root := NewRootCmd(BuildInfo{Version: "test"}) var so, se bytes.Buffer @@ -54,7 +55,7 @@ func execDatasetPush(t *testing.T, args []string) (exitCode int, stdout, stderr // "fall through" the local pre-checks into kubeconfig load // get a deterministic exit 3 (not a flaky "depends on whether // you have a real kubeconfig" outcome). - cmdArgs := append([]string{"dataset", "push", + cmdArgs := append([]string{"data", "ingest", "--kubeconfig=/tmp/tracebloc-cli-test-nonexistent-" + t.Name()}, args...) root.SetArgs(cmdArgs) @@ -63,7 +64,7 @@ func execDatasetPush(t *testing.T, args []string) (exitCode int, stdout, stderr return ExitCodeFromError(err), so.String(), se.String() } -// TestDatasetPush_UnsupportedCategory_ExitsTwo: the CLI-side category +// TestDataIngest_UnsupportedCategory_ExitsTwo: the CLI-side category // gate runs before schema validation so a customer who passes a // not-yet-supported category gets an actionable message (exit 2) // rather than the schema's confusing missing-property error. Today's @@ -71,7 +72,7 @@ func execDatasetPush(t *testing.T, args []string) (exitCode int, stdout, stderr // family; the other image categories (which need annotation/mask // sidecar staging), the text family, and nonsense values are gated // out here. Bugbot review-on-self caught the missing gate on PR-a. -func TestDatasetPush_UnsupportedCategory_ExitsTwo(t *testing.T) { +func TestDataIngest_UnsupportedCategory_ExitsTwo(t *testing.T) { root := imgcLayout(t) for _, badCategory := range []string{ "semantic_segmentation", // blocked on the ingestor (data-ingestors#136) @@ -79,7 +80,7 @@ func TestDatasetPush_UnsupportedCategory_ExitsTwo(t *testing.T) { "definitely-not-a-category", // nonsense; gate catches this too } { t.Run(badCategory, func(t *testing.T) { - code, _, _ := execDatasetPush(t, []string{ + code, _, _ := execDataIngest(t, []string{ root, "--table=t1", "--category=" + badCategory, @@ -93,18 +94,18 @@ func TestDatasetPush_UnsupportedCategory_ExitsTwo(t *testing.T) { } } -// TestDatasetPush_KnownUnsupportedCategory_PendingNote pins the Bugbot fix +// TestDataIngest_KnownUnsupportedCategory_PendingNote pins the Bugbot fix // (v0.4.0 RC): a registry-known but CLI-unsupported NON-image category // (causal_language_modeling) must get the registry's pending-support note, not -// the misleading "isn't a recognized task category" message. execDatasetPush +// the misleading "isn't a recognized task category" message. execDataIngest // discards the error and SilenceErrors swallows it, so run the command here and // inspect the returned error directly. -func TestDatasetPush_KnownUnsupportedCategory_PendingNote(t *testing.T) { +func TestDataIngest_KnownUnsupportedCategory_PendingNote(t *testing.T) { root := imgcLayout(t) rootCmd := NewRootCmd(BuildInfo{Version: "test"}) rootCmd.SetOut(&bytes.Buffer{}) rootCmd.SetErr(&bytes.Buffer{}) - rootCmd.SetArgs([]string{"dataset", "push", + rootCmd.SetArgs([]string{"data", "ingest", "--kubeconfig=/tmp/tracebloc-cli-test-nonexistent-" + t.Name(), root, "--table=t1", "--category=causal_language_modeling", "--intent=train", "--label-column=label"}) @@ -124,17 +125,17 @@ func TestDatasetPush_KnownUnsupportedCategory_PendingNote(t *testing.T) { } } -// TestDatasetPush_TraversalTableName_ExitsTwo is the security +// TestDataIngest_TraversalTableName_ExitsTwo is the security // regression pin at the CLI layer. --table=../../etc must be // rejected with exit 2 BEFORE any spec synthesis or cluster work — // the table name flows into the /data/shared/
/ PVC path, // and a traversal value would let PR-b's stage Pod escape that // subtree. Bugbot flagged this on PR #8 commit 4240097. -func TestDatasetPush_TraversalTableName_ExitsTwo(t *testing.T) { +func TestDataIngest_TraversalTableName_ExitsTwo(t *testing.T) { root := imgcLayout(t) for _, bad := range []string{"../../etc", "../foo", "foo/bar"} { t.Run(bad, func(t *testing.T) { - code, _, _ := execDatasetPush(t, []string{ + code, _, _ := execDataIngest(t, []string{ root, "--table=" + bad, "--category=image_classification", @@ -148,12 +149,12 @@ func TestDatasetPush_TraversalTableName_ExitsTwo(t *testing.T) { } } -// TestDatasetPush_MissingIntent_ExitsTwo: pins the "intent is +// TestDataIngest_MissingIntent_ExitsTwo: pins the "intent is // required" diagnostic path — different schema violation but the // same exit-code class. -func TestDatasetPush_MissingIntent_ExitsTwo(t *testing.T) { +func TestDataIngest_MissingIntent_ExitsTwo(t *testing.T) { root := imgcLayout(t) - code, _, stderr := execDatasetPush(t, []string{ + code, _, stderr := execDataIngest(t, []string{ root, "--table=t1", "--category=image_classification", @@ -168,7 +169,7 @@ func TestDatasetPush_MissingIntent_ExitsTwo(t *testing.T) { } } -// TestDatasetPush_NonexistentLocalPath_ExitsThree: the layout walk +// TestDataIngest_NonexistentLocalPath_ExitsThree: the layout walk // runs AFTER schema validation, so an invalid local path with // otherwise-valid flags surfaces at the walk stage with exit 3 // (the "local input or kubeconfig" code). @@ -179,8 +180,8 @@ func TestDatasetPush_MissingIntent_ExitsTwo(t *testing.T) { // ingest_test.go's TestIngestValidate_UnreadableFileExitsThree // pattern; the error-content surface is exercised at the package // level (internal/push.Discover's own tests). -func TestDatasetPush_NonexistentLocalPath_ExitsThree(t *testing.T) { - code, _, _ := execDatasetPush(t, []string{ +func TestDataIngest_NonexistentLocalPath_ExitsThree(t *testing.T) { + code, _, _ := execDataIngest(t, []string{ "/tmp/tracebloc-cli-test-no-such-dir-" + t.Name(), "--table=t1", "--category=image_classification", @@ -192,12 +193,12 @@ func TestDatasetPush_NonexistentLocalPath_ExitsThree(t *testing.T) { } } -// TestDatasetPush_MissingLabelsCSV_ExitsThree: most likely "real +// TestDataIngest_MissingLabelsCSV_ExitsThree: most likely "real // world" wrong-layout case — customer has images but forgot // labels.csv. Pins the exit-code contract for the common failure // mode; the diagnostic-text content is covered by // internal/push.TestDiscover_MissingLabelsCSV. -func TestDatasetPush_MissingLabelsCSV_ExitsThree(t *testing.T) { +func TestDataIngest_MissingLabelsCSV_ExitsThree(t *testing.T) { root := t.TempDir() imagesDir := filepath.Join(root, "images") if err := os.MkdirAll(imagesDir, 0o755); err != nil { @@ -208,7 +209,7 @@ func TestDatasetPush_MissingLabelsCSV_ExitsThree(t *testing.T) { t.Fatalf("write img: %v", err) } - code, _, _ := execDatasetPush(t, []string{ + code, _, _ := execDataIngest(t, []string{ root, "--table=t1", "--category=image_classification", @@ -220,14 +221,14 @@ func TestDatasetPush_MissingLabelsCSV_ExitsThree(t *testing.T) { } } -// TestDatasetPush_BadKubeconfig_ExitsThree: schema + layout both +// TestDataIngest_BadKubeconfig_ExitsThree: schema + layout both // pass; kubeconfig load fails because the injected path doesn't // exist. The exit-code contract matches `cluster info`'s — same // class of failure (3 = local input problem) surfaces with the // same code regardless of which command tripped it. -func TestDatasetPush_BadKubeconfig_ExitsThree(t *testing.T) { +func TestDataIngest_BadKubeconfig_ExitsThree(t *testing.T) { root := imgcLayout(t) - code, _, _ := execDatasetPush(t, []string{ + code, _, _ := execDataIngest(t, []string{ root, "--table=t1", "--category=image_classification", @@ -239,10 +240,10 @@ func TestDatasetPush_BadKubeconfig_ExitsThree(t *testing.T) { } } -// TestDatasetPush_RequiresExactlyOneArg: cobra-level Args check +// TestDataIngest_RequiresExactlyOneArg: cobra-level Args check // pins the command signature. Two positional args, or zero, should // fail before the runner even fires. -func TestDatasetPush_RequiresExactlyOneArg(t *testing.T) { +func TestDataIngest_RequiresExactlyOneArg(t *testing.T) { cases := []struct { name string args []string @@ -265,10 +266,69 @@ func TestDatasetPush_RequiresExactlyOneArg(t *testing.T) { } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - code, _, _ := execDatasetPush(t, c.args) + code, _, _ := execDataIngest(t, c.args) if code == 0 { t.Errorf("expected non-zero exit for %s, got 0", c.name) } }) } } + +// TestAliasResolution verifies that the deprecated aliases still dispatch +// to the same handlers as the canonical names: +// - "dataset" → same as "data" +// - "push" → same as "ingest" +// - "rm" → same as "delete" +// +// We use --help invocations because they complete without cluster access; +// the exit code 0 + non-empty output is sufficient to confirm the alias +// resolved correctly. +func TestAliasResolution(t *testing.T) { + cases := []struct { + name string + args []string + want string // substring expected in the combined output + }{ + { + name: "dataset alias resolves", + args: []string{"dataset", "--help"}, + want: "ingest", + }, + { + name: "dataset push alias resolves", + args: []string{"dataset", "push", "--help"}, + want: "Stages a local dataset", + }, + { + name: "data ingest canonical", + args: []string{"data", "ingest", "--help"}, + want: "Stages a local dataset", + }, + { + name: "dataset rm alias resolves", + args: []string{"dataset", "rm", "--help"}, + want: "Removes the in-cluster artifacts", + }, + { + name: "data delete canonical", + args: []string{"data", "delete", "--help"}, + want: "Removes the in-cluster artifacts", + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + rootCmd := NewRootCmd(BuildInfo{Version: "test"}) + var out bytes.Buffer + rootCmd.SetOut(&out) + rootCmd.SetErr(&out) + rootCmd.SetArgs(c.args) + if err := rootCmd.Execute(); err != nil { + t.Fatalf("Execute() = %v, want nil (help should not error)", err) + } + combined := out.String() + if !strings.Contains(combined, c.want) { + t.Errorf("output missing %q:\n%s", c.want, combined) + } + }) + } +} diff --git a/internal/cli/interactive.go b/internal/cli/interactive.go index bb6478d9..9ad71d01 100644 --- a/internal/cli/interactive.go +++ b/internal/cli/interactive.go @@ -17,8 +17,8 @@ import ( // promptCategories is the ordered list offered by the interactive // category picker. It derives from the push registry's CLI-supported -// set — the exact categories runDatasetPush's gate accepts — so the -// picker can't drift from what `dataset push` actually supports. +// set — the exact categories runDataIngest's gate accepts — so the +// picker can't drift from what `data ingest` actually supports. // semantic_/instance_segmentation are excluded (CLISupported=false) // until they're implemented. var promptCategories = push.SupportedCategoryIDs() @@ -30,7 +30,7 @@ var promptCategories = push.SupportedCategoryIDs() // uses to let cluster code run against a fake clientset. // errInteractiveCancelled is returned when the user declines the // confirm prompt or hits Ctrl-C. It's control flow, not a failure: -// runDatasetPush maps it to a clean exit (0) with a "Cancelled" note. +// runDataIngest maps it to a clean exit (0) with a "Cancelled" note. var errInteractiveCancelled = errors.New("cancelled by user") type prompter interface { @@ -102,7 +102,7 @@ func isInteractiveTTY() bool { return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) } -// runInteractive fills the gaps in a's core push fields by prompting, +// runInteractive fills the gaps in a's core ingest fields by prompting, // then returns. It only prompts for what's still missing, so flags the // user already passed win. categorySet says whether --category was set // explicitly (vs left at its non-empty default), which would otherwise @@ -110,8 +110,8 @@ func isInteractiveTTY() bool { // // Mutates a through the pointer. PR-b adds category-specific prompts // (target-size, schema, number-of-keypoints) + a confirm screen. -func runInteractive(p *ui.Printer, pr prompter, a *runDatasetPushArgs, categorySet bool) error { - p.PromptHeader("Let's set up your dataset push") +func runInteractive(p *ui.Printer, pr prompter, a *runDataIngestArgs, categorySet bool) error { + p.PromptHeader("Let's set up your data ingest") p.Hintf("Press Enter to accept a default; Ctrl-C to cancel.") prompted := false @@ -177,11 +177,11 @@ func runInteractive(p *ui.Printer, pr prompter, a *runDatasetPushArgs, categoryS } prompted = prompted || cp - // Confirm only when we actually prompted something — a push that's + // Confirm only when we actually prompted something — an ingest that's // fully specified by flags (on a TTY) isn't nagged with a confirm. if prompted { renderReview(p, a) - ok, err := pr.Confirm("Proceed with the push?", true) + ok, err := pr.Confirm("Proceed with the ingest?", true) if err != nil { return err } @@ -195,7 +195,7 @@ func runInteractive(p *ui.Printer, pr prompter, a *runDatasetPushArgs, categoryS // promptCategorySpecific prompts for the inputs a particular category // needs beyond the core fields, filling only the gaps. Returns whether // it prompted anything (so the caller knows to show the confirm). -func promptCategorySpecific(p *ui.Printer, pr prompter, a *runDatasetPushArgs) (bool, error) { +func promptCategorySpecific(p *ui.Printer, pr prompter, a *runDataIngestArgs) (bool, error) { cat := a.Spec.Category prompted := false switch { @@ -257,9 +257,9 @@ func promptCategorySpecific(p *ui.Printer, pr prompter, a *runDatasetPushArgs) ( return prompted, nil } -// renderReview prints the assembled push inputs before the confirm +// renderReview prints the assembled ingest inputs before the confirm // prompt, so the user sees exactly what's about to happen. -func renderReview(p *ui.Printer, a *runDatasetPushArgs) { +func renderReview(p *ui.Printer, a *runDataIngestArgs) { p.Section("Review") p.Field("path", a.LocalPath) p.Field("category", a.Spec.Category) diff --git a/internal/cli/interactive_test.go b/internal/cli/interactive_test.go index d2c9295d..d1daf298 100644 --- a/internal/cli/interactive_test.go +++ b/internal/cli/interactive_test.go @@ -61,7 +61,7 @@ func TestRunInteractive_FillsAllWhenEmpty(t *testing.T) { "Intent": "test", "Label column": "churned", }} - a := &runDatasetPushArgs{Spec: push.SpecArgs{Category: "image_classification"}} + a := &runDataIngestArgs{Spec: push.SpecArgs{Category: "image_classification"}} if err := runInteractive(discardPrinter(), f, a, false); err != nil { t.Fatalf("runInteractive: %v", err) @@ -92,7 +92,7 @@ func TestRunInteractive_ShowsExampleHints(t *testing.T) { "Path to your dataset directory": "./d", "Destination table name": "churn_train", }} - a := &runDatasetPushArgs{Spec: push.SpecArgs{Category: "tabular_regression"}} + a := &runDataIngestArgs{Spec: push.SpecArgs{Category: "tabular_regression"}} var buf bytes.Buffer p := ui.New(&buf, ui.WithColor(false)) @@ -118,7 +118,7 @@ func TestRunInteractive_SkipsProvidedValues(t *testing.T) { f := &fakePrompter{answers: map[string]string{}} // text_classification has no category-specific prompts, so with all // core fields set + an explicit --category, nothing is asked. - a := &runDatasetPushArgs{ + a := &runDataIngestArgs{ LocalPath: "./data", Spec: push.SpecArgs{ Category: "text_classification", Table: "t", Intent: "train", LabelColumn: "label", @@ -136,7 +136,7 @@ func TestRunInteractive_SkipsProvidedValues(t *testing.T) { // the optional resolution left blank means auto-detect. func TestRunInteractive_Keypoint(t *testing.T) { f := &fakePrompter{answers: map[string]string{"Number of keypoints per sample": "17"}} - a := &runDatasetPushArgs{ + a := &runDataIngestArgs{ LocalPath: "./kp", Spec: push.SpecArgs{Category: "keypoint_detection", Table: "kp_train", Intent: "train", LabelColumn: "image_label"}, } @@ -155,7 +155,7 @@ func TestRunInteractive_Keypoint(t *testing.T) { // (regression-class) and leaves the schema to inference. func TestRunInteractive_TabularRegression(t *testing.T) { f := &fakePrompter{answers: map[string]string{"Label policy": "passthrough"}} - a := &runDatasetPushArgs{ + a := &runDataIngestArgs{ LocalPath: "./tab", Spec: push.SpecArgs{Category: "tabular_regression", Table: "reg_train", Intent: "train", LabelColumn: "Target"}, } @@ -180,7 +180,7 @@ func TestRunInteractive_Cancel(t *testing.T) { } // path is prompted (→ prompted=true → a confirm is shown); the rest // is pre-set so we reach the confirm cleanly. - a := &runDatasetPushArgs{Spec: push.SpecArgs{ + a := &runDataIngestArgs{Spec: push.SpecArgs{ Category: "image_classification", Table: "t", Intent: "train", LabelColumn: "label", }} if err := runInteractive(discardPrinter(), f, a, true); !errors.Is(err, errInteractiveCancelled) { @@ -195,7 +195,7 @@ func TestRunInteractive_MLMSkipsLabel(t *testing.T) { "Destination table name": "mlm_train", "Intent": "train", }} - a := &runDatasetPushArgs{ + a := &runDataIngestArgs{ LocalPath: "./data", Spec: push.SpecArgs{Category: "masked_language_modeling"}, } @@ -216,7 +216,7 @@ func TestRunInteractive_MLMSkipsLabel(t *testing.T) { // push.ValidateTableName, so an unsafe name surfaces as an error. func TestRunInteractive_RejectsBadTable(t *testing.T) { f := &fakePrompter{answers: map[string]string{"Destination table name": "../bad"}} - a := &runDatasetPushArgs{ + a := &runDataIngestArgs{ LocalPath: "./data", Spec: push.SpecArgs{Category: "image_classification", Intent: "train", LabelColumn: "label"}, } diff --git a/internal/cli/root.go b/internal/cli/root.go index 6c21011c..84279c3c 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -48,7 +48,7 @@ developer's workstation. The dominant workflow: - tracebloc dataset push ./my-data \ + tracebloc data ingest ./my-data \ --table cats_dogs_train \ --category image_classification \ --intent train \ @@ -59,7 +59,7 @@ on the cluster's shared PVC, submitting the ingestion request, watching the resulting Job, and reporting the outcome. Customers never touch Helm, never edit YAML, never run kubectl cp manually. -This binary implements the full v0.1 ingestion path: ` + "`dataset push`" + ` +This binary implements the full v0.1 ingestion path: ` + "`data ingest`" + ` (the dominant workflow above), ` + "`ingest validate`" + ` for a local schema check, ` + "`cluster info`" + ` for discovery diagnostics, plus ` + "`version`" + ` and ` + "`completion`" + `. See @@ -85,7 +85,7 @@ what's planned next.`, root.AddCommand(newVersionCmd(info)) root.AddCommand(newIngestCmd()) root.AddCommand(newClusterCmd()) - root.AddCommand(newDatasetCmd()) + root.AddCommand(newDataCmd()) // RFC-0001 (backend#830): browser sign-in + client provisioning. root.AddCommand(newLoginCmd()) root.AddCommand(newLogoutCmd()) @@ -102,9 +102,9 @@ what's planned next.`, p := printerFor(cmd) p.Banner("tracebloc", "interactive data ingestion for your cluster") p.Section("Get started") - p.Infof("tracebloc dataset push — stage + ingest a dataset interactively (or use --help to see flags)") - p.Infof("tracebloc dataset list — list datasets ingested in the cluster") - p.Infof("tracebloc dataset rm
— delete a pushed dataset (its table + files)") + p.Infof("tracebloc data ingest — stage + ingest a dataset interactively (or use --help to see flags)") + p.Infof("tracebloc data list — list datasets ingested in the cluster") + p.Infof("tracebloc data delete
— delete an ingested dataset (its table + files)") p.Infof("tracebloc cluster info — check the CLI can reach your cluster") p.Infof("tracebloc ingest validate f.yaml — validate an ingest.yaml locally") p.Newline() diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 6ac7f028..218be56d 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -41,7 +41,7 @@ func TestRootCmd_HomeScreen(t *testing.T) { if err := root.Execute(); err != nil { t.Fatalf("bare root failed: %v\n%s", err, out.String()) } - for _, want := range []string{"tracebloc", "dataset push", "dataset list", "dataset rm", "cluster info"} { + for _, want := range []string{"tracebloc", "data ingest", "data list", "data delete", "cluster info"} { if !strings.Contains(out.String(), want) { t.Errorf("home screen missing %q:\n%s", want, out.String()) }