//! Contract tests for every CLI command's `execute()` function. //! //! ## Why these tests exist //! //! The `commands/*.rs` files call `tokio_postgres` queries against //! `pgtrickle.*` SQL functions. Previously, the only tests were CLI //! argument-parsing tests (`cli.rs`), which never touched a database. //! That meant bugs like wrong column names or missing functions went //! undetected until the binary was run against a real database. //! //! Each test here: //! 1. Spins up a fresh Postgres 18 container via Testcontainers. //! 2. Installs minimal stub functions whose signatures match the real //! extension (correct column names and types). //! 3. Calls the command's `execute()` with realistic arguments. //! 4. Asserts the call succeeds (or, for 0.14.0-only features, fails //! with the expected "requires >= 0.14.0" message). //! //! If a stub signature drifts from what the command file reads — wrong //! column name, wrong type, wrong positional index — the test fails here //! rather than at runtime against a production database. use crate::cli::OutputFormat; use crate::commands; use crate::test_db::PgtStubDb; // ── list ────────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_list_executes() { let db = PgtStubDb::new().await; let args = commands::list::ListArgs { format: OutputFormat::Table, }; commands::list::execute(&db.client, &args) .await .expect("list: execute() failed"); } // ── status ──────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_status_executes() { let db = PgtStubDb::new().await; let args = commands::status::StatusArgs { name: "test_table".into(), format: OutputFormat::Table, }; // Stub returns a row with pgt_name='test_table', so this should succeed commands::status::execute(&db.client, &args) .await .expect("status: execute() failed"); } #[tokio::test] async fn test_cmd_status_not_found_returns_error() { let db = PgtStubDb::new().await; let args = commands::status::StatusArgs { name: "nonexistent_table".into(), format: OutputFormat::Table, }; // Stub only returns 'test_table'; a different name yields empty result → NotFound let err = commands::status::execute(&db.client, &args) .await .expect_err("status: should return NotFound for unknown table"); assert!( matches!(err, crate::error::CliError::NotFound(_)), "expected NotFound, got: {err}" ); } // ── cdc ─────────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_cdc_executes() { let db = PgtStubDb::new().await; let args = commands::cdc::CdcArgs { format: OutputFormat::Table, }; commands::cdc::execute(&db.client, &args) .await .expect("cdc: execute() failed"); } // ── graph ───────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_graph_executes() { let db = PgtStubDb::new().await; let args = commands::graph::GraphArgs { format: OutputFormat::Table, }; commands::graph::execute(&db.client, &args) .await .expect("graph: execute() failed"); } // ── health ──────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_health_executes() { let db = PgtStubDb::new().await; let args = commands::health::HealthArgs { format: OutputFormat::Table, }; commands::health::execute(&db.client, &args) .await .expect("health: execute() failed"); } // ── config ──────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_config_list_executes() { let db = PgtStubDb::new().await; // No pg_trickle GUCs in a vanilla Postgres — expect empty table, not an error let args = commands::config::ConfigArgs { set: None, format: OutputFormat::Table, }; commands::config::execute(&db.client, &args) .await .expect("config list: execute() failed"); } // ── fuse ────────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_fuse_executes() { let db = PgtStubDb::new().await; let args = commands::fuse::FuseArgs { format: OutputFormat::Table, }; commands::fuse::execute(&db.client, &args) .await .expect("fuse: execute() failed"); } // ── watermarks ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_watermarks_executes() { let db = PgtStubDb::new().await; let args = commands::watermarks::WatermarksArgs { format: OutputFormat::Table, }; commands::watermarks::execute(&db.client, &args) .await .expect("watermarks: execute() failed"); } // ── workers ─────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_workers_executes() { let db = PgtStubDb::new().await; let args = commands::workers::WorkersArgs { format: OutputFormat::Table, }; commands::workers::execute(&db.client, &args) .await .expect("workers: execute() failed"); } // ── explain ─────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_explain_executes() { let db = PgtStubDb::new().await; let args = commands::explain::ExplainArgs { name: "public.test_table".into(), analyze: false, format: OutputFormat::Table, }; commands::explain::execute(&db.client, &args) .await .expect("explain: execute() failed"); } #[tokio::test] async fn test_cmd_explain_analyze_executes() { let db = PgtStubDb::new().await; let args = commands::explain::ExplainArgs { name: "public.test_table".into(), analyze: true, format: OutputFormat::Table, }; commands::explain::execute(&db.client, &args) .await .expect("explain --analyze: execute() failed"); } // ── diag ────────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_diag_all_executes() { let db = PgtStubDb::new().await; let args = commands::diag::DiagArgs { name: None, format: OutputFormat::Table, }; commands::diag::execute(&db.client, &args) .await .expect("diag (all): execute() failed"); } #[tokio::test] async fn test_cmd_diag_named_executes() { let db = PgtStubDb::new().await; let args = commands::diag::DiagArgs { name: Some("test_table".into()), format: OutputFormat::Table, }; commands::diag::execute(&db.client, &args) .await .expect("diag (named): execute() failed"); } // ── export ──────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_export_executes() { let db = PgtStubDb::new().await; let args = commands::export::ExportArgs { name: "test_table".into(), }; commands::export::execute(&db.client, &args) .await .expect("export: execute() failed"); } // ── create ──────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_create_executes() { let db = PgtStubDb::new().await; let args = commands::create::CreateArgs { name: "my_stream".into(), query: "SELECT 1 AS id".into(), schedule: None, mode: None, no_initialize: false, }; commands::create::execute(&db.client, &args) .await .expect("create: execute() failed"); } // ── drop ────────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_drop_executes() { let db = PgtStubDb::new().await; let args = commands::drop::DropArgs { name: "my_stream".into(), }; commands::drop::execute(&db.client, &args) .await .expect("drop: execute() failed"); } // ── refresh ─────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_refresh_all_executes() { let db = PgtStubDb::new().await; let args = commands::refresh::RefreshArgs { name: None, all: true, }; commands::refresh::execute(&db.client, &args) .await .expect("refresh --all: execute() failed"); } #[tokio::test] async fn test_cmd_refresh_named_executes() { let db = PgtStubDb::new().await; let args = commands::refresh::RefreshArgs { name: Some("test_table".into()), all: false, }; commands::refresh::execute(&db.client, &args) .await .expect("refresh : execute() failed"); } // ── alter ───────────────────────────────────────────────────────────────── #[tokio::test] async fn test_cmd_alter_mode_executes() { let db = PgtStubDb::new().await; let args = commands::alter::AlterArgs { name: "my_stream".into(), mode: Some("full".into()), schedule: None, tier: None, status: None, query: None, }; commands::alter::execute(&db.client, &args) .await .expect("alter --mode: execute() failed"); } #[tokio::test] async fn test_cmd_alter_no_flags_returns_error() { let db = PgtStubDb::new().await; // Calling alter with no flags is an error (nothing to change) let args = commands::alter::AlterArgs { name: "my_stream".into(), mode: None, schedule: None, tier: None, status: None, query: None, }; commands::alter::execute(&db.client, &args) .await .expect_err("alter with no flags should return an error"); } // ── output formats ──────────────────────────────────────────────────────── // Verify JSON and CSV output paths don't panic #[tokio::test] async fn test_cmd_list_json_format() { let db = PgtStubDb::new().await; let args = commands::list::ListArgs { format: OutputFormat::Json, }; commands::list::execute(&db.client, &args) .await .expect("list --format json: execute() failed"); } #[tokio::test] async fn test_cmd_fuse_csv_format() { let db = PgtStubDb::new().await; let args = commands::fuse::FuseArgs { format: OutputFormat::Csv, }; commands::fuse::execute(&db.client, &args) .await .expect("fuse --format csv: execute() failed"); } #[tokio::test] async fn test_cmd_workers_json_format() { let db = PgtStubDb::new().await; let args = commands::workers::WorkersArgs { format: OutputFormat::Json, }; commands::workers::execute(&db.client, &args) .await .expect("workers --format json: execute() failed"); } // ── execute_action — TUI interactive poller path ────────────────────────── // // These tests exercise crate::poller::execute_action(), which is the code // path the TUI uses for write actions and on-demand enrichment fetches. // The CLI command tests above exercise a *different* code path // (commands/*.rs) and do not catch bugs in the poller action handling. #[tokio::test] async fn test_action_fetch_delta_sql_with_schema_returns_sql() { let db = PgtStubDb::new().await; let result = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::FetchDeltaSql("public.test_table".into()), ) .await; assert!(result.success, "FetchDeltaSql with schema should succeed"); assert!( !result.message.is_empty(), "FetchDeltaSql should return non-empty SQL" ); assert!( result.message.contains("Seq Scan"), "FetchDeltaSql should return plan text, got: {}", result.message ); } #[tokio::test] async fn test_action_fetch_delta_sql_bare_name_returns_empty() { // The real explain_delta() requires a schema-qualified name. // The stub enforces this: bare names return empty, not an error. // The TUI must qualify names before sending FetchDeltaSql. let db = PgtStubDb::new().await; let result = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::FetchDeltaSql("test_table".into()), ) .await; assert!( result.success, "FetchDeltaSql with bare name should not error (returns empty rows)" ); assert!( result.message.is_empty(), "FetchDeltaSql with bare name should return empty string (no rows), got: {}", result.message ); } #[tokio::test] async fn test_action_fetch_ddl_with_schema_returns_ddl() { let db = PgtStubDb::new().await; let result = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::FetchDdl("public.test_table".into()), ) .await; assert!( result.success, "FetchDdl should succeed, got: {}", result.message ); assert!( !result.message.is_empty(), "FetchDdl should return DDL text" ); } #[tokio::test] async fn test_action_refresh_table_succeeds() { let db = PgtStubDb::new().await; let result = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::RefreshTable("test_table".into()), ) .await; assert!(result.success, "RefreshTable should succeed"); } #[tokio::test] async fn test_action_refresh_all_succeeds() { let db = PgtStubDb::new().await; let result = crate::poller::execute_action(&db.client, &crate::state::ActionRequest::RefreshAll).await; assert!(result.success, "RefreshAll should succeed"); } #[tokio::test] async fn test_action_pause_resume_table_succeeds() { let db = PgtStubDb::new().await; let pause = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::PauseTable("test_table".into()), ) .await; assert!(pause.success, "PauseTable should succeed"); let resume = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::ResumeTable("test_table".into()), ) .await; assert!(resume.success, "ResumeTable should succeed"); } #[tokio::test] async fn test_action_reset_fuse_succeeds() { let db = PgtStubDb::new().await; let result = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::ResetFuse("test_table".into(), "rearm".into()), ) .await; assert!(result.success, "ResetFuse should succeed"); } #[tokio::test] async fn test_action_validate_query_returns_results() { let db = PgtStubDb::new().await; let result = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::ValidateQuery("SELECT 1".into()), ) .await; assert!(result.success, "ValidateQuery should succeed"); assert!( result.message.contains("OK"), "ValidateQuery should return check results, got: {}", result.message ); } #[tokio::test] async fn test_action_fetch_diagnose_errors_with_schema_returns_data() { let db = PgtStubDb::new().await; let result = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::FetchDiagnoseErrors("public.test_table".into()), ) .await; assert!( result.success, "FetchDiagnoseErrors with schema should succeed" ); assert!( result.message.contains("division by zero"), "FetchDiagnoseErrors should return error data, got: {}", result.message ); } #[tokio::test] async fn test_action_fetch_explain_mode_with_schema_returns_data() { let db = PgtStubDb::new().await; let result = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::FetchExplainMode("public.test_table".into()), ) .await; assert!( result.success, "FetchExplainMode with schema should succeed" ); assert!( result.message.contains("DIFFERENTIAL"), "FetchExplainMode should return mode data, got: {}", result.message ); } #[tokio::test] async fn test_action_fetch_sources_with_schema_returns_data() { let db = PgtStubDb::new().await; let result = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::FetchSources("public.test_table".into()), ) .await; assert!(result.success, "FetchSources with schema should succeed"); assert!( result.message.contains("public.source"), "FetchSources should return source data, got: {}", result.message ); } #[tokio::test] async fn test_action_fetch_refresh_history_with_schema_returns_data() { let db = PgtStubDb::new().await; let result = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::FetchRefreshHistory("public.test_table".into()), ) .await; assert!( result.success, "FetchRefreshHistory with schema should succeed" ); assert!( result.message.contains("SUCCESS"), "FetchRefreshHistory should return history data, got: {}", result.message ); } #[tokio::test] async fn test_action_fetch_auxiliary_columns_with_schema_returns_data() { let db = PgtStubDb::new().await; let result = crate::poller::execute_action( &db.client, &crate::state::ActionRequest::FetchAuxiliaryColumns("public.test_table".into()), ) .await; assert!( result.success, "FetchAuxiliaryColumns with schema should succeed" ); assert!( result.message.contains("_pgt_id"), "FetchAuxiliaryColumns should return column data, got: {}", result.message ); }