Track task progress and organize Galaxy Drive plans
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
use super::*;
|
||||
|
||||
fn snapshot(tasks: &[(&str, TaskListStatus)]) -> TaskListSnapshot {
|
||||
TaskListSnapshot {
|
||||
tasks: tasks.iter().map(|(id, status)| TaskListItem {
|
||||
id: (*id).to_owned(),
|
||||
title: format!("Task {id}"),
|
||||
status: *status,
|
||||
}).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn task_with_operations(operations: Vec<api::message::update_todos::Operation>) -> api::Task {
|
||||
api::Task {
|
||||
id: "root".to_owned(),
|
||||
messages: operations.into_iter().map(|operation| api::Message {
|
||||
message: Some(api::message::Message::UpdateTodos(api::message::UpdateTodos {
|
||||
operation: Some(operation),
|
||||
})),
|
||||
..Default::default()
|
||||
}).collect(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn task_status_updates_reuse_the_list_and_survive_a_followup_request() {
|
||||
use TaskListStatus::{Completed, InProgress, Pending};
|
||||
use api::message::update_todos::Operation;
|
||||
|
||||
let initial = snapshot(&[("inspect", InProgress), ("implement", Pending), ("verify", Pending)]);
|
||||
let next = snapshot(&[("inspect", Completed), ("implement", InProgress), ("verify", Pending)]);
|
||||
let updates = todo_operations(Some(&initial), &next);
|
||||
assert!(matches!(&updates[0], Operation::UpdatePendingTodos(_)));
|
||||
assert!(matches!(&updates[1], Operation::MarkTodosCompleted(mark) if mark.todo_ids == ["inspect"]));
|
||||
let mut operations = todo_operations(None, &initial);
|
||||
operations.extend(updates);
|
||||
let root = task_with_operations(operations);
|
||||
let unrelated_child = api::Task { id: "child".to_owned(), ..Default::default() };
|
||||
assert_eq!(task_list_from_api_tasks(&[unrelated_child, root], "root"), Some(next.clone()));
|
||||
assert!(todo_operations(Some(&next), &next).is_empty());
|
||||
|
||||
let finished = snapshot(&[("inspect", Completed), ("implement", Completed), ("verify", Completed)]);
|
||||
let operations = todo_operations(Some(&next), &finished);
|
||||
let Operation::UpdatePendingTodos(update) = &operations[0] else {
|
||||
panic!("normal progress preserves the existing checklist");
|
||||
};
|
||||
assert_eq!(update.updated_pending_todos.len(), 2);
|
||||
assert!(!update.updated_pending_todos.iter().any(|task| task.id == "inspect"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reopening_completed_work_replaces_the_protocol_list_without_losing_ids() {
|
||||
use TaskListStatus::{Completed, InProgress};
|
||||
let old = snapshot(&[("inspect", Completed), ("verify", Completed)]);
|
||||
let reopened = snapshot(&[("inspect", Completed), ("verify", InProgress)]);
|
||||
let operations = todo_operations(Some(&old), &reopened);
|
||||
assert!(matches!(&operations[0], api::message::update_todos::Operation::CreateTodoList(_)));
|
||||
let root = task_with_operations(operations);
|
||||
assert_eq!(task_list_from_api_tasks(&[root], "root"), Some(reopened));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redelivered_progress_cannot_duplicate_completed_items_in_followup_state() {
|
||||
use TaskListStatus::{Completed, InProgress, Pending};
|
||||
let initial = snapshot(&[("inspect", InProgress), ("verify", Pending)]);
|
||||
let next = snapshot(&[("inspect", Completed), ("verify", InProgress)]);
|
||||
let update = todo_operations(Some(&initial), &next);
|
||||
let mut operations = todo_operations(None, &initial);
|
||||
operations.extend(update.clone());
|
||||
operations.extend(update);
|
||||
assert_eq!(task_list_from_api_tasks(&[task_with_operations(operations)], "root"), Some(next));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_snapshot_replaces_stale_prompt_state_without_accumulating_reminders() {
|
||||
let initial = snapshot(&[("inspect", TaskListStatus::InProgress)]);
|
||||
let current = snapshot(&[("inspect", TaskListStatus::Completed)]);
|
||||
let mut request = TurnRequest::new("model", Vec::new());
|
||||
request.system_prompt = Some("Follow user instructions.\n".to_owned());
|
||||
request.tools = vec![task_list_tool()];
|
||||
add_task_list_context(&mut request, Some(&initial));
|
||||
assert_eq!(initial_task_list(&request), Some(initial));
|
||||
add_task_list_context(&mut request, Some(¤t));
|
||||
let prompt = request.system_prompt.clone();
|
||||
add_task_list_context(&mut request, Some(¤t));
|
||||
assert_eq!(request.system_prompt, prompt);
|
||||
assert_eq!(initial_task_list(&request), Some(current));
|
||||
let prompt = request.system_prompt.unwrap();
|
||||
assert_eq!(prompt.matches(SNAPSHOT_START).count(), 1);
|
||||
assert_eq!(prompt.matches(TASK_LIST_REMINDER).count(), 1);
|
||||
assert!(prompt.starts_with("Follow user instructions."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_plan_creation_cannot_seed_visible_or_durable_tasks() {
|
||||
let batch = PendingToolBatch {
|
||||
work_id: galaxy_agent_core::ExternalWorkId {
|
||||
run_id: galaxy_agent_core::ProviderRunId::new("run"),
|
||||
epoch: galaxy_agent_core::RunEpoch::new(1),
|
||||
},
|
||||
calls: vec![galaxy_agent_core::PendingToolCall {
|
||||
call: galaxy_agent_core::ToolCall {
|
||||
id: "plan".to_owned(),
|
||||
name: "create_plan".to_owned(),
|
||||
arguments: serde_json::json!({"documents": [{"content": "## Tasks\n- [ ] Inspect\n- [ ] Verify"}]}),
|
||||
},
|
||||
state: PendingToolCallState::Resolved { result: galaxy_agent_core::ToolResult {
|
||||
call_id: "plan".to_owned(),
|
||||
content: "Not advertised".to_owned(),
|
||||
status: galaxy_agent_core::ToolResultStatus::Error,
|
||||
}},
|
||||
}],
|
||||
};
|
||||
assert!(task_list_from_plan_batch(&batch).is_none());
|
||||
}
|
||||
Reference in New Issue
Block a user