communique 1.1.0

Editorialized release notes powered by AI
use std::future::Future;
use std::path::Path;
use std::pin::Pin;
use std::sync::Mutex;

use serde_json::json;

use crate::error::Result;
use crate::llm::{
    Conversation, LlmClient, ToolCall, ToolDefinition, ToolResult, TurnResponse, Usage,
};

pub struct TempRepo {
    pub dir: tempfile::TempDir,
}

impl TempRepo {
    pub fn new() -> Self {
        let dir = tempfile::tempdir().unwrap();
        let p = dir.path();
        git(p, &["init"]);
        git(p, &["config", "user.email", "test@test.com"]);
        git(p, &["config", "user.name", "Test"]);
        Self { dir }
    }

    pub fn path(&self) -> &Path {
        self.dir.path()
    }

    pub fn write_file(&self, name: &str, content: &str) {
        let path = self.path().join(name);
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).unwrap();
        }
        std::fs::write(path, content).unwrap();
    }

    pub fn commit(&self, msg: &str) {
        git(self.path(), &["add", "-A"]);
        git(self.path(), &["commit", "-m", msg]);
    }

    pub fn tag(&self, name: &str) {
        git(self.path(), &["tag", name]);
    }
}

fn git(dir: &Path, args: &[&str]) {
    let output = std::process::Command::new("git")
        .args(args)
        .current_dir(dir)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "git {:?} failed: {}",
        args,
        String::from_utf8_lossy(&output.stderr)
    );
}

pub struct MockLlmClient {
    responses: Mutex<Vec<TurnResponse>>,
}

impl MockLlmClient {
    pub fn new(responses: Vec<TurnResponse>) -> Self {
        Self {
            responses: Mutex::new(responses),
        }
    }
}

impl LlmClient for MockLlmClient {
    fn new_conversation(&self, _user_message: &str) -> Conversation {
        Conversation {
            messages: Vec::new(),
        }
    }

    fn append_tool_results(&self, _conversation: &mut Conversation, _results: &[ToolResult]) {}

    fn send_turn<'a>(
        &'a self,
        _system: &'a str,
        _conversation: &'a mut Conversation,
        _tools: &'a [ToolDefinition],
    ) -> Pin<Box<dyn Future<Output = Result<TurnResponse>> + Send + 'a>> {
        let resp = self.responses.lock().unwrap().remove(0);
        Box::pin(async move { Ok(resp) })
    }
}

pub fn submit_tool_call(changelog: &str, title: &str, body: &str) -> ToolCall {
    ToolCall {
        id: "call_1".into(),
        name: "submit_release_notes".into(),
        input: json!({
            "changelog": changelog,
            "release_title": title,
            "release_body": body,
        }),
    }
}

pub fn fake_usage() -> Usage {
    Usage {
        input_tokens: 0,
        output_tokens: 0,
    }
}

pub fn fake_usage_with(input_tokens: u32, output_tokens: u32) -> Usage {
    Usage {
        input_tokens,
        output_tokens,
    }
}