use super::parsers::{detect_output_format, extract_by_format};
use super::types::{ExtractionOutput, OutputFormat};
use super::*;
fn extract_llm_output(output: &str, format: Option<OutputFormat>) -> ExtractionOutput {
let trimmed = output.trim();
if trimmed.is_empty() {
return ExtractionOutput::empty();
}
let detected_format = format.unwrap_or_else(|| detect_output_format(trimmed));
if let Some(content) = extract_by_format(trimmed, detected_format) {
return ExtractionOutput::structured(content, detected_format);
}
if format.is_some() {
for try_format in [
OutputFormat::Claude,
OutputFormat::Codex,
OutputFormat::Gemini,
OutputFormat::OpenCode,
] {
if try_format != detected_format {
if let Some(content) = extract_by_format(trimmed, try_format) {
return ExtractionOutput::structured(content, try_format);
}
}
}
}
let cleaned = clean_plain_text(trimmed);
if cleaned.is_empty() {
ExtractionOutput::empty()
} else {
ExtractionOutput::fallback(
cleaned,
"Used plain text fallback - no structured format detected",
)
}
}
#[test]
fn test_detect_claude_format() {
let content = r#"{"type":"system","subtype":"init","session_id":"abc123"}
{"type":"result","result":"test message"}"#;
assert_eq!(detect_output_format(content), OutputFormat::Claude);
}
#[test]
fn test_detect_codex_format() {
let content = r#"{"type":"thread.started","thread_id":"thread_123"}"#;
assert_eq!(detect_output_format(content), OutputFormat::Codex);
}
#[test]
fn test_detect_gemini_format() {
let content = r#"{"type":"init","session_id":"abc","model":"gemini-pro"}"#;
assert_eq!(detect_output_format(content), OutputFormat::Gemini);
}
#[test]
fn test_detect_opencode_format() {
let content = r#"{"type":"step_start","sessionID":"ses_123","part":{}}"#;
assert_eq!(detect_output_format(content), OutputFormat::OpenCode);
}
#[test]
fn test_detect_generic_format() {
let content = "Just some plain text output";
assert_eq!(detect_output_format(content), OutputFormat::Generic);
}
#[test]
fn test_claude_extract_result_event() {
let content = r#"{"type":"system","subtype":"init"}
{"type":"stream_event","event":{"type":"text_delta"}}
{"type":"result","subtype":"success","result":"feat: add new feature"}"#;
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert!(result.was_structured);
assert_eq!(result.content, "feat: add new feature");
}
#[test]
fn test_claude_extract_from_assistant_message() {
let content = r#"{"type":"system","subtype":"init"}
{"type":"assistant","message":{"content":[{"type":"text","text":"fix: resolve bug in parser"}]}}"#;
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert!(result.was_structured);
assert_eq!(result.content, "fix: resolve bug in parser");
}
#[test]
fn test_claude_prefers_result_over_assistant() {
let content = r#"{"type":"assistant","message":{"content":[{"type":"text","text":"assistant text"}]}}
{"type":"result","result":"result text"}"#;
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert_eq!(result.content, "result text");
}
#[test]
fn test_claude_real_world_streaming_output() {
let content = r#"{"type":"system","subtype":"init","cwd":"/test","session_id":"858002c2"}
{"type":"stream_event","event":{"type":"message_start","message":{"model":"claude-opus-4-5-20251101"}}}
{"type":"stream_event","event":{"type":"content_block_start","index":0}}
{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"docs"}}}
{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"(cli)"}}}
{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":": add feature"}}}
{"type":"assistant","message":{"model":"claude-opus-4-5-20251101","content":[{"type":"text","text":"docs(cli): add feature"}]}}
{"type":"stream_event","event":{"type":"message_stop"}}
{"type":"result","subtype":"success","result":"docs(cli): add feature","duration_ms":4688,"total_cost_usd":0.47}"#;
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert!(result.was_structured);
assert_eq!(result.format, OutputFormat::Claude);
assert_eq!(result.content, "docs(cli): add feature");
}
#[test]
fn test_claude_handles_empty_result() {
let content = r#"{"type":"system","subtype":"init"}
{"type":"result","result":""}"#;
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert!(!result.was_structured || result.content.is_empty());
}
#[test]
fn test_claude_filters_thinking_blocks_from_assistant_message() {
let content = r#"{"type":"assistant","message":{"content":[{"type":"thinking","thinking":"Looking at this diff, I can see..."},{"type":"text","text":"feat: add new feature"}]}}"#;
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert!(result.was_structured);
assert_eq!(result.content, "feat: add new feature");
assert!(!result.content.contains("Looking at this diff"));
}
#[test]
fn test_claude_filters_reasoning_blocks_from_assistant_message() {
let content = r#"{"type":"assistant","message":{"content":[{"type":"reasoning","reasoning":"Let me analyze this..."},{"type":"text","text":"fix: resolve parsing issue"}]}}"#;
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert!(result.was_structured);
assert_eq!(result.content, "fix: resolve parsing issue");
assert!(!result.content.contains("analyze"));
}
#[test]
fn test_claude_filters_multiple_thinking_and_text_blocks() {
let content = r#"{"type":"assistant","message":{"content":[{"type":"thinking","thinking":"First thought..."},{"type":"text","text":"docs: update"},{"type":"thinking","thinking":"Second thought..."},{"type":"text","text":" documentation"}]}}"#;
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert!(result.was_structured);
assert_eq!(result.content, " documentation");
assert!(!result.content.contains("First thought"));
assert!(!result.content.contains("Second thought"));
}
#[test]
fn test_claude_result_field_with_thinking_blocks_present() {
let content = r#"{"type":"assistant","message":{"content":[{"type":"thinking","thinking":"Analyzing the changes..."}]}}
{"type":"result","result":"chore: improve performance"}"#;
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert!(result.was_structured);
assert_eq!(result.content, "chore: improve performance");
assert!(!result.content.contains("Analyzing"));
}
#[test]
fn test_claude_only_thinking_blocks_falls_back_to_empty() {
let content = r#"{"type":"assistant","message":{"content":[{"type":"thinking","thinking":"This is only thinking content"}]}}"#;
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert!(result.content.is_empty() || !result.was_structured);
}
#[test]
fn test_codex_extract_agent_message() {
let content = r#"{"type":"thread.started","thread_id":"thread_abc"}
{"type":"turn.started"}
{"type":"item.started","item":{"type":"agent_message"}}
{"type":"item.completed","item":{"type":"agent_message","text":"chore: update dependencies"}}
{"type":"turn.completed","usage":{"input_tokens":100,"output_tokens":50}}"#;
let result = extract_llm_output(content, Some(OutputFormat::Codex));
assert!(result.was_structured);
assert_eq!(result.content, "chore: update dependencies");
}
#[test]
fn test_codex_uses_last_message() {
let content = r#"{"type":"item.completed","item":{"type":"agent_message","text":"first message"}}
{"type":"item.completed","item":{"type":"agent_message","text":"final message"}}"#;
let result = extract_llm_output(content, Some(OutputFormat::Codex));
assert_eq!(result.content, "final message");
}
#[test]
fn test_codex_ignores_non_agent_messages() {
let content = r#"{"type":"item.completed","item":{"type":"reasoning","text":"thinking..."}}
{"type":"item.completed","item":{"type":"agent_message","text":"actual output"}}"#;
let result = extract_llm_output(content, Some(OutputFormat::Codex));
assert_eq!(result.content, "actual output");
}
#[test]
fn test_gemini_extract_assistant_content() {
let content = r#"{"type":"init","session_id":"abc","model":"gemini-pro"}
{"type":"message","role":"assistant","content":"feat: add feature"}"#;
let result = extract_llm_output(content, Some(OutputFormat::Gemini));
assert!(result.was_structured);
assert_eq!(result.content, "feat: add feature");
}
#[test]
fn test_gemini_uses_last_message() {
let content = r#"{"type":"message","role":"assistant","content":"first"}
{"type":"message","role":"assistant","content":"final"}"#;
let result = extract_llm_output(content, Some(OutputFormat::Gemini));
assert_eq!(result.content, "final");
}
#[test]
fn test_gemini_handles_delta_accumulation() {
let content = r#"{"type":"message","role":"assistant","content":"feat","delta":true}
{"type":"message","role":"assistant","content":": ","delta":true}
{"type":"message","role":"assistant","content":"add","delta":true}
{"type":"message","role":"assistant","content":" feature","delta":true}"#;
let result = extract_llm_output(content, Some(OutputFormat::Gemini));
assert_eq!(result.content, "feat: add feature");
}
#[test]
fn test_opencode_extract_text_parts() {
let content = r#"{"type":"text","part":{"text":"feat:"}}
{"type":"text","part":{"text":" add feature"}}"#;
let result = extract_llm_output(content, Some(OutputFormat::OpenCode));
assert!(result.was_structured);
assert_eq!(result.content, "feat: add feature");
}
#[test]
fn test_opencode_handles_mixed_content() {
let content = r#"{"type":"step_start","step":"thinking"}
{"type":"text","part":{"text":"chore:"}}
{"type":"step_finish","step":"thinking"}
{"type":"text","part":{"text":" update code"}}"#;
let result = extract_llm_output(content, Some(OutputFormat::OpenCode));
assert_eq!(result.content, "chore: update code");
}
#[test]
fn test_fallback_plain_text() {
let content = "feat: some change";
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert_eq!(result.content, "feat: some change");
}
#[test]
fn test_fallback_removes_markdown_fences() {
let content = "```\nfeat: add feature\n```";
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert_eq!(result.content, "feat: add feature");
}
#[test]
fn test_fallback_removes_prefixes() {
let content = "Commit message: feat: add feature";
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert_eq!(result.content, "feat: add feature");
}
#[test]
fn test_fallback_with_ai_analysis() {
let content = "Looking at this diff, I can see changes.\n\nfeat: add feature";
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert_eq!(result.content, "feat: add feature");
}
#[test]
fn test_empty_input_returns_empty() {
let result = extract_llm_output("", Some(OutputFormat::Claude));
assert!(result.content.is_empty());
}
#[test]
fn test_whitespace_only_returns_empty() {
let result = extract_llm_output(" \n \n ", Some(OutputFormat::Claude));
assert!(result.content.is_empty());
}
#[test]
fn test_auto_detect_claude() {
let content = r#"{"type":"system","session_id":"abc"}
{"type":"result","result":"test"}"#;
let result = extract_llm_output(content, None);
assert_eq!(result.format, OutputFormat::Claude);
}
#[test]
fn test_auto_detect_codex() {
let content = r#"{"type":"thread.started","thread_id":"123"}
{"type":"item.completed","item":{"type":"agent_message","text":"feat: add feature"}}"#;
let result = extract_llm_output(content, None);
assert_eq!(result.format, OutputFormat::Codex);
assert_eq!(result.content, "feat: add feature");
}
#[test]
fn test_auto_detect_gemini() {
let content = r#"{"type":"init","model":"gemini-pro"}
{"type":"message","role":"assistant","content":"feat: add feature"}"#;
let result = extract_llm_output(content, None);
assert_eq!(result.format, OutputFormat::Gemini);
assert_eq!(result.content, "feat: add feature");
}
#[test]
fn test_auto_detect_generic_plain_text() {
let content = "Just plain text\nNo JSON here";
let result = extract_llm_output(content, None);
assert_eq!(result.format, OutputFormat::Generic);
}
#[test]
fn test_malformed_json_falls_back_gracefully() {
let content = r#"{"type":"result","result":incomplete json}
feat: actual message"#;
let result = extract_llm_output(content, Some(OutputFormat::Claude));
assert!(result.content.contains("feat: actual message"));
}
#[test]
fn test_unknown_format_tries_all_parsers() {
let content = r#"{"type":"unknown","result":"feat: add feature"}"#;
let result = extract_llm_output(content, Some(OutputFormat::Codex));
assert!(!result.content.is_empty() || result.warning.is_some());
}