agent-first-psql 0.4.0

Persistent PostgreSQL client for AI agents — SQL-native JSONL in, JSONL out
use crate::types::Output;
use agent_first_data::OutputFormat;
use std::io::Write;
use tokio::sync::mpsc;

pub async fn writer_task(mut rx: mpsc::Receiver<Output>, format: OutputFormat) {
    while let Some(output) = rx.recv().await {
        let rendered = crate::output_fmt::render_output(&output, format);

        let stdout = std::io::stdout();
        let mut out = stdout.lock();
        let _ = out.write_all(rendered.as_bytes());
        if !rendered.ends_with('\n') {
            let _ = out.write_all(b"\n");
        }
        let _ = out.flush();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{RuntimeConfig, SessionConfig};

    #[test]
    fn result_rows_json_keeps_payload_unredacted() {
        let out = Output::ResultRows {
            id: "q1".to_string(),
            rows: vec![serde_json::json!({"api_key_secret":"sk-live-1","n":1})],
            rows_batch_count: 1,
        };
        let rendered = crate::output_fmt::render_output(&out, OutputFormat::Json);
        assert!(rendered.contains("\"api_key_secret\":\"sk-live-1\""));
    }

    #[test]
    fn config_json_remains_redacted() {
        let mut cfg = RuntimeConfig::default();
        cfg.sessions.insert(
            "default".to_string(),
            SessionConfig {
                dsn_secret: Some("postgresql://user:pass@host/db".to_string()),
                ..SessionConfig::default()
            },
        );
        let out = Output::Config(cfg);
        let rendered = crate::output_fmt::render_output(&out, OutputFormat::Json);
        assert!(rendered.contains("\"dsn_secret\":\"***\""));
        assert!(!rendered.contains("postgresql://user:pass@host/db"));
    }
}