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) }}