Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs
T
kdletters 794f3d4cf8
Project CI / Repository checks (push) Failing after 45s
Project CI / Frontend tests (push) Successful in 3m39s
Project CI / Backend tests (push) Successful in 4m44s
Project CI / Native shell tests (push) Failing after 13m11s
修复Agent失败原因丢失与成功误判
解析 Codex app-server 稳定失败分类并生成安全可行动摘要
统一 Runtime 事件、任务、阶段记录和各 Agent 状态面的失败展示
限制自主构建最终回复 fallback,避免鉴权与网络错误被提交为成功
补充失败分类、防泄漏、待核对和 fallback 回归测试
同步技术方案与共享决策记录
2026-08-12 21:35:11 +08:00

5278 lines
197 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::*;
fn runtime_task_json_line(run_id: &str, status: &str, phase: &str) -> String {
serde_json::json!({
"schemaVersion": AGENT_RUNTIME_SCHEMA_VERSION,
"agentId": "design-director",
"taskId": "design-director",
"sessionId": "agent-session-design-director",
"runId": run_id,
"source": "test",
"task": "验证 Runtime task JSONL 读取",
"status": status,
"phase": phase,
"currentAction": "等待测试",
"updatedAt": 1,
})
.to_string()
}
#[test]
fn runtime_task_reader_ignores_only_unterminated_truncated_tail() {
let root = unique_project_path();
fs::create_dir_all(&root).expect("create runtime task fixture directory");
let path = root.join("tasks.jsonl");
let complete = runtime_task_json_line("complete-run", "pending", "queued");
fs::write(
&path,
format!("{complete}\n{{\"schemaVersion\":\"{AGENT_RUNTIME_SCHEMA_VERSION}\""),
)
.expect("write truncated runtime task tail");
let records = read_all_game_creator_agent_runtime_tasks(&path)
.expect("unterminated truncated tail should be ignored");
assert_eq!(records.len(), 1);
assert_eq!(records[0].run_id, "complete-run");
fs::remove_dir_all(root).ok();
}
#[test]
fn runtime_task_reader_rejects_malformed_middle_line() {
let root = unique_project_path();
fs::create_dir_all(&root).expect("create runtime task fixture directory");
let path = root.join("tasks.jsonl");
let first = runtime_task_json_line("first-run", "pending", "queued");
let last = runtime_task_json_line("last-run", "completed", "completed");
fs::write(&path, format!("{first}\n{{\"runId\":\n{last}"))
.expect("write malformed middle runtime task line");
let error = read_all_game_creator_agent_runtime_tasks(&path)
.expect_err("malformed middle line must fail closed");
assert!(error.contains("第 2 行 JSON 无效"), "{error}");
fs::remove_dir_all(root).ok();
}
#[test]
fn runtime_task_reader_rejects_newline_terminated_malformed_tail() {
let root = unique_project_path();
fs::create_dir_all(&root).expect("create runtime task fixture directory");
let path = root.join("tasks.jsonl");
let complete = runtime_task_json_line("complete-run", "pending", "queued");
fs::write(&path, format!("{complete}\n{{\"runId\":\n"))
.expect("write terminated malformed runtime task tail");
let error = read_all_game_creator_agent_runtime_tasks(&path)
.expect_err("newline-terminated malformed tail must fail closed");
assert!(error.contains("第 2 行 JSON 无效"), "{error}");
fs::remove_dir_all(root).ok();
}
#[test]
fn runtime_task_reader_rejects_unterminated_non_truncated_syntax_error() {
let root = unique_project_path();
fs::create_dir_all(&root).expect("create runtime task fixture directory");
let path = root.join("tasks.jsonl");
let complete = runtime_task_json_line("complete-run", "pending", "queued");
fs::write(&path, format!("{complete}\nnot-json"))
.expect("write unterminated invalid runtime task tail");
let error = read_all_game_creator_agent_runtime_tasks(&path)
.expect_err("unterminated syntax error must not be treated as a truncated record");
assert!(error.contains("第 2 行 JSON 无效"), "{error}");
fs::remove_dir_all(root).ok();
}
#[test]
fn runtime_events_expose_stable_ids_and_backend_owned_public_text_only() {
let root = unique_project_path();
fs::create_dir_all(&root).expect("create runtime event fixture directory");
let state = default_game_creator_agent_runtime_state("code-prototype", "public-event-run");
append_game_creator_agent_runtime_action_event(
&root,
&state,
"turn.progress",
"running",
"planning",
"正在生成首个可玩版本",
Some("taskSha256=private-hash"),
"public-event-progress-1",
)
.expect("append public progress");
append_game_creator_agent_runtime_action_event(
&root,
&state,
"observation",
"running",
"observation",
"runtime.plan_update:blocked · 内部计划门禁",
Some("fingerprint=private"),
"public-event-internal-1",
)
.expect("append internal observation");
append_game_creator_agent_runtime_action_event(
&root,
&state,
"turn.progress",
"running",
"planning",
"Bearer secret-token",
None,
"public-event-sensitive-1",
)
.expect("append sensitive progress");
append_game_creator_agent_runtime_action_event(
&root,
&state,
"action",
"running",
"action",
"调用工具 file.write",
Some("raw tool input must stay private"),
"public-event-action-1",
)
.expect("append public action");
append_game_creator_agent_runtime_action_event(
&root,
&state,
"action",
"running",
"action",
"调用工具 file.write",
Some("raw tool input must stay private"),
"public-event-action-1",
)
.expect("repeat public action idempotently");
append_game_creator_agent_runtime_action_event(
&root,
&state,
"turn.failed",
"failed",
"failed",
"Agent Runtime 本轮处理失败。",
Some("kind=codex-app-server-context-window-exceeded fingerprint=private"),
"public-event-failure-1",
)
.expect("append classified public failure");
let events = read_recent_game_creator_agent_runtime_events(
&game_creator_agent_runtime_event_path(&root, "code-prototype"),
)
.expect("read public runtime events");
assert_eq!(events.len(), 5);
assert!(events.iter().all(|event| !event.event_id.trim().is_empty()));
let mut event_ids = events
.iter()
.map(|event| event.event_id.as_str())
.collect::<Vec<_>>();
event_ids.sort_unstable();
event_ids.dedup();
assert_eq!(event_ids.len(), events.len());
assert_eq!(
events[0].public_text.as_deref(),
Some("正在生成首个可玩版本")
);
assert_eq!(events[1].public_text, None);
assert_eq!(events[2].public_text, None);
assert_eq!(
events[3].public_text.as_deref(),
Some("调用工具 file.write")
);
assert!(!events[3]
.public_text
.as_deref()
.unwrap_or_default()
.contains("raw tool input must stay private"));
assert_eq!(
events[4].public_text.as_deref(),
Some("专业 Agent 模型上下文已超限,请缩小任务范围后重试")
);
assert_eq!(events[4].detail, events[4].public_text);
assert!(!events[4]
.public_text
.as_deref()
.unwrap_or_default()
.contains("fingerprint"));
fs::remove_dir_all(root).ok();
}
#[test]
fn runtime_task_reader_rejects_unknown_status_and_phase() {
let root = unique_project_path();
fs::create_dir_all(&root).expect("create runtime task fixture directory");
let path = root.join("tasks.jsonl");
for (status, phase, expected) in [
("unknown-status", "queued", "status 无效"),
("pending", "unknown-phase", "phase 无效"),
] {
fs::write(
&path,
runtime_task_json_line("invalid-schema-run", status, phase),
)
.expect("write invalid runtime task schema");
let error = read_all_game_creator_agent_runtime_tasks(&path)
.expect_err("unknown status or phase must fail closed");
assert!(error.contains(expected), "{error}");
}
fs::remove_dir_all(root).ok();
}
#[test]
fn runtime_task_reader_accepts_rare_persisted_phases() {
let root = unique_project_path();
fs::create_dir_all(&root).expect("create runtime task fixture directory");
let path = root.join("tasks.jsonl");
let brief = runtime_task_json_line("brief-run", "running", "brief");
let parent_link_missing =
runtime_task_json_line("parent-link-missing-run", "failed", "parent-link-missing");
let public_status_write_failed = runtime_task_json_line(
"public-status-write-failed-run",
"failed",
"public-status-write-failed",
);
fs::write(
&path,
format!("{brief}\n{parent_link_missing}\n{public_status_write_failed}\n"),
)
.expect("write rare persisted task phases");
let records = read_all_game_creator_agent_runtime_tasks(&path)
.expect("known persisted task phases must remain readable");
assert_eq!(records.len(), 3);
assert_eq!(records[0].phase, "brief");
assert_eq!(records[1].phase, "parent-link-missing");
assert_eq!(records[2].phase, "public-status-write-failed");
fs::remove_dir_all(root).ok();
}
#[test]
fn parallel_jsonl_appends_keep_records_line_delimited() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "并行追加测试").expect("project init");
let conversation_path = root.join(".agent/conversations/agents/design-director.jsonl");
let worker_count = 8;
let records_per_worker = 20;
let gate = Arc::new((StdMutex::new(false), Condvar::new()));
let mut handles = Vec::new();
for worker_index in 0..worker_count {
let root = root.clone();
let conversation_path = conversation_path.clone();
let gate = Arc::clone(&gate);
handles.push(std::thread::spawn(move || {
let (lock, cvar) = &*gate;
let mut started = lock.lock().expect("start gate lock");
while !*started {
started = cvar.wait(started).expect("wait start gate");
}
drop(started);
for record_index in 0..records_per_worker {
let payload = "x".repeat(4096);
let line = serde_json::json!({
"schemaVersion": LOCAL_CONVERSATION_SCHEMA_VERSION,
"role": "assistant",
"content": format!("worker-{worker_index}-record-{record_index}-{payload}"),
"agentId": "design-director",
"updatedAt": unix_timestamp(),
})
.to_string();
append_jsonl_line(&conversation_path, &line, "测试对话记录")
.expect("append conversation line");
append_agent_db_record(
&root,
serde_json::json!({
"recordType": "test.parallel.append",
"workerIndex": worker_index,
"recordIndex": record_index,
}),
)
.expect("append agent db record");
}
}));
}
{
let (lock, cvar) = &*gate;
let mut started = lock.lock().expect("start gate lock");
*started = true;
cvar.notify_all();
}
for handle in handles {
handle.join().expect("append worker");
}
let expected_records = worker_count * records_per_worker;
let conversation = fs::read_to_string(&conversation_path).expect("conversation file");
let conversation_records = conversation.lines().collect::<Vec<_>>();
assert_eq!(conversation_records.len(), expected_records);
for line in conversation_records {
serde_json::from_str::<Value>(line).expect("conversation line should be valid json");
}
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
let mut parallel_records = 0;
for line in agent_db.lines() {
let value =
serde_json::from_str::<Value>(line).expect("agent db line should be valid json");
if value.get("recordType").and_then(Value::as_str) == Some("test.parallel.append") {
parallel_records += 1;
}
}
assert_eq!(parallel_records, expected_records);
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_lock_status_keeps_fresh_same_process_locks_busy() {
let root = unique_project_path();
let lock_path = root.join(".agent/runtime/locks/design-director.lock");
fs::create_dir_all(lock_path.parent().expect("lock parent")).expect("lock dir");
fs::write(
&lock_path,
serde_json::json!({
"agentId": "design-director",
"pid": std::process::id(),
"createdAt": unix_timestamp(),
})
.to_string(),
)
.expect("write lock");
let status = read_game_creator_agent_runtime_lock_status(&lock_path);
assert!(!status.is_stale);
assert!(!status.belongs_to_previous_process);
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_lock_status_marks_old_previous_process_locks_stale() {
let root = unique_project_path();
let lock_path = root.join(".agent/runtime/locks/design-director.lock");
fs::create_dir_all(lock_path.parent().expect("lock parent")).expect("lock dir");
fs::write(
&lock_path,
serde_json::json!({
"agentId": "design-director",
"pid": u64::from(std::process::id()) + 1000,
"createdAt": unix_timestamp()
.saturating_sub(AGENT_RUNTIME_LOCK_STALE_AFTER_SECONDS + 1),
})
.to_string(),
)
.expect("write lock");
let status = read_game_creator_agent_runtime_lock_status(&lock_path);
assert!(status.is_stale);
assert!(status.belongs_to_previous_process);
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_system_lock_allows_only_one_owner() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "锁互斥测试").expect("project init");
let first = try_acquire_game_creator_agent_runtime_task_lock(&root, "design-director")
.expect("first lock")
.expect("first owner acquires lock");
assert!(
try_acquire_game_creator_agent_runtime_task_lock(&root, "design-director")
.expect("second lock attempt")
.is_none()
);
drop(first);
let replacement = wait_to_acquire_agent_runtime_lock(&root, "design-director");
assert!(root
.join(".agent/runtime/locks/design-director.lock")
.exists());
drop(replacement);
fs::remove_dir_all(root).ok();
}
#[cfg(unix)]
#[test]
fn agent_runtime_system_lock_rejects_symlink_without_touching_target() {
use std::os::unix::fs::symlink;
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "锁链接测试").expect("project init");
let target = root.join("outside-lock-target.txt");
fs::write(&target, "keep-me").expect("write lock target");
let lock_path = root.join(".agent/runtime/locks/design-director.lock");
fs::create_dir_all(lock_path.parent().expect("lock parent")).expect("lock dir");
symlink(&target, &lock_path).expect("symlink lock path");
assert!(try_acquire_game_creator_agent_runtime_task_lock(&root, "design-director").is_err());
assert_eq!(fs::read_to_string(&target).expect("read target"), "keep-me");
fs::remove_dir_all(root).ok();
}
#[cfg(unix)]
#[test]
fn agent_runtime_system_lock_rejects_symlinked_parent_without_touching_target() {
use std::os::unix::fs::symlink;
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "锁父目录链接测试").expect("project init");
let outside_locks = root.join("outside-locks");
fs::create_dir(&outside_locks).expect("outside lock dir");
let target = outside_locks.join("design-director.lock");
fs::write(&target, "keep-me").expect("write lock target");
let locks_dir = root.join(".agent/runtime/locks");
if locks_dir.exists() {
fs::remove_dir_all(&locks_dir).expect("remove existing locks directory");
}
symlink(&outside_locks, &locks_dir).expect("symlink locks directory");
assert!(try_acquire_game_creator_agent_runtime_task_lock(&root, "design-director").is_err());
assert_eq!(fs::read_to_string(&target).expect("read target"), "keep-me");
fs::remove_dir_all(root).ok();
}
#[cfg(unix)]
#[test]
fn agent_runtime_system_lock_rejects_hard_link_without_touching_target() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "锁硬链接测试").expect("project init");
let target = root.join("outside-hard-link-target.txt");
fs::write(&target, "keep-me").expect("write lock target");
let lock_path = root.join(".agent/runtime/locks/design-director.lock");
fs::create_dir_all(lock_path.parent().expect("lock parent")).expect("lock dir");
fs::hard_link(&target, &lock_path).expect("hard-link lock path");
assert!(try_acquire_game_creator_agent_runtime_task_lock(&root, "design-director").is_err());
assert_eq!(fs::read_to_string(&target).expect("read target"), "keep-me");
fs::remove_dir_all(root).ok();
}
#[test]
fn missing_runtime_state_stays_idle_without_a_run_profile_binding() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-empty-runtime", "空 Runtime 项目")
.expect("project init");
let runtime =
read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
.expect("read default Supervisor runtime");
assert!(runtime.state.run_id.is_empty());
assert_eq!(runtime.state.status, "idle");
assert_eq!(runtime.state.phase, "idle");
assert!(runtime.state.error.is_none());
fs::remove_dir_all(root).ok();
}
#[test]
fn structured_plan_update_validates_and_advances_monotonically() {
let parsed = parse_game_creator_agent_tool_plan_response(
&serde_json::json!({
"thinkingSummary": "先建立可恢复计划",
"planUpdate": {
"explanation": "首次拆解",
"steps": [
{ "step": "读取项目", "status": "in_progress" },
{ "step": "验证结果", "status": "pending" }
]
},
"plan": ["旧计划应被结构化更新覆盖"],
"actions": [],
"response": ""
})
.to_string(),
)
.expect("parse structured plan");
assert!(parsed.plan_update.is_some());
let legacy = parse_game_creator_agent_tool_plan_response(
&serde_json::json!({
"thinkingSummary": "旧 Provider fallback",
"plan": ["旧步骤"],
"actions": [],
"response": ""
})
.to_string(),
)
.expect("parse legacy plan");
assert!(legacy.plan_update.is_none());
let mut runtime = default_game_creator_agent_runtime_state("design-director", "plan-run");
assert!(apply_agent_runtime_plan_update(
&mut runtime,
parsed.plan_update.as_ref().expect("plan update"),
)
.expect("apply first plan"));
assert_eq!(runtime.plan_revision, 1);
assert_eq!(runtime.active_plan_step_index, Some(0));
assert_eq!(runtime.plan_steps[0].status, "in_progress");
assert_eq!(runtime.plan, vec!["读取项目", "验证结果"]);
assert!(!apply_agent_runtime_plan_update(
&mut runtime,
parsed.plan_update.as_ref().expect("same plan update"),
)
.expect("idempotent plan update"));
assert_eq!(runtime.plan_revision, 1);
let progressed = AgentRuntimePlanUpdate {
explanation: "读取完成,开始验证".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "读取项目".to_string(),
status: "completed".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "验证结果".to_string(),
status: "in_progress".to_string(),
},
],
};
assert!(apply_agent_runtime_plan_update(&mut runtime, &progressed).expect("advance plan"));
assert_eq!(runtime.plan_revision, 2);
assert_eq!(runtime.plan_steps[0].status, "completed");
assert_eq!(runtime.active_plan_step_index, Some(1));
let regression = AgentRuntimePlanUpdate {
explanation: "错误回退".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "读取项目".to_string(),
status: "pending".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "验证结果".to_string(),
status: "in_progress".to_string(),
},
],
};
assert!(apply_agent_runtime_plan_update(&mut runtime, &regression)
.expect_err("completed step cannot regress")
.contains("不能让已完成步骤回退"));
assert_eq!(runtime.plan_revision, 2);
let completed = AgentRuntimePlanUpdate {
explanation: "全部完成".to_string(),
steps: vec![AgentRuntimePlanUpdateStep {
step: "验证结果".to_string(),
status: "completed".to_string(),
}],
};
assert!(apply_agent_runtime_plan_update(&mut runtime, &completed)
.expect("terminal step is retained"));
assert_eq!(runtime.plan_revision, 3);
assert_eq!(runtime.plan, vec!["读取项目", "验证结果"]);
assert!(runtime
.plan_steps
.iter()
.all(|step| step.status == "completed"));
assert!(structured_plan_completion_blocker(&runtime).is_none());
}
#[test]
fn structured_plan_update_rejects_invalid_shapes_and_ignores_action_indexes() {
let invalid_updates = [
AgentRuntimePlanUpdate {
explanation: " ".to_string(),
steps: vec![AgentRuntimePlanUpdateStep {
step: "步骤".to_string(),
status: "pending".to_string(),
}],
},
AgentRuntimePlanUpdate {
explanation: "重复".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "步骤".to_string(),
status: "pending".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "步骤".to_string(),
status: "completed".to_string(),
},
],
},
AgentRuntimePlanUpdate {
explanation: "多活动步骤".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "步骤一".to_string(),
status: "in_progress".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "步骤二".to_string(),
status: "in_progress".to_string(),
},
],
},
AgentRuntimePlanUpdate {
explanation: "未知状态".to_string(),
steps: vec![AgentRuntimePlanUpdateStep {
step: "步骤".to_string(),
status: "blocked".to_string(),
}],
},
];
for update in invalid_updates {
assert!(sanitize_agent_runtime_plan_update(&update).is_err());
}
let mut runtime = default_game_creator_agent_runtime_state("design-director", "guard-run");
let update = AgentRuntimePlanUpdate {
explanation: "等待工具真实结果".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "读取项目".to_string(),
status: "in_progress".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "验证结果".to_string(),
status: "pending".to_string(),
},
],
};
apply_agent_runtime_plan_update(&mut runtime, &update).expect("apply guarded plan");
let snapshot = runtime.plan_steps.clone();
activate_agent_runtime_plan_step(&mut runtime, 1, "工具数组第二项");
complete_agent_runtime_active_plan_step(&mut runtime, "completed", "工具成功");
retry_agent_runtime_active_plan_step(&mut runtime, "等待重试");
activate_agent_runtime_response_plan_step(&mut runtime, "准备回复");
complete_agent_runtime_remaining_plan_steps(&mut runtime, "自动收束");
assert_eq!(runtime.plan_steps, snapshot);
let blocker = structured_plan_completion_blocker(&runtime).expect("incomplete plan blocks");
assert_eq!(blocker.tool, "runtime.plan_update");
assert!(blocker.summary.contains("0/2"));
let blocker_detail = blocker.detail.expect("structured blocker detail");
assert!(blocker_detail.contains("planRevision=1"));
assert!(blocker_detail.contains("completed=0"));
assert!(blocker_detail.contains("pending=1"));
assert!(blocker_detail.contains("inProgress=1"));
assert!(blocker_detail.contains("failed=0"));
assert!(blocker_detail.contains("必须在同一响应调用具体动作函数"));
assert!(blocker_detail.contains("不能只改计划解释"));
assert!(blocker_detail.contains(&format!(
"in_progress:{:x}",
Sha256::digest("读取项目".as_bytes())
)));
assert!(blocker_detail.contains(&format!(
"pending:{:x}",
Sha256::digest("验证结果".as_bytes())
)));
assert!(!blocker.summary.contains("读取项目"));
assert!(!blocker.summary.contains("验证结果"));
assert!(!blocker_detail.contains("读取项目"));
assert!(!blocker_detail.contains("验证结果"));
}
#[test]
fn structured_plan_state_normalization_fails_closed_on_corruption() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "损坏计划状态项目").expect("project init");
let mut runtime = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"验证损坏计划失败关闭",
"structured-plan-corrupt-run",
"agent-background-task",
"损坏计划测试",
vec!["旧计划".to_string()],
)
.expect("start corrupt plan state");
apply_agent_runtime_plan_update(
&mut runtime,
&AgentRuntimePlanUpdate {
explanation: "当前步骤执行中".to_string(),
steps: vec![AgentRuntimePlanUpdateStep {
step: "执行验证".to_string(),
status: "in_progress".to_string(),
}],
},
)
.expect("apply plan before corruption");
runtime.active_plan_step_index = None;
write_game_creator_agent_runtime_state(&root, &runtime).expect("persist corrupt plan state");
let loaded = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read corrupt plan state")
.state;
assert_eq!(loaded.status, "needs-reconciliation");
assert_eq!(loaded.phase, "needs-reconciliation");
assert!(loaded.error.as_deref().is_some_and(|error| {
error.contains("activePlanStepIndex") && error.contains("in_progress")
}));
fs::remove_dir_all(root).ok();
}
#[test]
fn structured_plan_failure_after_all_steps_completed_preserves_terminal_snapshot() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "结构化计划失败收束项目").expect("project init");
let mut state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"计划完成后模拟 Runtime 失败",
"structured-plan-completed-before-failure-run",
"agent-background-task",
"准备失败收束回归",
vec!["旧计划占位".to_string()],
)
.expect("start completed structured plan fixture");
apply_agent_runtime_plan_update(
&mut state,
&AgentRuntimePlanUpdate {
explanation: "所有必要步骤已完成".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "完成实现".to_string(),
status: "completed".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "完成验证".to_string(),
status: "completed".to_string(),
},
],
},
)
.expect("apply fully completed structured plan");
write_game_creator_agent_runtime_state(&root, &state)
.expect("persist completed structured plan before failure");
let expected_revision = state.plan_revision;
let expected_explanation = state.plan_explanation.clone();
let expected_plan = state.plan.clone();
let expected_steps = state.plan_steps.clone();
let expected_active_index = state.active_plan_step_index;
assert_eq!(expected_revision, 1);
assert!(expected_steps.iter().all(|step| step.status == "completed"));
assert_eq!(expected_active_index, None);
let failed = fail_game_creator_agent_runtime_turn_at(
&root,
state,
"结构化计划完成后的模拟 Runtime 失败",
)
.expect("fail completed structured plan through real terminal path");
assert_eq!(failed.status, "failed");
assert_eq!(failed.phase, "failed");
assert_eq!(failed.plan_revision, expected_revision);
assert_eq!(failed.plan_explanation, expected_explanation);
assert_eq!(failed.plan, expected_plan);
assert_eq!(failed.plan_steps, expected_steps);
assert_eq!(failed.active_plan_step_index, expected_active_index);
assert!(failed
.plan_steps
.iter()
.all(|step| step.status == "completed"));
assert!(structured_plan_completion_blocker(&failed).is_none());
let persisted = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read failed structured plan state")
.state;
assert_eq!(persisted.status, "failed");
assert_eq!(persisted.phase, "failed");
assert_eq!(persisted.plan_revision, expected_revision);
assert_eq!(persisted.plan_explanation, expected_explanation);
assert_eq!(persisted.plan, expected_plan);
assert_eq!(persisted.plan_steps, expected_steps);
assert_eq!(persisted.active_plan_step_index, expected_active_index);
fs::remove_dir_all(root).ok();
}
#[test]
fn runtime_failure_public_audits_hash_private_delivery_diagnostics() {
for (
agent_id,
child_run_id,
parent_run_id,
parent_action_id,
private_error,
budget_exhausted,
terminal_event_type,
failure_kind,
) in [
(
"code-prototype",
"public-failure-audit-code-run",
"public-failure-audit-code-parent-run",
"action-111111111111111111111111",
"private deliveryStructuredResult:程序 Agent 失败诊断正文仅供私有返工使用",
false,
"turn.failed",
"tool-plan-failed",
),
(
"quality-review",
"public-failure-audit-quality-run",
"public-failure-audit-quality-parent-run",
"action-222222222222222222222222",
"private deliveryResult:质量 Agent 预算耗尽诊断正文仅供私有返工使用",
true,
"turn.budget_exhausted",
"loop-budget-exhausted",
),
] {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "公共失败审计正文隔离测试")
.expect("project init");
let parent = start_game_creator_agent_runtime_task_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"等待专业 Agent 私有失败回执",
parent_run_id,
"agent-chat",
"等待专业 Agent",
vec!["处理专业 Agent 回执".to_string()],
)
.expect("start parent runtime");
let mut child = start_game_creator_agent_runtime_task_at(
&root,
agent_id,
"执行专业 Agent 任务并保留私有失败诊断",
child_run_id,
"agent-delegate",
"执行专业任务",
vec!["返回私有诊断".to_string()],
)
.expect("start child runtime");
let delegation_id = agent_runtime_delegation_id(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
agent_id,
parent_action_id,
);
child.parent_agent_id = Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string());
child.parent_run_id = Some(parent_run_id.to_string());
child.delegation_id = Some(delegation_id.clone());
let delivery = new_static_delegate_delivery(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&parent.session_id,
parent_run_id,
parent_action_id,
&delegation_id,
agent_id,
&child.session_id,
child_run_id,
);
create_or_read_static_delegate_delivery_at(&root, &delivery)
.expect("create static delivery");
let failed = if budget_exhausted {
fail_game_creator_agent_runtime_budget_at(&root, child, private_error)
} else {
fail_game_creator_agent_runtime_turn_at(&root, child, private_error)
}
.expect("persist private failure state");
append_game_creator_agent_background_task_failed_audit(&root, &failed, failure_kind)
.expect("append public failure audit");
assert_eq!(failed.error.as_deref(), Some(private_error));
let private_delivery = read_static_delegate_delivery_at(&root, &delegation_id)
.expect("read private delivery")
.expect("private delivery exists");
assert_eq!(private_delivery.status, StaticDelegateDeliveryStatus::Ready);
assert_eq!(
private_delivery.result_summary.as_deref(),
Some(private_error)
);
assert_eq!(
private_delivery
.structured_result
.as_ref()
.and_then(|result| result.error.as_deref()),
Some(private_error)
);
let error_sha256 = format!("{:x}", Sha256::digest(private_error.as_bytes()));
let error_chars = private_error.chars().count();
let expected_public_detail = "专业 Agent 执行失败,请稍后重试";
let result = read_game_creator_agent_runtime_at(&root, agent_id)
.expect("read failed runtime projection");
let public_failure_events = result
.recent_events
.iter()
.filter(|event| {
event.run_id == child_run_id
&& matches!(
event.event_type.as_str(),
"error" | "turn.failed" | "turn.budget_exhausted"
)
})
.collect::<Vec<_>>();
assert_eq!(public_failure_events.len(), 2);
assert!(public_failure_events
.iter()
.any(|event| event.event_type == "error"));
assert!(public_failure_events
.iter()
.any(|event| event.event_type == terminal_event_type));
assert!(public_failure_events.iter().all(|event| {
event.detail.as_deref() == Some(expected_public_detail)
&& !event.summary.contains(private_error)
}));
let event_log = fs::read_to_string(game_creator_agent_runtime_event_path(&root, agent_id))
.expect("read public event log");
assert!(!event_log.contains(private_error));
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("read agent db");
assert!(!agent_db.contains(private_error));
let failure_records = agent_db
.lines()
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
.filter(|record| {
record.get("recordType").and_then(Value::as_str)
== Some("agent.runtime.background_task.failed")
&& record.get("runId").and_then(Value::as_str) == Some(child_run_id)
})
.collect::<Vec<_>>();
assert_eq!(failure_records.len(), 1);
let failure_record = &failure_records[0];
assert_eq!(failure_record["agentId"], agent_id);
assert_eq!(failure_record["taskId"], agent_id);
assert_eq!(failure_record["sessionId"], failed.session_id);
assert_eq!(failure_record["runId"], child_run_id);
assert_eq!(failure_record["source"], "agent-delegate");
assert_eq!(failure_record["failureKind"], failure_kind);
assert_eq!(failure_record["errorSha256"], error_sha256);
assert_eq!(
failure_record["errorChars"].as_u64(),
Some(error_chars as u64)
);
assert!(failure_record.get("error").is_none());
fs::remove_dir_all(root).ok();
}
}
#[tokio::test]
async fn background_final_reply_failure_keeps_private_conversation_and_hashes_public_audits() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "最终回复公共失败审计测试")
.expect("project init");
let config_dir = unique_project_path();
fs::create_dir_all(&config_dir).expect("create public failure audit config dir");
let config_guard = use_test_runtime_config_dir(config_dir.clone());
let config_path = config_dir.join(GAME_CREATOR_CONFIG_FILE_NAME);
let planning_response = serde_json::json!({
"thinkingSummary": "已有上下文足够,准备生成最终回复",
"plan": ["回复开发者"],
"actions": [],
"response": ""
})
.to_string();
let base_url = spawn_mock_llm_tool_plan_then_invalid_final_reply(planning_response);
replace_test_local_config(
&config_path,
format!(
r#"{{
"agentMode": "provider",
"agentLlm": {{
"design-director": {{
"apiKey": "design-key",
"baseUrl": {base_url:?},
"model": "design-runtime-model",
"apiKind": "openai_responses",
"maxRetries": 0
}}
}}
}}"#
),
);
let run_id = "background-final-reply-public-failure-audit-run";
start_game_creator_agent_background_task_at(
&root,
"design-director",
"验证最终回复失败的私有诊断和公共审计边界",
run_id,
)
.expect("start background task");
let runtime = wait_for_agent_runtime_terminal_and_lane_release_async(
&root,
"design-director",
run_id,
"failed",
"failed",
)
.await
.state;
assert_eq!(runtime.status, "failed");
assert_eq!(runtime.phase, "failed");
let private_error = runtime.error.clone().expect("private runtime error");
assert!(!private_error.trim().is_empty());
let error_sha256 = format!("{:x}", Sha256::digest(private_error.as_bytes()));
let error_chars = private_error.chars().count();
let expected_public_detail = "专业 Agent 服务请求失败,请稍后重试";
let result = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read public failure projections");
for event_type in ["error", "turn.failed"] {
let event = result
.recent_events
.iter()
.find(|event| event.run_id == run_id && event.event_type == event_type)
.expect("public failure event");
assert_eq!(event.detail.as_deref(), Some(expected_public_detail));
}
let event_log = fs::read_to_string(game_creator_agent_runtime_event_path(
&root,
"design-director",
))
.expect("read public event log");
assert!(!event_log.contains(&private_error));
let failure_records = read_agent_db_records_for_test(&root)
.into_iter()
.filter(|record| {
record.get("recordType").and_then(Value::as_str)
== Some("agent.runtime.background_task.failed")
&& record.get("runId").and_then(Value::as_str) == Some(run_id)
})
.collect::<Vec<_>>();
assert_eq!(failure_records.len(), 1);
let failure_record = &failure_records[0];
assert_eq!(failure_record["failureKind"], "final-reply-failed");
assert_eq!(failure_record["errorSha256"], error_sha256);
assert_eq!(
failure_record["errorChars"].as_u64(),
Some(error_chars as u64)
);
assert!(failure_record.get("error").is_none());
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("read agent db");
assert!(!agent_db.contains(&private_error));
let conversation = read_local_conversation_for_session_at(
&root,
Some("design-director"),
Some(&runtime.session_id),
)
.expect("read Runtime-owned public failure conversation");
assert!(conversation.messages.iter().any(|message| {
message.role == "assistant"
&& message.content == "专业 Agent 服务请求失败,请稍后重试"
&& message.message_id.as_deref().is_some_and(|message_id| {
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
})
}));
fs::remove_dir_all(root).ok();
drop(config_guard);
fs::remove_dir_all(config_dir).ok();
}
#[test]
fn structured_plan_failure_and_budget_preserve_last_trusted_progress() {
for budget_exhausted in [false, true] {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "结构化计划失败进度项目")
.expect("project init");
let run_id = if budget_exhausted {
"structured-plan-budget-preserves-progress-run"
} else {
"structured-plan-failure-preserves-progress-run"
};
let mut state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"保留失败前最后一次可信计划进度",
run_id,
"agent-background-task",
"准备失败进度回归",
vec!["旧计划占位".to_string()],
)
.expect("start structured failure fixture");
apply_agent_runtime_plan_update(
&mut state,
&AgentRuntimePlanUpdate {
explanation: "实现已完成,验证仍在执行".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "完成实现".to_string(),
status: "completed".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "运行验证".to_string(),
status: "in_progress".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "整理结果".to_string(),
status: "pending".to_string(),
},
],
},
)
.expect("apply structured failure plan");
write_game_creator_agent_runtime_state(&root, &state)
.expect("persist structured state before failure");
persist_game_creator_agent_runtime_context(
&root,
&state,
&state.current_task,
&AgentRuntimeToolPlan::default(),
&[],
0,
&AgentRuntimeContextWindowTracker::default(),
)
.expect("persist structured context before failure");
let expected_revision = state.plan_revision;
let expected_explanation = state.plan_explanation.clone();
let expected_plan = state.plan.clone();
let expected_steps = state.plan_steps.clone();
let expected_active_index = state.active_plan_step_index;
let failed = if budget_exhausted {
fail_game_creator_agent_runtime_budget_at(&root, state, "loop-budget-exhausted:test")
} else {
fail_game_creator_agent_runtime_turn_at(&root, state, "structured failure test")
}
.expect("persist structured terminal runtime");
assert_eq!(failed.plan_revision, expected_revision);
assert_eq!(failed.plan_explanation, expected_explanation);
assert_eq!(failed.plan, expected_plan);
assert_eq!(failed.plan_steps, expected_steps);
assert_eq!(failed.active_plan_step_index, expected_active_index);
assert_eq!(
failed.phase,
if budget_exhausted {
"budget-exhausted"
} else {
"failed"
}
);
let context = read_game_creator_agent_runtime_context_bundle(&root, &failed)
.expect("read terminal structured context")
.expect("terminal structured context exists");
assert_eq!(context.plan_revision, expected_revision);
assert_eq!(context.plan_steps, expected_steps);
assert_eq!(context.active_plan_step_index, expected_active_index);
fs::remove_dir_all(root).ok();
}
}
#[test]
fn structured_plan_state_write_failure_stops_before_context_and_audit() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "结构化计划状态写失败项目")
.expect("project init");
let (request_sender, request_receiver) = mpsc::channel();
let (release_sender, release_receiver) = mpsc::channel();
let response = serde_json::json!({
"thinkingSummary": "准备提交结构化计划",
"planUpdate": {
"explanation": "状态写入失败时不得推进其它投影",
"steps": [
{ "step": "完成检查", "status": "completed" }
]
},
"plan": [],
"actions": [],
"response": "该回复不得落盘"
})
.to_string();
let base_url = spawn_releasable_mock_llm_server_responses_with_capture(
vec![response],
request_sender,
release_receiver,
);
let _config_guard = write_test_local_config(format!(
r#"{{
"agentMode": "provider",
"agentLlm": {{
"design-director": {{
"apiKey": "design-key",
"baseUrl": {base_url:?},
"model": "design-runtime-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
let run_id = "structured-plan-state-write-failure-run";
start_game_creator_agent_background_task_at(
&root,
"design-director",
"验证结构化计划状态写失败边界",
run_id,
)
.expect("start structured state write failure task");
request_receiver
.recv_timeout(Duration::from_secs(2))
.expect("planning request reached provider");
let state_path = root.join(".agent/runtime/agents/design-director.json");
fs::remove_file(&state_path).expect("remove runtime state before response");
fs::create_dir(&state_path).expect("replace runtime state with directory");
release_sender
.send(())
.expect("release structured plan response");
drop(wait_to_acquire_agent_runtime_lock(&root, "design-director"));
let context_path =
game_creator_agent_runtime_context_bundle_path(&root, "design-director", run_id);
let context: Value = serde_json::from_str(
&fs::read_to_string(&context_path).expect("read unchanged context bundle"),
)
.expect("parse unchanged context bundle");
assert_eq!(context["planRevision"], 0);
let records = read_agent_db_records_for_test(&root);
assert!(!records.iter().any(|record| {
record["recordType"] == "agent.runtime.plan_update" && record["runId"] == run_id
}));
let agent_conversation = read_local_conversation_for_session_at(
&root,
Some("design-director"),
Some("agent-session-design-director"),
)
.expect("read state write failure Agent conversation");
assert!(!agent_conversation.messages.iter().any(|message| {
message.role == "assistant" && message.content.contains("该回复不得落盘")
}));
assert!(agent_conversation.messages.iter().any(|message| {
message.role == "assistant"
&& message.content == "专业 Agent 执行失败,请稍后重试"
&& message.message_id.as_deref().is_some_and(|message_id| {
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
})
}));
let conversation_text = serde_json::to_string(&agent_conversation)
.expect("serialize state write failure conversation");
assert!(!conversation_text.contains("absolute-path"));
assert!(!conversation_text.contains("redacted sensitive context"));
fs::remove_dir(&state_path).expect("remove sabotaged runtime state directory");
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn structured_plan_background_runtime_requires_explicit_completion() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "持久计划项目").expect("project init");
fs::write(root.join("game/notes.txt"), "结构化计划证据").expect("write notes");
let (sender, receiver) = mpsc::channel();
let responses = vec![
serde_json::json!({
"thinkingSummary": "先读取项目证据",
"planUpdate": {
"explanation": "首次拆解",
"steps": [
{ "step": "读取项目证据", "status": "in_progress" },
{ "step": "确认并回复", "status": "pending" }
]
},
"plan": [],
"actions": [{
"tool": "file.read",
"reason": "读取真实证据",
"input": { "path": "game/notes.txt" }
}],
"response": ""
})
.to_string(),
serde_json::json!({
"thinkingSummary": "证据已读,仍需完成确认",
"planUpdate": {
"explanation": "读取已完成",
"steps": [
{ "step": "读取项目证据", "status": "completed" },
{ "step": "确认并回复", "status": "in_progress" }
]
},
"plan": [],
"actions": [],
"response": "这条回复必须被未完成计划阻断"
})
.to_string(),
serde_json::json!({
"thinkingSummary": "全部必要步骤已经完成",
"planUpdate": {
"explanation": "确认完成并收束",
"steps": [
{ "step": "读取项目证据", "status": "completed" },
{ "step": "确认并回复", "status": "completed" }
]
},
"plan": [],
"actions": [],
"response": "结构化计划完成后才允许返回。"
})
.to_string(),
];
let base_url = spawn_mock_llm_server_responses_with_capture(responses, 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",
"读取证据并按持久计划收束",
"structured-plan-loop-run",
)
.expect("start structured plan task");
let requests = (0..3)
.map(|_| {
receiver
.recv_timeout(Duration::from_secs(3))
.expect("structured plan request")
})
.collect::<Vec<_>>();
assert!(requests[1].contains("结构化计划证据"));
assert!(requests[1].contains("结构化计划:revision=1"));
assert!(requests[1].contains("[in_progress] 读取项目证据"));
assert!(requests[2].contains("runtime.plan_update"));
assert!(requests[2].contains("结构化计划:revision=2"));
assert!(!requests[2].contains("这条回复必须被未完成计划阻断"));
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(runtime.status, "idle");
assert_eq!(runtime.phase, "completed");
assert_eq!(runtime.plan_revision, 3);
assert_eq!(runtime.plan_explanation, "确认完成并收束");
assert_eq!(runtime.active_plan_step_index, None);
assert!(runtime
.plan_steps
.iter()
.all(|step| step.status == "completed"));
assert_eq!(
runtime.last_response.as_deref(),
Some("结构化计划完成后才允许返回。")
);
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read project revision after plan updates")
.revision,
0,
"plan metadata must not advance the project revision"
);
let plan_audits = read_agent_db_records_for_test(&root)
.into_iter()
.filter(|record| {
record["recordType"] == "agent.runtime.plan_update"
&& record["runId"] == "structured-plan-loop-run"
})
.collect::<Vec<_>>();
assert_eq!(plan_audits.len(), 3);
assert_eq!(
plan_audits
.iter()
.filter_map(|record| record["planRevision"].as_u64())
.collect::<Vec<_>>(),
vec![1, 2, 3]
);
assert!(plan_audits.iter().all(|record| {
record["steps"].as_array().is_some_and(|steps| {
steps
.iter()
.filter(|step| step["status"] == "in_progress")
.count()
<= 1
})
}));
let plan_audit_json = serde_json::to_string(&plan_audits).expect("serialize plan audits");
assert!(!plan_audit_json.contains("读取项目证据"));
assert!(!plan_audit_json.contains("确认并回复"));
assert!(plan_audits.iter().all(|record| {
record["steps"].as_array().is_some_and(|steps| {
steps.iter().all(|step| {
step["stepSha256"]
.as_str()
.is_some_and(|value| value.len() == 64)
&& step.get("step").is_none()
})
})
}));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn structured_plan_same_run_stale_running_resume_preserves_v3_snapshot() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "结构化计划 Runner 恢复项目")
.expect("project init");
let task = "恢复同一 run 的结构化计划";
let run_id = "structured-plan-stale-running-resume-run";
let mut state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
task,
run_id,
"agent-background-task",
"Runner 中断前正在规划",
vec!["旧计划占位".to_string()],
)
.expect("start stale structured runtime");
apply_agent_runtime_plan_update(
&mut state,
&AgentRuntimePlanUpdate {
explanation: "恢复时必须保留完整计划快照".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "读取持久上下文".to_string(),
status: "completed".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "继续 Runner 恢复".to_string(),
status: "in_progress".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "完成最终核对".to_string(),
status: "pending".to_string(),
},
],
},
)
.expect("apply pre-restart structured plan");
state.updated_at = unix_timestamp().saturating_sub(600);
append_game_creator_agent_runtime_task(&root, &state).expect("append stale running task");
write_game_creator_agent_runtime_state(&root, &state)
.expect("persist stale structured runtime");
persist_game_creator_agent_runtime_context(
&root,
&state,
task,
&AgentRuntimeToolPlan::default(),
&[],
0,
&AgentRuntimeContextWindowTracker::default(),
)
.expect("persist pre-restart v3 context");
let persisted_before = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect("read pre-restart context")
.expect("pre-restart context exists");
assert_eq!(persisted_before.plan_revision, 1);
assert_eq!(persisted_before.plan_steps, state.plan_steps);
assert_eq!(persisted_before.active_plan_step_index, Some(1));
let lock_path = root.join(".agent/runtime/locks/design-director.lock");
fs::create_dir_all(lock_path.parent().expect("lock parent")).expect("create lock parent");
fs::write(
&lock_path,
serde_json::json!({
"agentId": "design-director",
"pid": 0,
"createdAt": 1,
})
.to_string(),
)
.expect("write stale Runner lock projection");
let (request_sender, request_receiver) = mpsc::channel();
let (response_sender, response_receiver) = mpsc::channel();
let base_url =
spawn_interactive_mock_llm_server_with_capture(1, request_sender, response_receiver);
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"
}}
}}
}}"#
));
let resumed = resume_game_creator_agent_background_tasks_at(&root)
.expect("resume stale structured running task");
assert_eq!(resumed.len(), 1);
assert_eq!(resumed[0].state.task_id, state.task_id);
assert_eq!(resumed[0].state.session_id, state.session_id);
assert_eq!(resumed[0].state.run_id, run_id);
assert_eq!(resumed[0].state.plan_revision, 1);
assert_eq!(resumed[0].state.plan_explanation, state.plan_explanation);
assert_eq!(resumed[0].state.plan_steps, state.plan_steps);
assert_eq!(resumed[0].state.active_plan_step_index, Some(1));
let replanning_request = request_receiver
.recv_timeout(Duration::from_secs(3))
.expect("same-run resumed planning request");
assert!(replanning_request.contains("结构化计划:revision=1"));
assert!(replanning_request.contains("恢复时必须保留完整计划快照"));
assert!(replanning_request.contains("[completed] 读取持久上下文"));
assert!(replanning_request.contains("[in_progress] 继续 Runner 恢复"));
assert!(replanning_request.contains("[pending] 完成最终核对"));
let running = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read resumed running state")
.state;
assert_eq!(running.task_id, state.task_id);
assert_eq!(running.session_id, state.session_id);
assert_eq!(running.run_id, run_id);
assert_eq!(running.plan_revision, 1);
assert_eq!(running.plan_explanation, state.plan_explanation);
assert_eq!(running.plan_steps, state.plan_steps);
assert_eq!(running.active_plan_step_index, Some(1));
let resumed_context = read_game_creator_agent_runtime_context_bundle(&root, &running)
.expect("read resumed v3 context")
.expect("resumed v3 context exists");
assert_eq!(resumed_context.plan_revision, 1);
assert_eq!(resumed_context.plan_explanation, running.plan_explanation);
assert_eq!(resumed_context.plan, running.plan);
assert_eq!(resumed_context.plan_steps, running.plan_steps);
assert_eq!(resumed_context.active_plan_step_index, Some(1));
response_sender
.send(
serde_json::json!({
"thinkingSummary": "恢复快照可信,可以完成剩余核对",
"planUpdate": {
"explanation": "Runner 恢复后完成剩余步骤",
"steps": [
{ "step": "读取持久上下文", "status": "completed" },
{ "step": "继续 Runner 恢复", "status": "completed" },
{ "step": "完成最终核对", "status": "completed" }
]
},
"plan": [],
"actions": [],
"response": "同一 run 已保留结构化计划并完成恢复。"
})
.to_string(),
)
.expect("release resumed planning response");
let completed = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(completed.task_id, state.task_id);
assert_eq!(completed.session_id, state.session_id);
assert_eq!(completed.run_id, run_id);
assert_eq!(completed.phase, "completed");
assert_eq!(completed.plan_revision, 2);
assert_eq!(completed.active_plan_step_index, None);
assert!(completed
.plan_steps
.iter()
.all(|step| step.status == "completed"));
assert_eq!(
completed.last_response.as_deref(),
Some("同一 run 已保留结构化计划并完成恢复。")
);
fs::remove_dir_all(root).ok();
}
#[test]
fn structured_plan_context_v3_revision_zero_rejects_each_legacy_snapshot_mismatch() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "revision zero context 项目")
.expect("project init");
let task = "验证 revision zero 的 v3 完整快照";
let state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
task,
"structured-plan-context-revision-zero-run",
"agent-background-task",
"准备 legacy context 快照",
vec![
"读取 legacy 项目".to_string(),
"完成 legacy 核对".to_string(),
],
)
.expect("start revision zero context state");
assert_eq!(state.plan_revision, 0);
assert_eq!(state.active_plan_step_index, Some(0));
let bundle = build_game_creator_agent_runtime_context_bundle(
&root,
&state,
task,
&AgentRuntimeToolPlan::default(),
&[],
0,
&AgentRuntimeContextWindowTracker::default(),
)
.expect("build revision zero v3 context");
assert_eq!(
bundle.schema_version,
AGENT_RUNTIME_CONTEXT_BUNDLE_SCHEMA_VERSION
);
assert_eq!(bundle.plan_revision, 0);
let mut plan_mismatch = bundle.clone();
plan_mismatch.plan[0] = "被篡改的 legacy plan".to_string();
write_game_creator_agent_runtime_context_bundle(&root, &plan_mismatch)
.expect("write revision zero plan mismatch");
assert!(
read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect_err("revision zero v3 legacy plan mismatch must fail")
.contains("计划快照与当前状态不匹配")
);
let mut steps_mismatch = bundle.clone();
steps_mismatch.plan_steps[0].title = "被篡改的 legacy planSteps".to_string();
write_game_creator_agent_runtime_context_bundle(&root, &steps_mismatch)
.expect("write revision zero planSteps mismatch");
assert!(
read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect_err("revision zero v3 legacy planSteps mismatch must fail")
.contains("计划快照与当前状态不匹配")
);
let mut active_index_mismatch = bundle.clone();
active_index_mismatch.active_plan_step_index = None;
write_game_creator_agent_runtime_context_bundle(&root, &active_index_mismatch)
.expect("write revision zero active index mismatch");
assert!(
read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect_err("revision zero v3 active index mismatch must fail")
.contains("计划快照与当前状态不匹配")
);
let bundle_path = game_creator_agent_runtime_context_bundle_path(
&root,
"design-director",
"structured-plan-context-revision-zero-run",
);
let mut legacy_v2 = serde_json::to_value(&bundle).expect("serialize revision zero v3 bundle");
let legacy_object = legacy_v2.as_object_mut().expect("legacy v2 bundle object");
legacy_object.insert(
"schemaVersion".to_string(),
Value::String("game-creator-runtime-context-bundle.v2".to_string()),
);
legacy_object.remove("planRevision");
legacy_object.remove("planExplanation");
legacy_object.remove("planSteps");
legacy_object.remove("activePlanStepIndex");
fs::write(
&bundle_path,
serde_json::to_string_pretty(&legacy_v2).expect("serialize revision zero v2 bundle"),
)
.expect("write compatible revision zero v2 bundle");
let migrated = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect("read compatible revision zero v2 bundle")
.expect("revision zero v2 bundle exists");
assert_eq!(
migrated.schema_version,
AGENT_RUNTIME_CONTEXT_BUNDLE_SCHEMA_VERSION
);
assert_eq!(migrated.plan_revision, 0);
assert_eq!(migrated.plan, state.plan);
assert_eq!(migrated.plan_steps, state.plan_steps);
assert_eq!(migrated.active_plan_step_index, Some(0));
fs::remove_dir_all(root).ok();
}
#[test]
fn legacy_context_and_pending_records_fail_closed() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "旧版恢复失败关闭项目").expect("project init");
let state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"拒绝恢复缺少 verification gate 的旧记录",
"legacy-v1-run",
"agent-background-task",
"旧版恢复测试",
vec!["失败关闭".to_string()],
)
.expect("start legacy recovery runtime");
let context_path =
game_creator_agent_runtime_context_bundle_path(&root, "design-director", "legacy-v1-run");
fs::create_dir_all(context_path.parent().expect("context parent"))
.expect("create context parent");
fs::write(
&context_path,
serde_json::json!({
"schemaVersion": "game-creator-runtime-context-bundle.v1",
"agentId": "design-director",
"runId": "legacy-v1-run"
})
.to_string(),
)
.expect("write legacy context bundle");
let context_error = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect_err("legacy context must not restore without a gate snapshot");
assert!(context_error.contains("不支持的 Agent Runtime context bundle 版本"));
let pending_path = game_creator_agent_runtime_pending_tool_action_path(
&root,
"design-director",
"legacy-v1-run",
);
fs::create_dir_all(pending_path.parent().expect("pending parent"))
.expect("create pending parent");
for version in [
"game-creator-pending-action.v1",
"game-creator-pending-action.v2",
"game-creator-pending-action.v3",
"game-creator-pending-action.v4",
] {
fs::write(
&pending_path,
serde_json::json!({
"schemaVersion": version,
"agentId": "design-director",
"runId": "legacy-v1-run"
})
.to_string(),
)
.expect("write legacy pending action");
let pending_error = read_game_creator_agent_runtime_pending_tool_action(
&root,
"design-director",
"legacy-v1-run",
)
.expect_err("legacy pending action must not execute without revision snapshots");
assert!(pending_error.contains("不支持的 Agent Runtime 待确认动作版本"));
assert!(pending_error.contains(version));
}
let resumed = resume_game_creator_agent_background_tasks_at(&root)
.expect("legacy pending action should fail only its own run");
assert!(resumed.iter().any(|runtime| {
runtime.state.run_id == "legacy-v1-run"
&& runtime.state.status == "failed"
&& runtime.state.error.as_deref().is_some_and(|error| {
error.contains("待确认动作恢复失败") && error.contains("pending-action.v4")
})
}));
let art_state = start_game_creator_agent_runtime_task_at(
&root,
"art-director",
"拒绝恢复旧 context bundle",
"legacy-v1-context-run",
"agent-background-task",
"旧 context 恢复测试",
vec!["失败关闭".to_string()],
)
.expect("start legacy context runtime");
let art_context_path = game_creator_agent_runtime_context_bundle_path(
&root,
"art-director",
"legacy-v1-context-run",
);
fs::create_dir_all(art_context_path.parent().expect("art context parent"))
.expect("create art context parent");
fs::write(
&art_context_path,
serde_json::json!({
"schemaVersion": "game-creator-runtime-context-bundle.v1",
"agentId": "art-director",
"sessionId": art_state.session_id,
"runId": "legacy-v1-context-run"
})
.to_string(),
)
.expect("write art legacy context");
resume_game_creator_agent_background_tasks_at(&root)
.expect("legacy context should fail only its own run");
let mut art_runtime = read_game_creator_agent_runtime_at(&root, "art-director")
.expect("read art runtime")
.state;
for _ in 0..100 {
if art_runtime.status == "failed" {
break;
}
std::thread::sleep(Duration::from_millis(20));
art_runtime = read_game_creator_agent_runtime_at(&root, "art-director")
.expect("poll art runtime")
.state;
}
assert_eq!(art_runtime.status, "failed");
assert!(art_runtime.error.as_deref().is_some_and(|error| {
error.contains("context bundle") && error.contains("context-bundle.v1")
}));
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_context_bundle_rejects_task_source_and_loop_identity_mismatches() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "上下文身份项目").expect("project init");
let task = "验证 context bundle 完整身份绑定";
let state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
task,
"design-context-identity-run",
"agent-background-task",
"身份绑定测试",
vec!["验证身份字段".to_string()],
)
.expect("start identity runtime state");
let context_tracker = AgentRuntimeContextWindowTracker::default();
let valid_bundle = build_game_creator_agent_runtime_context_bundle(
&root,
&state,
task,
&AgentRuntimeToolPlan::default(),
&[],
0,
&context_tracker,
)
.expect("build valid identity context bundle");
let mut mismatched_task = valid_bundle.clone();
mismatched_task.task_id = "code-prototype".to_string();
write_game_creator_agent_runtime_context_bundle(&root, &mismatched_task)
.expect("write task-mismatched bundle");
let error = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect_err("taskId mismatch must fail closed");
assert!(error.contains("身份与当前任务不匹配"));
let mut mismatched_source = valid_bundle.clone();
mismatched_source.source = "agent-ready-task-scheduler".to_string();
write_game_creator_agent_runtime_context_bundle(&root, &mismatched_source)
.expect("write source-mismatched bundle");
let error = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect_err("source mismatch must fail closed");
assert!(error.contains("身份与当前任务不匹配"));
write_game_creator_agent_runtime_context_bundle(&root, &valid_bundle)
.expect("write project-bound bundle");
let manifest_path = root.join(".agent/manifest.json");
let mut manifest: Value =
serde_json::from_str(&fs::read_to_string(&manifest_path).expect("read identity manifest"))
.expect("parse identity manifest");
manifest["projectId"] = Value::String("project-2".to_string());
fs::write(
&manifest_path,
serde_json::to_string_pretty(&manifest).expect("serialize changed identity manifest"),
)
.expect("write changed identity manifest");
let error = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect_err("projectId mismatch must fail closed");
assert!(error.contains("身份与当前任务不匹配"));
manifest["projectId"] = Value::String("project-1".to_string());
fs::write(
&manifest_path,
serde_json::to_string_pretty(&manifest).expect("serialize restored identity manifest"),
)
.expect("restore identity manifest");
let mut mismatched_loop = valid_bundle;
mismatched_loop.next_loop_index = 2;
mismatched_loop.context_window = 1;
write_game_creator_agent_runtime_context_bundle(&root, &mismatched_loop)
.expect("write loop-mismatched bundle");
let error = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect_err("loop mismatch must fail closed");
assert!(error.contains("轮次与当前状态不匹配"));
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_context_bundle_redacts_paths_and_secrets_and_limits_plan() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "上下文安全项目").expect("project init");
let mut state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"验证上下文安全持久化",
"design-context-safety-run",
"agent-background-task",
"安全持久化测试",
vec!["生成安全 context bundle".to_string()],
)
.expect("start runtime state");
let root_display = root.to_string_lossy();
let secret = ["s", "k-1234567890abcdef"].concat();
let plan = AgentRuntimeToolPlan {
thinking_summary: format!("检查 {root_display},凭据为 {secret}"),
plan_update: None,
plan: (0..AGENT_RUNTIME_PLAN_STEP_LIMIT + 3)
.map(|index| format!("步骤 {index} 读取 {root_display}"))
.collect(),
actions: Vec::new(),
response: format!(r#"结果位于 {root_display},{{"credential":"{secret}"}}"#),
};
state.plan = plan.plan.clone();
let observations = vec![AgentRuntimeToolObservation {
tool: "project.search".to_string(),
status: "ok".to_string(),
summary: format!("在 {root_display} 找到结果"),
detail: Some(format!(r#"{{"credential":"{secret}"}}"#)),
}];
let context_tracker = AgentRuntimeContextWindowTracker::default();
persist_game_creator_agent_runtime_context(
&root,
&state,
&format!("分析 {root_display}"),
&plan,
&observations,
0,
&context_tracker,
)
.expect("persist sanitized context bundle");
let bundle_path = game_creator_agent_runtime_context_bundle_path(
&root,
"design-director",
"design-context-safety-run",
);
let content = fs::read_to_string(&bundle_path).expect("read sanitized context bundle");
let bundle: AgentRuntimeContextBundle =
serde_json::from_str(&content).expect("parse sanitized context bundle");
assert!(!content.contains(root_display.as_ref()));
assert!(!content.contains(&secret));
assert!(content.contains("$PROJECT_ROOT"));
assert!(content.contains("[redacted-secret]"));
assert_eq!(bundle.plan.len(), AGENT_RUNTIME_PLAN_STEP_LIMIT);
assert!(
fs::metadata(&bundle_path)
.expect("context bundle metadata")
.len()
<= AGENT_RUNTIME_CONTEXT_BUNDLE_MAX_BYTES as u64
);
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_context_bundle_preserves_latest_bounded_content_diff() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "内容差异上下文项目").expect("project init");
let state = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"审查较大的多文件内容差异",
"code-content-diff-context-run",
"agent-background-task",
"持久化内容差异",
vec!["保留最新内容 diff".to_string()],
)
.expect("start runtime state");
let large_hunk = format!(
"checkpointId: checkpoint-large\ncontentFileCount: 2\ncontentTruncated: false\ndiff --git a/game/a.js b/game/a.js\n{}",
"x".repeat(10_000)
);
let mut observations = vec![
AgentRuntimeToolObservation {
tool: "project.diff".to_string(),
status: "ok".to_string(),
summary: "已对比 checkpoint checkpoint-large 的内容".to_string(),
detail: Some(large_hunk),
},
AgentRuntimeToolObservation {
tool: "git.inspect".to_string(),
status: "ok".to_string(),
summary: "已审阅 Git 工作树".to_string(),
detail: Some(format!(
"gitContentFileCount: 2\ngitContentTruncated: false\ndiff --git a/game/a.js b/game/a.js\n{}",
"g".repeat(5_000)
)),
},
];
observations.extend((0..20).map(|index| AgentRuntimeToolObservation {
tool: "file.read".to_string(),
status: "ok".to_string(),
summary: format!("读取后续文件 {index}"),
detail: Some(format!("game/file-{index}.txt")),
}));
let retained_observations =
sanitize_game_creator_agent_runtime_context_observations_for_storage(&root, &observations);
let retained = retained_observations
.iter()
.find(|observation| observation.tool == "project.diff")
.and_then(|observation| observation.detail.as_deref())
.expect("latest content diff retained during compaction");
assert!(retained.chars().count() > 10_000);
assert!(retained_observations.iter().any(|observation| {
observation.tool == "git.inspect"
&& observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("gitContentFileCount: 2"))
}));
persist_game_creator_agent_runtime_context(
&root,
&state,
&state.current_task,
&AgentRuntimeToolPlan::default(),
&observations,
0,
&AgentRuntimeContextWindowTracker::default(),
)
.expect("persist content diff context bundle");
let bundle = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect("read content diff context bundle")
.expect("content diff context bundle exists");
let persisted = bundle
.observations
.iter()
.find(|observation| observation.tool == "project.diff")
.and_then(|observation| observation.detail.as_deref())
.expect("content diff survives context persistence");
assert!(persisted.chars().count() > 10_000);
assert!(persisted.contains("contentFileCount: 2"));
assert!(bundle.observations.iter().any(|observation| {
observation.tool == "git.inspect"
&& observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("gitContentFileCount: 2"))
}));
assert!(
fs::metadata(game_creator_agent_runtime_context_bundle_path(
&root,
"code-prototype",
"code-content-diff-context-run",
))
.expect("content diff bundle metadata")
.len()
<= AGENT_RUNTIME_CONTEXT_BUNDLE_MAX_BYTES as u64
);
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_context_bundle_keeps_completed_milestones_until_sidecar_compaction() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "里程碑上下文项目").expect("project init");
let state = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"保留完整里程碑直到 token-aware 压缩",
"milestone-context-run",
"agent-background-task",
"持久化完整观察",
vec!["保留规范动作事实".to_string()],
)
.expect("start milestone context runtime");
let mut observations = vec![
AgentRuntimeToolObservation {
tool: "agent.spawn_isolated".to_string(),
status: "ok".to_string(),
summary: "已创建 3 个动态隔离 Agent,等待 all join".to_string(),
detail: Some("instanceCount=3 · joinMode=all".to_string()),
},
AgentRuntimeToolObservation {
tool: "project.patchset".to_string(),
status: "ok".to_string(),
summary: "project.patchset 已原子应用 2 项变更".to_string(),
detail: Some("checkpointId=checkpoint-milestone".to_string()),
},
AgentRuntimeToolObservation {
tool: "agent.run_status".to_string(),
status: "ok".to_string(),
summary: "已读取 16 个 Agent 状态,并取得 1 个 ready all-join".to_string(),
detail: Some("readyIsolatedJoins: reviewer 已完成独立审查".to_string()),
},
AgentRuntimeToolObservation {
tool: "agent.action_history".to_string(),
status: "ok".to_string(),
summary: "已读取当前 Agent 的 1 条终态动作".to_string(),
detail: Some(
"{\"actionId\":\"action-111111111111111111111111\",\"tool\":\"project.patchset\",\"status\":\"ok\"}"
.to_string(),
),
},
];
observations.extend((0..20).map(|index| AgentRuntimeToolObservation {
tool: "file.read".to_string(),
status: "ok".to_string(),
summary: format!("读取后续文件 {index}"),
detail: Some(format!("game/file-{index}.txt")),
}));
let bundle = build_game_creator_agent_runtime_context_bundle(
&root,
&state,
&state.current_task,
&AgentRuntimeToolPlan::default(),
&observations,
0,
&AgentRuntimeContextWindowTracker::default(),
)
.expect("build full milestone context bundle");
assert_eq!(bundle.observations.len(), observations.len());
assert!(!bundle.observations.iter().any(|observation| {
matches!(
observation.tool.as_str(),
"runtime.context" | "runtime.milestones"
)
}));
assert!(bundle.observations.iter().any(|observation| {
observation.tool == "agent.action_history"
&& observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("action-111111111111111111111111"))
}));
assert!(bundle.observations.iter().any(|observation| {
observation.tool == "agent.run_status"
&& observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("reviewer 已完成独立审查"))
}));
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_context_bundle_size_limit_includes_trailing_newline() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "上下文大小项目").expect("project init");
let mut state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"验证上下文大小边界",
"design-context-size-run",
"agent-background-task",
"大小边界测试",
vec!["生成边界 context bundle".to_string()],
)
.expect("start runtime state");
let plan = AgentRuntimeToolPlan {
thinking_summary: "上下文大小边界".to_string(),
plan_update: None,
plan: (0..AGENT_RUNTIME_PLAN_STEP_LIMIT)
.map(|_| "p".repeat(180))
.collect(),
actions: Vec::new(),
response: String::new(),
};
state.plan = plan.plan.clone();
let context_tracker = AgentRuntimeContextWindowTracker::default();
let build_candidate = |content_diff_chars: usize| {
let mut observations = (0..186)
.map(|_| AgentRuntimeToolObservation {
tool: "project.search".to_string(),
status: "ok".to_string(),
summary: "s".repeat(320),
detail: Some("界".repeat(1_600)),
})
.collect::<Vec<_>>();
observations.push(AgentRuntimeToolObservation {
tool: "project.diff".to_string(),
status: "ok".to_string(),
summary: "已对比 checkpoint boundary 的内容".to_string(),
detail: Some(format!(
"checkpointId: boundary\ncontentFileCount: 2\ncontentTruncated: false\n{}",
"界".repeat(content_diff_chars)
)),
});
build_game_creator_agent_runtime_context_bundle(
&root,
&state,
&"t".repeat(AGENT_RUNTIME_TASK_MAX_CHARS),
&plan,
&observations,
0,
&context_tracker,
)
.expect("build candidate context bundle")
};
let mut boundary_bundle = None;
let mut low = 0_usize;
let mut high = 24_256_usize;
while low <= high {
let midpoint = low + (high - low) / 2;
let bundle = build_candidate(midpoint);
let serialized_len = serde_json::to_string_pretty(&bundle)
.expect("serialize candidate context bundle")
.len();
if serialized_len <= AGENT_RUNTIME_CONTEXT_BUNDLE_MAX_BYTES {
boundary_bundle = Some((bundle, serialized_len));
low = midpoint.saturating_add(1);
} else if midpoint == 0 {
break;
} else {
high = midpoint - 1;
}
}
let (mut bundle, serialized_len) =
boundary_bundle.expect("find context bundle just below byte limit");
let remaining = AGENT_RUNTIME_CONTEXT_BUNDLE_MAX_BYTES - serialized_len;
assert!(remaining <= 1_200, "remaining context bytes: {remaining}");
bundle.fallback_response = "x".repeat(remaining);
assert_eq!(
serde_json::to_string_pretty(&bundle)
.expect("serialize boundary context bundle")
.len(),
AGENT_RUNTIME_CONTEXT_BUNDLE_MAX_BYTES
);
let error = write_game_creator_agent_runtime_context_bundle(&root, &bundle)
.expect_err("trailing newline must count toward context bundle size limit");
assert!(error.contains("超过") && error.contains("字节上限"));
fs::remove_dir_all(root).ok();
}
#[cfg(unix)]
#[test]
fn agent_runtime_context_bundle_rejects_symlinked_parent_directory() {
use std::os::unix::fs::symlink;
let root = unique_project_path();
let outside = unique_project_path();
init_local_game_project_at(&root, "project-1", "上下文符号链接项目").expect("project init");
fs::create_dir_all(&outside).expect("create outside context directory");
let state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"验证上下文符号链接边界",
"design-context-symlink-run",
"agent-background-task",
"符号链接边界测试",
vec!["拒绝符号链接父目录".to_string()],
)
.expect("start symlink runtime state");
let tracker = AgentRuntimeContextWindowTracker::default();
let bundle = build_game_creator_agent_runtime_context_bundle(
&root,
&state,
&state.current_task,
&AgentRuntimeToolPlan::default(),
&[],
0,
&tracker,
)
.expect("build symlink context bundle");
let context_root = root.join(".agent/runtime/context-bundles");
fs::create_dir_all(&context_root).expect("create context root");
symlink(&outside, context_root.join("design-director"))
.expect("symlink context agent directory");
let error = write_game_creator_agent_runtime_context_bundle(&root, &bundle)
.expect_err("symlinked context parent must fail closed");
assert!(error.contains("符号链接"));
assert!(fs::read_dir(&outside)
.expect("read outside context directory")
.next()
.is_none());
fs::remove_dir_all(root).ok();
fs::remove_dir_all(outside).ok();
}
#[test]
fn agent_runtime_context_bundle_read_is_bounded_by_actual_file_bytes() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "上下文读取上限项目").expect("project init");
let state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"验证上下文读取上限",
"design-context-read-limit-run",
"agent-background-task",
"读取上限测试",
vec!["有界读取 context bundle".to_string()],
)
.expect("start bounded-read runtime state");
let bundle_path = game_creator_agent_runtime_context_bundle_path(
&root,
"design-director",
"design-context-read-limit-run",
);
fs::create_dir_all(bundle_path.parent().expect("bundle parent"))
.expect("create bounded-read bundle parent");
fs::write(
&bundle_path,
vec![b'x'; AGENT_RUNTIME_CONTEXT_BUNDLE_MAX_BYTES + 1],
)
.expect("write oversized context bundle");
let error = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect_err("oversized context bundle must fail before parsing");
assert!(error.contains("超过") && error.contains("字节上限"));
fs::remove_dir_all(root).ok();
}
#[test]
fn repository_context_v1_pending_fingerprint_blocks_project_mutation() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "旧仓库指令快照迁移").expect("project init");
fs::write(root.join("AGENTS.md"), "root rules\n").expect("write root rules");
fs::write(root.join("game/AGENTS.md"), "game rules\n").expect("write scoped rules");
let state = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"验证旧 repository context 不会直接写入",
"repository-context-v1-pending-run",
"agent-background-task",
"准备旧指令快照动作",
vec!["重新确认 scoped AGENTS 指令".to_string()],
)
.expect("start runtime");
let mut pending = pending_tool_action_for_test(
&root,
&state,
AgentRuntimeToolAction {
tool: "file.write".to_string(),
reason: Some("基于旧指令快照写文件".to_string()),
input: serde_json::json!({
"path": "game/legacy-context-write.txt",
"content": "must not land\n"
}),
},
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED,
None,
);
let mut legacy_context = build_repository_startup_context_at(&root).expect("current context");
legacy_context.schema_version = "repository-startup-context-v1".to_string();
for document in &mut legacy_context.documents {
document.scope.clear();
}
pending.planned_repository_context_fingerprint =
repository_startup_context_fingerprint(&legacy_context);
let observation = pending_repository_context_drift_observation(&root, &pending)
.expect("evaluate repository drift")
.expect("legacy fingerprint must drift");
assert_eq!(observation.status, "blocked");
assert!(observation.summary.contains("旧动作未执行"));
assert!(observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("repositoryContextDrift=true")));
assert!(!root.join("game/legacy-context-write.txt").exists());
fs::remove_dir_all(root).ok();
}
#[test]
fn repository_context_v2_pending_fingerprint_blocks_project_mutation() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "旧 Skill catalog 快照迁移")
.expect("project init");
fs::create_dir_all(root.join(".codex/skills/release-capsule")).expect("create skill dir");
fs::write(
root.join(".codex/skills/release-capsule/SKILL.md"),
"---\nname: release-capsule\ndescription: Use for release capsule tasks.\n---\nSKILL_V3_BODY\n",
)
.expect("write skill");
let state = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"验证 v2 repository context 不会绕过 Skill 加载直接写入",
"repository-context-v2-pending-run",
"agent-background-task",
"准备旧 Skill catalog 快照动作",
vec!["重新确认项目 Skill".to_string()],
)
.expect("start runtime");
let mut pending = pending_tool_action_for_test(
&root,
&state,
AgentRuntimeToolAction {
tool: "file.write".to_string(),
reason: Some("基于 v2 快照写文件".to_string()),
input: serde_json::json!({
"path": "game/legacy-skill-context-write.txt",
"content": "must not land\n"
}),
},
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED,
None,
);
let mut legacy_context = build_repository_startup_context_at(&root).expect("current context");
legacy_context.schema_version = "repository-startup-context-v2".to_string();
legacy_context.skills.clear();
legacy_context
.source_paths
.retain(|path| !path.ends_with("/SKILL.md"));
pending.planned_repository_context_fingerprint =
repository_startup_context_fingerprint(&legacy_context);
let observation = pending_repository_context_drift_observation(&root, &pending)
.expect("evaluate repository drift")
.expect("v2 fingerprint must drift");
assert_eq!(observation.status, "blocked");
assert!(observation.summary.contains("旧动作未执行"));
assert!(observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("repositoryContextDrift=true")));
assert!(!root.join("game/legacy-skill-context-write.txt").exists());
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn runtime_v11_closure_repository_context_drift_replans_before_auto_mutations() {
const DRIFT_COMMAND: &str = r#"node -e "require('fs').writeFileSync('AGENTS.md','drifted rules\\n');process.stdout.write('DRIFTED')""#;
for tool in [
"file.write",
"file.patch",
"file.delete",
"project.patchset",
"project.restore",
] {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "启动上下文漂移门禁").expect("project init");
fs::write(root.join("AGENTS.md"), "baseline rules\n").expect("write baseline rules");
fs::write(
root.join("package.json"),
serde_json::to_string_pretty(&serde_json::json!({
"name": "repository-context-drift-fixture",
"private": true,
"scripts": { "check:drift": DRIFT_COMMAND }
}))
.expect("serialize drift package json"),
)
.expect("write drift package json");
let target = root.join(format!("game/{}-target.txt", tool.replace('.', "-")));
let action_input = match tool {
"file.write" => serde_json::json!({
"path": "game/file-write-target.txt",
"content": "stale write must not land\n"
}),
"file.patch" => {
fs::write(&target, "mode = draft\n").expect("write patch target");
serde_json::json!({
"path": "game/file-patch-target.txt",
"oldText": "draft",
"newText": "ready",
"expectedReplacements": 1
})
}
"file.delete" => {
fs::write(&target, "delete target must survive\n").expect("write delete target");
serde_json::json!({ "path": "game/file-delete-target.txt" })
}
"project.patchset" => {
let content = "mode = draft\n";
fs::write(&target, content).expect("write patchset target");
serde_json::json!({
"changes": [{
"operation": "update",
"path": "game/project-patchset-target.txt",
"expectedSha256": format!("{:x}", Sha256::digest(content.as_bytes())),
"oldText": "draft",
"newText": "ready",
"expectedReplacements": 1
}]
})
}
"project.restore" => {
fs::write(&target, "checkpoint version\n").expect("write checkpoint target");
let checkpoint =
create_local_project_checkpoint_at(&root).expect("create restore checkpoint");
fs::write(&target, "current version\n").expect("write current target");
serde_json::json!({ "checkpointId": checkpoint.checkpoint_id })
}
_ => unreachable!(),
};
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow automatic runtime actions");
let action_plan = serde_json::json!({
"thinkingSummary": "先读取验证脚本,再执行会改变规范文件的验证,最后尝试旧修改动作",
"plan": ["读取 package.json", "运行验证脚本", "执行项目修改"],
"actions": [
{
"tool": "file.read",
"reason": "读取原始 script 定义",
"input": { "path": "package.json", "startLine": 1, "maxLines": 40 }
},
{
"tool": "project.verify",
"reason": "执行固定验证脚本",
"input": {
"script": "check:drift",
"expectedCommand": DRIFT_COMMAND,
"timeoutSeconds": 15
}
},
{
"tool": tool,
"reason": "这条动作基于 planning 时的旧仓库规范",
"input": action_input
}
],
"response": ""
})
.to_string();
let final_response = format!("{tool} 已因启动上下文漂移而重新规划。");
let (sender, receiver) = mpsc::channel();
let base_url = spawn_mock_llm_server_responses_with_capture(
vec![
action_plan,
final_tool_plan_response(final_response.clone()),
],
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"
}}
}}
}}"#
));
let run_id = format!("runtime-v11-context-drift-{}", tool.replace('.', "-"));
start_game_creator_agent_background_task_at(
&root,
"design-director",
"验证 planning 后仓库上下文漂移门禁",
&run_id,
)
.expect("start drift runtime task");
receiver
.recv_timeout(Duration::from_secs(2))
.expect("initial planning request");
let replanning_request = receiver
.recv_timeout(Duration::from_secs(10))
.expect("same-run replanning request after drift");
assert!(replanning_request.contains(&run_id));
assert!(replanning_request.contains("repositoryContextDrift=true"));
assert!(replanning_request.contains("旧动作未执行"));
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(runtime.run_id, run_id);
assert_eq!(
runtime.last_response.as_deref(),
Some(final_response.as_str())
);
assert!(runtime.recent_tool_calls.iter().any(|call| {
call.tool == tool && call.status == "blocked" && call.summary.contains("旧动作未执行")
}));
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read unchanged revision")
.revision,
0
);
match tool {
"file.write" => assert!(!target.exists()),
"file.patch" => assert_eq!(
fs::read_to_string(&target).expect("read unpatched target"),
"mode = draft\n"
),
"file.delete" => assert_eq!(
fs::read_to_string(&target).expect("read undeleted target"),
"delete target must survive\n"
),
"project.patchset" => assert_eq!(
fs::read_to_string(&target).expect("read unapplied patchset target"),
"mode = draft\n"
),
"project.restore" => assert_eq!(
fs::read_to_string(&target).expect("read unrestored target"),
"current version\n"
),
_ => unreachable!(),
}
assert_eq!(
fs::read_to_string(root.join("AGENTS.md")).expect("read drifted rules"),
"drifted rules\n"
);
fs::remove_dir_all(root).ok();
}
}
#[test]
fn runtime_v11_closure_parent_terminals_cancel_children_and_suppress_join_once() {
use platform_agent::game_creation::{
GameCreationIsolatedAgentChildResult, GameCreationIsolatedAgentChildSpec,
GameCreationIsolatedAgentJoinMode, GameCreationIsolatedAgentResultStatus,
GameCreationIsolatedAgentSpawnRequest,
};
for terminal in ["failed", "budget-exhausted", "cancelled"] {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "父终态取消隔离子任务")
.expect("project init");
let run_id = format!("runtime-v11-parent-{terminal}");
let mut state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"父任务等待动态隔离子任务",
&run_id,
"agent-background-task",
"等待动态隔离子任务",
vec!["等待 all-join".to_string()],
)
.expect("start parent state");
let request = GameCreationIsolatedAgentSpawnRequest {
children: vec![
GameCreationIsolatedAgentChildSpec {
template_agent_id: "code-prototype".to_string(),
task: "实现 feature-a".to_string(),
acceptance_criteria: vec!["产物存在".to_string()],
expected_artifacts: vec!["game/feature-a/output.txt".to_string()],
write_scopes: vec!["game/feature-a/**".to_string()],
},
GameCreationIsolatedAgentChildSpec {
template_agent_id: "code-prototype".to_string(),
task: "实现 feature-b".to_string(),
acceptance_criteria: vec!["产物存在".to_string()],
expected_artifacts: vec!["game/feature-b/output.txt".to_string()],
write_scopes: vec!["game/feature-b/**".to_string()],
},
],
join_mode: GameCreationIsolatedAgentJoinMode::All,
};
let group = create_or_read_isolated_group_at(
&root,
"design-director",
&run_id,
&state.session_id,
&format!("runtime-v11-terminal-action-{terminal}"),
&request,
)
.expect("create isolated group");
state = match terminal {
"failed" => fail_game_creator_agent_runtime_turn_at(&root, state, "父任务普通失败")
.expect("fail parent"),
"budget-exhausted" => {
fail_game_creator_agent_runtime_budget_at(&root, state, "父任务预算耗尽")
.expect("exhaust parent budget")
}
"cancelled" => {
mark_game_creator_agent_runtime_cancelled_at(
&root,
&mut state,
"父任务已取消",
None,
)
.expect("cancel parent");
state
}
_ => unreachable!(),
};
assert_eq!(state.phase, terminal);
for instance_id in &group.instance_ids {
let instance = resolve_isolated_agent_instance_at(&root, instance_id)
.expect("resolve child instance");
let tombstone = root
.join(".agent/runtime/cancel")
.join(&instance.instance_id)
.join(format!("{}.json", instance.run_id));
let payload: Value = serde_json::from_str(
&fs::read_to_string(&tombstone).expect("read child cancellation tombstone"),
)
.expect("parse child cancellation tombstone");
assert_eq!(payload["agentId"], instance.instance_id);
assert_eq!(payload["runId"], instance.run_id);
}
if terminal == "failed" {
let mut join = None;
for instance_id in &group.instance_ids {
let instance = resolve_isolated_agent_instance_at(&root, instance_id)
.expect("resolve cancelled child");
join = record_isolated_child_result_at(
&root,
&GameCreationIsolatedAgentChildResult {
delegation_id: instance.delegation_id,
instance_id: instance.instance_id,
template_agent_id: instance.template_agent_id,
run_id: instance.run_id,
status: GameCreationIsolatedAgentResultStatus::Cancelled,
summary: "父任务终止后子任务取消".to_string(),
artifacts: Vec::new(),
evidence: Vec::new(),
verified_revision: None,
error: None,
},
)
.expect("record cancelled child result")
.or(join);
}
let join = join.expect("all cancelled children produce a join");
dispatch_isolated_agent_join_at(&root, join.clone()).expect("suppress terminal join");
dispatch_isolated_agent_join_at(&root, join.clone())
.expect("repeat terminal join suppression");
for recovered in reconcile_all_isolated_groups_at(&root).expect("reconcile joins") {
dispatch_isolated_agent_join_at(&root, recovered)
.expect("recovered terminal join stays suppressed");
}
assert_eq!(
read_isolated_join_delivery_at(&root, &join)
.expect("read suppressed delivery")
.expect("suppressed delivery exists")
.status,
IsolatedAgentJoinDeliveryStatus::Suppressed
);
assert!(read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read failed parent runtime")
.recent_tasks
.iter()
.all(|task| task.source != AGENT_RUNTIME_ISOLATED_JOIN_SOURCE));
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert_eq!(
agent_db
.matches("agent.runtime.agent.isolated_join.suppressed")
.count(),
1
);
}
fs::remove_dir_all(root).ok();
}
}
#[test]
fn runtime_v11_closure_started_join_cannot_be_reclaimed_by_parent() {
use platform_agent::game_creation::{
GameCreationIsolatedAgentArtifact, GameCreationIsolatedAgentChildResult,
GameCreationIsolatedAgentChildSpec, GameCreationIsolatedAgentEvidence,
GameCreationIsolatedAgentJoinMode, GameCreationIsolatedAgentResultStatus,
GameCreationIsolatedAgentSpawnRequest,
};
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "启动后 join 不可重认领").expect("project init");
let parent_state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"等待隔离结果",
"runtime-v11-started-join-parent",
"agent-background-task",
"等待隔离结果",
vec!["等待 all-join".to_string()],
)
.expect("start parent state");
let parent_lock = try_acquire_game_creator_agent_runtime_task_lock(&root, "design-director")
.expect("acquire parent lane")
.expect("parent lane available");
let request = GameCreationIsolatedAgentSpawnRequest {
children: vec![GameCreationIsolatedAgentChildSpec {
template_agent_id: "code-prototype".to_string(),
task: "完成独立功能".to_string(),
acceptance_criteria: vec!["产物存在".to_string()],
expected_artifacts: vec!["game/feature/output.txt".to_string()],
write_scopes: vec!["game/feature/**".to_string()],
}],
join_mode: GameCreationIsolatedAgentJoinMode::All,
};
let group = create_or_read_isolated_group_at(
&root,
"design-director",
&parent_state.run_id,
&parent_state.session_id,
"runtime-v11-started-join-action",
&request,
)
.expect("create isolated group");
let instance =
resolve_isolated_agent_instance_at(&root, &group.instance_ids[0]).expect("resolve child");
let join = record_isolated_child_result_at(
&root,
&GameCreationIsolatedAgentChildResult {
delegation_id: instance.delegation_id,
instance_id: instance.instance_id,
template_agent_id: instance.template_agent_id,
run_id: instance.run_id,
status: GameCreationIsolatedAgentResultStatus::Completed,
summary: "独立功能已完成".to_string(),
artifacts: vec![GameCreationIsolatedAgentArtifact {
path: "game/feature/output.txt".to_string(),
sha256: "a".repeat(64),
}],
evidence: vec![GameCreationIsolatedAgentEvidence {
kind: "project.verify".to_string(),
summary: "验证通过".to_string(),
path: None,
sha256: None,
}],
verified_revision: Some(1),
error: None,
},
)
.expect("record child result")
.expect("join ready");
write_agent_runtime_task_record_for_test(
&root,
&AgentRuntimeTaskRecord {
goal_id: None,
goal_revision: 0,
goal_status: None,
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: "design-director".to_string(),
task_id: parent_state.task_id.clone(),
session_id: parent_state.session_id.clone(),
run_id: group.join_run_id.clone(),
source: AGENT_RUNTIME_ISOLATED_JOIN_SOURCE.to_string(),
run_profile: default_agent_runtime_run_profile(),
run_profile_binding_fingerprint: String::new(),
parent_agent_id: None,
parent_run_id: Some(parent_state.run_id.clone()),
delegation_id: Some(group.delegation_group_id.clone()),
task: join.prompt.clone(),
status: "pending".to_string(),
phase: "queued".to_string(),
current_action: "等待执行旧版 join continuation".to_string(),
terminal_detail: None,
error: None,
updated_at: unix_timestamp(),
},
);
let legacy_delivery = write_isolated_join_delivery_at(
&root,
&join,
IsolatedAgentJoinDeliveryStatus::Dispatched,
Some(&group.join_run_id),
None,
)
.expect("write legacy continuation delivery");
assert_eq!(
legacy_delivery.delivery_target,
IsolatedAgentJoinDeliveryTarget::Continuation
);
let join_task = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read parent tasks")
.recent_tasks
.into_iter()
.find(|task| task.run_id == group.join_run_id)
.expect("queued join task");
append_game_creator_agent_runtime_task_record(
&root,
&AgentRuntimeTaskRecord {
goal_id: None,
goal_revision: 0,
goal_status: None,
status: "completed".to_string(),
phase: "completed".to_string(),
current_action: "join continuation 已经调用父 Agent 并完成".to_string(),
terminal_detail: Some("join continuation 已完成".to_string()),
updated_at: unix_timestamp(),
..join_task
},
)
.expect("mark join continuation completed");
let claim = observe_agent_runtime_run_status(
&root,
"design-director",
&parent_state.run_id,
Some("runtime-v11-late-claim-action"),
&serde_json::json!({ "scope": "all" }),
);
assert_eq!(claim.status, "failed");
assert!(claim.summary.contains("join continuation 已开始"));
dispatch_isolated_agent_join_at(&root, join.clone()).expect("repeat join reconciliation");
let delivery = read_isolated_join_delivery_at(&root, &join)
.expect("read join delivery")
.expect("join delivery exists");
assert_eq!(delivery.status, IsolatedAgentJoinDeliveryStatus::Dispatched);
assert_eq!(
delivery.delivery_target,
IsolatedAgentJoinDeliveryTarget::Continuation
);
assert_eq!(
delivery.queued_run_id.as_deref(),
Some(group.join_run_id.as_str())
);
assert!(delivery.claimed_by_action_id.is_none());
let join_tasks = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read stable parent tasks")
.recent_tasks
.into_iter()
.filter(|task| task.source == AGENT_RUNTIME_ISOLATED_JOIN_SOURCE)
.collect::<Vec<_>>();
assert_eq!(join_tasks.len(), 1);
assert_eq!(join_tasks[0].run_id, group.join_run_id);
assert_eq!(join_tasks[0].status, "completed");
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(!agent_db.contains("agent.runtime.agent.isolated_join.claimed_by_parent"));
drop(parent_lock);
fs::remove_dir_all(root).ok();
}
#[test]
fn background_task_does_not_execute_when_user_message_cannot_persist() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "后台任务对话一致性测试").expect("project init");
let session_id = resolve_agent_conversation_session_id_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
None,
true,
)
.expect("resolve Supervisor session");
fs::write(
root.join(".agent/runtime/test-fail-next-background-conversation"),
b"fail once\n",
)
.expect("arm one-shot background conversation failure");
let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)
.expect("acquire runtime lock")
.expect("runtime lock available");
let result = start_game_creator_agent_background_task_for_session_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
Some(&session_id),
"这条任务必须先持久化对话",
"conversation-write-failure-run",
);
assert!(result.is_err());
let task_log = fs::read_to_string(root.join(".agent/runtime/tasks/project-supervisor.jsonl"))
.expect("read failed queued task log");
let task = task_log
.lines()
.filter_map(|line| serde_json::from_str::<AgentRuntimeTaskRecord>(line).ok())
.filter(|task| task.run_id == "conversation-write-failure-run")
.last()
.expect("failed queued task exists");
assert_eq!(task.status, "failed");
assert_eq!(task.phase, "conversation-write-failed");
assert!(!root
.join(".agent/runtime/test-fail-next-background-conversation")
.exists());
let conversation = read_local_conversation_for_session_at(
&root,
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
Some(&session_id),
)
.expect("read conversation after injected user write failure");
assert!(conversation
.messages
.iter()
.all(|message| message.content != "这条任务必须先持久化对话"));
drop(runtime_lock);
fs::remove_dir_all(root).ok();
}
#[test]
fn background_task_closes_when_user_message_identity_conflicts() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "后台任务用户消息身份冲突测试")
.expect("project init");
let session_id = resolve_agent_conversation_session_id_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
None,
true,
)
.expect("resolve Supervisor session");
let run_id = "conversation-identity-conflict-run";
let message_id = agent_runtime_background_task_message_id(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&session_id,
run_id,
"agent-background-task",
);
append_local_conversation_message_for_session_idempotent_at(
&root,
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
Some(&session_id),
LocalConversationMessage {
role: "user".to_string(),
content: "占用同一消息身份的冲突内容".to_string(),
agent_id: None,
},
&message_id,
)
.expect("append conflicting user message");
let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)
.expect("acquire runtime lock")
.expect("runtime lock available");
let result = start_game_creator_agent_background_task_for_session_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
Some(&session_id),
"不能借用冲突消息执行",
run_id,
);
assert!(result.is_err());
let latest = read_latest_game_creator_agent_runtime_task_by_run_id(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect("read identity-conflicted task")
.expect("identity-conflicted task exists");
assert_eq!(latest.status, "failed");
assert_eq!(latest.phase, "conversation-write-failed");
assert!(read_next_runnable_game_creator_agent_runtime_task(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)
.expect("read runnable task after identity conflict")
.is_none());
drop(runtime_lock);
fs::remove_dir_all(root).ok();
}
#[test]
fn background_task_does_not_execute_when_public_start_status_cannot_persist() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "后台任务启动确认一致性测试")
.expect("project init");
let project_conversation_path = root.join(".agent/conversations/project.jsonl");
if project_conversation_path.is_file() {
fs::remove_file(&project_conversation_path).expect("remove project conversation fixture");
}
fs::create_dir_all(&project_conversation_path)
.expect("replace project conversation file with directory");
let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)
.expect("acquire runtime lock")
.expect("runtime lock available");
let result = start_game_creator_agent_background_task_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"这条任务必须先公开确认启动",
"public-start-status-write-failure-run",
);
assert!(result.is_err());
let task_log = fs::read_to_string(root.join(".agent/runtime/tasks/project-supervisor.jsonl"))
.expect("read failed queued task log");
let tasks = task_log
.lines()
.filter_map(|line| serde_json::from_str::<AgentRuntimeTaskRecord>(line).ok())
.filter(|task| task.run_id == "public-start-status-write-failure-run")
.collect::<Vec<_>>();
assert_eq!(
tasks
.iter()
.map(|task| (task.status.as_str(), task.phase.as_str()))
.collect::<Vec<_>>(),
vec![
("preparing", "public-status-pending"),
("failed", "public-status-write-failed"),
]
);
let task = tasks.last().expect("failed queued task exists");
assert_eq!(task.status, "failed");
assert_eq!(task.phase, "public-status-write-failed");
assert!(read_next_runnable_game_creator_agent_runtime_task(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)
.expect("read runnable task after public status failure")
.is_none());
let conversation =
read_local_conversation_at(&root, Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID))
.expect("read Agent conversation after public status failure");
assert!(conversation.messages.iter().any(|message| {
message.role == "user" && message.content == "这条任务必须先公开确认启动"
}));
assert!(conversation
.messages
.iter()
.all(|message| message.role != "assistant"));
drop(runtime_lock);
fs::remove_dir_all(root).ok();
}
#[test]
fn supervisor_start_continues_when_accepted_is_durable_but_audit_write_fails() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "启动确认审计失败恢复测试")
.expect("project init");
let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)
.expect("acquire runtime lock")
.expect("runtime lock available");
let failure_path = root.join(".agent/runtime/test-fail-next-agent-db-record");
fs::write(
&failure_path,
b"conversation.message:runtime-public-status-\n",
)
.expect("inject accepted conversation audit failure");
let run_id = "public-start-status-audit-failure-run";
start_game_creator_agent_background_task_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"accepted 已落盘时必须继续入队",
run_id,
)
.expect("durable accepted message remains authoritative");
assert!(!failure_path.exists());
let latest = read_latest_game_creator_agent_runtime_task_by_run_id(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect("read queued task")
.expect("queued task exists");
assert_eq!(latest.status, "pending");
assert_eq!(latest.phase, "queued");
let project_conversation =
read_local_conversation_at(&root, None).expect("read accepted project conversation");
assert_eq!(
project_conversation
.messages
.iter()
.filter(|message| {
message.message_id.as_deref().is_some_and(|message_id| {
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
})
})
.count(),
1
);
drop(runtime_lock);
fs::remove_dir_all(root).ok();
}
#[test]
fn supervisor_start_continues_when_user_message_is_durable_but_audit_write_fails() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "启动用户消息审计失败恢复测试")
.expect("project init");
let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)
.expect("acquire runtime lock")
.expect("runtime lock available");
let failure_path = root.join(".agent/runtime/test-fail-next-agent-db-record");
fs::write(&failure_path, b"conversation.message:runtime-task-\n")
.expect("inject user conversation audit failure");
let run_id = "user-message-audit-failure-run";
start_game_creator_agent_background_task_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"用户消息已落盘时必须继续入队",
run_id,
)
.expect("durable user message remains authoritative");
assert!(!failure_path.exists());
let latest = read_latest_game_creator_agent_runtime_task_by_run_id(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect("read queued task")
.expect("queued task exists");
assert_eq!(latest.status, "pending");
assert_eq!(latest.phase, "queued");
let supervisor_conversation = read_local_conversation_for_session_at(
&root,
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
Some(&latest.session_id),
)
.expect("read durable user conversation");
assert_eq!(
supervisor_conversation
.messages
.iter()
.filter(|message| message.role == "user"
&& message.content == "用户消息已落盘时必须继续入队")
.count(),
1
);
let project_conversation =
read_local_conversation_at(&root, None).expect("read accepted project conversation");
assert_eq!(
project_conversation
.messages
.iter()
.filter(|message| message.content
== game_creator_agent_runtime_accepted_public_message(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
))
.count(),
1
);
drop(runtime_lock);
fs::remove_dir_all(root).ok();
}
#[test]
fn supervisor_background_task_persists_one_public_start_status_before_execution() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "Supervisor 启动确认测试")
.expect("project init");
let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)
.expect("acquire runtime lock")
.expect("runtime lock available");
let run_id = "supervisor-public-start-status-run";
let queued = start_game_creator_agent_background_task_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"先确认启动,再执行长任务",
run_id,
)
.expect("queue Supervisor task");
let task_log = fs::read_to_string(root.join(".agent/runtime/tasks/project-supervisor.jsonl"))
.expect("read Supervisor start gate task log");
let task_states = task_log
.lines()
.filter_map(|line| serde_json::from_str::<AgentRuntimeTaskRecord>(line).ok())
.filter(|task| task.run_id == run_id)
.map(|task| (task.status, task.phase))
.collect::<Vec<_>>();
assert_eq!(
task_states,
vec![
("preparing".to_string(), "public-status-pending".to_string(),),
("pending".to_string(), "queued".to_string()),
]
);
append_game_creator_agent_runtime_public_status_message_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&queued.state.session_id,
run_id,
"accepted",
&game_creator_agent_runtime_accepted_public_message(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
),
)
.expect("repeat accepted public status idempotently");
let project_conversation =
read_local_conversation_at(&root, None).expect("read public start status");
let public_statuses = project_conversation
.messages
.iter()
.filter(|message| {
message.message_id.as_deref().is_some_and(|message_id| {
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
})
})
.collect::<Vec<_>>();
assert_eq!(public_statuses.len(), 1);
assert_eq!(
public_statuses[0].content,
"任务已接收,项目总控 Agent 正在启动处理。"
);
let supervisor_conversation = read_local_conversation_for_session_at(
&root,
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
Some(&queued.state.session_id),
)
.expect("read Supervisor prompt conversation");
assert_eq!(supervisor_conversation.messages.len(), 1);
assert_eq!(supervisor_conversation.messages[0].role, "user");
assert_eq!(
supervisor_conversation.messages[0].content,
"先确认启动,再执行长任务"
);
let prompt_context = render_local_conversation_prompt_context_for_session(
&root,
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
Some(&queued.state.session_id),
)
.expect("render Supervisor prompt conversation");
assert!(prompt_context.contains("先确认启动,再执行长任务"));
assert!(!prompt_context.contains("任务已接收"));
let runtime_prompt_history = prepare_game_creator_agent_runtime_prompt_history(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&queued.state.session_id,
run_id,
&[],
1_000,
)
.expect("render Runtime prompt history");
assert!(runtime_prompt_history
.context
.contains("先确认启动,再执行长任务"));
assert!(!runtime_prompt_history.context.contains("任务已接收"));
drop(runtime_lock);
fs::remove_dir_all(root).ok();
}
#[test]
fn accepted_supervisor_preparing_task_recovers_to_runnable_queue() {
let _config_guard = write_test_local_config("{}".to_string());
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "Supervisor 启动确认恢复测试")
.expect("project init");
let session_id = resolve_agent_conversation_session_id_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
None,
true,
)
.expect("resolve Supervisor session");
let run_id = "supervisor-public-start-status-recovery-run";
let preparing = append_unique_game_creator_agent_runtime_public_status_preparing_task(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&session_id,
"验证 accepted 后可恢复入队",
run_id,
"agent-background-task",
None,
None,
)
.expect("append preparing task");
assert_eq!(preparing.status, "preparing");
let task_path = root.join(".agent/runtime/tasks/project-supervisor.jsonl");
let task_log_before_preflight = fs::read(&task_path).expect("read preparing task journal");
assert!(
!has_recoverable_game_creator_agent_background_tasks_at(&root)
.expect("preflight task before accepted status")
);
assert_eq!(
fs::read(&task_path).expect("reread preparing task journal"),
task_log_before_preflight
);
assert!(resume_game_creator_agent_background_tasks_at(&root)
.expect("scan task before accepted status")
.is_empty());
let user_message_id = agent_runtime_background_task_message_id(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&session_id,
run_id,
"agent-background-task",
);
append_local_conversation_message_for_session_idempotent_at(
&root,
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
Some(&session_id),
LocalConversationMessage {
role: "user".to_string(),
content: "验证 accepted 后可恢复入队".to_string(),
agent_id: None,
},
&user_message_id,
)
.expect("append persisted user message before accepted crash window");
let task_log_after_user_message =
fs::read(&task_path).expect("read user-persisted preparing journal");
assert!(
has_recoverable_game_creator_agent_background_tasks_at(&root)
.expect("preflight user-persisted preparing task")
);
assert_eq!(
fs::read(&task_path).expect("reread user-persisted preparing journal"),
task_log_after_user_message
);
let resumed = resume_game_creator_agent_background_tasks_at(&root)
.expect("repair accepted status and recover preparing task through Runner scan");
assert_eq!(resumed.len(), 1);
assert_eq!(resumed[0].state.run_id, run_id);
let task_log = fs::read_to_string(root.join(".agent/runtime/tasks/project-supervisor.jsonl"))
.expect("read recovered task journal");
let recovered_states = task_log
.lines()
.filter_map(|line| serde_json::from_str::<AgentRuntimeTaskRecord>(line).ok())
.filter(|task| task.run_id == run_id)
.map(|task| (task.status, task.phase))
.collect::<Vec<_>>();
assert!(recovered_states.contains(&("pending".to_string(), "queued".to_string())));
let project_conversation =
read_local_conversation_at(&root, None).expect("read repaired accepted public status");
assert!(project_conversation.messages.iter().any(|message| {
message.content
== game_creator_agent_runtime_accepted_public_message(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)
&& message.message_id.as_deref().is_some_and(|message_id| {
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
})
}));
drop(wait_to_acquire_agent_runtime_lock(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
));
fs::remove_dir_all(root).ok();
}
#[test]
fn supervisor_terminal_failure_persists_one_public_status() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "Supervisor 终态失败公开消息测试")
.expect("project init");
let state = start_game_creator_agent_runtime_task_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"验证终态失败公开消息",
"supervisor-terminal-public-status-run",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
"准备验证失败收束",
vec!["验证终态失败公开消息".to_string()],
)
.expect("start Supervisor runtime");
let failed = fail_game_creator_agent_runtime_turn_at(&root, state, "private provider failure")
.expect("fail Supervisor runtime");
append_game_creator_agent_runtime_terminal_public_message_at(
&root,
&failed,
"private provider failure",
)
.expect("repeat terminal public status idempotently");
let project_conversation =
read_local_conversation_at(&root, None).expect("read public terminal status");
let public_statuses = project_conversation
.messages
.iter()
.filter(|message| {
message.message_id.as_deref().is_some_and(|message_id| {
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
})
})
.collect::<Vec<_>>();
assert_eq!(public_statuses.len(), 1);
assert_eq!(
public_statuses[0].content,
"项目总控 Agent 执行失败,请稍后重试"
);
assert!(!serde_json::to_string(&project_conversation)
.expect("serialize public terminal conversation")
.contains("private provider failure"));
fs::remove_dir_all(root).ok();
}
#[test]
fn supervisor_terminal_failure_retries_one_shot_public_status_write() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "Supervisor 终态公开消息重试测试")
.expect("project init");
let state = start_game_creator_agent_runtime_task_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"验证终态公开消息瞬时失败重试",
"supervisor-terminal-public-status-retry-run",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
"准备验证失败收束",
vec!["验证终态公开消息瞬时失败重试".to_string()],
)
.expect("start Supervisor runtime");
let failure_path = root.join(".agent/runtime/test-fail-next-conversation-append");
fs::write(&failure_path, b"assistant\n").expect("arm one-shot public status failure");
let failed = fail_game_creator_agent_runtime_turn_at(&root, state, "private provider failure")
.expect("retry and fail Supervisor runtime");
assert_eq!(failed.status, "failed");
assert!(!failure_path.exists());
let project_conversation =
read_local_conversation_at(&root, None).expect("read retried public terminal status");
assert_eq!(
project_conversation
.messages
.iter()
.filter(|message| message.content == "项目总控 Agent 执行失败,请稍后重试")
.count(),
1
);
fs::remove_dir_all(root).ok();
}
#[test]
fn supervisor_continuation_failure_stays_out_of_project_conversation() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "Supervisor continuation 终态边界测试")
.expect("project init");
let session_id = resolve_agent_conversation_session_id_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
None,
true,
)
.expect("resolve Supervisor session");
let parent = append_unique_game_creator_agent_runtime_pending_task(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&session_id,
"Supervisor parent task",
"supervisor-continuation-parent-run",
"agent-background-task",
None,
None,
)
.expect("append Supervisor parent task");
let continuation = append_unique_game_creator_agent_runtime_pending_task(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&session_id,
"Supervisor receipt continuation",
"supervisor-continuation-child-run",
"agent-delegate-receipt",
None,
Some(&AgentRuntimeTaskLink {
parent_agent_id: None,
parent_run_id: Some(parent.run_id.clone()),
delegation_id: Some("supervisor-continuation-delegation".to_string()),
}),
)
.expect("append Supervisor continuation task");
let state = agent_runtime_state_from_task_record(&continuation);
fail_game_creator_agent_runtime_turn_at(&root, state, "private continuation failure")
.expect("fail Supervisor continuation");
let project_conversation =
read_local_conversation_at(&root, None).expect("read project conversation");
assert!(project_conversation.messages.iter().all(|message| {
!message.message_id.as_deref().is_some_and(|message_id| {
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
})
}));
let supervisor_conversation = read_local_conversation_for_session_at(
&root,
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
Some(&session_id),
)
.expect("read Supervisor continuation conversation");
assert!(supervisor_conversation.messages.iter().all(|message| {
!message.message_id.as_deref().is_some_and(|message_id| {
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
})
}));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_task_recovers_when_assistant_message_cannot_persist() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "后台回复对话一致性测试").expect("project init");
let (request_sender, request_receiver) = mpsc::channel();
let (release_sender, release_receiver) = mpsc::channel();
let base_url = spawn_releasable_mock_llm_server_responses_with_capture(
vec![final_tool_plan_response("这条后台回复必须先持久化。")],
request_sender,
release_receiver,
);
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",
"验证后台 assistant 回复落盘失败",
"assistant-conversation-write-failure-run",
)
.expect("start background task");
request_receiver
.recv_timeout(Duration::from_secs(2))
.expect("background planning request in flight");
let session_id =
resolve_agent_conversation_session_id_at(&root, "design-director", None, false)
.expect("resolve agent session");
let conversation_failure_path = root.join(".agent/runtime/test-fail-next-conversation-append");
fs::write(&conversation_failure_path, b"assistant\n")
.expect("arm one-shot assistant conversation failure");
release_sender
.send(())
.expect("release background planning response");
let mut result = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read assistant persistence runtime");
for _ in 0..100 {
if result.state.phase == "finalizing" {
break;
}
std::thread::sleep(Duration::from_millis(20));
result = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read assistant persistence runtime");
}
assert_eq!(result.state.status, "running");
assert_eq!(result.state.phase, "finalizing");
assert!(result.state.error.is_some());
assert!(!conversation_failure_path.exists());
assert!(result.recent_tasks.iter().any(|task| {
task.run_id == "assistant-conversation-write-failure-run"
&& task.status == "running"
&& task.phase == "finalizing"
}));
assert!(!result.recent_tasks.iter().any(|task| {
task.run_id == "assistant-conversation-write-failure-run" && task.status == "completed"
}));
assert!(result
.recent_events
.iter()
.any(|event| event.event_type == "finalization.pending"));
assert!(!result
.recent_events
.iter()
.any(|event| event.event_type == "turn.failed"));
assert!(!result
.recent_events
.iter()
.any(|event| event.event_type == "turn.completed"));
let conversation =
read_local_conversation_for_session_at(&root, Some("design-director"), Some(&session_id))
.expect("read failed assistant conversation");
assert!(conversation
.messages
.iter()
.all(|message| message.role != "assistant"));
let mut audit_records = Vec::new();
for _ in 0..100 {
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
audit_records = agent_db
.lines()
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
.collect::<Vec<_>>();
if audit_records.iter().any(|record| {
record.get("recordType").and_then(Value::as_str)
== Some("agent.runtime.background_task.finalization_pending")
&& record.get("runId").and_then(Value::as_str)
== Some("assistant-conversation-write-failure-run")
}) {
break;
}
std::thread::sleep(Duration::from_millis(20));
}
assert!(audit_records.iter().any(|record| {
record.get("recordType").and_then(Value::as_str)
== Some("agent.runtime.background_task.finalization_pending")
&& record.get("runId").and_then(Value::as_str)
== Some("assistant-conversation-write-failure-run")
}));
assert!(!audit_records.iter().any(|record| {
record.get("recordType").and_then(Value::as_str)
== Some("agent.runtime.background_task.failed")
&& record.get("runId").and_then(Value::as_str)
== Some("assistant-conversation-write-failure-run")
}));
assert!(!audit_records.iter().any(|record| {
record.get("recordType").and_then(Value::as_str)
== Some("agent.runtime.background_task.completed")
&& record.get("runId").and_then(Value::as_str)
== Some("assistant-conversation-write-failure-run")
}));
assert!(!audit_records.iter().any(|record| {
record.get("recordType").and_then(Value::as_str) == Some("agent.runtime.completed")
&& record.get("runId").and_then(Value::as_str)
== Some("assistant-conversation-write-failure-run")
}));
let resumed = resume_game_creator_agent_background_tasks_at(&root)
.expect("resume assistant conversation persistence");
assert_eq!(resumed.len(), 1);
assert!(request_receiver
.recv_timeout(Duration::from_millis(250))
.is_err());
let completed = wait_for_agent_runtime_terminal_and_lane_release(
&root,
"design-director",
"assistant-conversation-write-failure-run",
"idle",
"completed",
)
.state;
assert_eq!(completed.phase, "completed");
assert_eq!(completed.run_id, "assistant-conversation-write-failure-run");
assert_eq!(
completed.last_response.as_deref(),
Some("这条后台回复必须先持久化。")
);
let conversation =
read_local_conversation_for_session_at(&root, Some("design-director"), Some(&session_id))
.expect("read recovered assistant conversation");
assert_eq!(
conversation
.messages
.iter()
.filter(|message| {
message.role == "assistant" && message.content == "这条后台回复必须先持久化。"
})
.count(),
1
);
assert!(read_game_creator_agent_runtime_finalization_journal(
&root,
"design-director",
"assistant-conversation-write-failure-run",
)
.expect("read recovered assistant finalization journal")
.is_none());
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn task_update_requires_registered_visual_asset_before_completion() {
let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"visual-task-update-test-key"}}"#.to_string(),
);
for (task_id, local_path, kind, missing_summary) in [
(
"art-director",
"assets/art-spec.png",
"icon-spec",
"统一视觉规范图尚未按正式视觉流程生成并登记",
),
(
"design-foundation",
"assets/ui-prototype.png",
"ui-prototype",
"策划界面原型图尚未按正式视觉流程生成并登记",
),
(
"art-asset-plan",
"assets/art-spritesheet.png",
"art-spritesheet",
"首版美术素材图尚未按正式视觉流程生成并登记",
),
] {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "视觉任务完成门禁测试")
.expect("project init");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow task update");
let run_id = format!("visual-task-update-{task_id}");
let action = AgentRuntimeToolAction {
tool: "task.update".to_string(),
reason: Some("标记视觉任务完成".to_string()),
input: serde_json::json!({
"taskId": task_id,
"status": "completed"
}),
};
let missing = execute_game_creator_agent_runtime_tool_action_with_action_id(
&root,
task_id,
&run_id,
"完成视觉任务",
&action,
Some("visual-task-update-missing"),
)
.await;
assert_eq!(missing.status, "failed");
assert!(missing.summary.contains(missing_summary));
let manifest = read_manifest_for_project(&root).expect("manifest after rejected update");
assert_eq!(
manifest
.tasks
.iter()
.find(|task| task.id == task_id)
.expect("visual task")
.status,
GameCreationAppTaskStatus::Pending
);
if task_id != "art-director" {
register_canvas_visual_asset_fixture(&root, "assets/art-spec.png", "icon-spec");
}
if task_id == "art-asset-plan" {
register_canvas_visual_asset_fixture(&root, "assets/ui-prototype.png", "ui-prototype");
}
register_canvas_visual_asset_fixture(&root, local_path, kind);
if task_id == "design-foundation" {
append_ui_prototype_inspection_fixture(&root, &run_id, false);
let scene_rejected = execute_game_creator_agent_runtime_tool_action_with_action_id(
&root,
task_id,
&run_id,
"拒绝用场景图完成 UI 原型任务",
&action,
Some("visual_task_update_scene_rejected"),
)
.await;
assert_eq!(scene_rejected.status, "failed");
assert!(scene_rejected.summary.contains("结构化 UI 视觉检查"));
let manifest =
read_manifest_for_project(&root).expect("manifest after rejected scene image");
assert_eq!(
manifest
.tasks
.iter()
.find(|task| task.id == task_id)
.expect("design visual task")
.status,
GameCreationAppTaskStatus::Pending
);
append_ui_prototype_inspection_fixture(&root, &run_id, true);
}
let completed = execute_game_creator_agent_runtime_tool_action_with_action_id(
&root,
task_id,
&run_id,
"完成视觉任务",
&action,
Some("visual-task-update-completed"),
)
.await;
assert_eq!(completed.status, "ok", "{completed:?}");
let manifest = read_manifest_for_project(&root).expect("manifest after visual completion");
assert_eq!(
manifest
.tasks
.iter()
.find(|task| task.id == task_id)
.expect("visual task")
.status,
GameCreationAppTaskStatus::Completed
);
fs::remove_dir_all(root).ok();
}
}
#[test]
fn schedule_ready_manifest_tasks_command_requires_auto_policy() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let error = schedule_game_creator_agent_ready_tasks(root.to_string_lossy().into_owned(), 0)
.expect_err("default policy should require confirmation");
assert!(error.contains("项目权限策略要求用户确认:agent.schedule_ready"));
let manifest: Value =
serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap())
.expect("manifest json");
assert_task_status(&manifest, "design-director", "pending");
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(!agent_db.contains("\"recordType\":\"agent.runtime.ready_task.scheduled\""));
fs::remove_dir_all(root).ok();
}
#[cfg(unix)]
#[test]
fn agent_db_rejects_symlinked_project_root() {
use std::os::unix::fs::symlink;
let root = unique_project_path();
let link = unique_project_path();
init_local_game_project_at(&root, "project-1", "Agent DB 根目录链接测试")
.expect("project init");
let agent_db_path = root.join(".agent/agent.db");
let before = fs::read(&agent_db_path).expect("read agent db before symlink attempt");
symlink(&root, &link).expect("create project root symlink");
let error = append_agent_db_record(
&link,
serde_json::json!({
"recordType": "test.symlinked-project-root",
"mustNotPersist": true,
}),
)
.expect_err("symlinked project root must be rejected");
assert!(!error.trim().is_empty());
assert_eq!(
fs::read(&agent_db_path).expect("read unchanged agent db"),
before
);
fs::remove_file(link).ok();
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_db_idempotent_append_reopens_storage_after_directory_replacement() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "Agent DB 身份替换测试").expect("project init");
let record_type = "agent.runtime.action_receipt";
let agent_id = "design-director";
let run_id = "design-agent-db-replacement-run";
let action_id = "action-111111111111111111111111";
let record = serde_json::json!({
"recordType": record_type,
"agentId": agent_id,
"taskId": agent_id,
"sessionId": "session-agent-db-replacement",
"runId": run_id,
"actionId": action_id,
"actionFingerprint": "1".repeat(64),
"tool": "file.list",
"executionMode": "auto",
"status": "ok",
"inputSummary": serde_json::Value::Null,
"summary": "旧目录中的同 key 记录",
"safeDetail": serde_json::Value::Null,
"detailUnavailable": true,
});
assert!(append_agent_db_record_if_missing_for_action(
&root,
record_type,
agent_id,
run_id,
action_id,
record.clone(),
)
.expect("append original identity"));
let swap_root = root.clone();
let appended = append_agent_db_record_if_missing_for_action_with_before_lock(
&root,
record_type,
agent_id,
run_id,
action_id,
record,
move || {
fs::rename(swap_root.join(".agent"), swap_root.join(".agent-replaced"))
.expect("move old agent directory");
fs::create_dir(swap_root.join(".agent")).expect("create replacement agent directory");
},
)
.expect("append must rescan replacement database");
assert!(
appended,
"the pre-lock directory identity must not suppress append to the new database"
);
let records = fs::read_to_string(root.join(".agent/agent.db"))
.expect("read replacement agent db")
.lines()
.map(|line| serde_json::from_str::<Value>(line).expect("replacement record json"))
.collect::<Vec<_>>();
assert_eq!(
records
.iter()
.filter(|record| {
record["recordType"] == record_type && record["actionId"] == action_id
})
.count(),
1
);
fs::remove_dir_all(root).ok();
}
#[test]
fn bounded_agent_db_read_keeps_complete_records_before_partial_utf8_tail() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "Agent DB UTF-8 尾部测试")
.expect("project init");
append_agent_db_record(
&root,
serde_json::json!({
"recordType": "test.before-partial-utf8",
"marker": "完整中文记录",
}),
)
.expect("append complete record");
let mut file = fs::OpenOptions::new()
.append(true)
.open(root.join(".agent/agent.db"))
.expect("open agent db for partial utf8 tail");
file.write_all(b"{\"recordType\":\"test.partial\",\"text\":\"")
.expect("write partial json prefix");
file.write_all(&[0xe4, 0xb8])
.expect("write incomplete utf8 codepoint");
drop(file);
let (records, truncated) =
read_agent_db_records_bounded(&root, 32 * 1024 * 1024).expect("read bounded records");
assert!(truncated);
assert!(records.iter().any(|record| {
record["recordType"] == "test.before-partial-utf8" && record["marker"] == "完整中文记录"
}));
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_db_tail_repair_accepts_exact_one_mib_record_boundary() {
const ONE_MIB: usize = 1024 * 1024;
for valid_tail in [true, false] {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "Agent DB 精确尾窗测试")
.expect("project init");
let path = root.join(".agent/agent.db");
let prefix = b"{\"recordType\":\"test.boundary-prefix\"}\n";
let mut content = prefix.to_vec();
if valid_tail {
content.extend_from_slice(&exact_agent_db_json_record(
"test.exact-one-mib-tail",
ONE_MIB,
));
} else {
content.extend_from_slice(b"{\"recordType\":\"test.incomplete-one-mib-tail\",");
content.resize(prefix.len() + ONE_MIB, b'x');
}
fs::write(&path, content).expect("write exact one MiB tail fixture");
append_agent_db_record(
&root,
serde_json::json!({
"recordType": "test.after-exact-one-mib-tail",
"validTail": valid_tail,
}),
)
.expect("repair exact boundary and append record");
let records = fs::read_to_string(&path)
.expect("read repaired exact boundary database")
.lines()
.map(|line| serde_json::from_str::<Value>(line).expect("all repaired lines are json"))
.collect::<Vec<_>>();
assert!(records
.iter()
.any(|record| record["recordType"] == "test.boundary-prefix"));
assert!(records.iter().any(|record| {
record["recordType"] == "test.after-exact-one-mib-tail"
&& record["validTail"] == valid_tail
}));
assert_eq!(
records
.iter()
.any(|record| record["recordType"] == "test.exact-one-mib-tail"),
valid_tail
);
fs::remove_dir_all(root).ok();
}
}
#[test]
fn agent_db_action_scan_rejects_oversized_complete_line() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "Agent DB 超长单行测试").expect("project init");
let path = root.join(".agent/agent.db");
let mut file = fs::OpenOptions::new()
.append(true)
.open(&path)
.expect("open agent db for oversized line");
file.write_all(&exact_agent_db_json_record(
"test.oversized-complete-line",
2 * 1024 * 1024,
))
.expect("write oversized complete record");
file.write_all(b"\n").expect("finish oversized record");
drop(file);
let error = append_agent_db_record_if_missing_for_action(
&root,
AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE,
"design-director",
"design-oversized-line-run",
"action-222222222222222222222222",
serde_json::json!({
"recordType": AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE,
"agentId": "design-director",
"taskId": "design-director",
"sessionId": "session-oversized-line",
"runId": "design-oversized-line-run",
"actionId": "action-222222222222222222222222",
"actionFingerprint": "2".repeat(64),
"tool": "file.list",
"executionMode": "auto",
"status": "ok",
"inputSummary": serde_json::Value::Null,
"summary": "不得越过超长审计行",
"safeDetail": serde_json::Value::Null,
"detailUnavailable": true,
}),
)
.expect_err("oversized complete line must fail closed");
assert!(error.contains("单条记录"), "unexpected error: {error}");
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn pending_terminal_observation_persists_receipt_before_cancellation() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "终态观察取消顺序测试").expect("project init");
let mut state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"在取消前持久化已完成观察",
"design-terminal-observation-cancel-run",
"agent-background-task",
"先写回执再取消",
vec!["保持动作审计完整".to_string()],
)
.expect("start runtime");
state.loop_iteration = 1;
let action = AgentRuntimeToolAction {
tool: "file.list".to_string(),
reason: Some("读取项目目录".to_string()),
input: serde_json::json!({}),
};
let observation = AgentRuntimeToolObservation {
tool: "file.list".to_string(),
status: "blocked".to_string(),
summary: "开发者拒绝待确认工具动作".to_string(),
detail: Some("取消请求到达前 observation 已持久化".to_string()),
};
let pending = pending_tool_action_for_test(
&root,
&state,
action,
AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED,
Some(observation),
);
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
.expect("write terminal observed pending");
state.status = "running".to_string();
state.phase = "observation".to_string();
state.pending_tool_action = Some(pending.summary());
append_game_creator_agent_runtime_task(&root, &state).expect("append observation task");
write_game_creator_agent_runtime_state(&root, &state).expect("write observation state");
write_game_creator_agent_runtime_cancel_request(
&root,
"design-director",
&state.run_id,
"测试终态 observation 与取消竞态",
)
.expect("write cancellation tombstone");
continue_game_creator_agent_pending_tool_action(
root.clone(),
"design-director".to_string(),
pending.clone(),
state.clone(),
)
.await;
let receipts = read_agent_db_records_for_test(&root)
.into_iter()
.filter(|record| {
record["recordType"] == AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE
&& record["actionId"] == pending.action_id
})
.collect::<Vec<_>>();
assert_eq!(receipts.len(), 1);
assert_eq!(receipts[0]["status"], "blocked");
let result = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read cancelled runtime");
assert_eq!(result.state.status, "cancelled");
assert_eq!(result.state.phase, "cancelled");
assert!(!game_creator_agent_runtime_pending_tool_action_exists(
&root,
"design-director",
&state.run_id
));
fs::remove_dir_all(root).ok();
}
#[test]
fn first_pass_reconciliation_observation_applies_cancellation_after_receipt() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "首轮核对态取消顺序测试").expect("project init");
let mut state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"首轮核对态先写回执再取消",
"design-first-pass-reconciliation-cancel-run",
"agent-background-task",
"保持回执完整后结束任务",
vec!["验证取消顺序".to_string()],
)
.expect("start runtime");
state.loop_iteration = 1;
let action = AgentRuntimeToolAction {
tool: "file.write".to_string(),
reason: Some("模拟已返回但需核对的写入".to_string()),
input: serde_json::json!({
"path": "game/reconciliation-result.txt",
"content": "动作不会在测试中重放"
}),
};
let observation = AgentRuntimeToolObservation {
tool: "file.write".to_string(),
status: AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION.to_string(),
summary: "首轮工具结果需要人工核对".to_string(),
detail: Some("工具已返回终态,取消只能在 receipt 后生效".to_string()),
};
let mut pending = pending_tool_action_for_test(
&root,
&state,
action,
AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_APPROVED,
Some(observation.clone()),
);
pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string();
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
.expect("write reconciliation pending");
state.pending_tool_action = Some(pending.summary());
write_game_creator_agent_runtime_state(&root, &state).expect("write running state");
write_game_creator_agent_runtime_cancel_request(
&root,
"design-director",
&state.run_id,
"测试首轮 reconciliation 与取消竞态",
)
.expect("write cancellation tombstone");
assert!(
persist_agent_runtime_reconciliation_observation_before_cancellation(
&root,
&mut state,
&pending,
&observation,
)
);
assert_eq!(state.status, "cancelled");
assert_eq!(state.phase, "cancelled");
assert!(!game_creator_agent_runtime_pending_tool_action_exists(
&root,
"design-director",
&state.run_id
));
let receipts = read_agent_db_records_for_test(&root)
.into_iter()
.filter(|record| {
record["recordType"] == AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE
&& record["actionId"] == pending.action_id
})
.collect::<Vec<_>>();
assert_eq!(receipts.len(), 1);
assert_eq!(
receipts[0]["status"],
AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION
);
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn receipt_failure_keeps_terminal_pending_when_cancellation_is_already_requested() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "回执失败取消竞态测试").expect("project init");
let mut state = start_game_creator_agent_runtime_task_at(
&root,
"design-director",
"回执失败时保留终态动作",
"design-receipt-failure-cancel-run",
"agent-background-task",
"失败关闭且不清理 pending",
vec!["保留恢复入口".to_string()],
)
.expect("start runtime");
state.loop_iteration = 1;
let action = AgentRuntimeToolAction {
tool: "file.list".to_string(),
reason: Some("读取项目目录".to_string()),
input: serde_json::json!({}),
};
let observation = AgentRuntimeToolObservation {
tool: "file.list".to_string(),
status: "blocked".to_string(),
summary: "开发者拒绝待确认工具动作".to_string(),
detail: None,
};
let pending = pending_tool_action_for_test(
&root,
&state,
action,
AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED,
Some(observation),
);
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
.expect("write terminal observed pending");
state.status = "running".to_string();
state.phase = "observation".to_string();
state.pending_tool_action = Some(pending.summary());
append_game_creator_agent_runtime_task(&root, &state).expect("append observation task");
write_game_creator_agent_runtime_state(&root, &state).expect("write observation state");
append_agent_db_record_fixture(
&root,
serde_json::json!({
"recordType": AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE,
"agentId": state.agent_id,
"taskId": state.task_id,
"sessionId": state.session_id,
"runId": state.run_id,
"actionId": pending.action_id,
"actionFingerprint": pending.action_fingerprint,
"tool": pending.action.tool,
"executionMode": pending.execution_mode,
"status": "blocked",
"inputSummary": pending.input_summary,
"summary": "故意制造身份冲突的既有回执",
"safeDetail": serde_json::Value::Null,
"detailUnavailable": false,
}),
)
.expect("append conflicting receipt fixture");
write_game_creator_agent_runtime_cancel_request(
&root,
"design-director",
&state.run_id,
"测试 receipt 失败与取消同时发生",
)
.expect("write cancellation tombstone");
continue_game_creator_agent_pending_tool_action(
root.clone(),
"design-director".to_string(),
pending.clone(),
state.clone(),
)
.await;
let result = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read reconciliation runtime");
assert_eq!(result.state.status, "failed");
assert_eq!(result.state.phase, "needs-reconciliation");
assert!(result
.state
.error
.as_deref()
.is_some_and(|error| error.contains("持久动作回执")));
assert!(game_creator_agent_runtime_pending_tool_action_exists(
&root,
"design-director",
&state.run_id
));
assert!(root
.join(".agent/runtime/cancel/design-director")
.join(format!("{}.json", state.run_id))
.is_file());
let receipts = read_agent_db_records_for_test(&root)
.into_iter()
.filter(|record| {
record["recordType"] == AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE
&& record["actionId"] == pending.action_id
})
.collect::<Vec<_>>();
assert_eq!(receipts.len(), 1);
assert_eq!(receipts[0]["summary"], "故意制造身份冲突的既有回执");
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn foreground_agent_chat_rejects_same_agent_background_lane_overlap() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let (request_sender, request_receiver) = mpsc::channel();
let (release_sender, release_receiver) = mpsc::channel();
let base_url = spawn_releasable_mock_llm_server_responses_with_capture(
vec![serde_json::json!({
"thinkingSummary": "后台任务占用同 Agent lane",
"plan": [],
"actions": [],
"response": "后台任务完成。"
})
.to_string()],
request_sender,
release_receiver,
);
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",
"正在执行的后台任务",
"design-background-lane-run",
)
.expect("start background task");
request_receiver
.recv_timeout(Duration::from_secs(2))
.expect("background request in flight");
let error = chat_with_game_creator_role_agent(
root.to_string_lossy().into_owned(),
"design-director".to_string(),
None,
"这条前台消息不能并发覆盖状态".to_string(),
)
.await
.expect_err("foreground chat must respect the background lane");
assert!(error.contains("Agent 正在执行其他前台或后台任务:design-director"));
let running = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read background runtime");
assert_eq!(running.state.run_id, "design-background-lane-run");
assert_eq!(running.state.status, "running");
release_sender
.send(())
.expect("release background response");
let completed = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(completed.run_id, "design-background-lane-run");
fs::remove_dir_all(root).ok();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn foreground_agent_chat_releases_lane_then_drains_queued_background_task() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let (request_sender, request_receiver) = mpsc::channel();
let (release_sender, release_receiver) = mpsc::channel();
let base_url = spawn_releasable_mock_llm_server_responses_with_capture(
vec![
"前台 Agent 已完成回复。".to_string(),
serde_json::json!({
"thinkingSummary": "前台结束后处理排队任务",
"plan": [],
"actions": [],
"response": "排队后台任务完成。"
})
.to_string(),
],
request_sender,
release_receiver,
);
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"
}}
}}
}}"#
));
let project_path = root.to_string_lossy().into_owned();
let foreground = tokio::spawn(async move {
chat_with_game_creator_role_agent(
project_path,
"design-director".to_string(),
None,
"先完成前台对话".to_string(),
)
.await
});
tokio::task::yield_now().await;
let foreground_request = request_receiver
.recv_timeout(Duration::from_secs(2))
.expect("foreground request in flight");
assert!(foreground_request.contains("先完成前台对话"));
let project_lock = acquire_project_write_lock(&root, "test.concurrent-project-write")
.expect("foreground LLM wait must not hold the project write lock");
drop(project_lock);
let queued = start_game_creator_agent_background_task_at(
&root,
"design-director",
"前台结束后执行的后台任务",
"design-after-foreground-run",
)
.expect("queue background task while foreground runs");
assert!(queued
.recent_tasks
.iter()
.any(|task| { task.run_id == "design-after-foreground-run" && task.status == "pending" }));
assert!(request_receiver
.recv_timeout(Duration::from_millis(200))
.is_err());
release_sender
.send(())
.expect("release foreground response");
let foreground_reply = foreground
.await
.expect("foreground join")
.expect("foreground reply");
assert_eq!(foreground_reply.reply_text, "前台 Agent 已完成回复。");
let background_request = request_receiver
.recv_timeout(Duration::from_secs(2))
.expect("queued background request after foreground release");
assert!(background_request.contains("前台结束后执行的后台任务"));
let completed = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(completed.run_id, "design-after-foreground-run");
assert_eq!(
completed.last_response.as_deref(),
Some("排队后台任务完成。")
);
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_run_control_updates_lifecycle_and_jsonl() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
write_agent_run_trace(
&root,
"run-control-1",
"做一个可控 run",
"running",
1,
&[agent_trace_step(
1,
"Planner",
"completed",
&[".agent/manifest.json"],
&[".agent/spec.md"],
"已拆解目标",
"llm.chat.planner",
)],
None,
)
.expect("run trace");
let killed = update_agent_run_lifecycle(&root, "kill", None).expect("kill should update trace");
assert_eq!(killed.status, "killed");
assert_eq!(killed.lifecycle_status, "killed");
assert_eq!(killed.next_step, "resume-or-retry");
let trace: Value =
serde_json::from_str(&fs::read_to_string(root.join(".agent/run.latest.json")).unwrap())
.expect("run trace json");
assert_eq!(trace["status"], "killed");
assert_eq!(trace["lifecycleStatus"], "killed");
assert_eq!(trace["stopReason"], "killed");
let retried =
update_agent_run_lifecycle(&root, "retry", None).expect("retry should mark pending");
assert_eq!(retried.status, "pending");
assert_eq!(retried.lifecycle_status, "pending");
assert_eq!(retried.next_step, "rerun-now");
let resumed = update_agent_run_lifecycle(&root, "resume", Some("继续修复输入监听"))
.expect("resume should mark pending");
assert_eq!(resumed.status, "pending");
assert_eq!(resumed.lifecycle_status, "pending");
assert_eq!(resumed.next_step, "rerun-now");
let trace: Value =
serde_json::from_str(&fs::read_to_string(root.join(".agent/run.latest.json")).unwrap())
.expect("run trace json after resume");
assert_eq!(trace["stopReason"], "human-resume");
assert_eq!(trace["error"], Value::Null);
let status =
update_agent_run_lifecycle(&root, "status", None).expect("status should read trace");
assert_eq!(status.status, "pending");
assert_eq!(status.lifecycle_status, "pending");
let activity = fs::read_to_string(root.join(".agent/activity.jsonl")).expect("activity");
assert!(activity.contains("agent.kill"));
assert!(activity.contains("agent.retry"));
assert!(activity.contains("agent.resume"));
assert!(activity.contains("agent.run_status"));
let output = fs::read_to_string(root.join(".agent/output.jsonl")).expect("output");
assert!(output.contains("agent.kill"));
assert!(output.contains("agent.resume"));
assert!(root.join(".agent/context.bundle.json").exists());
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_run_status_derives_missing_lifecycle_from_status() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
write_agent_run_trace(
&root,
"run-control-legacy",
"做一个旧 trace",
"passed",
1,
&[agent_trace_step(
1,
"Playtest",
"completed",
&["game/index.html"],
&[".agent/logs/command.log"],
"静态入口自检通过",
"game.static_smoke",
)],
None,
)
.expect("run trace");
let trace_path = root.join(".agent/run.latest.json");
let mut trace: Value =
serde_json::from_str(&fs::read_to_string(&trace_path).unwrap()).expect("trace json");
trace
.as_object_mut()
.expect("trace object")
.remove("lifecycleStatus");
fs::write(
&trace_path,
serde_json::to_string_pretty(&trace).expect("trace json"),
)
.expect("write legacy trace");
let status =
update_agent_run_lifecycle(&root, "status", None).expect("status should read trace");
assert_eq!(status.status, "passed");
assert_eq!(status.lifecycle_status, "done");
fs::remove_dir_all(root).ok();
}
#[test]
fn structured_plan_same_run_steer_preserves_monotonic_runtime_and_v3_context() {
let root = unique_project_path();
let mut state = start_agent_runtime_steer_fixture(&root, "structured-plan-steer-run");
let task = state.current_task.clone();
let session_id = state.session_id.clone();
let task_id = state.task_id.clone();
let plan = AgentRuntimeToolPlan::default();
let tracker = AgentRuntimeContextWindowTracker::default();
let mut observations = Vec::new();
let initial_update = AgentRuntimePlanUpdate {
explanation: "已读取项目,开始实现键盘控制".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "读取项目".to_string(),
status: "completed".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "实现键盘控制".to_string(),
status: "in_progress".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "运行定向验证".to_string(),
status: "pending".to_string(),
},
],
};
assert!(apply_agent_runtime_plan_update(&mut state, &initial_update)
.expect("apply pre-steer structured plan"));
let pre_steer_revision = state.plan_revision;
assert_eq!(pre_steer_revision, 1);
write_game_creator_agent_runtime_state(&root, &state)
.expect("persist pre-steer structured runtime");
persist_game_creator_agent_runtime_context(
&root,
&state,
&task,
&plan,
&observations,
0,
&tracker,
)
.expect("persist pre-steer v3 context");
let pre_steer_context = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect("read pre-steer context")
.expect("pre-steer context exists");
assert_eq!(
pre_steer_context.schema_version,
AGENT_RUNTIME_CONTEXT_BUNDLE_SCHEMA_VERSION
);
assert_eq!(pre_steer_context.plan_revision, pre_steer_revision);
assert_eq!(pre_steer_context.plan_steps, state.plan_steps);
steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"structured-plan-steer-1",
"保留已完成的读取步骤,先运行定向验证,再继续实现键盘控制。",
"test",
)
.expect("queue same-run steer");
assert!(consume_game_creator_agent_runtime_steers(
&root,
&mut state,
&task,
&plan,
&mut observations,
0,
&tracker,
)
.expect("consume same-run steer"));
assert_eq!(state.plan_revision, pre_steer_revision);
assert!(state
.plan_steps
.iter()
.any(|step| { step.title == "读取项目" && step.status == "completed" }));
let consumed_state = read_game_creator_agent_runtime_at(&root, &state.agent_id)
.expect("read consumed runtime")
.state;
assert_eq!(consumed_state.task_id, task_id);
assert_eq!(consumed_state.session_id, session_id);
assert_eq!(consumed_state.run_id, state.run_id);
assert_eq!(consumed_state.plan_revision, pre_steer_revision);
assert_eq!(consumed_state.plan_steps, state.plan_steps);
let consumed_context = read_game_creator_agent_runtime_context_bundle(&root, &consumed_state)
.expect("read consumed context")
.expect("consumed context exists");
assert_eq!(
consumed_context.schema_version,
AGENT_RUNTIME_CONTEXT_BUNDLE_SCHEMA_VERSION
);
assert_eq!(consumed_context.plan_revision, consumed_state.plan_revision);
assert_eq!(
consumed_context.plan_explanation,
consumed_state.plan_explanation
);
assert_eq!(consumed_context.plan, consumed_state.plan);
assert_eq!(consumed_context.plan_steps, consumed_state.plan_steps);
assert_eq!(
consumed_context.active_plan_step_index,
consumed_state.active_plan_step_index
);
assert!(consumed_context.plan_revision >= pre_steer_context.plan_revision);
state = consumed_state;
let rejected_revision = state.plan_revision;
let completed_regression = AgentRuntimePlanUpdate {
explanation: "错误地回退已完成步骤".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "读取项目".to_string(),
status: "pending".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "运行定向验证".to_string(),
status: "in_progress".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "实现键盘控制".to_string(),
status: "pending".to_string(),
},
],
};
assert!(
apply_agent_runtime_plan_update(&mut state, &completed_regression)
.expect_err("same-run steer replan cannot regress completed steps")
.contains("不能让已完成步骤回退")
);
assert_eq!(state.plan_revision, rejected_revision);
let steered_update = AgentRuntimePlanUpdate {
explanation: "按追加要求重排未完成步骤".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "运行定向验证".to_string(),
status: "in_progress".to_string(),
},
AgentRuntimePlanUpdateStep {
step: "实现键盘控制".to_string(),
status: "pending".to_string(),
},
],
};
assert!(apply_agent_runtime_plan_update(&mut state, &steered_update)
.expect("apply post-steer structured plan"));
assert!(state.plan_revision > rejected_revision);
assert!(state
.plan_steps
.iter()
.any(|step| { step.title == "读取项目" && step.status == "completed" }));
write_game_creator_agent_runtime_state(&root, &state)
.expect("persist post-steer structured runtime");
persist_game_creator_agent_runtime_context(
&root,
&state,
&task,
&plan,
&observations,
0,
&tracker,
)
.expect("persist post-steer v3 context");
let persisted_state = read_game_creator_agent_runtime_at(&root, &state.agent_id)
.expect("read post-steer runtime")
.state;
let persisted_context = read_game_creator_agent_runtime_context_bundle(&root, &persisted_state)
.expect("read post-steer context")
.expect("post-steer context exists");
assert_eq!(
persisted_context.schema_version,
AGENT_RUNTIME_CONTEXT_BUNDLE_SCHEMA_VERSION
);
assert_eq!(
persisted_context.plan_revision,
persisted_state.plan_revision
);
assert_eq!(
persisted_context.plan_explanation,
persisted_state.plan_explanation
);
assert_eq!(persisted_context.plan, persisted_state.plan);
assert_eq!(persisted_context.plan_steps, persisted_state.plan_steps);
assert_eq!(
persisted_context.active_plan_step_index,
persisted_state.active_plan_step_index
);
assert!(persisted_context.plan_revision > consumed_context.plan_revision);
assert!(persisted_context
.plan_steps
.iter()
.any(|step| { step.title == "读取项目" && step.status == "completed" }));
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_steer_is_idempotent_rejects_conflicts_and_enforces_capacity() {
let root = unique_project_path();
let state = start_agent_runtime_steer_fixture(&root, "steer-contract-run");
let first = steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"steer-1",
"先保留现有结构,但把移动改成键盘控制。",
"test",
)
.expect("accept first steer");
let duplicate = steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"steer-1",
"先保留现有结构,但把移动改成键盘控制。",
"test",
)
.expect("retry same steer");
assert_eq!(first.sequence, 1);
assert_eq!(duplicate.sequence, 1);
assert_eq!(duplicate.status, "queued");
assert_eq!(
read_agent_runtime_steer_jsonl(&root, &state.agent_id, &state.run_id).len(),
3,
"prepared/conversation-persisted/queued must remain exactly once"
);
let conversation = read_local_conversation_for_session_at(
&root,
Some(&state.agent_id),
Some(&state.session_id),
)
.expect("read steer conversation");
assert_eq!(
conversation
.messages
.iter()
.filter(|message| message.role == "user")
.count(),
1
);
let conflict = steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"steer-1",
"换成完全不同的指令。",
"test",
)
.expect_err("same steer id with different body must fail");
assert!(conflict.contains("steer-id-conflict"));
for index in 2..=AGENT_RUNTIME_STEER_MAX_RUN_COUNT {
steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
&format!("steer-{index}"),
&format!("第 {index} 条追加要求。"),
"test",
)
.expect("accept steer within count limit");
}
let overflow = steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"steer-overflow",
"超过数量上限的追加要求。",
"test",
)
.expect_err("seventeenth steer must fail");
assert!(overflow.contains("容量上限"));
assert!(steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
"wrong-session",
&state.run_id,
"wrong-session-steer",
"不应接受。",
"test",
)
.is_err());
assert!(steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
"wrong-run",
"wrong-run-steer",
"不应接受。",
"test",
)
.is_err());
}
#[test]
fn root_supervisor_steer_persists_one_correlated_public_acknowledgement() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-steer-public-ack", "总控追加消息确认测试")
.expect("initialize Supervisor steer fixture");
let state = start_game_creator_agent_runtime_task_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"制作一个可玩的游戏",
"supervisor-steer-public-ack-run",
"project-supervisor-gui",
"正在制作游戏",
vec!["完成可玩版本".to_string()],
)
.expect("start root Supervisor runtime");
for _ in 0..2 {
steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"supervisor-steer-public-ack-1",
"你在做什么?",
"test",
)
.expect("queue idempotent Supervisor steer");
}
for _ in 0..2 {
persist_game_creator_agent_runtime_steer_decision_and_reply_at(
&root,
&state,
"supervisor-steer-public-ack-1",
1,
AgentRuntimeSteerDecision {
reply: "我正在完成玩法实现;这是状态询问,当前任务会继续。".to_string(),
interrupt_current_provider: false,
reason: "状态询问不需要中断当前 Provider。".to_string(),
},
)
.expect("persist idempotent Supervisor steer reply");
}
let supervisor_conversation = read_local_conversation_for_session_at(
&root,
Some(&state.agent_id),
Some(&state.session_id),
)
.expect("read Supervisor steer conversation");
let steer_message = supervisor_conversation
.messages
.iter()
.find(|message| message.role == "user" && message.content == "你在做什么?")
.expect("find persisted steer user message");
let correlation_id = steer_message
.message_id
.as_deref()
.and_then(|message_id| message_id.strip_prefix("agent-steer-"))
.expect("read steer correlation id");
let project_conversation =
read_local_conversation_at(&root, None).expect("read public steer acknowledgement");
let acknowledgements = project_conversation
.messages
.iter()
.filter(|message| {
message.content == "收到。我正在判断这条消息是否需要调整当前任务;现有任务会继续运行。"
})
.collect::<Vec<_>>();
assert_eq!(acknowledgements.len(), 1);
assert!(acknowledgements[0]
.message_id
.as_deref()
.is_some_and(|message_id| {
message_id.starts_with(&format!(
"{AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX}{correlation_id}-"
))
}));
assert_eq!(
project_conversation
.messages
.iter()
.filter(|message| {
message.content == "我正在完成玩法实现;这是状态询问,当前任务会继续。"
})
.count(),
1
);
let decision = read_game_creator_agent_runtime_steer_decision_at(
&root,
&state.agent_id,
&state.run_id,
"supervisor-steer-public-ack-1",
)
.expect("read persisted Supervisor steer decision")
.expect("Supervisor steer decision exists");
assert!(!decision.interrupt_current_provider);
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_steer_sequences_are_unique_under_concurrent_acceptance() {
let root = unique_project_path();
let state = start_agent_runtime_steer_fixture(&root, "steer-concurrent-run");
let root = Arc::new(root);
let state = Arc::new(state);
let mut workers = Vec::new();
for index in 0..8 {
let root = root.clone();
let state = state.clone();
workers.push(std::thread::spawn(move || {
steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
&format!("steer-concurrent-{index}"),
&format!("并发追加指令 {index}"),
"test",
)
.expect("accept concurrent steer")
.sequence
}));
}
let mut sequences = workers
.into_iter()
.map(|worker| worker.join().expect("join steer worker"))
.collect::<Vec<_>>();
sequences.sort_unstable();
assert_eq!(sequences, (1..=8).collect::<Vec<_>>());
}
#[test]
fn agent_runtime_steer_rejects_invalid_body_and_reconciliation_state() {
let root = unique_project_path();
let mut state = start_agent_runtime_steer_fixture(&root, "steer-invalid-run");
assert!(steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"steer-too-large",
&"x".repeat(AGENT_RUNTIME_STEER_MAX_INSTRUCTION_BYTES + 1),
"test",
)
.expect_err("oversized steer must fail")
.contains("字节上限"));
assert!(steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"steer-control",
"非法\u{0007}控制字符",
"test",
)
.expect_err("control character steer must fail")
.contains("控制字符"));
state.phase = "needs-reconciliation".to_string();
state.current_action = "等待人工核对".to_string();
write_game_creator_agent_runtime_state(&root, &state).expect("write reconciliation state");
assert!(steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"steer-reconciliation",
"不应越过待核对边界。",
"test",
)
.expect_err("reconciliation run must reject steer")
.contains("不接受追加指令"));
}
#[test]
fn agent_runtime_steer_blocks_stale_auto_action_and_defers_pending_confirmation() {
let root = unique_project_path();
let mut state = start_agent_runtime_steer_fixture(&root, "steer-action-gate-run");
let action = AgentRuntimeToolAction {
tool: "file.write".to_string(),
reason: Some("执行旧计划的第二步".to_string()),
input: serde_json::json!({
"path": "game/stale-action.txt",
"content": "must-not-run"
}),
};
let mut stale_auto = pending_tool_action_for_test(
&root,
&state,
action.clone(),
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED,
None,
);
stale_auto.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string();
steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"steer-before-action-2",
"停止旧计划,不要执行第二个写入动作。",
"test",
)
.expect("queue steer before second auto action");
assert!(
!mark_game_creator_agent_runtime_auto_action_executing_if_current(&root, &mut stale_auto,)
.expect("check stale auto action gate")
);
assert!(!root.join("game/stale-action.txt").exists());
let pending = pending_tool_action_for_test(
&root,
&state,
action,
AGENT_RUNTIME_PENDING_ACTION_STATUS_PENDING,
None,
);
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
.expect("persist confirmation pending before steer");
state.status = "waiting-for-confirmation".to_string();
state.phase = "waiting-for-confirmation".to_string();
state.pending_tool_action = Some(pending.summary());
write_game_creator_agent_runtime_state(&root, &state)
.expect("persist waiting confirmation state");
steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"steer-during-confirmation",
"待确认动作保持原样,之后再应用这条要求。",
"test",
)
.expect("queue steer during confirmation");
let restored =
read_game_creator_agent_runtime_pending_tool_action(&root, &state.agent_id, &state.run_id)
.expect("read pending confirmation after steer");
assert_eq!(restored.status, AGENT_RUNTIME_PENDING_ACTION_STATUS_PENDING);
assert_eq!(restored.action_id, pending.action_id);
assert_eq!(restored.action_fingerprint, pending.action_fingerprint);
}
#[test]
fn agent_runtime_steer_context_cursor_repairs_missing_applied_transition() {
let root = unique_project_path();
let mut state = start_agent_runtime_steer_fixture(&root, "steer-recovery-run");
steer_game_creator_agent_runtime_task_at(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
"steer-recovery-1",
"改为先修复失败测试,再继续实现。",
"test",
)
.expect("queue recovery steer");
let plan = AgentRuntimeToolPlan::default();
let tracker = AgentRuntimeContextWindowTracker::default();
let mut observations = Vec::new();
assert!(consume_game_creator_agent_runtime_steers(
&root,
&mut state,
"实现一个可验证的键盘操作原型",
&plan,
&mut observations,
0,
&tracker,
)
.expect("consume steer"));
assert_eq!(state.applied_steer_cursor, 1);
assert_eq!(state.applied_steer_refs.len(), 1);
assert!(render_game_creator_agent_runtime_steers_for_prompt(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
)
.expect("render applied steers")
.contains("先修复失败测试"));
let context = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect("read context bundle")
.expect("context exists");
assert_eq!(context.applied_steer_cursor, 1);
assert_eq!(context.applied_steer_refs, state.applied_steer_refs);
let ledger_path =
game_creator_agent_runtime_steer_ledger_path(&root, &state.agent_id, &state.run_id);
let retained = fs::read_to_string(&ledger_path)
.expect("read ledger before simulated crash")
.lines()
.filter(|line| {
serde_json::from_str::<serde_json::Value>(line)
.ok()
.and_then(|record| record["status"].as_str().map(str::to_string))
.as_deref()
!= Some("applied")
})
.collect::<Vec<_>>()
.join("\n");
fs::write(&ledger_path, format!("{retained}\n")).expect("remove applied transition");
assert!(!consume_game_creator_agent_runtime_steers(
&root,
&mut state,
"实现一个可验证的键盘操作原型",
&plan,
&mut observations,
0,
&tracker,
)
.expect("repair missing applied transition"));
let applied_count = read_agent_runtime_steer_jsonl(&root, &state.agent_id, &state.run_id)
.iter()
.filter(|record| record["status"] == "applied")
.count();
assert_eq!(applied_count, 1);
}