ccc 0.2.0

Unified Rust library and CLI for invoking coding-agent CLIs with shared parsing, planning, and transcript utilities
Documentation
use call_coding_clis::{
    parse_tokens_with_config, sugar, CccConfig, Client, CompletedRun, Event, OutputMode, Request,
    Run, Runner, RunnerKind, Transcript,
};

#[test]
fn library_api_tests_request_builder_sets_expected_fields() {
    let request = Request::new("review this patch")
        .with_runner(RunnerKind::Codex)
        .with_provider("openai")
        .with_model("gpt-5.4-mini")
        .with_output_mode(OutputMode::StreamFormatted);

    assert_eq!(request.prompt(), "review this patch");
    assert_eq!(request.runner(), Some(RunnerKind::Codex));
    assert_eq!(request.provider(), Some("openai"));
    assert_eq!(request.model(), Some("gpt-5.4-mini"));
    assert_eq!(request.output_mode(), Some(OutputMode::StreamFormatted));
}

#[test]
fn library_api_tests_sugar_parse_tokens_supports_provider_model_sugar() {
    let parsed = sugar::parse_tokens(["c", ":openai:gpt-5.4-mini", "debug this"])
        .expect("parse should succeed");

    assert_eq!(parsed.request.runner(), Some(RunnerKind::Codex));
    assert_eq!(parsed.request.provider(), Some("openai"));
    assert_eq!(parsed.request.model(), Some("gpt-5.4-mini"));
}

#[test]
fn library_api_tests_parse_tokens_with_config_uses_explicit_defaults() {
    let config = CccConfig {
        default_runner: "codex".to_string(),
        ..CccConfig::default()
    };
    let parsed = parse_tokens_with_config(["debug this"], &config).expect("parse should succeed");
    let plan = Client::new()
        .with_config(config)
        .plan(&parsed.request)
        .expect("plan should resolve");

    assert_eq!(plan.runner(), RunnerKind::Codex);
}

#[test]
fn library_api_tests_client_plan_resolves_to_non_empty_command_spec() {
    let client = Client::new();
    let request = Request::new("explain this module").with_runner(RunnerKind::OpenCode);
    let plan = client.plan(&request).expect("plan should resolve");

    assert!(!plan.command_spec().argv.is_empty());
    assert_eq!(plan.runner(), RunnerKind::OpenCode);
}

#[test]
fn library_api_tests_client_plan_preserves_provider_only_request() {
    let client = Client::new();
    let request = Request::new("explain this module")
        .with_runner(RunnerKind::OpenCode)
        .with_provider("openai");
    let plan = client.plan(&request).expect("plan should resolve");

    assert_eq!(
        plan.command_spec()
            .env
            .get("CCC_PROVIDER")
            .map(|s| s.as_str()),
        Some("openai")
    );
}

#[test]
fn library_api_tests_client_with_config_uses_injected_defaults() {
    let client = Client::new().with_config(CccConfig {
        default_runner: "codex".to_string(),
        ..CccConfig::default()
    });
    let request = Request::new("explain this module");
    let plan = client.plan(&request).expect("plan should resolve");

    assert_eq!(plan.runner(), RunnerKind::Codex);
    assert_eq!(
        plan.command_spec().argv.first().map(|s| s.as_str()),
        Some("codex")
    );
}

#[test]
fn library_api_tests_typed_transcript_and_run_surface_are_available() {
    let transcript = Transcript {
        events: vec![Event::Text("hello".into())],
        final_text: "hello".into(),
        session_id: Some("sess-abc".into()),
        usage: Default::default(),
        error: None,
        unknown_json_lines: vec!["{\"mystery\":true}".into()],
    };

    assert_eq!(transcript.final_text, "hello");
    assert_eq!(transcript.unknown_json_lines.len(), 1);
}

#[test]
fn library_api_tests_client_run_exposes_parsed_output_for_formatted_modes() {
    let client =
        Client::new().with_runtime_runner(Runner::with_executor(Box::new(|spec| CompletedRun {
            argv: spec.argv,
            exit_code: 0,
            stdout: "{\"response\":\"hello\"}\n".into(),
            stderr: String::new(),
        })));
    let request = Request::new("hello")
        .with_runner(RunnerKind::OpenCode)
        .with_output_mode(OutputMode::Formatted);

    let run: Run = client.run(&request).expect("run should succeed");

    assert_eq!(run.final_text(), "hello");
    assert!(run.parsed_output().is_some());
}

#[test]
fn library_api_tests_client_run_returns_tool_failed_for_non_zero_exit() {
    let client =
        Client::new().with_runtime_runner(Runner::with_executor(Box::new(|spec| CompletedRun {
            argv: spec.argv,
            exit_code: 7,
            stdout: String::new(),
            stderr: "runner failed".into(),
        })));
    let request = Request::new("hello")
        .with_runner(RunnerKind::OpenCode)
        .with_output_mode(OutputMode::Text);

    let error = client.run(&request).expect_err("run should fail");

    match error {
        call_coding_clis::Error::ToolFailed { exit_code, stderr } => {
            assert_eq!(exit_code, 7);
            assert_eq!(stderr, "runner failed");
        }
        other => panic!("expected ToolFailed error, got {other}"),
    }
}