warble-claude-code 0.19.0

Warble back-end for the Claude Code CLI target: IR → static agent files + reference renderer + capability manifest.
Documentation
//! Unsupported IR versions are rejected before this dispatcher writes any output.

use warble_claude_code::ir::{WarbleIr, SUPPORTED_IR_VERSION};
use warble_claude_code::{emit_claude_code, emit_codex_interactive};

const VERSION_MISMATCH_FIXTURE: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../conformance-fixtures/ir-version-mismatch.json"
);
const COMPOSITION_FIXTURE: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../conformance-fixtures/component-composition-unsupported.json"
);
const CLOSURE_FIXTURE: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../conformance-fixtures/component-call-closure.json"
);

fn load_ir(relative: &str) -> WarbleIr {
    let path = format!("{}/{relative}", env!("CARGO_MANIFEST_DIR"));
    let raw = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
    serde_json::from_str(&raw).unwrap_or_else(|e| panic!("parse {path}: {e}"))
}

#[test]
fn emit_rejects_an_out_of_range_ir_version_explicitly() {
    let mut ir = load_ir("../../examples/analysis-agent/ir.golden.json");
    ir.warble_ir_version = "0.2".to_string();

    let tmp = tempfile::tempdir().expect("tempdir");
    let err = emit_claude_code(
        &ir,
        tmp.path(),
        "claude-code:headless",
        warble_claude_code::DEFAULT_RENDER_FLAVOR,
    )
    .expect_err("an out-of-range IR version must not silently emit");

    let message = err.to_string();
    assert!(
        message.contains("0.2"),
        "error should name the rejected version, got: {message}"
    );
    assert!(
        message.contains(SUPPORTED_IR_VERSION),
        "error should name the supported version, got: {message}"
    );
    assert!(
        !tmp.path().join("RUN.md").exists(),
        "a rejected version must not have written any output"
    );
}

#[test]
fn emit_rejects_the_shared_cross_back_end_version_mismatch_fixture() {
    let raw = std::fs::read_to_string(VERSION_MISMATCH_FIXTURE)
        .unwrap_or_else(|e| panic!("read {VERSION_MISMATCH_FIXTURE}: {e}"));
    let fixture: serde_json::Value = serde_json::from_str(&raw).expect("fixture is valid JSON");
    let ir: WarbleIr =
        serde_json::from_value(fixture["ir"].clone()).expect("fixture ir deserializes");
    let expected: Vec<&str> = fixture["expected_error_contains"]
        .as_array()
        .expect("expected_error_contains is an array")
        .iter()
        .map(|v| {
            v.as_str()
                .expect("expected_error_contains entries are strings")
        })
        .collect();

    let tmp = tempfile::tempdir().expect("tempdir");
    let err = emit_claude_code(
        &ir,
        tmp.path(),
        "claude-code:headless",
        warble_claude_code::DEFAULT_RENDER_FLAVOR,
    )
    .expect_err("the shared fixture's out-of-range version must not silently emit");

    let message = err.to_string();
    for substring in expected {
        assert!(
            message.contains(substring),
            "error should contain '{substring}', got: {message}"
        );
    }
}

fn load_composition_fixture() -> (WarbleIr, serde_json::Value) {
    let raw = std::fs::read_to_string(COMPOSITION_FIXTURE)
        .unwrap_or_else(|e| panic!("read {COMPOSITION_FIXTURE}: {e}"));
    let fixture: serde_json::Value = serde_json::from_str(&raw).expect("fixture is valid JSON");
    let ir = serde_json::from_value(fixture["ir"].clone()).expect("fixture ir deserializes");
    (ir, fixture)
}

fn assert_expected_fragments(message: &str, fixture: &serde_json::Value, key: &str) {
    for substring in fixture[key]
        .as_array()
        .unwrap_or_else(|| panic!("{key} is an array"))
        .iter()
        .map(|value| value.as_str().expect("expected fragments are strings"))
    {
        assert!(
            message.contains(substring),
            "error should contain '{substring}', got: {message}"
        );
    }
}

fn assert_closure_wall_fragments(message: &str) {
    let raw = std::fs::read_to_string(CLOSURE_FIXTURE)
        .unwrap_or_else(|e| panic!("read {CLOSURE_FIXTURE}: {e}"));
    let fixture: serde_json::Value = serde_json::from_str(&raw).expect("fixture is valid JSON");
    assert_expected_fragments(message, &fixture["unsupported_target"], "error_contains");
}

#[test]
fn every_claude_code_file_target_rejects_component_calls_before_writing_output() {
    let (ir, fixture) = load_composition_fixture();
    assert_eq!(
        ir.components[0].llm_calls[0].component_calls[0].alias,
        "answer"
    );
    assert!(!ir.components[1].entrypoint);

    for target in ["claude-code:headless", "claude-code:interactive"] {
        let tmp = tempfile::tempdir().expect("tempdir");
        let err = emit_claude_code(
            &ir,
            tmp.path(),
            target,
            warble_claude_code::DEFAULT_RENDER_FLAVOR,
        )
        .expect_err("an unsupported component call must wall-hit");
        assert_expected_fragments(&err.to_string(), &fixture, "expected_call_error_contains");
        assert_closure_wall_fragments(&err.to_string());
        assert!(
            std::fs::read_dir(tmp.path())
                .expect("temporary output root exists")
                .next()
                .is_none(),
            "target {target} must fail before writing any executable output"
        );
    }
}

#[test]
fn codex_interactive_rejects_component_calls_before_writing_output() {
    let (ir, fixture) = load_composition_fixture();
    let tmp = tempfile::tempdir().expect("tempdir");
    let err = emit_codex_interactive(&ir, tmp.path(), None, None, None)
        .expect_err("an unsupported component call must wall-hit");
    assert_expected_fragments(&err.to_string(), &fixture, "expected_call_error_contains");
    assert_closure_wall_fragments(&err.to_string());
    assert!(
        std::fs::read_dir(tmp.path())
            .expect("temporary output root exists")
            .next()
            .is_none(),
        "codex interactive must fail before writing discovery artifacts"
    );
}

#[test]
fn file_target_omits_an_unreachable_callee_only_mount() {
    let (mut ir, _) = load_composition_fixture();
    ir.components[0].llm_calls[0].component_calls.clear();
    ir.components[0]
        .required_capabilities
        .retain(|capability| capability != "component_invocation");
    let tmp = tempfile::tempdir().expect("tempdir");
    emit_claude_code(
        &ir,
        tmp.path(),
        "claude-code:headless",
        warble_claude_code::DEFAULT_RENDER_FLAVOR,
    )
    .expect("an unreachable internal mount does not block advertised entries");
    let agents = tmp.path().join(".claude/agents");
    assert!(agents.join("caller.md").is_file());
    assert!(!agents.join("callee.md").exists());
}