From 4da7330b96bf510a81081156d8dd6ea083191d97 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Mon, 25 May 2026 09:42:28 -0700 Subject: [PATCH 1/5] fix(cli): resolve 6 bugs found during E2E testing - datasets create: add missing -o/--output flag; move banner to stderr - sandbox new: move "Sandbox created" banner to stderr so -o json is parseable - sandbox read: use println! instead of print! to ensure trailing newline - sandbox: add delete subcommand (DELETE /sandboxes/{id}) - workspaces set: check HOTDATA_SANDBOX instead of HOTDATA_WORKSPACE; update error message - context push: surface friendly hint when server blocks push inside an active sandbox --- src/command.rs | 10 ++++++++++ src/context.rs | 23 ++++++++++++++++++++++- src/datasets.rs | 31 ++++++++++++++++++++----------- src/main.rs | 6 ++++++ src/sandbox.rs | 17 +++++++++++++++-- src/workspace.rs | 4 ++-- 6 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/command.rs b/src/command.rs index 9f36e77..a8c33ef 100644 --- a/src/command.rs +++ b/src/command.rs @@ -478,6 +478,10 @@ pub enum DatasetsCommands { /// Saved query ID to create the dataset from #[arg(long, conflicts_with = "sql", required_unless_present = "sql")] query_id: Option, + + /// Output format + #[arg(long = "output", short = 'o', default_value = "table", value_parser = ["table", "json", "yaml"])] + output: String, }, /// Update a dataset's description and/or name @@ -854,6 +858,12 @@ pub enum SandboxCommands { #[arg(trailing_var_arg = true, required = true)] cmd: Vec, }, + + /// Delete a sandbox permanently + Delete { + /// Sandbox ID to delete + id: String, + }, } #[derive(Subcommand)] diff --git a/src/context.rs b/src/context.rs index 8b7a44f..f59d189 100644 --- a/src/context.rs +++ b/src/context.rs @@ -311,7 +311,28 @@ pub fn push(workspace_id: &str, database_id: &str, name: &str, dry_run: bool) { let api = ApiClient::new(Some(workspace_id)); let body = json!({ "name": &name, "content": content }); - let resp: UpsertResponse = api.post(&format!("/databases/{database_id}/context"), &body); + let (status, resp_body) = api.post_raw(&format!("/databases/{database_id}/context"), &body); + + if !status.is_success() { + let msg = crate::util::api_error(resp_body.clone()); + if msg.to_lowercase().contains("not allowed within a session") { + eprintln!("{}", msg.red()); + eprintln!( + "{}", + "hint: context push is blocked inside an active sandbox. \ +Run 'hotdata sandbox set' (no args) to clear the active sandbox first." + .dark_grey() + ); + } else { + eprintln!("{}", msg.red()); + } + std::process::exit(1); + } + + let resp: UpsertResponse = serde_json::from_str(&resp_body).unwrap_or_else(|e| { + eprintln!("error parsing response: {e}"); + std::process::exit(1); + }); println!( "{}", diff --git a/src/datasets.rs b/src/datasets.rs index 1cb6548..735031e 100644 --- a/src/datasets.rs +++ b/src/datasets.rs @@ -17,7 +17,7 @@ fn default_schema() -> String { "main".to_string() } -#[derive(Deserialize)] +#[derive(Deserialize, Serialize)] struct CreateResponse { id: String, label: String, @@ -75,6 +75,7 @@ fn create_dataset( description: Option<&str>, name: &str, source: serde_json::Value, + format: &str, ) { let label = description.unwrap_or(name); let body = json!({ "table_name": name, "label": label, "source": source }); @@ -96,18 +97,25 @@ fn create_dataset( }; use crossterm::style::Stylize; - println!("{}", "Dataset created".green()); - println!("id: {}", dataset.id); - println!("label: {}", dataset.label); - println!( - "full_name: datasets.{}.{}", - dataset.schema_name, dataset.table_name - ); + match format { + "json" => println!("{}", serde_json::to_string_pretty(&dataset).unwrap()), + "yaml" => print!("{}", serde_yaml::to_string(&dataset).unwrap()), + "table" => { + eprintln!("{}", "Dataset created".green()); + println!("id: {}", dataset.id); + println!("label: {}", dataset.label); + println!( + "full_name: datasets.{}.{}", + dataset.schema_name, dataset.table_name + ); + } + _ => unreachable!(), + } } -pub fn create_from_query(workspace_id: &str, sql: &str, description: Option<&str>, name: &str) { +pub fn create_from_query(workspace_id: &str, sql: &str, description: Option<&str>, name: &str, format: &str) { let api = ApiClient::new(Some(workspace_id)); - create_dataset(&api, description, name, json!({ "type": "sql_query", "sql": sql })); + create_dataset(&api, description, name, json!({ "type": "sql_query", "sql": sql }), format); } pub fn create_from_saved_query( @@ -115,9 +123,10 @@ pub fn create_from_saved_query( query_id: &str, description: Option<&str>, name: &str, + format: &str, ) { let api = ApiClient::new(Some(workspace_id)); - create_dataset(&api, description, name, json!({ "type": "saved_query", "saved_query_id": query_id })); + create_dataset(&api, description, name, json!({ "type": "saved_query", "saved_query_id": query_id }), format); } pub fn list(workspace_id: &str, limit: Option, offset: Option, format: &str) { diff --git a/src/main.rs b/src/main.rs index cd8ecdc..e5cb8dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -216,6 +216,7 @@ fn main() { description, sql, query_id, + output, }) => { if let Some(sql) = sql { datasets::create_from_query( @@ -223,6 +224,7 @@ fn main() { &sql, description.as_deref(), &name, + &output, ) } else { datasets::create_from_saved_query( @@ -230,6 +232,7 @@ fn main() { query_id.as_deref().unwrap_or_else(|| unreachable!("clap enforces --sql or --query-id")), description.as_deref(), &name, + &output, ) } } @@ -957,6 +960,9 @@ fn main() { Some(SandboxCommands::Set { id: set_id }) => { sandbox::set(set_id.as_deref(), &workspace_id) } + Some(SandboxCommands::Delete { id: delete_id }) => { + sandbox::delete(&delete_id, &workspace_id) + } None => match id { Some(id) => sandbox::get(&id, &workspace_id, &output), None => { diff --git a/src/sandbox.rs b/src/sandbox.rs index d0313af..075c215 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -135,7 +135,7 @@ pub fn read(sandbox_id: &str, workspace_id: &str) { if body.sandbox.markdown.is_empty() { eprintln!("{}", "Sandbox markdown is empty.".dark_grey()); } else { - print!("{}", body.sandbox.markdown); + println!("{}", body.sandbox.markdown); } } @@ -195,7 +195,7 @@ pub fn new(workspace_id: &str, name: Option<&str>, format: &str) { eprintln!("warning: could not save sandbox to config: {e}"); } - println!("{}", "Sandbox created".green()); + eprintln!("{}", "Sandbox created".green()); match format { "json" => println!("{}", serde_json::json!({"public_id": sandbox_id})), "yaml" => print!( @@ -340,6 +340,19 @@ pub fn set(sandbox_id: Option<&str>, workspace_id: &str) { } } +pub fn delete(sandbox_id: &str, workspace_id: &str) { + let api = ApiClient::new(Some(workspace_id)); + let path = format!("/sandboxes/{sandbox_id}"); + let (status, resp_body) = api.delete_raw(&path); + + if !status.is_success() { + eprintln!("{}", crate::util::api_error(resp_body).red()); + std::process::exit(1); + } + + eprintln!("{}", "Sandbox deleted".green()); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/workspace.rs b/src/workspace.rs index 2dac178..f95959a 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -17,10 +17,10 @@ struct ListResponse { } pub fn set(workspace_id: Option<&str>) { - if std::env::var("HOTDATA_WORKSPACE").is_ok() + if std::env::var("HOTDATA_SANDBOX").is_ok() || crate::sandbox::find_sandbox_run_ancestor().is_some() { - eprintln!("error: workspace is locked"); + eprintln!("error: workspace cannot be changed inside a sandbox"); std::process::exit(1); } let api = ApiClient::new(None); From 6fae6c930bcc2df8cf98f510c52adb9f87d7f155 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Mon, 25 May 2026 09:54:46 -0700 Subject: [PATCH 2/5] fix(sandbox): move update banner to stderr; add functional test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sandbox update: move "Sandbox updated" banner to eprintln! (stderr) so -o json stdout is clean — same fix as sandbox new - scripts/test_commands.sh: new comprehensive functional test script covering all 128 command/subcommand/flag/output-format combinations, with real API calls, resource lifecycle, and cleanup --- scripts/test_commands.sh | 475 +++++++++++++++++++++++++++++++++++++++ src/sandbox.rs | 2 +- 2 files changed, 476 insertions(+), 1 deletion(-) create mode 100755 scripts/test_commands.sh diff --git a/scripts/test_commands.sh b/scripts/test_commands.sh new file mode 100755 index 0000000..f97d0f9 --- /dev/null +++ b/scripts/test_commands.sh @@ -0,0 +1,475 @@ +#!/usr/bin/env bash +# Comprehensive functional test suite for the hotdata CLI. +# Tests each command and subcommand with table/json/yaml outputs, +# edge cases, and flag variations. Creates and cleans up real resources. +# +# Usage: +# ./scripts/test_commands.sh # uses 'hotdata' from PATH +# HOTDATA_BIN=./target/debug/hotdata ./scripts/test_commands.sh + +set -uo pipefail + +# Resolve BIN to an absolute path so pushd/popd doesn't break relative paths. +_raw_bin="${HOTDATA_BIN:-hotdata}" +if [[ "$_raw_bin" == ./* || "$_raw_bin" == /* ]]; then + BIN="$(cd "$(dirname "$_raw_bin")" && pwd)/$(basename "$_raw_bin")" +else + BIN="$_raw_bin" +fi +unset _raw_bin +PASS=0 +FAIL=0 +SKIP=0 + +# Resources created during the run — cleaned up on exit. +CREATED_SANDBOX="" +CREATED_DB="" +CREATED_DATASET="" +CONTEXT_TMPDIR="" + +# ── Colors ──────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +GREY='\033[0;90m'; BOLD='\033[1m'; NC='\033[0m' + +# ── Helpers ─────────────────────────────────────────────────────────────────── +section() { echo; echo -e "${BOLD}=== $* ===${NC}"; } + +_pass() { echo -e " ${GREEN}✓${NC} $1"; PASS=$((PASS+1)); } +_fail() { + echo -e " ${RED}✗${NC} $1" + [[ -n "${2:-}" ]] && echo -e "${GREY} $2${NC}" + FAIL=$((FAIL+1)) +} +_skip() { echo -e " ${YELLOW}~${NC} $1 (skipped)"; SKIP=$((SKIP+1)); } + +# Run a command, check its exit code matches $expected (default 0). +check() { + local name="$1" expected="${2:-0}"; shift 2 + local actual=0 out + out=$("$@" 2>&1) || actual=$? + if [[ "$actual" -eq "$expected" ]]; then + _pass "$name" + else + _fail "$name → exit $actual (expected $expected)" \ + "cmd: $* | out: $(echo "$out" | head -2 | tr '\n' ' ')" + fi +} + +# Run a command; verify stdout is valid JSON. +check_json() { + local name="$1"; shift + local actual=0 out + out=$("$@" 2>/dev/null) || actual=$? + if [[ "$actual" -ne 0 ]]; then + _fail "$name → non-zero exit ($actual)" + return + fi + if echo "$out" | jq . > /dev/null 2>&1; then + _pass "$name" + else + _fail "$name → invalid JSON" "out: $(echo "$out" | head -1)" + fi +} + +# Run a command; verify stdout is valid YAML. +check_yaml() { + local name="$1"; shift + local actual=0 out + out=$("$@" 2>/dev/null) || actual=$? + if [[ "$actual" -ne 0 ]]; then + _fail "$name → non-zero exit ($actual)" + return + fi + if python3 -c "import sys,yaml; yaml.safe_load(sys.stdin)" <<< "$out" > /dev/null 2>&1; then + _pass "$name" + else + _fail "$name → invalid YAML" "out: $(echo "$out" | head -1)" + fi +} + +# Run a command that should output non-empty text to stdout. +check_nonempty() { + local name="$1"; shift + local actual=0 out + out=$("$@" 2>/dev/null) || actual=$? + if [[ "$actual" -ne 0 ]]; then + _fail "$name → non-zero exit ($actual)" + elif [[ -z "$out" ]]; then + _fail "$name → empty output" + else + _pass "$name" + fi +} + +# Capture a jq value from a command's JSON stdout into a variable. +# capture_into VARNAME JQ_PATH CMD [args...] +capture_into() { + local varname="$1" jqpath="$2"; shift 2 + local out + out=$("$@" 2>/dev/null) || return 1 + local val + val=$(echo "$out" | jq -r "$jqpath" 2>/dev/null) || return 1 + [[ "$val" == "null" || -z "$val" ]] && return 1 + printf -v "$varname" '%s' "$val" +} + +# ── Cleanup ─────────────────────────────────────────────────────────────────── +cleanup() { + echo + section "Cleanup" + if [[ -n "$CREATED_SANDBOX" ]]; then + "$BIN" sandbox set 2>/dev/null || true + if "$BIN" sandbox delete "$CREATED_SANDBOX" 2>/dev/null; then + echo " deleted sandbox $CREATED_SANDBOX" + else + echo -e " ${YELLOW}warning: failed to delete sandbox $CREATED_SANDBOX${NC}" + fi + fi + if [[ -n "$CREATED_DATASET" ]]; then + echo " note: dataset $CREATED_DATASET has no delete command — left in place" + fi + if [[ -n "$CREATED_DB" ]]; then + if "$BIN" databases delete "$CREATED_DB" 2>/dev/null; then + echo " deleted database $CREATED_DB" + else + echo -e " ${YELLOW}warning: failed to delete database $CREATED_DB${NC}" + fi + fi + if [[ -n "$CONTEXT_TMPDIR" ]]; then + rm -rf "$CONTEXT_TMPDIR" + fi + echo + echo -e "${BOLD}Results: ${GREEN}${PASS} passed${NC} ${RED}${FAIL} failed${NC} ${YELLOW}${SKIP} skipped${NC}" + [[ $FAIL -eq 0 ]] +} +trap cleanup EXIT + +# ── Verify binary exists ────────────────────────────────────────────────────── +if ! command -v "$BIN" > /dev/null 2>&1 && [[ ! -x "$BIN" ]]; then + echo "error: binary not found: $BIN" + exit 1 +fi +echo -e "${BOLD}hotdata CLI functional test suite${NC}" +echo "binary: $(command -v "$BIN" 2>/dev/null || echo "$BIN")" +echo "version: $("$BIN" --version 2>&1 | head -1)" + +# ───────────────────────────────────────────────────────────────────────────── +# AUTH +# ───────────────────────────────────────────────────────────────────────────── +section "auth" +check "auth status" 0 $BIN auth status + +# ───────────────────────────────────────────────────────────────────────────── +# WORKSPACES +# ───────────────────────────────────────────────────────────────────────────── +section "workspaces" +check "workspaces list (table)" 0 $BIN workspaces list +check_json "workspaces list (json)" $BIN workspaces list -o json +check_yaml "workspaces list (yaml)" $BIN workspaces list -o yaml + +WS_ID="" +capture_into WS_ID '.[0].public_id' $BIN workspaces list -o json || true +if [[ -n "$WS_ID" ]]; then + check "workspaces set " 0 $BIN workspaces set "$WS_ID" + check "workspaces set " 1 $BIN workspaces set "ws_doesnotexist_99999" --no-input +else + _skip "workspaces set (no workspace found)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# CONNECTIONS +# ───────────────────────────────────────────────────────────────────────────── +section "connections" +check "connections list (table)" 0 $BIN connections list +check_json "connections list (json)" $BIN connections list -o json +check_yaml "connections list (yaml)" $BIN connections list -o yaml + +CONN_ID="" +capture_into CONN_ID '.[0].id' $BIN connections list -o json || true +if [[ -n "$CONN_ID" ]]; then + check "connections get (table)" 0 $BIN connections "$CONN_ID" + check_json "connections get (json)" $BIN connections "$CONN_ID" -o json + check_yaml "connections get (yaml)" $BIN connections "$CONN_ID" -o yaml + check "connections get " 1 $BIN connections "conn_doesnotexist_99999" +else + _skip "connections get (no connections)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# TABLES +# ───────────────────────────────────────────────────────────────────────────── +section "tables" +check "tables list (table)" 0 $BIN tables list +check_json "tables list (json)" $BIN tables list -o json +check_yaml "tables list (yaml)" $BIN tables list -o yaml +if [[ -n "$CONN_ID" ]]; then + check "tables list --connection-id (table)" 0 $BIN tables list --connection-id "$CONN_ID" + check_json "tables list --connection-id (json)" $BIN tables list --connection-id "$CONN_ID" -o json +else + _skip "tables list --connection-id (no connections)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# DATABASES +# ───────────────────────────────────────────────────────────────────────────── +section "databases" +check "databases list (table)" 0 $BIN databases list +check_json "databases list (json)" $BIN databases list -o json +check_yaml "databases list (yaml)" $BIN databases list -o yaml + +# Create a test database +DB_OUT="" +DB_OUT=$($BIN databases create --description "hotdata-cli test $(date +%s)" -o json 2>/dev/null) || true +if echo "$DB_OUT" | jq -e '.id' > /dev/null 2>&1; then + CREATED_DB=$(echo "$DB_OUT" | jq -r '.id') + _pass "databases create (json)" + + check "databases show (table)" 0 $BIN databases show "$CREATED_DB" + check_json "databases show (json)" $BIN databases "$CREATED_DB" -o json + check_yaml "databases show (yaml)" $BIN databases "$CREATED_DB" -o yaml + check "databases show " 1 $BIN databases show "db_doesnotexist_99999" + + check "databases tables shorthand" 0 $BIN databases tables "$CREATED_DB" + check_json "databases tables list (json)" $BIN databases tables list --database "$CREATED_DB" -o json + check_yaml "databases tables list (yaml)" $BIN databases tables list --database "$CREATED_DB" -o yaml + + check "databases set " 0 $BIN databases set "$CREATED_DB" +else + _fail "databases create (could not create test database)" + _skip "databases show / tables / set (no test database)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# QUERY +# ───────────────────────────────────────────────────────────────────────────── +section "query" +check "query SELECT (table)" 0 $BIN query "SELECT 1 AS n, 'hello' AS s" +check_json "query SELECT (json)" $BIN query "SELECT 1 AS n" -o json +check "query SELECT (csv)" 0 $BIN query "SELECT 1 AS n" -o csv +check "query invalid SQL" 1 $BIN query "THIS IS NOT VALID SQL !!!" +check "query multiline" 0 $BIN query "SELECT 1 AS a, 2 AS b, 3 AS c" +if [[ -n "$CREATED_DB" ]]; then + check "query -d " 0 $BIN query "SELECT 1" -d "$CREATED_DB" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# QUERIES (run history) +# ───────────────────────────────────────────────────────────────────────────── +section "queries" +check "queries list (table)" 0 $BIN queries list +check_json "queries list (json)" $BIN queries list -o json +check_yaml "queries list (yaml)" $BIN queries list -o yaml +check "queries list --status running" 0 $BIN queries list --status running +check "queries list --status failed" 0 $BIN queries list --status failed +check "queries list --limit 5" 0 $BIN queries list --limit 5 + +QUERY_RUN_ID="" +capture_into QUERY_RUN_ID '.[0].id' $BIN queries list -o json || true +if [[ -n "$QUERY_RUN_ID" ]]; then + check "queries get (table)" 0 $BIN queries "$QUERY_RUN_ID" + check_json "queries get (json)" $BIN queries "$QUERY_RUN_ID" -o json + check_yaml "queries get (yaml)" $BIN queries "$QUERY_RUN_ID" -o yaml +else + _skip "queries get (no runs in history)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# RESULTS +# ───────────────────────────────────────────────────────────────────────────── +section "results" +check "results list (table)" 0 $BIN results list +check_json "results list (json)" $BIN results list -o json +check_yaml "results list (yaml)" $BIN results list -o yaml +check "results list --limit 5" 0 $BIN results list --limit 5 + +RESULT_ID="" +capture_into RESULT_ID '.[0].id' $BIN results list -o json || true +if [[ -n "$RESULT_ID" ]]; then + check "results get (table)" 0 $BIN results "$RESULT_ID" + check_json "results get (json)" $BIN results "$RESULT_ID" -o json +else + _skip "results get (no stored results)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# JOBS +# ───────────────────────────────────────────────────────────────────────────── +section "jobs" +check "jobs list (table)" 0 $BIN jobs list +check_json "jobs list (json)" $BIN jobs list -o json +check_yaml "jobs list (yaml)" $BIN jobs list -o yaml +check "jobs list --status running" 0 $BIN jobs list --status running +check "jobs list --all" 0 $BIN jobs list --all +check "jobs list --all (json)" 0 $BIN jobs list --all -o json # just check exit + +JOB_ID="" +capture_into JOB_ID '.[0].id' $BIN jobs list --all -o json || true +if [[ -n "$JOB_ID" ]]; then + check "jobs get (table)" 0 $BIN jobs "$JOB_ID" + check_json "jobs get (json)" $BIN jobs "$JOB_ID" -o json + check_yaml "jobs get (yaml)" $BIN jobs "$JOB_ID" -o yaml +else + _skip "jobs get (no jobs in history)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# DATASETS +# ───────────────────────────────────────────────────────────────────────────── +section "datasets" +check "datasets list (table)" 0 $BIN datasets list +check_json "datasets list (json)" $BIN datasets list -o json +check_yaml "datasets list (yaml)" $BIN datasets list -o yaml +check "datasets list --limit 5" 0 $BIN datasets list --limit 5 + +# Create a test dataset from SQL — use -o json to detect success and capture ID +DS_NAME="cli_test_$(date +%s)" +DS_JSON="" +DS_JSON=$($BIN datasets create --name "$DS_NAME" --sql "SELECT 1 AS n" -o json 2>/dev/null) || true +if echo "$DS_JSON" | jq -e '.id' > /dev/null 2>&1; then + CREATED_DATASET=$(echo "$DS_JSON" | jq -r '.id') + _pass "datasets create --sql -o json" + + check "datasets create --sql (table)" 0 $BIN datasets create --name "${DS_NAME}_t" --sql "SELECT 1" + check_yaml "datasets create --sql (yaml)" $BIN datasets create --name "${DS_NAME}_y" --sql "SELECT 1" -o yaml + + check "datasets get (table)" 0 $BIN datasets "$CREATED_DATASET" + check_json "datasets get (json)" $BIN datasets "$CREATED_DATASET" -o json + check_yaml "datasets get (yaml)" $BIN datasets "$CREATED_DATASET" -o yaml + + check "datasets update --description (table)" 0 $BIN datasets update "$CREATED_DATASET" --description "updated label" + check_json "datasets update --description (json)" $BIN datasets update "$CREATED_DATASET" --description "updated again" -o json + check_yaml "datasets update --description (yaml)" $BIN datasets update "$CREATED_DATASET" --description "updated yaml" -o yaml + check "datasets update --name" 0 $BIN datasets update "$CREATED_DATASET" --name "${DS_NAME}_renamed" + check "datasets update (missing flags)" 1 $BIN datasets update "$CREATED_DATASET" + + check "datasets refresh" 0 $BIN datasets refresh "$CREATED_DATASET" + check "datasets refresh --async" 0 $BIN datasets refresh "$CREATED_DATASET" --async +else + _fail "datasets create (non-zero exit or invalid JSON)" + _skip "datasets get / update / refresh (no test dataset)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# INDEXES +# ───────────────────────────────────────────────────────────────────────────── +section "indexes" +check "indexes list (table)" 0 $BIN indexes list +check_json "indexes list (json)" $BIN indexes list -o json +check_yaml "indexes list (yaml)" $BIN indexes list -o yaml +if [[ -n "$CONN_ID" ]]; then + check "indexes list --connection-id" 0 $BIN indexes list --connection-id "$CONN_ID" + check_json "indexes list --connection-id (json)" $BIN indexes list --connection-id "$CONN_ID" -o json +fi + +# ───────────────────────────────────────────────────────────────────────────── +# EMBEDDING PROVIDERS +# ───────────────────────────────────────────────────────────────────────────── +section "embedding-providers" +check "embedding-providers list (table)" 0 $BIN embedding-providers list +check_json "embedding-providers list (json)" $BIN embedding-providers list -o json +check_yaml "embedding-providers list (yaml)" $BIN embedding-providers list -o yaml + +EP_ID="" +capture_into EP_ID '.[0].id' $BIN embedding-providers list -o json || true +if [[ -n "$EP_ID" ]]; then + check "embedding-providers get (table)" 0 $BIN embedding-providers get "$EP_ID" + check_json "embedding-providers get (json)" $BIN embedding-providers get "$EP_ID" -o json + check_yaml "embedding-providers get (yaml)" $BIN embedding-providers get "$EP_ID" -o yaml +else + _skip "embedding-providers get (none configured)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# SANDBOX +# ───────────────────────────────────────────────────────────────────────────── +section "sandbox" +check "sandbox list (table)" 0 $BIN sandbox list +check_json "sandbox list (json)" $BIN sandbox list -o json +check_yaml "sandbox list (yaml)" $BIN sandbox list -o yaml + +# Create a test sandbox, capture its ID via JSON output +SB_OUT="" +SB_OUT=$($BIN sandbox new --name "cli-test-$(date +%s)" -o json 2>/dev/null) || true +if echo "$SB_OUT" | jq -e '.public_id' > /dev/null 2>&1; then + CREATED_SANDBOX=$(echo "$SB_OUT" | jq -r '.public_id') + _pass "sandbox new -o json (public_id parseable)" + + check "sandbox get (table)" 0 $BIN sandbox "$CREATED_SANDBOX" + check_json "sandbox get (json)" $BIN sandbox "$CREATED_SANDBOX" -o json + check_yaml "sandbox get (yaml)" $BIN sandbox "$CREATED_SANDBOX" -o yaml + check "sandbox get " 1 $BIN sandbox "s_doesnotexist_99999" + + check "sandbox update --name" 0 $BIN sandbox update "$CREATED_SANDBOX" --name "cli-test-updated" + check "sandbox update --markdown" 0 $BIN sandbox update "$CREATED_SANDBOX" --markdown "# Test\n\nHello world" + check "sandbox update (missing flags)" 1 $BIN sandbox update "$CREATED_SANDBOX" + check_json "sandbox update (json)" $BIN sandbox update "$CREATED_SANDBOX" --name "cli-test-json" -o json + + check "sandbox read (active)" 0 $BIN sandbox "$CREATED_SANDBOX" read + + check "sandbox set " 0 $BIN sandbox set "$CREATED_SANDBOX" + check "sandbox set (clear)" 0 $BIN sandbox set + + # Delete — cleaned up here so cleanup() doesn't try again + check "sandbox delete " 0 $BIN sandbox delete "$CREATED_SANDBOX" + CREATED_SANDBOX="" +else + _fail "sandbox new (could not create or parse public_id)" + _skip "sandbox get / update / read / set / delete (no test sandbox)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# CONTEXT +# ───────────────────────────────────────────────────────────────────────────── +section "context" +if [[ -n "$CREATED_DB" ]]; then + check "context list (table)" 0 $BIN context list -d "$CREATED_DB" + check_json "context list (json)" $BIN context list -d "$CREATED_DB" -o json + check_yaml "context list (yaml)" $BIN context list -d "$CREATED_DB" -o yaml + check "context list --prefix" 0 $BIN context list -d "$CREATED_DB" --prefix "TESTCTX" + + CONTEXT_TMPDIR=$(mktemp -d) + echo "# Test context" > "$CONTEXT_TMPDIR/TESTCTX.md" + echo "This is a CLI test context entry." >> "$CONTEXT_TMPDIR/TESTCTX.md" + + pushd "$CONTEXT_TMPDIR" > /dev/null + check "context push --dry-run" 0 $BIN context push TESTCTX --dry-run -d "$CREATED_DB" + check "context push" 0 $BIN context push TESTCTX -d "$CREATED_DB" + check "context push (update)" 0 $BIN context push TESTCTX -d "$CREATED_DB" + check "context show" 0 $BIN context show TESTCTX -d "$CREATED_DB" + check "context show .md suffix" 0 $BIN context show TESTCTX.md -d "$CREATED_DB" + rm TESTCTX.md + check "context pull --dry-run" 0 $BIN context pull TESTCTX --dry-run -d "$CREATED_DB" + check "context pull" 0 $BIN context pull TESTCTX -d "$CREATED_DB" + check "context pull (exists, no force)" 1 $BIN context pull TESTCTX -d "$CREATED_DB" + check "context pull --force" 0 $BIN context pull TESTCTX --force -d "$CREATED_DB" + popd > /dev/null + + check "context show (nonexistent)" 1 $BIN context show nonexistent_ctx_xyz -d "$CREATED_DB" + check "context push (reserved word)" 1 $BIN context push select -d "$CREATED_DB" +else + _skip "context tests (no test database created)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# SKILLS +# ───────────────────────────────────────────────────────────────────────────── +section "skills" +check "skills status" 0 $BIN skills status +check "skills list" 0 $BIN skills list + +# ───────────────────────────────────────────────────────────────────────────── +# COMPLETIONS +# ───────────────────────────────────────────────────────────────────────────── +section "completions" +check_nonempty "completions bash" $BIN completions bash +check_nonempty "completions zsh" $BIN completions zsh +check_nonempty "completions fish" $BIN completions fish + +# ───────────────────────────────────────────────────────────────────────────── +# GLOBAL FLAGS +# ───────────────────────────────────────────────────────────────────────────── +section "global flags" +check "version flag short" 0 $BIN -v +check "version flag long" 0 $BIN --version +check "help flag" 0 $BIN --help +check "no-input flag" 0 $BIN workspaces list --no-input diff --git a/src/sandbox.rs b/src/sandbox.rs index 075c215..b33aa5d 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -238,7 +238,7 @@ pub fn update( let resp: DetailResponse = api.patch(&path, &body); let s = &resp.sandbox; - println!("{}", "Sandbox updated".green()); + eprintln!("{}", "Sandbox updated".green()); match format { "json" => println!("{}", serde_json::to_string_pretty(s).unwrap()), "yaml" => print!("{}", serde_yaml::to_string(s).unwrap()), From 3418fc9ce6cf8c125fdba0238279f6809a74035b Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Tue, 26 May 2026 20:08:38 -0700 Subject: [PATCH 3/5] fix(sandbox): clear active session after deleting the active sandbox When the deleted sandbox is the currently active one (tracked via HOTDATA_SANDBOX env var or config.sandbox), clear the cached sandbox session and config pointer so subsequent commands do not keep routing through a stale JWT -- mirrors what sandbox set (no args) already does. --- src/sandbox.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/sandbox.rs b/src/sandbox.rs index b33aa5d..6da6ab1 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -350,6 +350,19 @@ pub fn delete(sandbox_id: &str, workspace_id: &str) { std::process::exit(1); } + // If the deleted sandbox was the active one, clear the cached session + // and config pointer so subsequent commands don't keep routing through + // a stale sandbox JWT — mirroring what `sandbox set` (no args) does. + let active = std::env::var("HOTDATA_SANDBOX") + .ok() + .or_else(|| config::load("default").ok().and_then(|p| p.sandbox)); + if active.as_deref() == Some(sandbox_id) { + sandbox_session::clear(); + if let Err(e) = config::clear_sandbox("default") { + eprintln!("warning: could not clear sandbox from config: {e}"); + } + } + eprintln!("{}", "Sandbox deleted".green()); } From da541e2447de474c7d39030fc7abe970b7cf77f0 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Tue, 26 May 2026 20:09:34 -0700 Subject: [PATCH 4/5] fix(sandbox): block delete inside an active sandbox run Add check_sandbox_lock() to sandbox::delete for consistency with new, run, and set -- prevents silently deleting the sandbox you are currently running inside. --- src/sandbox.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sandbox.rs b/src/sandbox.rs index 6da6ab1..0572e6b 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -341,6 +341,7 @@ pub fn set(sandbox_id: Option<&str>, workspace_id: &str) { } pub fn delete(sandbox_id: &str, workspace_id: &str) { + check_sandbox_lock(); let api = ApiClient::new(Some(workspace_id)); let path = format!("/sandboxes/{sandbox_id}"); let (status, resp_body) = api.delete_raw(&path); From fccd65f3958747b8b667407d337d0a2db291dcbf Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Tue, 26 May 2026 20:12:28 -0700 Subject: [PATCH 5/5] revert: remove test script from PR --- scripts/test_commands.sh | 475 --------------------------------------- 1 file changed, 475 deletions(-) delete mode 100755 scripts/test_commands.sh diff --git a/scripts/test_commands.sh b/scripts/test_commands.sh deleted file mode 100755 index f97d0f9..0000000 --- a/scripts/test_commands.sh +++ /dev/null @@ -1,475 +0,0 @@ -#!/usr/bin/env bash -# Comprehensive functional test suite for the hotdata CLI. -# Tests each command and subcommand with table/json/yaml outputs, -# edge cases, and flag variations. Creates and cleans up real resources. -# -# Usage: -# ./scripts/test_commands.sh # uses 'hotdata' from PATH -# HOTDATA_BIN=./target/debug/hotdata ./scripts/test_commands.sh - -set -uo pipefail - -# Resolve BIN to an absolute path so pushd/popd doesn't break relative paths. -_raw_bin="${HOTDATA_BIN:-hotdata}" -if [[ "$_raw_bin" == ./* || "$_raw_bin" == /* ]]; then - BIN="$(cd "$(dirname "$_raw_bin")" && pwd)/$(basename "$_raw_bin")" -else - BIN="$_raw_bin" -fi -unset _raw_bin -PASS=0 -FAIL=0 -SKIP=0 - -# Resources created during the run — cleaned up on exit. -CREATED_SANDBOX="" -CREATED_DB="" -CREATED_DATASET="" -CONTEXT_TMPDIR="" - -# ── Colors ──────────────────────────────────────────────────────────────────── -RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' -GREY='\033[0;90m'; BOLD='\033[1m'; NC='\033[0m' - -# ── Helpers ─────────────────────────────────────────────────────────────────── -section() { echo; echo -e "${BOLD}=== $* ===${NC}"; } - -_pass() { echo -e " ${GREEN}✓${NC} $1"; PASS=$((PASS+1)); } -_fail() { - echo -e " ${RED}✗${NC} $1" - [[ -n "${2:-}" ]] && echo -e "${GREY} $2${NC}" - FAIL=$((FAIL+1)) -} -_skip() { echo -e " ${YELLOW}~${NC} $1 (skipped)"; SKIP=$((SKIP+1)); } - -# Run a command, check its exit code matches $expected (default 0). -check() { - local name="$1" expected="${2:-0}"; shift 2 - local actual=0 out - out=$("$@" 2>&1) || actual=$? - if [[ "$actual" -eq "$expected" ]]; then - _pass "$name" - else - _fail "$name → exit $actual (expected $expected)" \ - "cmd: $* | out: $(echo "$out" | head -2 | tr '\n' ' ')" - fi -} - -# Run a command; verify stdout is valid JSON. -check_json() { - local name="$1"; shift - local actual=0 out - out=$("$@" 2>/dev/null) || actual=$? - if [[ "$actual" -ne 0 ]]; then - _fail "$name → non-zero exit ($actual)" - return - fi - if echo "$out" | jq . > /dev/null 2>&1; then - _pass "$name" - else - _fail "$name → invalid JSON" "out: $(echo "$out" | head -1)" - fi -} - -# Run a command; verify stdout is valid YAML. -check_yaml() { - local name="$1"; shift - local actual=0 out - out=$("$@" 2>/dev/null) || actual=$? - if [[ "$actual" -ne 0 ]]; then - _fail "$name → non-zero exit ($actual)" - return - fi - if python3 -c "import sys,yaml; yaml.safe_load(sys.stdin)" <<< "$out" > /dev/null 2>&1; then - _pass "$name" - else - _fail "$name → invalid YAML" "out: $(echo "$out" | head -1)" - fi -} - -# Run a command that should output non-empty text to stdout. -check_nonempty() { - local name="$1"; shift - local actual=0 out - out=$("$@" 2>/dev/null) || actual=$? - if [[ "$actual" -ne 0 ]]; then - _fail "$name → non-zero exit ($actual)" - elif [[ -z "$out" ]]; then - _fail "$name → empty output" - else - _pass "$name" - fi -} - -# Capture a jq value from a command's JSON stdout into a variable. -# capture_into VARNAME JQ_PATH CMD [args...] -capture_into() { - local varname="$1" jqpath="$2"; shift 2 - local out - out=$("$@" 2>/dev/null) || return 1 - local val - val=$(echo "$out" | jq -r "$jqpath" 2>/dev/null) || return 1 - [[ "$val" == "null" || -z "$val" ]] && return 1 - printf -v "$varname" '%s' "$val" -} - -# ── Cleanup ─────────────────────────────────────────────────────────────────── -cleanup() { - echo - section "Cleanup" - if [[ -n "$CREATED_SANDBOX" ]]; then - "$BIN" sandbox set 2>/dev/null || true - if "$BIN" sandbox delete "$CREATED_SANDBOX" 2>/dev/null; then - echo " deleted sandbox $CREATED_SANDBOX" - else - echo -e " ${YELLOW}warning: failed to delete sandbox $CREATED_SANDBOX${NC}" - fi - fi - if [[ -n "$CREATED_DATASET" ]]; then - echo " note: dataset $CREATED_DATASET has no delete command — left in place" - fi - if [[ -n "$CREATED_DB" ]]; then - if "$BIN" databases delete "$CREATED_DB" 2>/dev/null; then - echo " deleted database $CREATED_DB" - else - echo -e " ${YELLOW}warning: failed to delete database $CREATED_DB${NC}" - fi - fi - if [[ -n "$CONTEXT_TMPDIR" ]]; then - rm -rf "$CONTEXT_TMPDIR" - fi - echo - echo -e "${BOLD}Results: ${GREEN}${PASS} passed${NC} ${RED}${FAIL} failed${NC} ${YELLOW}${SKIP} skipped${NC}" - [[ $FAIL -eq 0 ]] -} -trap cleanup EXIT - -# ── Verify binary exists ────────────────────────────────────────────────────── -if ! command -v "$BIN" > /dev/null 2>&1 && [[ ! -x "$BIN" ]]; then - echo "error: binary not found: $BIN" - exit 1 -fi -echo -e "${BOLD}hotdata CLI functional test suite${NC}" -echo "binary: $(command -v "$BIN" 2>/dev/null || echo "$BIN")" -echo "version: $("$BIN" --version 2>&1 | head -1)" - -# ───────────────────────────────────────────────────────────────────────────── -# AUTH -# ───────────────────────────────────────────────────────────────────────────── -section "auth" -check "auth status" 0 $BIN auth status - -# ───────────────────────────────────────────────────────────────────────────── -# WORKSPACES -# ───────────────────────────────────────────────────────────────────────────── -section "workspaces" -check "workspaces list (table)" 0 $BIN workspaces list -check_json "workspaces list (json)" $BIN workspaces list -o json -check_yaml "workspaces list (yaml)" $BIN workspaces list -o yaml - -WS_ID="" -capture_into WS_ID '.[0].public_id' $BIN workspaces list -o json || true -if [[ -n "$WS_ID" ]]; then - check "workspaces set " 0 $BIN workspaces set "$WS_ID" - check "workspaces set " 1 $BIN workspaces set "ws_doesnotexist_99999" --no-input -else - _skip "workspaces set (no workspace found)" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# CONNECTIONS -# ───────────────────────────────────────────────────────────────────────────── -section "connections" -check "connections list (table)" 0 $BIN connections list -check_json "connections list (json)" $BIN connections list -o json -check_yaml "connections list (yaml)" $BIN connections list -o yaml - -CONN_ID="" -capture_into CONN_ID '.[0].id' $BIN connections list -o json || true -if [[ -n "$CONN_ID" ]]; then - check "connections get (table)" 0 $BIN connections "$CONN_ID" - check_json "connections get (json)" $BIN connections "$CONN_ID" -o json - check_yaml "connections get (yaml)" $BIN connections "$CONN_ID" -o yaml - check "connections get " 1 $BIN connections "conn_doesnotexist_99999" -else - _skip "connections get (no connections)" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# TABLES -# ───────────────────────────────────────────────────────────────────────────── -section "tables" -check "tables list (table)" 0 $BIN tables list -check_json "tables list (json)" $BIN tables list -o json -check_yaml "tables list (yaml)" $BIN tables list -o yaml -if [[ -n "$CONN_ID" ]]; then - check "tables list --connection-id (table)" 0 $BIN tables list --connection-id "$CONN_ID" - check_json "tables list --connection-id (json)" $BIN tables list --connection-id "$CONN_ID" -o json -else - _skip "tables list --connection-id (no connections)" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# DATABASES -# ───────────────────────────────────────────────────────────────────────────── -section "databases" -check "databases list (table)" 0 $BIN databases list -check_json "databases list (json)" $BIN databases list -o json -check_yaml "databases list (yaml)" $BIN databases list -o yaml - -# Create a test database -DB_OUT="" -DB_OUT=$($BIN databases create --description "hotdata-cli test $(date +%s)" -o json 2>/dev/null) || true -if echo "$DB_OUT" | jq -e '.id' > /dev/null 2>&1; then - CREATED_DB=$(echo "$DB_OUT" | jq -r '.id') - _pass "databases create (json)" - - check "databases show (table)" 0 $BIN databases show "$CREATED_DB" - check_json "databases show (json)" $BIN databases "$CREATED_DB" -o json - check_yaml "databases show (yaml)" $BIN databases "$CREATED_DB" -o yaml - check "databases show " 1 $BIN databases show "db_doesnotexist_99999" - - check "databases tables shorthand" 0 $BIN databases tables "$CREATED_DB" - check_json "databases tables list (json)" $BIN databases tables list --database "$CREATED_DB" -o json - check_yaml "databases tables list (yaml)" $BIN databases tables list --database "$CREATED_DB" -o yaml - - check "databases set " 0 $BIN databases set "$CREATED_DB" -else - _fail "databases create (could not create test database)" - _skip "databases show / tables / set (no test database)" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# QUERY -# ───────────────────────────────────────────────────────────────────────────── -section "query" -check "query SELECT (table)" 0 $BIN query "SELECT 1 AS n, 'hello' AS s" -check_json "query SELECT (json)" $BIN query "SELECT 1 AS n" -o json -check "query SELECT (csv)" 0 $BIN query "SELECT 1 AS n" -o csv -check "query invalid SQL" 1 $BIN query "THIS IS NOT VALID SQL !!!" -check "query multiline" 0 $BIN query "SELECT 1 AS a, 2 AS b, 3 AS c" -if [[ -n "$CREATED_DB" ]]; then - check "query -d " 0 $BIN query "SELECT 1" -d "$CREATED_DB" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# QUERIES (run history) -# ───────────────────────────────────────────────────────────────────────────── -section "queries" -check "queries list (table)" 0 $BIN queries list -check_json "queries list (json)" $BIN queries list -o json -check_yaml "queries list (yaml)" $BIN queries list -o yaml -check "queries list --status running" 0 $BIN queries list --status running -check "queries list --status failed" 0 $BIN queries list --status failed -check "queries list --limit 5" 0 $BIN queries list --limit 5 - -QUERY_RUN_ID="" -capture_into QUERY_RUN_ID '.[0].id' $BIN queries list -o json || true -if [[ -n "$QUERY_RUN_ID" ]]; then - check "queries get (table)" 0 $BIN queries "$QUERY_RUN_ID" - check_json "queries get (json)" $BIN queries "$QUERY_RUN_ID" -o json - check_yaml "queries get (yaml)" $BIN queries "$QUERY_RUN_ID" -o yaml -else - _skip "queries get (no runs in history)" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# RESULTS -# ───────────────────────────────────────────────────────────────────────────── -section "results" -check "results list (table)" 0 $BIN results list -check_json "results list (json)" $BIN results list -o json -check_yaml "results list (yaml)" $BIN results list -o yaml -check "results list --limit 5" 0 $BIN results list --limit 5 - -RESULT_ID="" -capture_into RESULT_ID '.[0].id' $BIN results list -o json || true -if [[ -n "$RESULT_ID" ]]; then - check "results get (table)" 0 $BIN results "$RESULT_ID" - check_json "results get (json)" $BIN results "$RESULT_ID" -o json -else - _skip "results get (no stored results)" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# JOBS -# ───────────────────────────────────────────────────────────────────────────── -section "jobs" -check "jobs list (table)" 0 $BIN jobs list -check_json "jobs list (json)" $BIN jobs list -o json -check_yaml "jobs list (yaml)" $BIN jobs list -o yaml -check "jobs list --status running" 0 $BIN jobs list --status running -check "jobs list --all" 0 $BIN jobs list --all -check "jobs list --all (json)" 0 $BIN jobs list --all -o json # just check exit - -JOB_ID="" -capture_into JOB_ID '.[0].id' $BIN jobs list --all -o json || true -if [[ -n "$JOB_ID" ]]; then - check "jobs get (table)" 0 $BIN jobs "$JOB_ID" - check_json "jobs get (json)" $BIN jobs "$JOB_ID" -o json - check_yaml "jobs get (yaml)" $BIN jobs "$JOB_ID" -o yaml -else - _skip "jobs get (no jobs in history)" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# DATASETS -# ───────────────────────────────────────────────────────────────────────────── -section "datasets" -check "datasets list (table)" 0 $BIN datasets list -check_json "datasets list (json)" $BIN datasets list -o json -check_yaml "datasets list (yaml)" $BIN datasets list -o yaml -check "datasets list --limit 5" 0 $BIN datasets list --limit 5 - -# Create a test dataset from SQL — use -o json to detect success and capture ID -DS_NAME="cli_test_$(date +%s)" -DS_JSON="" -DS_JSON=$($BIN datasets create --name "$DS_NAME" --sql "SELECT 1 AS n" -o json 2>/dev/null) || true -if echo "$DS_JSON" | jq -e '.id' > /dev/null 2>&1; then - CREATED_DATASET=$(echo "$DS_JSON" | jq -r '.id') - _pass "datasets create --sql -o json" - - check "datasets create --sql (table)" 0 $BIN datasets create --name "${DS_NAME}_t" --sql "SELECT 1" - check_yaml "datasets create --sql (yaml)" $BIN datasets create --name "${DS_NAME}_y" --sql "SELECT 1" -o yaml - - check "datasets get (table)" 0 $BIN datasets "$CREATED_DATASET" - check_json "datasets get (json)" $BIN datasets "$CREATED_DATASET" -o json - check_yaml "datasets get (yaml)" $BIN datasets "$CREATED_DATASET" -o yaml - - check "datasets update --description (table)" 0 $BIN datasets update "$CREATED_DATASET" --description "updated label" - check_json "datasets update --description (json)" $BIN datasets update "$CREATED_DATASET" --description "updated again" -o json - check_yaml "datasets update --description (yaml)" $BIN datasets update "$CREATED_DATASET" --description "updated yaml" -o yaml - check "datasets update --name" 0 $BIN datasets update "$CREATED_DATASET" --name "${DS_NAME}_renamed" - check "datasets update (missing flags)" 1 $BIN datasets update "$CREATED_DATASET" - - check "datasets refresh" 0 $BIN datasets refresh "$CREATED_DATASET" - check "datasets refresh --async" 0 $BIN datasets refresh "$CREATED_DATASET" --async -else - _fail "datasets create (non-zero exit or invalid JSON)" - _skip "datasets get / update / refresh (no test dataset)" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# INDEXES -# ───────────────────────────────────────────────────────────────────────────── -section "indexes" -check "indexes list (table)" 0 $BIN indexes list -check_json "indexes list (json)" $BIN indexes list -o json -check_yaml "indexes list (yaml)" $BIN indexes list -o yaml -if [[ -n "$CONN_ID" ]]; then - check "indexes list --connection-id" 0 $BIN indexes list --connection-id "$CONN_ID" - check_json "indexes list --connection-id (json)" $BIN indexes list --connection-id "$CONN_ID" -o json -fi - -# ───────────────────────────────────────────────────────────────────────────── -# EMBEDDING PROVIDERS -# ───────────────────────────────────────────────────────────────────────────── -section "embedding-providers" -check "embedding-providers list (table)" 0 $BIN embedding-providers list -check_json "embedding-providers list (json)" $BIN embedding-providers list -o json -check_yaml "embedding-providers list (yaml)" $BIN embedding-providers list -o yaml - -EP_ID="" -capture_into EP_ID '.[0].id' $BIN embedding-providers list -o json || true -if [[ -n "$EP_ID" ]]; then - check "embedding-providers get (table)" 0 $BIN embedding-providers get "$EP_ID" - check_json "embedding-providers get (json)" $BIN embedding-providers get "$EP_ID" -o json - check_yaml "embedding-providers get (yaml)" $BIN embedding-providers get "$EP_ID" -o yaml -else - _skip "embedding-providers get (none configured)" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# SANDBOX -# ───────────────────────────────────────────────────────────────────────────── -section "sandbox" -check "sandbox list (table)" 0 $BIN sandbox list -check_json "sandbox list (json)" $BIN sandbox list -o json -check_yaml "sandbox list (yaml)" $BIN sandbox list -o yaml - -# Create a test sandbox, capture its ID via JSON output -SB_OUT="" -SB_OUT=$($BIN sandbox new --name "cli-test-$(date +%s)" -o json 2>/dev/null) || true -if echo "$SB_OUT" | jq -e '.public_id' > /dev/null 2>&1; then - CREATED_SANDBOX=$(echo "$SB_OUT" | jq -r '.public_id') - _pass "sandbox new -o json (public_id parseable)" - - check "sandbox get (table)" 0 $BIN sandbox "$CREATED_SANDBOX" - check_json "sandbox get (json)" $BIN sandbox "$CREATED_SANDBOX" -o json - check_yaml "sandbox get (yaml)" $BIN sandbox "$CREATED_SANDBOX" -o yaml - check "sandbox get " 1 $BIN sandbox "s_doesnotexist_99999" - - check "sandbox update --name" 0 $BIN sandbox update "$CREATED_SANDBOX" --name "cli-test-updated" - check "sandbox update --markdown" 0 $BIN sandbox update "$CREATED_SANDBOX" --markdown "# Test\n\nHello world" - check "sandbox update (missing flags)" 1 $BIN sandbox update "$CREATED_SANDBOX" - check_json "sandbox update (json)" $BIN sandbox update "$CREATED_SANDBOX" --name "cli-test-json" -o json - - check "sandbox read (active)" 0 $BIN sandbox "$CREATED_SANDBOX" read - - check "sandbox set " 0 $BIN sandbox set "$CREATED_SANDBOX" - check "sandbox set (clear)" 0 $BIN sandbox set - - # Delete — cleaned up here so cleanup() doesn't try again - check "sandbox delete " 0 $BIN sandbox delete "$CREATED_SANDBOX" - CREATED_SANDBOX="" -else - _fail "sandbox new (could not create or parse public_id)" - _skip "sandbox get / update / read / set / delete (no test sandbox)" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# CONTEXT -# ───────────────────────────────────────────────────────────────────────────── -section "context" -if [[ -n "$CREATED_DB" ]]; then - check "context list (table)" 0 $BIN context list -d "$CREATED_DB" - check_json "context list (json)" $BIN context list -d "$CREATED_DB" -o json - check_yaml "context list (yaml)" $BIN context list -d "$CREATED_DB" -o yaml - check "context list --prefix" 0 $BIN context list -d "$CREATED_DB" --prefix "TESTCTX" - - CONTEXT_TMPDIR=$(mktemp -d) - echo "# Test context" > "$CONTEXT_TMPDIR/TESTCTX.md" - echo "This is a CLI test context entry." >> "$CONTEXT_TMPDIR/TESTCTX.md" - - pushd "$CONTEXT_TMPDIR" > /dev/null - check "context push --dry-run" 0 $BIN context push TESTCTX --dry-run -d "$CREATED_DB" - check "context push" 0 $BIN context push TESTCTX -d "$CREATED_DB" - check "context push (update)" 0 $BIN context push TESTCTX -d "$CREATED_DB" - check "context show" 0 $BIN context show TESTCTX -d "$CREATED_DB" - check "context show .md suffix" 0 $BIN context show TESTCTX.md -d "$CREATED_DB" - rm TESTCTX.md - check "context pull --dry-run" 0 $BIN context pull TESTCTX --dry-run -d "$CREATED_DB" - check "context pull" 0 $BIN context pull TESTCTX -d "$CREATED_DB" - check "context pull (exists, no force)" 1 $BIN context pull TESTCTX -d "$CREATED_DB" - check "context pull --force" 0 $BIN context pull TESTCTX --force -d "$CREATED_DB" - popd > /dev/null - - check "context show (nonexistent)" 1 $BIN context show nonexistent_ctx_xyz -d "$CREATED_DB" - check "context push (reserved word)" 1 $BIN context push select -d "$CREATED_DB" -else - _skip "context tests (no test database created)" -fi - -# ───────────────────────────────────────────────────────────────────────────── -# SKILLS -# ───────────────────────────────────────────────────────────────────────────── -section "skills" -check "skills status" 0 $BIN skills status -check "skills list" 0 $BIN skills list - -# ───────────────────────────────────────────────────────────────────────────── -# COMPLETIONS -# ───────────────────────────────────────────────────────────────────────────── -section "completions" -check_nonempty "completions bash" $BIN completions bash -check_nonempty "completions zsh" $BIN completions zsh -check_nonempty "completions fish" $BIN completions fish - -# ───────────────────────────────────────────────────────────────────────────── -# GLOBAL FLAGS -# ───────────────────────────────────────────────────────────────────────────── -section "global flags" -check "version flag short" 0 $BIN -v -check "version flag long" 0 $BIN --version -check "help flag" 0 $BIN --help -check "no-input flag" 0 $BIN workspaces list --no-input