允许Agent恢复项目快照
后台 Runtime 工具箱新增 project.restore Agent 可按权限策略请求恢复到指定 checkpoint 补充恢复成功和确认阻断测试 同步 Runtime V1 文档和项目记忆
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -5543,6 +5543,190 @@ async fn background_agent_runtime_project_diff_respects_project_policy() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_can_restore_checkpoint() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||
write_local_project_file_at(&root, "game/notes.txt", "v1").expect("write notes");
|
||||
let checkpoint = create_local_project_checkpoint_at(&root).expect("create checkpoint");
|
||||
write_local_project_file_at(&root, "game/notes.txt", "v2").expect("change notes");
|
||||
write_local_project_file_at(&root, "game/temp.txt", "temp").expect("add temp file");
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow project restore");
|
||||
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let plan_json = serde_json::json!({
|
||||
"thinkingSummary": "本轮修改需要回滚到 checkpoint",
|
||||
"plan": ["恢复 checkpoint", "确认恢复结果"],
|
||||
"actions": [
|
||||
{
|
||||
"tool": "project.restore",
|
||||
"reason": "撤销本轮偏离目标的文件修改",
|
||||
"input": { "checkpointId": checkpoint.checkpoint_id }
|
||||
}
|
||||
],
|
||||
"response": ""
|
||||
})
|
||||
.to_string();
|
||||
let base_url = spawn_mock_llm_server_responses_with_capture(
|
||||
vec![plan_json, "checkpoint 已恢复。".to_string()],
|
||||
Some(sender),
|
||||
);
|
||||
let _config_guard = write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentLlm": {{
|
||||
"design-director": {{
|
||||
"apiKey": "design-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "design-runtime-model",
|
||||
"apiKind": "openai_responses"
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
|
||||
start_game_creator_agent_background_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"后台恢复到安全 checkpoint",
|
||||
"design-project-restore-run",
|
||||
)
|
||||
.expect("start background task");
|
||||
|
||||
let plan_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("plan llm request");
|
||||
assert!(plan_request.contains("project.restore"));
|
||||
assert!(plan_request.contains("checkpointId"));
|
||||
let final_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("final reply llm request");
|
||||
assert!(final_request.contains("project.restore"));
|
||||
assert!(final_request.contains("已恢复 checkpoint"));
|
||||
assert!(final_request.contains("restoredCount="));
|
||||
assert!(final_request.contains("deletedCount=1"));
|
||||
|
||||
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
|
||||
assert_eq!(runtime.status, "idle");
|
||||
assert!(runtime
|
||||
.tool_policy
|
||||
.auto_tools
|
||||
.contains(&"project.restore".to_string()));
|
||||
assert!(runtime
|
||||
.observations
|
||||
.iter()
|
||||
.any(|item| item.contains("project.restore:ok · 已恢复 checkpoint")));
|
||||
assert!(runtime.recent_tool_calls.iter().any(|call| {
|
||||
call.tool == "project.restore"
|
||||
&& call.status == "ok"
|
||||
&& call
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("deletedCount=1"))
|
||||
}));
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join("game/notes.txt")).expect("restored notes"),
|
||||
"v1"
|
||||
);
|
||||
assert!(!root.join("game/temp.txt").exists());
|
||||
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
|
||||
assert!(agent_db.contains("\"recordType\":\"project.restore\""));
|
||||
assert!(agent_db.contains("\"tool\":\"project.restore\""));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_project_restore_respects_project_policy() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||
write_local_project_file_at(&root, "game/blocked-notes.txt", "v1").expect("write notes");
|
||||
let checkpoint = create_local_project_checkpoint_at(&root).expect("create checkpoint");
|
||||
write_local_project_file_at(&root, "game/blocked-notes.txt", "v2").expect("change notes");
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["project.restore".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let plan_json = serde_json::json!({
|
||||
"thinkingSummary": "尝试恢复 checkpoint",
|
||||
"plan": ["恢复 checkpoint", "等待权限结果"],
|
||||
"actions": [
|
||||
{
|
||||
"tool": "project.restore",
|
||||
"reason": "恢复项目到安全状态",
|
||||
"input": { "checkpointId": checkpoint.checkpoint_id }
|
||||
}
|
||||
],
|
||||
"response": ""
|
||||
})
|
||||
.to_string();
|
||||
let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender));
|
||||
let _config_guard = write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentLlm": {{
|
||||
"design-director": {{
|
||||
"apiKey": "design-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "design-runtime-model",
|
||||
"apiKind": "openai_responses"
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
|
||||
start_game_creator_agent_background_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"后台尝试恢复 checkpoint",
|
||||
"design-project-restore-policy-run",
|
||||
)
|
||||
.expect("start background task");
|
||||
|
||||
let plan_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("plan llm request");
|
||||
assert!(plan_request.contains("confirmTools"));
|
||||
assert!(plan_request.contains("project.restore"));
|
||||
assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err());
|
||||
|
||||
let runtime = wait_for_agent_runtime_confirmation(&root, "design-director");
|
||||
assert_eq!(runtime.status, "waiting-for-confirmation");
|
||||
assert_eq!(runtime.task_queue.waiting_for_confirmation, 1);
|
||||
assert!(runtime
|
||||
.tool_policy
|
||||
.confirm_tools
|
||||
.contains(&"project.restore".to_string()));
|
||||
assert!(runtime.observations.iter().any(|item| item.contains(
|
||||
"project.restore:waiting-for-confirmation · 项目权限策略要求用户确认:project.restore"
|
||||
)));
|
||||
assert!(!runtime
|
||||
.observations
|
||||
.iter()
|
||||
.any(|item| item.contains("project.restore:ok")));
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join("game/blocked-notes.txt")).expect("blocked notes"),
|
||||
"v2"
|
||||
);
|
||||
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
|
||||
assert!(!agent_db.contains("\"recordType\":\"project.restore\""));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_can_read_other_agent_status() {
|
||||
let root = unique_project_path();
|
||||
|
||||
Reference in New Issue
Block a user