Codex Handbook
cloud-tasks-mock-client/src/mock.rs 267 lines
use chrono::Utc;use codex_cloud_tasks_client::ApplyOutcome;use codex_cloud_tasks_client::ApplyStatus;use codex_cloud_tasks_client::AttemptStatus;use codex_cloud_tasks_client::CloudBackend;use codex_cloud_tasks_client::CloudBackendFuture;use codex_cloud_tasks_client::CloudTaskError;use codex_cloud_tasks_client::CreatedTask;use codex_cloud_tasks_client::DiffSummary;use codex_cloud_tasks_client::Result;use codex_cloud_tasks_client::TaskId;use codex_cloud_tasks_client::TaskListPage;use codex_cloud_tasks_client::TaskStatus;use codex_cloud_tasks_client::TaskSummary;use codex_cloud_tasks_client::TaskText;use codex_cloud_tasks_client::TurnAttempt;#[derive(Clone, Default)]pub struct MockClient;impl MockClient {    async fn list_tasks(        &self,        _env: Option<&str>,        _limit: Option<i64>,        _cursor: Option<&str>,    ) -> Result<TaskListPage> {        // Slightly vary content by env to aid tests that rely on the mock        let rows = match _env {            Some("env-A") => vec![("T-2000", "A: First", TaskStatus::Ready)],            Some("env-B") => vec![                ("T-3000", "B: One", TaskStatus::Ready),                ("T-3001", "B: Two", TaskStatus::Pending),            ],            _ => vec![                ("T-1000", "Update README formatting", TaskStatus::Ready),                ("T-1001", "Fix clippy warnings in core", TaskStatus::Pending),                ("T-1002", "Add contributing guide", TaskStatus::Ready),            ],        };        let environment_id = _env.map(str::to_string);        let environment_label = match _env {            Some("env-A") => Some("Env A".to_string()),            Some("env-B") => Some("Env B".to_string()),            Some(other) => Some(other.to_string()),            None => Some("Global".to_string()),        };        let mut out = Vec::new();        for (id_str, title, status) in rows {            let id = TaskId(id_str.to_string());            let diff = mock_diff_for(&id);            let (a, d) = count_from_unified(&diff);            out.push(TaskSummary {                id,                title: title.to_string(),                status,                updated_at: Utc::now(),                environment_id: environment_id.clone(),                environment_label: environment_label.clone(),                summary: DiffSummary {                    files_changed: 1,                    lines_added: a,                    lines_removed: d,                },                is_review: false,                attempt_total: Some(if id_str == "T-1000" { 2 } else { 1 }),            });        }        Ok(TaskListPage {            tasks: out,            cursor: None,        })    }    async fn get_task_summary(&self, id: TaskId) -> Result<TaskSummary> {        let tasks = self            .list_tasks(/*env*/ None, /*limit*/ None, /*cursor*/ None)            .await?            .tasks;        tasks            .into_iter()            .find(|t| t.id == id)            .ok_or_else(|| CloudTaskError::Msg(format!("Task {} not found (mock)", id.0)))    }    async fn get_task_diff(&self, id: TaskId) -> Result<Option<String>> {        Ok(Some(mock_diff_for(&id)))    }    async fn get_task_messages(&self, _id: TaskId) -> Result<Vec<String>> {        Ok(vec![            "Mock assistant output: this task contains no diff.".to_string(),        ])    }    async fn get_task_text(&self, _id: TaskId) -> Result<TaskText> {        Ok(TaskText {            prompt: Some("Why is there no diff?".to_string()),            messages: vec!["Mock assistant output: this task contains no diff.".to_string()],            turn_id: Some("mock-turn".to_string()),            sibling_turn_ids: Vec::new(),            attempt_placement: Some(0),            attempt_status: AttemptStatus::Completed,        })    }    async fn apply_task(&self, id: TaskId, _diff_override: Option<String>) -> Result<ApplyOutcome> {        Ok(ApplyOutcome {            applied: true,            status: ApplyStatus::Success,            message: format!("Applied task {} locally (mock)", id.0),            skipped_paths: Vec::new(),            conflict_paths: Vec::new(),        })    }    async fn apply_task_preflight(        &self,        id: TaskId,        _diff_override: Option<String>,    ) -> Result<ApplyOutcome> {        Ok(ApplyOutcome {            applied: false,            status: ApplyStatus::Success,            message: format!("Preflight passed for task {} (mock)", id.0),            skipped_paths: Vec::new(),            conflict_paths: Vec::new(),        })    }    async fn list_sibling_attempts(        &self,        task: TaskId,        _turn_id: String,    ) -> Result<Vec<TurnAttempt>> {        if task.0 == "T-1000" {            return Ok(vec![TurnAttempt {                turn_id: "T-1000-attempt-2".to_string(),                attempt_placement: Some(1),                created_at: Some(Utc::now()),                status: AttemptStatus::Completed,                diff: Some(mock_diff_for(&task)),                messages: vec!["Mock alternate attempt".to_string()],            }]);        }        Ok(Vec::new())    }    async fn create_task(        &self,        env_id: &str,        prompt: &str,        git_ref: &str,        qa_mode: bool,        best_of_n: usize,    ) -> Result<CreatedTask> {        let _ = (env_id, prompt, git_ref, qa_mode, best_of_n);        let id = format!("task_local_{}", chrono::Utc::now().timestamp_millis());        Ok(CreatedTask { id: TaskId(id) })    }}impl CloudBackend for MockClient {    fn list_tasks<'a>(        &'a self,        env: Option<&'a str>,        limit: Option<i64>,        cursor: Option<&'a str>,    ) -> CloudBackendFuture<'a, TaskListPage> {        Box::pin(MockClient::list_tasks(self, env, limit, cursor))    }    fn get_task_summary(&self, id: TaskId) -> CloudBackendFuture<'_, TaskSummary> {        Box::pin(MockClient::get_task_summary(self, id))    }    fn get_task_diff(&self, id: TaskId) -> CloudBackendFuture<'_, Option<String>> {        Box::pin(MockClient::get_task_diff(self, id))    }    fn get_task_messages(&self, id: TaskId) -> CloudBackendFuture<'_, Vec<String>> {        Box::pin(MockClient::get_task_messages(self, id))    }    fn get_task_text(&self, id: TaskId) -> CloudBackendFuture<'_, TaskText> {        Box::pin(MockClient::get_task_text(self, id))    }    fn apply_task(        &self,        id: TaskId,        diff_override: Option<String>,    ) -> CloudBackendFuture<'_, ApplyOutcome> {        Box::pin(MockClient::apply_task(self, id, diff_override))    }    fn apply_task_preflight(        &self,        id: TaskId,        diff_override: Option<String>,    ) -> CloudBackendFuture<'_, ApplyOutcome> {        Box::pin(MockClient::apply_task_preflight(self, id, diff_override))    }    fn list_sibling_attempts(        &self,        task: TaskId,        turn_id: String,    ) -> CloudBackendFuture<'_, Vec<TurnAttempt>> {        Box::pin(MockClient::list_sibling_attempts(self, task, turn_id))    }    fn create_task<'a>(        &'a self,        env_id: &'a str,        prompt: &'a str,        git_ref: &'a str,        qa_mode: bool,        best_of_n: usize,    ) -> CloudBackendFuture<'a, CreatedTask> {        Box::pin(MockClient::create_task(            self, env_id, prompt, git_ref, qa_mode, best_of_n,        ))    }}fn mock_diff_for(id: &TaskId) -> String {    match id.0.as_str() {        "T-1000" => {            "diff --git a/README.md b/README.md\nindex 000000..111111 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,2 +1,3 @@\n Intro\n-Hello\n+Hello, world!\n+Task: T-1000\n".to_string()        }        "T-1001" => {            "diff --git a/core/src/lib.rs b/core/src/lib.rs\nindex 000000..111111 100644\n--- a/core/src/lib.rs\n+++ b/core/src/lib.rs\n@@ -1,2 +1,1 @@\n-use foo;\n use bar;\n".to_string()        }        _ => {            "diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md\nindex 000000..111111 100644\n--- /dev/null\n+++ b/CONTRIBUTING.md\n@@ -0,0 +1,3 @@\n+## Contributing\n+Please open PRs.\n+Thanks!\n".to_string()        }    }}fn count_from_unified(diff: &str) -> (usize, usize) {    if let Ok(patch) = diffy::Patch::from_str(diff) {        patch            .hunks()            .iter()            .flat_map(diffy::Hunk::lines)            .fold((0, 0), |(a, d), l| match l {                diffy::Line::Insert(_) => (a + 1, d),                diffy::Line::Delete(_) => (a, d + 1),                _ => (a, d),            })    } else {        let mut a = 0;        let mut d = 0;        for l in diff.lines() {            if l.starts_with("+++") || l.starts_with("---") || l.starts_with("@@") {                continue;            }            match l.as_bytes().first() {                Some(b'+') => a += 1,                Some(b'-') => d += 1,                _ => {}            }        }        (a, d)    }}