From 518ef08a0f69f880211698805c943733f22afff9 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:56:53 -0400 Subject: [PATCH 1/9] Inject query effects for CLI unit tests skip-changelog: exercise CLI query output without starting the index --- packages/rust/src/cli/run.rs | 109 ++++++++++++++++++++++++++++++++--- testing-conventions.toml | 6 -- 2 files changed, 101 insertions(+), 14 deletions(-) diff --git a/packages/rust/src/cli/run.rs b/packages/rust/src/cli/run.rs index b9db5f050..7b23efa6f 100644 --- a/packages/rust/src/cli/run.rs +++ b/packages/rust/src/cli/run.rs @@ -15,8 +15,10 @@ //! The `dirsql` binary is a shim over [`run_cli`], so `cargo install dirsql //! --features cli` and every other entry path run the same code. -use std::io::IsTerminal; +use std::future::Future; +use std::io::{IsTerminal, Write}; use std::path::PathBuf; +use std::pin::Pin; use super::{ AppState, ServerConfig, execute::execute_query, init::InitOptions, repl::run_repl, @@ -398,26 +400,60 @@ async fn run_default(cli: Cli) -> u8 { /// message — the same string the HTTP `{"error": …}` body carries — to /// stderr with a non-zero exit. async fn run_query(args: QueryArgs) -> u8 { - let parser = match resolve_on_file(&args.on_file) { + let terminal = std::io::stdout().is_terminal(); + let mut out = std::io::stdout(); + let mut err = std::io::stderr(); + run_query_with( + args, + resolve_on_file, + load_state, + |state, body| Box::pin(execute_query(state, body, None)), + terminal, + &mut out, + &mut err, + ) + .await +} + +async fn run_query_with( + args: QueryArgs, + resolve: R, + load: L, + execute: E, + terminal: bool, + out: &mut dyn Write, + err: &mut dyn Write, +) -> u8 +where + R: FnOnce(&[String]) -> std::result::Result, String>, + L: FnOnce(&ConfigArgs, Option) -> AppState, + E: for<'a> FnOnce( + &'a AppState, + String, + ) -> Pin< + Box> + 'a>, + >, +{ + let parser = match resolve(&args.on_file) { Ok(parser) => parser, Err(message) => { - eprintln!("dirsql query: {message}"); + let _ = writeln!(err, "dirsql query: {message}"); return 1; } }; - let state = load_state(&args.common, parser); + let state = load(&args.common, parser); // Unbounded: the process IS the query, so `timeout(1)` expresses any cap // natively; only the long-lived server enforces `query_timeout` (408). - let format = args.format.resolve(std::io::stdout().is_terminal()); + let format = args.format.resolve(terminal); - match execute_query(&state, query_body(&args.sql), None).await { + match execute(&state, query_body(&args.sql)).await { Ok(value) => { - print!("{}", render_rows(&value, format)); + let _ = write!(out, "{}", render_rows(&value, format)); 0 } Err(failure) => { - eprintln!("dirsql query: {}", failure.message()); + let _ = writeln!(err, "dirsql query: {}", failure.message()); 1 } } @@ -664,6 +700,63 @@ mod tests { assert_eq!(query_body(" "), r#"{"sql":" "}"#); } + #[tokio::test] + async fn run_query_with_writes_rows_and_returns_success() { + let args = match Cli::parse_from(["dirsql", "query", "SELECT 1"]).command { + Some(Command::Query(args)) => args, + other => panic!("expected query args, got {other:?}"), + }; + let mut out = Vec::new(); + let mut err = Vec::new(); + let code = run_query_with( + args, + |_| Ok(None), + |_, _| AppState::Unavailable("test state".to_owned()), + |_, body| { + Box::pin(async move { + assert_eq!(body, r#"{"sql":"SELECT 1"}"#); + Ok::<_, super::super::execute::QueryFailure>(one_row()) + }) + }, + false, + &mut out, + &mut err, + ) + .await; + assert_eq!(code, 0); + assert!(!out.is_empty()); + assert!(err.is_empty()); + } + + #[tokio::test] + async fn run_query_with_reports_query_failure_and_returns_error() { + let args = match Cli::parse_from(["dirsql", "query", "SELECT 1"]).command { + Some(Command::Query(args)) => args, + other => panic!("expected query args, got {other:?}"), + }; + let mut out = Vec::new(); + let mut err = Vec::new(); + let code = run_query_with( + args, + |_| Ok(None), + |_, _| AppState::Unavailable("test state".to_owned()), + |_, _| { + Box::pin(async { + Err(super::super::execute::QueryFailure::BadRequest( + "invalid SQL".to_owned(), + )) + }) + }, + false, + &mut out, + &mut err, + ) + .await; + assert_eq!(code, 1); + assert!(out.is_empty()); + assert_eq!(err, b"dirsql query: invalid SQL\n"); + } + /// The `ConfigArgs` parsed from a `query` subcommand invocation (#609: /// config flags are subcommand-local, so they live on the Query variant). fn query_common(argv: &[&str]) -> ConfigArgs { diff --git a/testing-conventions.toml b/testing-conventions.toml index 7b3f00b89..64cc26416 100644 --- a/testing-conventions.toml +++ b/testing-conventions.toml @@ -141,12 +141,6 @@ rules = ["mutation"] lines = [360, 371] reason = "rows walks and stats a real tree and column hands a cell to SQLite; the cell choice is unit-tested in `cell`, and tests/vtab.rs stat_columns_carry_real_values and content_is_excluded_from_star_but_selectable_by_name assert both through a real connection" -[[rust.exempt]] -path = "src/cli/run.rs" -rules = ["mutation"] -lines = [351, 392] -reason = "run_default and run_query build a real index and drive stdout, stderr and the process exit; tests/on_file_failure_e2e.rs (a_clean_scan_exits_zero, a_failed_command_fails_the_run) and tests/repl_pipe_e2e.rs assert their codes through the built binary" - [[rust.exempt]] path = "src/vtab.rs" rules = ["mutation"] From abfa0dbf6664c9296b071755be962e868161f83a Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:58:26 -0400 Subject: [PATCH 2/9] e2e attestation for claude/1330-cli-exit --- packages/python/e2e-attestations/claude-1330-cli-exit.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/python/e2e-attestations/claude-1330-cli-exit.json diff --git a/packages/python/e2e-attestations/claude-1330-cli-exit.json b/packages/python/e2e-attestations/claude-1330-cli-exit.json new file mode 100644 index 000000000..f02214afe --- /dev/null +++ b/packages/python/e2e-attestations/claude-1330-cli-exit.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/ -k \"declared_table_name or query_contract\" -p no:cacheprovider", + "ran_at": 1791251905, + "exit_code": 0, + "commit": "518ef08a0f69f880211698805c943733f22afff9", + "branch": "claude/1330-cli-exit" +} From 6222889c66b3bbe16a70a279d59258ec1330a068 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:59:38 -0400 Subject: [PATCH 3/9] e2e attestation for claude/1330-cli-exit --- packages/ts/e2e-attestations/claude-1330-cli-exit.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/ts/e2e-attestations/claude-1330-cli-exit.json diff --git a/packages/ts/e2e-attestations/claude-1330-cli-exit.json b/packages/ts/e2e-attestations/claude-1330-cli-exit.json new file mode 100644 index 000000000..6339d1615 --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1330-cli-exit.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1791251978, + "exit_code": 0, + "commit": "abfa0dbf6664c9296b071755be962e868161f83a", + "branch": "claude/1330-cli-exit" +} From be884fce304748901d47b46f93df3076d9b71eb6 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:21:58 -0400 Subject: [PATCH 4/9] Keep query failure test within its module skip-changelog: unit-test isolation import cleanup --- packages/rust/src/cli/run.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/rust/src/cli/run.rs b/packages/rust/src/cli/run.rs index 7b23efa6f..2e23e28bd 100644 --- a/packages/rust/src/cli/run.rs +++ b/packages/rust/src/cli/run.rs @@ -21,7 +21,10 @@ use std::path::PathBuf; use std::pin::Pin; use super::{ - AppState, ServerConfig, execute::execute_query, init::InitOptions, repl::run_repl, + AppState, ServerConfig, + execute::{QueryFailure, execute_query}, + init::InitOptions, + repl::run_repl, serve_with_state, table, }; use crate::{DirSQL, Extension, Row, Table}; @@ -430,9 +433,7 @@ where E: for<'a> FnOnce( &'a AppState, String, - ) -> Pin< - Box> + 'a>, - >, + ) -> Pin> + 'a>>, { let parser = match resolve(&args.on_file) { Ok(parser) => parser, @@ -715,7 +716,7 @@ mod tests { |_, body| { Box::pin(async move { assert_eq!(body, r#"{"sql":"SELECT 1"}"#); - Ok::<_, super::super::execute::QueryFailure>(one_row()) + Ok::<_, QueryFailure>(one_row()) }) }, false, @@ -740,13 +741,7 @@ mod tests { args, |_| Ok(None), |_, _| AppState::Unavailable("test state".to_owned()), - |_, _| { - Box::pin(async { - Err(super::super::execute::QueryFailure::BadRequest( - "invalid SQL".to_owned(), - )) - }) - }, + |_, _| Box::pin(async { Err(QueryFailure::BadRequest("invalid SQL".to_owned())) }), false, &mut out, &mut err, From 5e7dcafd3a0ece8090f803fb49c9ef6a3d9e42a3 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:22:21 -0400 Subject: [PATCH 5/9] e2e attestation for claude/1330-cli-exit --- packages/python/e2e-attestations/claude-1330-cli-exit.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/e2e-attestations/claude-1330-cli-exit.json b/packages/python/e2e-attestations/claude-1330-cli-exit.json index f02214afe..4e1fac4d8 100644 --- a/packages/python/e2e-attestations/claude-1330-cli-exit.json +++ b/packages/python/e2e-attestations/claude-1330-cli-exit.json @@ -1,7 +1,7 @@ { "command": "uv run python -m pytest tests/e2e/ -k \"declared_table_name or query_contract\" -p no:cacheprovider", - "ran_at": 1791251905, + "ran_at": 1791253341, "exit_code": 0, - "commit": "518ef08a0f69f880211698805c943733f22afff9", + "commit": "be884fce304748901d47b46f93df3076d9b71eb6", "branch": "claude/1330-cli-exit" } From e26bfed7243e8b870caba0d9c87ccd170efa6fa5 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:23:38 -0400 Subject: [PATCH 6/9] e2e attestation for claude/1330-cli-exit --- packages/ts/e2e-attestations/claude-1330-cli-exit.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1330-cli-exit.json b/packages/ts/e2e-attestations/claude-1330-cli-exit.json index 6339d1615..695392bb7 100644 --- a/packages/ts/e2e-attestations/claude-1330-cli-exit.json +++ b/packages/ts/e2e-attestations/claude-1330-cli-exit.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791251978, + "ran_at": 1791253418, "exit_code": 0, - "commit": "abfa0dbf6664c9296b071755be962e868161f83a", + "commit": "5e7dcafd3a0ece8090f803fb49c9ef6a3d9e42a3", "branch": "claude/1330-cli-exit" } From aefbb64d22ec4d27643a7adf9097f6431aea0eff Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:08:26 -0400 Subject: [PATCH 7/9] Remove incomplete Python e2e receipt skip-changelog: receipt withdrawn until full suite passes --- packages/python/e2e-attestations/claude-1330-cli-exit.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 packages/python/e2e-attestations/claude-1330-cli-exit.json diff --git a/packages/python/e2e-attestations/claude-1330-cli-exit.json b/packages/python/e2e-attestations/claude-1330-cli-exit.json deleted file mode 100644 index 4e1fac4d8..000000000 --- a/packages/python/e2e-attestations/claude-1330-cli-exit.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "command": "uv run python -m pytest tests/e2e/ -k \"declared_table_name or query_contract\" -p no:cacheprovider", - "ran_at": 1791253341, - "exit_code": 0, - "commit": "be884fce304748901d47b46f93df3076d9b71eb6", - "branch": "claude/1330-cli-exit" -} From 5076e5ad0909a14bb7dde96058de54bb3aec05e3 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Tue, 6 Oct 2026 03:05:06 -0400 Subject: [PATCH 8/9] test(rust): assert CLI query exit codes --- packages/rust/src/cli/run.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/rust/src/cli/run.rs b/packages/rust/src/cli/run.rs index 2e23e28bd..4a0b6f2d4 100644 --- a/packages/rust/src/cli/run.rs +++ b/packages/rust/src/cli/run.rs @@ -1177,6 +1177,18 @@ mod tests { assert_eq!(run_cli(vec!["dirsql".into(), "--help".into()]), 0); } + #[test] + fn run_cli_returns_the_query_exit_code() { + assert_eq!( + run_cli(vec!["dirsql".into(), "query".into(), "SELECT 1".into()]), + 0 + ); + assert_eq!( + run_cli(vec!["dirsql".into(), "query".into(), "SELECT FROM".into()]), + 1 + ); + } + #[test] fn context_parses_as_its_own_subcommand_not_as_sql() { let cli = Cli::try_parse_from(["dirsql", "context"]).unwrap(); From 0ed0cc61cb221f92ff173ae2142b4ae453c6fe1e Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Tue, 6 Oct 2026 03:05:42 -0400 Subject: [PATCH 9/9] e2e attestation for claude/1330-cli-exit --- packages/ts/e2e-attestations/claude-1330-cli-exit.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1330-cli-exit.json b/packages/ts/e2e-attestations/claude-1330-cli-exit.json index 695392bb7..ca3dea682 100644 --- a/packages/ts/e2e-attestations/claude-1330-cli-exit.json +++ b/packages/ts/e2e-attestations/claude-1330-cli-exit.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791253418, + "ran_at": 1791270342, "exit_code": 0, - "commit": "5e7dcafd3a0ece8090f803fb49c9ef6a3d9e42a3", + "commit": "5076e5ad0909a14bb7dde96058de54bb3aec05e3", "branch": "claude/1330-cli-exit" }