use super::*;
#[test]
fn static_smoke_finish_rejects_same_revision_entry_rewrite_before_credential_binding() {
let root = unique_project_path();
init_local_game_project_at(
&root,
"project-static-smoke-toctou",
"静态验证入口摘要时序测试",
)
.expect("project init");
fs::write(
root.join("game/index.html"),
fake_llm_game_draft().game_html,
)
.expect("write initially valid game entry");
let run_id = "static-smoke-toctou-run";
let _lock = acquire_project_write_lock(&root, "test.static-smoke.toctou")
.expect("acquire project write lock");
prepare_agent_runtime_project_mutation_locked(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
"file.write",
)
.expect("prepare project mutation");
let (expected_revision, gate) = begin_agent_runtime_project_verification_locked(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
"game.static_smoke",
)
.expect("begin static smoke");
fs::write(
root.join("game/index.html"),
"
unverified replacement",
)
.expect("replace entry without advancing revision");
let error =
finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true)
.expect_err("replacement bytes must be smoke-validated before credential binding");
assert!(error.contains("game.static_smoke"), "{error}");
let gate = read_game_creator_agent_runtime_verification_gate(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect("read failed static smoke gate");
assert_eq!(gate.verified_revision, None);
assert_eq!(gate.static_smoke_verified_revision, None);
assert_eq!(gate.static_smoke_verified_game_index_sha256, None);
assert_eq!(
gate.last_verification_status.as_deref(),
Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED)
);
drop(_lock);
fs::remove_dir_all(root).ok();
}
#[test]
fn failed_autonomous_preview_invalidates_static_smoke_verification_gate() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-preview-gate", "试玩失败凭证失效测试")
.expect("project init");
write_supervisor_collaboration_policy_at(&root, SupervisorCollaborationPolicy::default())
.expect("write default collaboration policy");
let run_id = "preview-gate-failure-run";
fs::write(
root.join("game/index.html"),
fake_llm_game_draft().game_html,
)
.expect("write smoke-valid game entry");
let revision = prepare_agent_runtime_project_mutation_locked(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
"file.write",
)
.expect("prepare project mutation");
let (expected_revision, gate) = begin_agent_runtime_project_verification_locked(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
"game.static_smoke",
)
.expect("begin static smoke");
finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true)
.expect("finish static smoke");
invalidate_agent_runtime_project_verification_after_preview_failure_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
revision,
)
.expect("invalidate failed browser playtest");
let gate = read_game_creator_agent_runtime_verification_gate(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect("read invalidated gate");
assert!(gate.requires_verification);
assert_eq!(gate.mutation_revision, Some(revision));
assert_eq!(gate.verified_revision, None);
assert_eq!(
gate.last_verification_tool.as_deref(),
Some("preview.validate")
);
assert_eq!(gate.last_verification_status.as_deref(), Some("failed"));
assert_eq!(gate.failed_playtest_revision, Some(revision));
// A later static smoke pass must not erase the durable interactive failure.
let (expected_revision, gate) = begin_agent_runtime_project_verification_locked(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
"game.static_smoke",
)
.expect("begin static smoke after failed playtest");
finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true)
.expect("finish static smoke after failed playtest");
let gate = read_game_creator_agent_runtime_verification_gate(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect("read gate after static smoke");
assert_eq!(gate.failed_playtest_revision, Some(revision));
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_context_bundle_preserves_project_verification_gate_evidence() {
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-verification-context-run",
"agent-background-task",
"验证凭证恢复测试",
vec!["保留当前 revision 验证凭证".to_string()],
)
.expect("start verification context runtime");
advance_project_revision_for_test(
&root,
"design-director",
"design-verification-context-run",
"file.patch",
);
persist_project_verification_for_test(
&root,
"design-director",
"design-verification-context-run",
"project.verify",
true,
);
let mut observations = vec![
verification_gate_observation("file.patch", "ok", "已更新 game/main.js"),
verification_gate_observation("project.verify", "ok", "test 已通过"),
];
observations.extend((0..20).map(|index| {
verification_gate_observation("file.read", "ok", &format!("已回读第 {index} 个文件片段"))
}));
let mut tracker = AgentRuntimeContextWindowTracker::default();
assert_eq!(
tracker.complete_loop(1),
AgentRuntimeContextCheckpoint::Continue
);
let bundle = build_game_creator_agent_runtime_context_bundle(
&root,
&state,
&state.current_task,
&AgentRuntimeToolPlan::default(),
&observations,
1,
&tracker,
)
.expect("build verification context bundle");
assert!(bundle
.observations
.iter()
.any(|observation| observation.tool == "file.patch"));
assert!(bundle
.observations
.iter()
.any(|observation| observation.tool == "project.verify"));
assert_eq!(bundle.verification_gate.mutation_revision, Some(1));
assert_eq!(bundle.verification_gate.verified_revision, Some(1));
write_game_creator_agent_runtime_context_bundle(&root, &bundle)
.expect("write verification context bundle");
state.loop_iteration = 1;
let mut loaded = read_game_creator_agent_runtime_context_bundle(&root, &state)
.expect("read verification context bundle")
.expect("verification context bundle exists");
assert!(project_verification_completion_blocker(&loaded.observations).is_none());
assert!(project_verification_completion_blocker_at(
&root,
"design-director",
"design-verification-context-run",
&loaded.observations,
)
.is_none());
advance_project_revision_for_test(&root, "code-prototype", "code-run", "file.write");
loaded.observations.extend((0..20).map(|index| {
verification_gate_observation(
"file.read",
"ok",
&format!("恢复后回读第 {index} 个文件片段"),
)
}));
let retained = sanitize_game_creator_agent_runtime_context_observations_for_storage(
&root,
&loaded.observations,
);
assert!(project_verification_completion_blocker(&retained).is_none());
assert!(project_verification_completion_blocker_at(
&root,
"design-director",
"design-verification-context-run",
&retained,
)
.is_none());
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_project_verify_uses_independent_permission_policy() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let check_command = r#"node -e "process.stdout.write('SHOULD_NOT_RUN')""#;
fs::write(
root.join("package.json"),
serde_json::to_string_pretty(&serde_json::json!({
"name": "project-verify-policy-fixture",
"private": true,
"scripts": { "check": check_command }
}))
.expect("serialize package json"),
)
.expect("write package json");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: vec!["project.verify".to_string()],
agent_policies: BTreeMap::new(),
},
)
.expect("require verification confirmation");
let (sender, receiver) = mpsc::channel();
let plan_json = serde_json::json!({
"thinkingSummary": "尝试运行项目检查",
"plan": ["运行 check"],
"actions": [{
"tool": "project.verify",
"reason": "测试确认策略",
"input": {
"script": "check",
"expectedCommand": check_command,
"timeoutSeconds": 15
}
}],
"response": ""
})
.to_string();
let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender));
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"code-prototype": {{
"apiKey": "code-key",
"baseUrl": {base_url:?},
"model": "code-runtime-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
start_game_creator_agent_background_task_at(
&root,
"code-prototype",
"等待确认后运行项目检查",
"code-project-verify-policy-run",
)
.expect("start background task");
receiver
.recv_timeout(Duration::from_secs(2))
.expect("plan llm request");
let runtime = wait_for_agent_runtime_confirmation(&root, "code-prototype");
assert_eq!(runtime.status, "waiting-for-confirmation");
assert!(runtime.observations.iter().any(|item| item.contains(
"project.verify:waiting-for-confirmation · 项目权限策略要求用户确认:project.verify"
)));
let pending = runtime.pending_tool_action.expect("pending verify action");
assert_eq!(pending.tool, "project.verify");
assert!(pending
.input_summary
.as_deref()
.is_some_and(|summary| summary.contains("script=check")));
assert!(!root.join(".agent/logs/command.log").exists());
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_confirms_project_verify_and_replans_with_output() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let check_command = r#"node -e "process.stdout.write('PROJECT_VERIFY_CONFIRMED')""#;
fs::write(
root.join("package.json"),
serde_json::to_string_pretty(&serde_json::json!({
"name": "project-verify-confirm-fixture",
"private": true,
"scripts": { "check": check_command }
}))
.expect("serialize package json"),
)
.expect("write package json");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: vec!["project.verify".to_string(), "agent.resume".to_string()],
agent_policies: BTreeMap::new(),
},
)
.expect("require verification confirmation");
let (sender, receiver) = mpsc::channel();
let verify_plan = serde_json::json!({
"thinkingSummary": "修改完成后运行项目检查",
"plan": ["运行 check", "根据真实输出收束"],
"actions": [{
"tool": "project.verify",
"reason": "确认当前修改通过项目检查",
"input": {
"script": "check",
"expectedCommand": check_command,
"timeoutSeconds": 15
}
}],
"response": ""
})
.to_string();
let final_plan = serde_json::json!({
"thinkingSummary": "项目检查已经通过",
"plan": [],
"actions": [],
"response": "已根据 project.verify 的真实输出确认 check 通过。"
})
.to_string();
let base_url =
spawn_mock_llm_server_responses_with_capture(vec![verify_plan, final_plan], Some(sender));
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"code-prototype": {{
"apiKey": "code-key",
"baseUrl": {base_url:?},
"model": "code-runtime-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
start_game_creator_agent_background_task_at(
&root,
"code-prototype",
"运行项目检查并依据结果回复",
"code-project-verify-confirm-run",
)
.expect("start background task");
receiver
.recv_timeout(Duration::from_secs(2))
.expect("initial plan request");
let waiting_runtime = wait_for_agent_runtime_confirmation(&root, "code-prototype");
let pending_action = waiting_runtime
.pending_tool_action
.as_ref()
.expect("pending project verify action");
assert_eq!(pending_action.tool, "project.verify");
assert!(!root.join(".agent/logs/command.log").exists());
let confirmed = confirm_game_creator_agent_runtime_task(
root.to_string_lossy().into_owned(),
"code-prototype".to_string(),
"code-project-verify-confirm-run".to_string(),
pending_action.action_id.clone(),
"允许执行项目 check".to_string(),
)
.expect("confirm project verify");
assert_eq!(confirmed.state.status, "running");
let continued_request = receiver
.recv_timeout(Duration::from_secs(4))
.expect("continued plan request");
assert!(continued_request.contains("project.verify"));
assert!(continued_request.contains("check"));
let runtime = wait_for_agent_runtime_idle(&root, "code-prototype");
assert_eq!(runtime.status, "idle");
assert_eq!(
runtime.last_response.as_deref(),
Some("已根据 project.verify 的真实输出确认 check 通过。")
);
assert!(runtime
.observations
.iter()
.any(|item| item.contains("project.verify:ok · check 已通过")));
let command_log =
fs::read_to_string(root.join(".agent/logs/command.log")).expect("command log");
assert!(command_log.contains("PROJECT_VERIFY_CONFIRMED"));
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(agent_db.contains("\"recordType\":\"agent.runtime.tool_confirmation.approved\""));
assert!(agent_db.contains("\"recordType\":\"agent.runtime.project.verify\""));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_command_exec_requires_confirmation_by_default() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "受控命令确认项目").expect("project init");
fs::create_dir_all(root.join("test")).expect("create test dir");
fs::write(
root.join("test/confirmation.test.mjs"),
"console.log('COMMAND_EXEC_SHOULD_WAIT');\n",
)
.expect("write command fixture");
assert!(ProjectPermissionPolicy::default()
.confirm_commands
.contains(&"command.exec".to_string()));
let plan = serde_json::json!({
"thinkingSummary": "运行定向测试前等待开发者确认",
"plan": ["运行 node 定向测试"],
"actions": [{
"tool": "command.exec",
"reason": "取得真实测试输出",
"input": {
"program": "node",
"args": ["--test", "test/confirmation.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}
}],
"response": ""
})
.to_string();
let (sender, receiver) = mpsc::channel();
let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan], Some(sender));
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"code-prototype": {{
"apiKey": "code-key",
"baseUrl": {base_url:?},
"model": "code-runtime-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
start_game_creator_agent_background_task_at(
&root,
"code-prototype",
"运行定向测试并读取真实输出",
"code-command-exec-confirm-default-run",
)
.expect("start command task");
receiver
.recv_timeout(Duration::from_secs(2))
.expect("command plan request");
let runtime = wait_for_agent_runtime_confirmation(&root, "code-prototype");
assert_eq!(runtime.status, "waiting-for-confirmation");
let pending = runtime.pending_tool_action.expect("pending command action");
assert_eq!(pending.tool, "command.exec");
assert!(pending
.input_summary
.as_deref()
.is_some_and(|summary| summary.contains("program=node") && summary.contains("cwd=.")));
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read revision before confirmation")
.revision,
0
);
assert!(!root.join(".agent/logs/command.log").exists());
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_command_exec_repairs_failure_and_finishes_once() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "受控命令修复项目").expect("project init");
fs::create_dir_all(root.join("src")).expect("create source dir");
fs::create_dir_all(root.join("test")).expect("create test dir");
fs::write(root.join("src/answer.mjs"), "export const answer = 1;\n")
.expect("write broken source");
fs::write(
root.join("test/answer.test.mjs"),
concat!(
"import { answer } from '../src/answer.mjs';\n",
"if (answer !== 2) throw new Error('COMMAND_EXEC_REPAIR_REQUIRED');\n",
"console.log('COMMAND_EXEC_RECOVERY_PASS');\n",
),
)
.expect("write command test");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: vec!["command.exec".to_string()],
agent_policies: BTreeMap::new(),
},
)
.expect("require command confirmation only");
let command_action = || {
serde_json::json!({
"tool": "command.exec",
"reason": "运行定向失败测试并取得真实输出",
"input": {
"program": "node",
"args": ["--test", "test/answer.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}
})
};
let failed_command_plan = serde_json::json!({
"thinkingSummary": "先复现未知位置的测试失败",
"plan": ["运行定向测试", "依据 stderr 定位并修复", "重新运行测试"],
"actions": [command_action()],
"response": ""
})
.to_string();
let patch_plan = serde_json::json!({
"thinkingSummary": "失败输出已定位 answer 值错误",
"plan": ["精确修复源码", "重新运行同一测试"],
"actions": [{
"tool": "file.patch",
"reason": "修正失败测试暴露的错误值",
"input": {
"path": "src/answer.mjs",
"oldText": "export const answer = 1;\n",
"newText": "export const answer = 2;\n",
"expectedReplacements": 1
}
}],
"response": ""
})
.to_string();
let passing_command_plan = serde_json::json!({
"thinkingSummary": "源码已修复,重新执行同一测试",
"plan": ["重新运行定向测试"],
"actions": [command_action()],
"response": ""
})
.to_string();
let response = "已根据失败输出修复 answer,并以 COMMAND_EXEC_RECOVERY_PASS 完成验证。";
let final_plan = final_tool_plan_response(response);
let (sender, receiver) = mpsc::channel();
let base_url = spawn_mock_llm_server_responses_with_capture(
vec![
failed_command_plan,
patch_plan,
passing_command_plan,
final_plan,
],
Some(sender),
);
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"code-prototype": {{
"apiKey": "code-key",
"baseUrl": {base_url:?},
"model": "code-runtime-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
let run_id = "code-command-exec-repair-run";
start_game_creator_agent_background_task_at(
&root,
"code-prototype",
"定位并修复失败测试,最后重新验证",
run_id,
)
.expect("start command repair task");
receiver
.recv_timeout(Duration::from_secs(2))
.expect("initial command plan request");
let first_waiting = wait_for_agent_runtime_confirmation(&root, "code-prototype");
let first_pending = first_waiting
.pending_tool_action
.as_ref()
.expect("first command confirmation");
assert_eq!(first_pending.tool, "command.exec");
confirm_game_creator_agent_runtime_task(
root.to_string_lossy().into_owned(),
"code-prototype".to_string(),
run_id.to_string(),
first_pending.action_id.clone(),
"允许运行失败测试".to_string(),
)
.expect("confirm failed command");
let repair_request = receiver
.recv_timeout(Duration::from_secs(5))
.expect("repair plan request");
assert!(repair_request.contains("command.exec"));
assert!(repair_request.contains("command-failed") || repair_request.contains("failed"));
let retry_request = receiver
.recv_timeout(Duration::from_secs(5))
.expect("retry command plan request");
assert!(retry_request.contains("file.patch"));
assert!(retry_request.contains("已局部修改 src/answer.mjs"));
let second_waiting = wait_for_agent_runtime_confirmation(&root, "code-prototype");
let second_pending = second_waiting
.pending_tool_action
.as_ref()
.expect("second command confirmation");
assert_eq!(second_pending.tool, "command.exec");
assert_ne!(second_pending.action_id, first_pending.action_id);
confirm_game_creator_agent_runtime_task(
root.to_string_lossy().into_owned(),
"code-prototype".to_string(),
run_id.to_string(),
second_pending.action_id.clone(),
"允许重新运行测试".to_string(),
)
.expect("confirm passing command");
let final_request = receiver
.recv_timeout(Duration::from_secs(5))
.expect("final plan request");
assert!(final_request.contains("COMMAND_EXEC_RECOVERY_PASS"));
let runtime = wait_for_agent_runtime_idle(&root, "code-prototype");
assert_eq!(runtime.status, "idle");
assert_eq!(runtime.phase, "completed");
assert_eq!(runtime.run_id, run_id);
assert_eq!(runtime.last_response.as_deref(), Some(response));
assert_eq!(
fs::read_to_string(root.join("src/answer.mjs")).expect("read repaired source"),
"export const answer = 2;\n"
);
assert!(runtime
.observations
.iter()
.any(|item| item.contains("command.exec:command-failed")));
assert!(runtime
.observations
.iter()
.any(|item| item.contains("file.patch:ok")));
assert!(runtime
.observations
.iter()
.any(|item| item.contains("command.exec:ok")));
let revision = read_game_creator_agent_runtime_project_revision(&root)
.expect("read command repair revision");
assert_eq!(revision.revision, 3);
let gate = read_game_creator_agent_runtime_verification_gate(&root, "code-prototype", run_id)
.expect("read command repair gate");
assert_eq!(gate.verified_revision, Some(3));
assert_eq!(gate.last_verification_status.as_deref(), Some("passed"));
let records = read_agent_db_records_for_test(&root);
let command_records = records
.iter()
.filter(|record| record["recordType"] == "agent.runtime.command.exec")
.collect::>();
assert_eq!(command_records.len(), 2);
assert!(command_records.iter().all(|record| {
record.get("args").is_none()
&& record["argsCount"] == 2
&& record["verificationEligible"] == true
&& record["argsSha256"]
.as_str()
.is_some_and(|value| value.len() == 64)
}));
assert!(records
.iter()
.filter(|record| {
matches!(
record["recordType"].as_str(),
Some(
"agent.runtime.tool_confirmation_required"
| "agent.runtime.tool_confirmation.approved"
)
) && record["tool"] == "command.exec"
})
.all(|record| {
record["inputSummary"].as_str().is_some_and(|summary| {
summary.contains("argsSha256=") && !summary.contains("answer.test")
})
}));
let conversation = read_local_conversation_for_session_at(
&root,
Some("code-prototype"),
Some(&runtime.session_id),
)
.expect("read command repair conversation");
assert_eq!(
conversation
.messages
.iter()
.filter(|message| message.role == "assistant" && message.content == response)
.count(),
1
);
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_reads_long_command_output_without_leaking_lines_to_audit() {
const ROOT_MARKER: &str = "ROOT_CAUSE_中段标记_Ω";
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "长命令输出分页项目").expect("project init");
fs::create_dir_all(root.join("test")).expect("create test dir");
let mut script = "import test from 'node:test';\ntest('long output', () => {\n".to_string();
for line in 1..=240 {
let payload = if line == 140 {
ROOT_MARKER.to_string()
} else {
format!("TRACE_LINE_{line:03}_{}", "x".repeat(24))
};
script.push_str(&format!(
" console.log({});\n",
serde_json::to_string(&payload).expect("serialize output line")
));
}
script.push_str("});\n");
fs::write(root.join("test/long-output.test.mjs"), script).expect("write long output test");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: vec!["command.exec".to_string()],
agent_policies: BTreeMap::new(),
},
)
.expect("require command confirmation only");
let command_plan = serde_json::json!({
"thinkingSummary": "先运行长输出定向测试",
"plan": ["执行测试", "分页读取中部输出", "继续规划"],
"actions": [{
"tool": "command.exec",
"reason": "取得完整测试输出 sidecar",
"input": {
"program": "node",
"args": ["--test", "test/long-output.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}
}],
"response": ""
})
.to_string();
let (request_sender, request_receiver) = mpsc::channel();
let (response_sender, response_receiver) = mpsc::channel();
let base_url =
spawn_interactive_mock_llm_server_with_capture(3, request_sender, response_receiver);
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"code-prototype": {{
"apiKey": "code-key",
"baseUrl": {base_url:?},
"model": "code-runtime-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
let run_id = "code-command-output-read-background-run";
start_game_creator_agent_background_task_at(
&root,
"code-prototype",
"从长命令输出中读取中部诊断信息",
run_id,
)
.expect("start long output background task");
request_receiver
.recv_timeout(Duration::from_secs(2))
.expect("initial command plan request");
response_sender
.send(command_plan)
.expect("release command plan response");
let waiting = wait_for_agent_runtime_confirmation(&root, "code-prototype");
let source_action_id = if let Some(command_pending) = waiting.pending_tool_action.as_ref() {
assert_eq!(command_pending.tool, "command.exec");
let action_id = command_pending.action_id.clone();
confirm_game_creator_agent_runtime_task(
root.to_string_lossy().into_owned(),
"code-prototype".to_string(),
run_id.to_string(),
action_id.clone(),
"允许运行长输出测试".to_string(),
)
.expect("confirm long output command");
action_id
} else {
read_agent_db_records_for_test(&root)
.iter()
.find(|record| {
record["recordType"] == AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE
&& record["runId"] == run_id
&& record["tool"] == "command.exec"
})
.and_then(|record| record["actionId"].as_str())
.map(str::to_string)
.expect("auto-executed long output command receipt")
};
let short_observation_request = request_receiver
.recv_timeout(Duration::from_secs(5))
.expect("followup request after command exec");
assert!(short_observation_request.contains("outputRef"));
assert!(short_observation_request.contains("totalLines"));
assert!(short_observation_request.contains(&format!("sourceActionId={source_action_id}")));
assert!(!short_observation_request.contains("TRACE_LINE_001"));
assert!(!short_observation_request.contains(ROOT_MARKER));
assert!(!short_observation_request.contains("TRACE_LINE_139"));
let source_records = read_agent_db_records_for_test(&root);
let source_receipt = source_records
.iter()
.find(|record| {
record["recordType"] == AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE
&& record["actionId"] == source_action_id
&& record["tool"] == "command.exec"
})
.expect("source command terminal receipt");
assert_eq!(source_receipt["status"], "ok");
let source_safe_detail: Value = serde_json::from_str(
source_receipt["safeDetail"]
.as_str()
.expect("source command safe detail"),
)
.expect("parse source command safe detail");
assert!(
source_safe_detail["totalLines"]
.as_u64()
.unwrap_or_default()
>= 220
);
let output_ref = source_safe_detail["outputRef"]
.as_str()
.expect("source output ref");
let output_sidecar = fs::read_to_string(root.join(output_ref)).expect("read output sidecar");
assert!(output_sidecar.contains(ROOT_MARKER));
let output_read_plan = serde_json::json!({
"thinkingSummary": "短 observation 不含中部根因,分页读取源输出",
"plan": ["读取第 100-199 行", "依据中部标记继续规划"],
"actions": [{
"tool": "command.output_read",
"reason": "读取短 observation 之外的中部输出",
"input": {
"actionId": source_action_id,
"startLine": 100,
"maxLines": 100
}
}],
"response": ""
})
.to_string();
response_sender
.send(output_read_plan)
.expect("release output read plan response");
let output_read_request = request_receiver
.recv_timeout(Duration::from_secs(5))
.expect("followup request after output read");
assert!(output_read_request.contains(ROOT_MARKER));
assert!(output_read_request.contains("TRACE_LINE_139"));
assert!(output_read_request.contains("nextLine"));
assert!(output_read_request.contains("totalLines"));
let bundle_path =
game_creator_agent_runtime_context_bundle_path(&root, "code-prototype", run_id);
let bundle: AgentRuntimeContextBundle = serde_json::from_str(
&fs::read_to_string(bundle_path).expect("read output read context bundle"),
)
.expect("parse output read context bundle");
let output_read_observation = bundle
.observations
.iter()
.find(|observation| observation.tool == "command.output_read")
.expect("output read observation in context bundle");
let source_command_observation = bundle
.observations
.iter()
.find(|observation| observation.tool == "command.exec")
.expect("source command observation in context bundle");
assert!(source_command_observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains(&format!("sourceActionId={source_action_id}"))));
assert_eq!(output_read_observation.status, "ok");
let output_page: Value = serde_json::from_str(
output_read_observation
.detail
.as_deref()
.expect("output read page detail"),
)
.expect("parse output read page");
assert!(output_page["lines"]
.as_str()
.is_some_and(|lines| lines.contains(ROOT_MARKER) && lines.contains("TRACE_LINE_139")));
assert_eq!(output_page["nextLine"], 200);
assert!(output_page["totalLines"].as_u64().unwrap_or_default() >= 220);
let event_log = fs::read_to_string(game_creator_agent_runtime_event_path(
&root,
"code-prototype",
))
.expect("read runtime events");
assert!(!event_log.contains(ROOT_MARKER));
assert!(!event_log.contains("TRACE_LINE_139"));
let records = read_agent_db_records_for_test(&root);
let output_read_audits = records
.iter()
.filter(|record| record["recordType"] == "agent.runtime.command.output_read")
.collect::>();
assert_eq!(output_read_audits.len(), 1);
assert!(output_read_audits[0].get("lines").is_none());
let output_read_receipts = records
.iter()
.filter(|record| {
record["recordType"] == AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE
&& record["tool"] == "command.output_read"
})
.collect::>();
assert_eq!(output_read_receipts.len(), 1);
let receipt_safe_detail: Value = serde_json::from_str(
output_read_receipts[0]["safeDetail"]
.as_str()
.expect("output read receipt safe detail"),
)
.expect("parse output read receipt safe detail");
assert!(receipt_safe_detail.get("lines").is_none());
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("read agent db");
assert!(!agent_db.contains(ROOT_MARKER));
assert!(!agent_db.contains("TRACE_LINE_139"));
assert_eq!(
records
.iter()
.filter(|record| record["recordType"] == "agent.runtime.command.exec")
.count(),
1
);
let command_log =
fs::read_to_string(root.join(".agent/logs/command.log")).expect("read command log");
assert_eq!(command_log.matches(ROOT_MARKER).count(), 1);
response_sender
.send(final_tool_plan_response(
"已通过 command.output_read 读取中部 Unicode 根因并继续完成规划。",
))
.expect("release final response");
let runtime = wait_for_agent_runtime_idle(&root, "code-prototype");
assert_eq!(runtime.status, "idle");
assert_eq!(runtime.phase, "completed");
assert_eq!(runtime.run_id, run_id);
assert_eq!(
runtime.last_response.as_deref(),
Some("已通过 command.output_read 读取中部 Unicode 根因并继续完成规划。")
);
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn command_exec_output_sidecar_failure_runs_once_and_requires_reconciliation() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "命令输出 sidecar 失败项目")
.expect("project init");
fs::create_dir_all(root.join("test")).expect("create test dir");
fs::write(
root.join("test/output-sidecar-failure.test.mjs"),
"import fs from 'node:fs';\nfs.appendFileSync('command-execution-count.txt', 'run\\n');\n",
)
.expect("write sidecar failure command fixture");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow command fixture");
let run_id = "code-command-output-sidecar-failure-run";
let mut state = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"验证输出 sidecar 失败不重放命令",
run_id,
"agent-background-task",
"准备执行命令",
vec!["执行一次并保持失败门禁".to_string()],
)
.expect("start sidecar failure runtime");
state.loop_iteration = 1;
let action = AgentRuntimeToolAction {
tool: "command.exec".to_string(),
reason: Some("执行后模拟 transcript sidecar 写入失败".to_string()),
input: serde_json::json!({
"program": "node",
"args": ["--test", "test/output-sidecar-failure.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}),
};
let mut pending = pending_tool_action_for_test(
&root,
&state,
action.clone(),
AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING,
None,
);
pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string();
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
.expect("write command pending");
state.status = "running".to_string();
state.phase = "action".to_string();
state.pending_tool_action = Some(pending.summary());
append_game_creator_agent_runtime_task(&root, &state).expect("append command task");
write_game_creator_agent_runtime_state(&root, &state).expect("write command state");
fs::write(root.join(".agent/runtime/command-outputs"), b"blocked")
.expect("block command output directory");
let observation = execute_game_creator_agent_runtime_tool_action_with_pending_action(
&root,
"code-prototype",
run_id,
&state.current_task,
&action,
Some(&pending.action_id),
Some(&pending),
)
.await;
assert_eq!(observation.status, "needs-reconciliation");
assert!(observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("输出 sidecar")));
assert_eq!(
fs::read_to_string(root.join("command-execution-count.txt"))
.expect("read command execution count"),
"run\n"
);
assert!(!root.join(".agent/logs/command.log").exists());
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read command revision")
.revision,
1
);
let gate = read_game_creator_agent_runtime_verification_gate(&root, "code-prototype", run_id)
.expect("read command gate");
assert_eq!(gate.last_verification_status.as_deref(), Some("failed"));
let records = read_agent_db_records_for_test(&root);
assert_eq!(
records
.iter()
.filter(|record| record["recordType"] == "agent.runtime.command.exec")
.count(),
1
);
assert!(records.iter().any(|record| {
record["recordType"] == "agent.runtime.command.exec"
&& record["status"] == "execution-unknown"
&& record["errorStage"] == "output-sidecar"
}));
let persisted =
read_game_creator_agent_runtime_pending_tool_action(&root, "code-prototype", run_id)
.expect("read command pending");
assert_eq!(
persisted.status,
AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING
);
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn command_output_read_allows_same_agent_history_and_rejects_cross_agent_action_id() {
const HISTORY_MARKER: &str = "HISTORICAL_OUTPUT_历史读取";
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "命令输出身份门禁项目").expect("project init");
fs::create_dir_all(root.join("test")).expect("create test dir");
fs::write(
root.join("test/history-output.test.mjs"),
format!("console.log({HISTORY_MARKER:?});\n"),
)
.expect("write history command fixture");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow command output tools");
let source_run_id = "code-command-output-history-source-run";
let source_plan = serde_json::json!({
"thinkingSummary": "生成可供后续 run 读取的命令输出",
"plan": ["运行定向测试"],
"actions": [{
"tool": "command.exec",
"reason": "生成历史输出 sidecar",
"input": {
"program": "node",
"args": ["--test", "test/history-output.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}
}],
"response": ""
})
.to_string();
let base_url = spawn_mock_llm_server_responses(vec![
source_plan,
final_tool_plan_response("历史命令输出已持久化。"),
]);
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"code-prototype": {{
"apiKey": "code-key",
"baseUrl": {base_url:?},
"model": "code-runtime-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
start_game_creator_agent_background_task_at(
&root,
"code-prototype",
"生成历史命令输出",
source_run_id,
)
.expect("start source command run");
let source_runtime = wait_for_agent_runtime_idle(&root, "code-prototype");
assert!(matches!(
source_runtime.phase.as_str(),
"completed" | "planning"
));
let source_records = read_agent_db_records_for_test(&root);
let source_receipt = source_records
.iter()
.find(|record| {
record["recordType"] == AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE
&& record["runId"] == source_run_id
&& record["tool"] == "command.exec"
})
.expect("source command receipt");
let source_action_id = source_receipt["actionId"]
.as_str()
.expect("source action id")
.to_string();
let mut same_agent_state = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"读取上一 run 的命令输出",
"code-command-output-history-reader-run",
"agent-background-task",
"准备读取历史输出",
vec!["读取历史 sidecar".to_string()],
)
.expect("start same agent reader state");
same_agent_state.loop_iteration = 1;
let same_agent_action = AgentRuntimeToolAction {
tool: "command.output_read".to_string(),
reason: Some("读取同 Agent 历史 run 输出".to_string()),
input: serde_json::json!({
"actionId": source_action_id,
"startLine": 1,
"maxLines": 20
}),
};
let mut same_agent_pending = pending_tool_action_for_test(
&root,
&same_agent_state,
same_agent_action.clone(),
AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING,
None,
);
same_agent_pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string();
write_game_creator_agent_runtime_pending_tool_action(&root, &same_agent_pending)
.expect("write same agent output read pending");
same_agent_state.status = "running".to_string();
same_agent_state.phase = "action".to_string();
same_agent_state.pending_tool_action = Some(same_agent_pending.summary());
append_game_creator_agent_runtime_task(&root, &same_agent_state)
.expect("append same agent reader task");
write_game_creator_agent_runtime_state(&root, &same_agent_state)
.expect("write same agent reader state");
let same_agent_observation =
execute_game_creator_agent_runtime_tool_action_with_pending_action(
&root,
"code-prototype",
&same_agent_state.run_id,
&same_agent_state.current_task,
&same_agent_action,
Some(&same_agent_pending.action_id),
Some(&same_agent_pending),
)
.await;
assert_eq!(same_agent_observation.status, "ok");
let same_agent_page: Value = serde_json::from_str(
same_agent_observation
.detail
.as_deref()
.expect("same agent output page"),
)
.expect("parse same agent output page");
assert_eq!(same_agent_page["sourceRunId"], source_run_id);
assert!(same_agent_page["lines"]
.as_str()
.is_some_and(|lines| lines.contains(HISTORY_MARKER)));
let mut cross_agent_state = start_game_creator_agent_runtime_task_at(
&root,
"art-director",
"尝试读取其他 Agent 的命令输出",
"art-command-output-cross-agent-run",
"agent-background-task",
"准备验证跨 Agent 门禁",
vec!["拒绝跨 Agent actionId".to_string()],
)
.expect("start cross agent reader state");
cross_agent_state.loop_iteration = 1;
let cross_agent_action = AgentRuntimeToolAction {
tool: "command.output_read".to_string(),
reason: Some("验证跨 Agent actionId 不可读".to_string()),
input: serde_json::json!({
"actionId": source_action_id,
"startLine": 1,
"maxLines": 20
}),
};
let mut cross_agent_pending = pending_tool_action_for_test(
&root,
&cross_agent_state,
cross_agent_action.clone(),
AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING,
None,
);
cross_agent_pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string();
write_game_creator_agent_runtime_pending_tool_action(&root, &cross_agent_pending)
.expect("write cross agent output read pending");
cross_agent_state.status = "running".to_string();
cross_agent_state.phase = "action".to_string();
cross_agent_state.pending_tool_action = Some(cross_agent_pending.summary());
append_game_creator_agent_runtime_task(&root, &cross_agent_state)
.expect("append cross agent reader task");
write_game_creator_agent_runtime_state(&root, &cross_agent_state)
.expect("write cross agent reader state");
let cross_agent_observation =
execute_game_creator_agent_runtime_tool_action_with_pending_action(
&root,
"art-director",
&cross_agent_state.run_id,
&cross_agent_state.current_task,
&cross_agent_action,
Some(&cross_agent_pending.action_id),
Some(&cross_agent_pending),
)
.await;
assert_eq!(cross_agent_observation.status, "failed");
assert_eq!(
cross_agent_observation.summary,
"command.output_read 无法验证源命令身份"
);
assert!(cross_agent_observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("未找到当前 Agent 的源动作回执")));
fs::remove_dir_all(root).ok();
}
#[test]
fn isolated_child_command_output_read_rechecks_template_deny_policy_after_project_lock() {
use platform_agent::game_creation::{
GameCreationIsolatedAgentChildSpec, GameCreationIsolatedAgentJoinMode,
GameCreationIsolatedAgentSpawnRequest,
};
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "隔离子 Agent 输出策略复核项目")
.expect("project init");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow output read before lock wait");
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-a/output.txt".to_string()],
write_scopes: vec!["game/feature-a/**".to_string()],
}],
join_mode: GameCreationIsolatedAgentJoinMode::All,
};
let group = create_or_read_isolated_group_at(
&root,
"design-director",
"isolated-output-policy-parent-run",
"isolated-output-policy-parent-session",
"isolated-output-policy-parent-action",
&request,
)
.expect("create isolated output policy group");
let instance = resolve_isolated_agent_instance_at(&root, &group.instance_ids[0])
.expect("resolve isolated output reader");
ensure_agent_conversation_session_at(
&root,
&instance.instance_id,
&instance.session_id,
"隔离输出读取",
)
.expect("ensure isolated reader session");
let mut state = start_game_creator_agent_runtime_task_for_session_at(
&root,
&instance.instance_id,
Some(&instance.session_id),
&instance.task,
&instance.run_id,
AGENT_RUNTIME_ISOLATED_CHILD_SOURCE,
"等待项目锁后读取命令输出",
vec!["锁内复核模板策略".to_string()],
)
.expect("start isolated output reader state");
state.loop_iteration = 1;
let action = AgentRuntimeToolAction {
tool: "command.output_read".to_string(),
reason: Some("验证模板 deny 在锁内生效".to_string()),
input: serde_json::json!({
"actionId": "action-000000000000000000000001",
"startLine": 1,
"maxLines": 1
}),
};
let mut pending = pending_tool_action_for_test(
&root,
&state,
action.clone(),
AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING,
None,
);
pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string();
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
.expect("write isolated output read pending");
state.status = "running".to_string();
state.phase = "action".to_string();
state.pending_tool_action = Some(pending.summary());
append_game_creator_agent_runtime_task(&root, &state)
.expect("append isolated output reader task");
write_game_creator_agent_runtime_state(&root, &state)
.expect("write isolated output reader state");
let project_lock = acquire_project_write_lock(&root, "test.isolated-output-policy-writer")
.expect("acquire project writer lock");
let thread_root = root.clone();
let thread_state = state.clone();
let thread_action = action.clone();
let thread_pending = pending.clone();
let (started_sender, started_receiver) = mpsc::channel();
let (finished_sender, finished_receiver) = mpsc::channel();
let reader = std::thread::spawn(move || {
started_sender.send(()).expect("signal child reader start");
let observation = tauri::async_runtime::block_on(
execute_game_creator_agent_runtime_tool_action_with_pending_action(
&thread_root,
&thread_state.agent_id,
&thread_state.run_id,
&thread_state.current_task,
&thread_action,
Some(&thread_pending.action_id),
Some(&thread_pending),
),
);
finished_sender
.send(())
.expect("signal child reader completion");
observation
});
started_receiver
.recv_timeout(Duration::from_secs(1))
.expect("isolated output reader starts");
assert!(
finished_receiver
.recv_timeout(Duration::from_millis(80))
.is_err(),
"child command.output_read must wait for the project consistency lock"
);
let mut agent_policies = BTreeMap::new();
agent_policies.insert(
"code-prototype".to_string(),
ProjectAgentPermissionPolicy {
denied_commands: vec!["command.output_read".to_string()],
confirm_commands: Vec::new(),
},
);
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies,
},
)
.expect("deny output read through child template policy while locked");
drop(project_lock);
let observation = reader.join().expect("join isolated output reader");
assert_eq!(observation.status, "blocked");
assert_eq!(
observation.summary,
"Agent 权限策略拒绝执行:code-prototype / command.output_read"
);
assert!(!read_agent_db_records_for_test(&root)
.iter()
.any(|record| record["recordType"] == "agent.runtime.command.output_read"));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn agent_runtime_command_exec_revalidates_revision_after_acquiring_project_lock() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "命令锁内复核项目").expect("project init");
fs::create_dir_all(root.join("test")).expect("create test dir");
fs::write(
root.join("test/stale-command.test.mjs"),
"console.log('STALE_COMMAND_MUST_NOT_RUN');\n",
)
.expect("write stale command fixture");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow command fixture");
let run_id = "code-command-lock-revalidation-run";
let state = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"运行等待执行的定向测试",
run_id,
"agent-background-task",
"准备执行 command.exec",
vec!["运行定向测试".to_string()],
)
.expect("start command runtime state");
let action = AgentRuntimeToolAction {
tool: "command.exec".to_string(),
reason: Some("运行定向测试".to_string()),
input: serde_json::json!({
"program": "node",
"args": ["--test", "test/stale-command.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}),
};
let pending = pending_tool_action_for_test(
&root,
&state,
action.clone(),
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED,
None,
);
assert_eq!(
advance_project_revision_for_test(
&root,
"art-director",
"art-concurrent-command-revalidation-run",
"file.write",
),
1
);
let observation = execute_game_creator_agent_runtime_tool_action_with_pending_action(
&root,
"code-prototype",
run_id,
&state.current_task,
&action,
Some(&pending.action_id),
Some(&pending),
)
.await;
assert_eq!(observation.status, "verification-failed");
assert!(observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("项目 revision 已变化")));
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read unchanged command revision")
.revision,
1
);
assert!(!root.join(".agent/logs/command.log").exists());
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn agent_runtime_command_exec_requires_reconciliation_after_audit_failure() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "命令审计失败项目").expect("project init");
fs::create_dir_all(root.join("test")).expect("create test dir");
fs::write(
root.join("test/audit.test.mjs"),
"console.log('COMMAND_EXEC_AUDIT_RAN');\n",
)
.expect("write audit command fixture");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow command fixture");
let command_log = root.join(".agent/logs/command.log");
fs::create_dir_all(command_log.parent().expect("command log parent"))
.expect("create command log parent");
fs::create_dir(&command_log).expect("replace command log with directory");
let run_id = "code-command-audit-reconciliation-run";
let action = AgentRuntimeToolAction {
tool: "command.exec".to_string(),
reason: Some("运行命令并模拟执行后审计失败".to_string()),
input: serde_json::json!({
"program": "node",
"args": ["--test", "test/audit.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}),
};
let observation = execute_game_creator_agent_runtime_tool_action(
&root,
"code-prototype",
run_id,
"验证命令执行后审计失败语义",
&action,
)
.await;
assert_eq!(observation.status, "needs-reconciliation");
assert!(observation.summary.contains("执行结果不完整"));
assert!(observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("写入命令日志失败")));
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read audit failure revision")
.revision,
1
);
let gate = read_game_creator_agent_runtime_verification_gate(&root, "code-prototype", run_id)
.expect("read audit failure gate");
assert_eq!(gate.last_verification_status.as_deref(), Some("failed"));
let records = read_agent_db_records_for_test(&root);
assert!(records.iter().any(|record| {
record["recordType"] == "agent.runtime.command.exec"
&& record["status"] == "execution-unknown"
&& record["errorStage"] == "audit-log"
}));
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_command_exec_recovery_keeps_started_revision_without_replay() {
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 command fixture");
let run_id = "code-command-started-recovery-run";
let mut state = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"恢复执行中断的命令",
run_id,
"agent-background-task",
"模拟 command.exec 已开始",
vec!["运行定向测试".to_string()],
)
.expect("start command recovery state");
state.loop_iteration = 1;
let action = AgentRuntimeToolAction {
tool: "command.exec".to_string(),
reason: Some("模拟执行中的命令".to_string()),
input: serde_json::json!({
"program": "node",
"args": ["--test", "test/interrupted.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}),
};
let mut pending = pending_tool_action_for_test(
&root,
&state,
action,
AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING,
None,
);
pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string();
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
.expect("write executing command action");
{
let _lock = acquire_project_write_lock(&root, "test.command.exec.started")
.expect("acquire command mutation lock");
assert_eq!(
prepare_agent_runtime_project_mutation_locked(
&root,
"code-prototype",
run_id,
"command.exec",
)
.expect("prepare command mutation"),
1
);
let (revision, gate) = begin_agent_runtime_project_verification_locked(
&root,
"code-prototype",
run_id,
"command.exec",
)
.expect("begin command verification");
assert_eq!(revision.revision, 1);
assert_eq!(gate.last_verification_status.as_deref(), Some("running"));
}
state.status = "running".to_string();
state.phase = "action".to_string();
state.pending_tool_action = Some(pending.summary());
append_game_creator_agent_runtime_task(&root, &state).expect("append executing command task");
write_game_creator_agent_runtime_state(&root, &state).expect("write executing command state");
resume_game_creator_agent_background_tasks_at(&root).expect("resume interrupted command");
let runtime = read_game_creator_agent_runtime_at(&root, "code-prototype")
.expect("read reconciled command runtime")
.state;
assert_eq!(runtime.status, "failed");
assert_eq!(runtime.phase, "needs-reconciliation");
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read preserved command revision")
.revision,
1
);
let gate = read_game_creator_agent_runtime_verification_gate(&root, "code-prototype", run_id)
.expect("read preserved command gate");
assert!(gate.requires_verification);
assert_eq!(gate.mutation_revision, Some(1));
assert_eq!(gate.verified_revision, None);
assert_eq!(gate.last_verification_status.as_deref(), Some("running"));
assert!(!root.join(".agent/logs/command.log").exists());
resume_game_creator_agent_background_tasks_at(&root)
.expect("repeat resume remains reconciliation");
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read command revision after repeat resume")
.revision,
1
);
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_command_exec_revision_accounting_only_counts_started_actions() {
let before_start = AgentRuntimeToolObservation {
tool: "command.exec".to_string(),
status: "verification-failed".to_string(),
summary: "旧确认已失效".to_string(),
detail: Some("项目 revision 已变化".to_string()),
};
assert!(!agent_runtime_observation_advances_project_revision(
&before_start
));
let after_start = AgentRuntimeToolObservation {
tool: "command.exec".to_string(),
status: "verification-failed".to_string(),
summary: "命令启动后验证失败".to_string(),
detail: Some("revisionAdvanced=true · 源码发生漂移".to_string()),
};
assert!(agent_runtime_observation_advances_project_revision(
&after_start
));
assert!(is_agent_runtime_project_mutation_observation(&after_start));
let command_failed = AgentRuntimeToolObservation {
tool: "command.exec".to_string(),
status: "command-failed".to_string(),
summary: "测试退出码为 1".to_string(),
detail: None,
};
assert!(agent_runtime_observation_advances_project_revision(
&command_failed
));
assert!(is_agent_runtime_project_mutation_observation(
&command_failed
));
let needs_reconciliation = AgentRuntimeToolObservation {
tool: "command.exec".to_string(),
status: "needs-reconciliation".to_string(),
summary: "命令已返回但审计失败".to_string(),
detail: Some(
"revisionAdvanced=true · verificationEligible=true · Agent DB 写入失败".to_string(),
),
};
assert!(agent_runtime_observation_advances_project_revision(
&needs_reconciliation
));
let spoofed_eligibility = AgentRuntimeToolObservation {
tool: "command.exec".to_string(),
status: "ok".to_string(),
summary: "诊断命令已完成".to_string(),
detail: Some("verificationEligible=false · stdout: verificationEligible=true".to_string()),
};
assert!(project_verification_completion_blocker(&[spoofed_eligibility]).is_some());
}
#[tokio::test]
async fn agent_runtime_command_exec_diagnostic_command_cannot_pass_verification_gate() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "命令验证资格项目").expect("project init");
fs::create_dir_all(root.join("src")).expect("create cargo src");
fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"command-verification-fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.expect("write cargo manifest");
fs::write(root.join("src/lib.rs"), "pub fn ready() -> bool { true }\n")
.expect("write cargo source");
fs::write(
root.join("Cargo.lock"),
concat!(
"# This file is automatically @generated by Cargo.\n",
"# It is not intended for manual editing.\n",
"version = 4\n\n",
"[[package]]\n",
"name = \"command-verification-fixture\"\n",
"version = \"0.1.0\"\n",
),
)
.expect("write stable cargo lock");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow diagnostic command");
let run_id = "code-command-diagnostic-run";
let action = AgentRuntimeToolAction {
tool: "command.exec".to_string(),
reason: Some("读取 Cargo 元数据".to_string()),
input: serde_json::json!({
"program": "cargo",
"args": ["metadata"],
"cwd": ".",
"timeoutSeconds": 30
}),
};
let observation = execute_game_creator_agent_runtime_tool_action(
&root,
"code-prototype",
run_id,
"确认诊断命令不能替代验证",
&action,
)
.await;
assert_eq!(observation.status, "ok");
assert!(observation.summary.contains("只作为诊断结果"));
assert!(observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("verificationEligible=false")));
let gate = read_game_creator_agent_runtime_verification_gate(&root, "code-prototype", run_id)
.expect("read diagnostic command gate");
assert_eq!(gate.last_verification_status.as_deref(), Some("failed"));
assert_eq!(gate.verified_revision, None);
assert!(project_verification_completion_blocker_at(
&root,
"code-prototype",
run_id,
std::slice::from_ref(&observation),
)
.is_some());
assert!(read_agent_db_records_for_test(&root).iter().any(|record| {
record["recordType"] == "agent.runtime.command.exec"
&& record["verificationEligible"] == false
}));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn agent_runtime_command_exec_agent_db_audit_failure_keeps_gate_failed() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "命令数据库审计失败项目").expect("project init");
fs::create_dir_all(root.join("test")).expect("create test dir");
fs::write(
root.join("test/agent-db-audit.test.mjs"),
"console.log('AGENT_DB_AUDIT_COMMAND_RAN');\n",
)
.expect("write command fixture");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow command fixture");
let agent_db = root.join(".agent/agent.db");
if agent_db.exists() {
fs::remove_file(&agent_db).expect("remove agent db file");
}
fs::create_dir(&agent_db).expect("replace agent db with directory");
let run_id = "code-command-agent-db-audit-run";
let action = AgentRuntimeToolAction {
tool: "command.exec".to_string(),
reason: Some("模拟 Agent DB 审计失败".to_string()),
input: serde_json::json!({
"program": "node",
"args": ["--test", "test/agent-db-audit.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}),
};
let observation = execute_game_creator_agent_runtime_tool_action(
&root,
"code-prototype",
run_id,
"验证 Agent DB 审计失败时门禁关闭",
&action,
)
.await;
assert_eq!(observation.status, "needs-reconciliation");
assert!(observation.summary.contains("执行审计无法完整落盘"));
let gate = read_game_creator_agent_runtime_verification_gate(&root, "code-prototype", run_id)
.expect("read failed audit gate");
assert_eq!(gate.last_verification_status.as_deref(), Some("failed"));
assert_eq!(gate.verified_revision, None);
assert!(root.join(".agent/logs/command.log").is_file());
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn agent_runtime_command_exec_rejects_changed_pending_action_identity_after_lock() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "命令动作身份复核项目").expect("project init");
fs::create_dir_all(root.join("test")).expect("create test dir");
fs::write(
root.join("test/original.test.mjs"),
"console.log('ORIGINAL_COMMAND');\n",
)
.expect("write original command");
fs::write(
root.join("test/changed.test.mjs"),
"console.log('CHANGED_COMMAND_MUST_NOT_RUN');\n",
)
.expect("write changed command");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow command fixture");
let run_id = "code-command-pending-identity-run";
let state = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"运行已确认的原始测试",
run_id,
"agent-background-task",
"准备执行原始命令",
vec!["运行原始测试".to_string()],
)
.expect("start command runtime state");
let original = AgentRuntimeToolAction {
tool: "command.exec".to_string(),
reason: Some("运行原始测试".to_string()),
input: serde_json::json!({
"program": "node",
"args": ["--test", "test/original.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}),
};
let pending = pending_tool_action_for_test(
&root,
&state,
original.clone(),
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED,
None,
);
let changed = AgentRuntimeToolAction {
tool: "command.exec".to_string(),
reason: Some("替换为未确认测试".to_string()),
input: serde_json::json!({
"program": "node",
"args": ["--test", "test/changed.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}),
};
let wrong_id_observation = execute_game_creator_agent_runtime_tool_action_with_pending_action(
&root,
"code-prototype",
run_id,
&state.current_task,
&original,
Some("command-action-wrong-id"),
Some(&pending),
)
.await;
assert_eq!(wrong_id_observation.status, "verification-failed");
assert!(wrong_id_observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("actionId 或动作指纹已变化")));
let observation = execute_game_creator_agent_runtime_tool_action_with_pending_action(
&root,
"code-prototype",
run_id,
&state.current_task,
&changed,
Some(&pending.action_id),
Some(&pending),
)
.await;
assert_eq!(observation.status, "verification-failed");
assert!(observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("actionId 或动作指纹已变化")));
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read unchanged revision")
.revision,
0
);
assert!(!root.join(".agent/logs/command.log").exists());
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_command_exec_rechecks_deny_policy_after_project_lock() {
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-command-policy-recheck-run",
"agent-background-task",
"准备执行测试",
vec!["运行测试".to_string()],
)
.expect("runtime state");
let action = AgentRuntimeToolAction {
tool: "command.exec".to_string(),
reason: Some("运行测试".to_string()),
input: serde_json::json!({
"program": "node",
"args": ["--test", "test/policy.test.mjs"],
"cwd": ".",
"timeoutSeconds": 15
}),
};
let mut pending = pending_tool_action_for_test(
&root,
&state,
action,
AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING,
None,
);
pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_CONFIRMATION.to_string();
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: vec!["command.exec".to_string()],
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("deny command while confirmed action waits for lock");
assert!(matches!(
game_creator_agent_runtime_tool_policy_block_after_lock(
&root,
"code-prototype",
"command.exec",
Some(&pending),
),
Some(AgentRuntimeToolPolicyBlock::Denied(_))
));
fs::remove_dir_all(root).ok();
}
#[test]
fn project_verification_gate_scopes_credential_to_exact_agent_run_and_revision() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "验证凭证隔离项目").expect("project init");
assert_eq!(
advance_project_revision_for_test(&root, "agent-a", "run-a", "file.write"),
1
);
persist_project_verification_for_test(&root, "agent-b", "run-b", "project.verify", true);
assert!(project_verification_completion_blocker_at(&root, "agent-a", "run-a", &[]).is_some());
persist_project_verification_for_test(&root, "agent-a", "run-a", "project.verify", true);
assert!(project_verification_completion_blocker_at(&root, "agent-a", "run-a", &[]).is_none());
assert_eq!(
advance_project_revision_for_test(&root, "agent-b", "run-b", "file.patch"),
2
);
persist_project_verification_for_test(&root, "agent-b", "run-b", "project.verify", true);
let agent_a_gate = read_game_creator_agent_runtime_verification_gate(&root, "agent-a", "run-a")
.expect("read agent A gate");
let agent_b_gate = read_game_creator_agent_runtime_verification_gate(&root, "agent-b", "run-b")
.expect("read agent B gate");
assert_eq!(agent_a_gate.verified_revision, Some(1));
assert_eq!(agent_b_gate.verified_revision, Some(2));
assert!(project_verification_completion_blocker_at(&root, "agent-a", "run-a", &[]).is_none());
assert!(project_verification_completion_blocker_at(&root, "agent-b", "run-b", &[]).is_none());
fs::remove_dir_all(root).ok();
}
#[test]
fn project_verification_gate_allows_read_only_run_at_nonzero_revision() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "只读验证门禁项目").expect("project init");
advance_project_revision_for_test(&root, "writer", "writer-run", "file.write");
let observations = vec![verification_gate_observation(
"file.read",
"ok",
"已读取 package.json",
)];
assert!(project_verification_completion_blocker_at(
&root,
"reader",
"reader-run",
&observations,
)
.is_none());
assert!(
!game_creator_agent_runtime_verification_gate_path(&root, "reader", "reader-run").exists()
);
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read revision")
.revision,
1
);
assert!(game_creator_agent_runtime_project_revision_path(&root).is_file());
fs::remove_dir_all(root).ok();
}
#[test]
fn failed_project_verification_clears_previous_run_credential() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "失败验证清凭证项目").expect("project init");
advance_project_revision_for_test(&root, "agent-a", "run-a", "file.patch");
persist_project_verification_for_test(&root, "agent-a", "run-a", "project.verify", true);
assert_eq!(
read_game_creator_agent_runtime_verification_gate(&root, "agent-a", "run-a")
.expect("read passed gate")
.verified_revision,
Some(1)
);
persist_project_verification_for_test(&root, "agent-a", "run-a", "project.verify", false);
let gate = read_game_creator_agent_runtime_verification_gate(&root, "agent-a", "run-a")
.expect("read failed gate");
assert_eq!(gate.verified_revision, None);
assert_eq!(
gate.last_verification_status.as_deref(),
Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED)
);
assert!(project_verification_completion_blocker_at(&root, "agent-a", "run-a", &[]).is_some());
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn invalid_project_verification_preserves_previous_run_credential() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "无效验证保留凭证项目").expect("project init");
advance_project_revision_for_test(&root, "agent-a", "run-a", "file.patch");
persist_project_verification_for_test(&root, "agent-a", "run-a", "project.verify", true);
let observation = observe_agent_runtime_project_verify(
&root,
"agent-a",
"run-a",
Some("action-invalid-verify"),
"fingerprint-invalid-verify",
&serde_json::json!({ "script": "check" }),
)
.await;
assert_eq!(observation.status, "failed");
let gate = read_game_creator_agent_runtime_verification_gate(&root, "agent-a", "run-a")
.expect("read preserved gate");
assert_eq!(gate.verified_revision, Some(1));
assert_eq!(gate.last_verification_status.as_deref(), Some("passed"));
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read revision")
.revision,
1
);
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn relaxed_autonomous_validation_tools_skip_without_side_effects() {
let root = unique_project_path();
init_local_game_project_at(&root, "relaxed-validation-skip", "自主构建跳过验证工具测试")
.expect("project init");
let run_id = "relaxed-validation-skip-run";
bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("bind relaxed autonomous run profile");
let agent_db_path = root.join(".agent/agent.db");
let agent_db_before = fs::read(&agent_db_path).expect("read initial agent db");
let verification_gate_path = game_creator_agent_runtime_verification_gate_path(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
);
assert!(!verification_gate_path.exists());
let project_verify = observe_agent_runtime_project_verify(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
None,
"relaxed-project-verify",
&serde_json::json!({}),
)
.await;
let limited_command = observe_agent_runtime_limited_command(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
&serde_json::json!({}),
);
let preview_start = observe_agent_runtime_preview_start(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
);
let preview_validate = observe_agent_runtime_preview_validate(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
None,
"relaxed-preview-validate",
&serde_json::json!({}),
)
.await;
for observation in [
&project_verify,
&limited_command,
&preview_start,
&preview_validate,
] {
assert_eq!(observation.status, "ok", "{observation:?}");
assert!(observation.summary.contains("跳过"), "{observation:?}");
assert!(observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("relaxedAutonomous=true")));
}
assert_eq!(
fs::read(&agent_db_path).expect("read unchanged agent db"),
agent_db_before
);
assert!(!verification_gate_path.exists());
assert!(!root.join(".agent/logs/command.log").exists());
fs::remove_dir_all(root).ok();
}
#[cfg(unix)]
#[tokio::test]
async fn executed_project_verification_audit_failure_requires_reconciliation() {
use std::os::unix::fs::symlink;
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "验证审计失败项目").expect("project init");
let check_command = r#"node -e "require('fs').writeFileSync('verify-ran.txt','once')""#;
fs::write(
root.join("package.json"),
serde_json::to_string_pretty(&serde_json::json!({
"name": "verify-audit-failure",
"private": true,
"scripts": { "check": check_command }
}))
.expect("serialize package json"),
)
.expect("write package json");
fs::remove_file(root.join(".agent/agent.db")).expect("remove initial agent db");
let outside = root.parent().expect("project parent").join(format!(
"verify-audit-outside-{}",
TEST_PROJECT_COUNTER.fetch_add(1, Ordering::Relaxed)
));
fs::write(&outside, "outside").expect("write outside agent db target");
symlink(&outside, root.join(".agent/agent.db")).expect("symlink agent db");
let observation = observe_agent_runtime_project_verify(
&root,
"agent-a",
"run-a",
Some("action-verify-audit-failure"),
"fingerprint-verify-audit-failure",
&serde_json::json!({
"script": "check",
"expectedCommand": check_command,
"timeoutSeconds": 15
}),
)
.await;
assert_eq!(
observation.status,
AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION
);
assert_eq!(
fs::read_to_string(root.join("verify-ran.txt")).expect("verification marker"),
"once"
);
let gate = read_game_creator_agent_runtime_verification_gate(&root, "agent-a", "run-a")
.expect("read failed gate");
assert_eq!(gate.verified_revision, None);
assert_eq!(gate.last_verification_status.as_deref(), Some("failed"));
fs::remove_file(root.join(".agent/agent.db")).ok();
fs::remove_file(outside).ok();
fs::remove_dir_all(root).ok();
}
#[test]
fn project_verification_gate_accepts_only_exact_game_static_smoke() {
let root = unique_project_path();
init_existing_html_project_at(&root, "project-1", "精确静态验证项目").expect("project init");
advance_project_revision_for_test(&root, "playtest", "playtest-run", "file.write");
fs::write(
root.join("game/index.html"),
fake_llm_game_draft().game_html,
)
.expect("write playable game");
let near_match = observe_agent_runtime_limited_command(
&root,
"playtest",
"playtest-run",
&serde_json::json!({ "commandId": "game.static_smoke.backup" }),
);
assert_eq!(near_match.status, "failed");
let observations = vec![
verification_gate_observation("file.write", "ok", "已写入 game/index.html"),
verification_gate_observation(
"command.run_limited",
"ok",
"game.static_smoke backup 已完成",
),
];
assert!(project_verification_completion_blocker(&observations).is_some());
assert_eq!(
read_game_creator_agent_runtime_verification_gate(&root, "playtest", "playtest-run")
.expect("read gate before exact smoke")
.verified_revision,
None
);
let exact = observe_agent_runtime_limited_command(
&root,
"playtest",
"playtest-run",
&serde_json::json!({ "commandId": "game.static_smoke" }),
);
assert_eq!(exact.status, "ok", "{}", exact.summary);
let gate = read_game_creator_agent_runtime_verification_gate(&root, "playtest", "playtest-run")
.expect("read exact smoke gate");
assert_eq!(
gate.last_verification_tool.as_deref(),
Some("game.static_smoke")
);
assert_eq!(gate.verified_revision, Some(1));
assert!(project_verification_completion_blocker_at(
&root,
"playtest",
"playtest-run",
&[exact],
)
.is_none());
fs::remove_dir_all(root).ok();
}
#[test]
fn successful_canvas_generation_verifies_prior_visual_replacement() {
let observations = vec![
verification_gate_observation("file.delete", "ok", "已删除 assets/ui-prototype.png"),
verification_gate_observation(
"canvas.asset_generate",
"ok",
"已生成美术素材:assets/ui-prototype.png",
),
];
assert!(project_verification_completion_blocker(&observations).is_none());
}
#[test]
fn project_verification_sidecar_failure_blocks_completion() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "验证门禁失败关闭项目").expect("project init");
let mut contradictory =
read_game_creator_agent_runtime_verification_gate(&root, "agent-a", "run-a")
.expect("read default gate");
contradictory.mutation_revision = Some(1);
contradictory.last_mutation_tool = Some("file.write".to_string());
assert!(
write_game_creator_agent_runtime_verification_gate(&root, &contradictory)
.expect_err("requiresVerification=false cannot retain mutation evidence")
.contains("requiresVerification=false")
);
let gate_path = game_creator_agent_runtime_verification_gate_path(&root, "agent-a", "run-a");
fs::create_dir_all(&gate_path).expect("replace verification sidecar with directory");
let error = {
let _lock = acquire_project_write_lock(&root, "test.sidecar.failure")
.expect("acquire sidecar failure lock");
prepare_agent_runtime_project_mutation_locked(&root, "agent-a", "run-a", "file.write")
.expect_err("invalid gate sidecar must stop mutation")
};
assert!(error.contains("普通文件"));
let blocker = project_verification_completion_blocker_at(&root, "agent-a", "run-a", &[])
.expect("unreadable sidecar must fail completion closed");
assert!(blocker.summary.contains("verification gate"));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn agent_runtime_file_delete_removes_file_and_advances_verification_gate() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let target = root.join("game/obsolete-notes.txt");
fs::write(&target, "这份旧笔记应被删除\n").expect("write delete target");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow file delete");
let observation = execute_agent_runtime_file_delete_for_test(
&root,
"file-delete-success-run",
Some("game/obsolete-notes.txt"),
)
.await;
assert_eq!(observation.tool, "file.delete");
assert_eq!(observation.status, "ok");
assert_eq!(observation.summary, "已删除 game/obsolete-notes.txt");
assert_eq!(observation.detail.as_deref(), Some("deleted=true"));
assert!(!target.exists());
let delete_records = read_agent_db_records_for_test(&root)
.into_iter()
.filter(|record| record["recordType"] == "agent.runtime.file.delete")
.collect::>();
assert_eq!(delete_records.len(), 1);
assert_eq!(delete_records[0]["agentId"], "design-director");
assert_eq!(delete_records[0]["path"], "game/obsolete-notes.txt");
assert_eq!(delete_records[0]["deleted"], true);
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read delete revision")
.revision,
1
);
let gate = read_game_creator_agent_runtime_verification_gate(
&root,
"design-director",
"file-delete-success-run",
)
.expect("read delete verification gate");
assert!(gate.requires_verification);
assert_eq!(gate.mutation_revision, Some(1));
assert_eq!(gate.verified_revision, None);
assert_eq!(gate.last_mutation_tool.as_deref(), Some("file.delete"));
assert!(project_verification_completion_blocker_at(
&root,
"design-director",
"file-delete-success-run",
std::slice::from_ref(&observation),
)
.is_some());
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_project_verify_summary_hashes_command_without_head_or_tail() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "验证命令公开摘要项目").expect("project init");
let command = format!(
"curl https://user:password@verify.example/private --password SUPER_SECRET --data GOAL_VERIFY_CANARY {}",
root.join("private/verify-target").display()
);
let action = AgentRuntimeToolAction {
tool: "project.verify".to_string(),
reason: Some("执行项目验证".to_string()),
input: serde_json::json!({
"script": "test",
"expectedCommand": command,
"timeoutSeconds": 120
}),
};
let summary =
agent_runtime_tool_action_input_summary(&root, &action).expect("verify input summary");
assert!(summary.contains("script=test"));
assert!(summary.contains("expectedCommandSha256="));
assert!(summary.contains("expectedCommandChars="));
assert!(!summary.contains("head="));
assert!(!summary.contains("tail="));
for forbidden in [
"verify.example",
"password",
"SUPER_SECRET",
"GOAL_VERIFY_CANARY",
root.to_string_lossy().as_ref(),
] {
assert!(
!summary.contains(forbidden),
"verify summary leaked {forbidden}"
);
}
fs::remove_dir_all(root).ok();
}
#[test]
fn limited_local_command_runs_static_game_smoke_and_writes_log() {
let root = unique_project_path();
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
fs::write(
root.join("game/index.html"),
fake_llm_game_draft().game_html,
)
.expect("write playable game html");
let result = run_limited_local_command_at(&root, "game.static_smoke").expect("static smoke");
assert_eq!(result.command_id, "game.static_smoke");
assert_eq!(result.status, "completed", "{}", result.output);
assert!(result.output.contains("game/index.html"));
assert_eq!(result.log_path, ".agent/logs/command.log");
assert!(!result.output.contains(root.to_string_lossy().as_ref()));
let log = fs::read_to_string(root.join(".agent/logs/command.log")).expect("command log");
assert!(log.contains("command.run_limited game.static_smoke"));
assert!(!log.contains(root.to_string_lossy().as_ref()));
let manifest: Value =
serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap())
.expect("manifest json");
assert_eq!(manifest["commandRuns"][0]["commandId"], "game.static_smoke");
assert_eq!(manifest["commandRuns"][0]["status"], "completed");
assert_eq!(
manifest["commandRuns"][0]["logPath"],
".agent/logs/command.log"
);
assert!(!manifest["commandRuns"][0]["output"]
.as_str()
.unwrap_or_default()
.contains(root.to_string_lossy().as_ref()));
fs::remove_dir_all(root).ok();
}
#[test]
fn project_verification_resolves_npm_script_and_rejects_command_drift() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "代码验证项目").expect("project init");
let check_command = "node scripts/check.mjs";
fs::write(
root.join("package.json"),
serde_json::to_string_pretty(&serde_json::json!({
"name": "verify-contract-fixture",
"private": true,
"packageManager": "npm@10.0.0",
"scripts": {
"check": check_command,
"check:ci": "node scripts/check-ci.mjs",
"test:unit": "node --test",
"lint:strict": "eslint . --max-warnings 0",
"typecheck:app": "tsc --noEmit",
"build:preview": "vite build --mode preview",
"verify:contracts": "node scripts/verify-contracts.mjs",
"validate:schema": "node scripts/validate-schema.mjs",
"deploy": "node scripts/deploy.mjs"
}
}))
.expect("serialize package json"),
)
.expect("write package json");
let spec = resolve_project_verification_spec_at(&root, "check", check_command, 30)
.expect("resolve check script");
assert_eq!(spec.script, "check");
assert_eq!(spec.expected_command, check_command);
assert_eq!(spec.program, project_verification_npm_program());
assert_eq!(
spec.arguments,
vec!["run", "--silent", "--ignore-scripts", "check"]
);
assert_eq!(spec.timeout_seconds, 30);
let named_test = resolve_project_verification_spec_at(&root, "test:unit", "node --test", 30)
.expect("resolve named unit test script");
assert_eq!(
named_test.arguments,
vec!["run", "--silent", "--ignore-scripts", "test:unit"]
);
for (script, command) in [
("check:ci", "node scripts/check-ci.mjs"),
("test:unit", "node --test"),
("lint:strict", "eslint . --max-warnings 0"),
("typecheck:app", "tsc --noEmit"),
("build:preview", "vite build --mode preview"),
("verify:contracts", "node scripts/verify-contracts.mjs"),
("validate:schema", "node scripts/validate-schema.mjs"),
] {
resolve_project_verification_spec_at(&root, script, command, 30)
.unwrap_or_else(|error| panic!("resolve named verification script {script}: {error}"));
}
let drift = resolve_project_verification_spec_at(&root, "check", "node stale.mjs", 30)
.expect_err("changed package script should fail closed");
assert!(drift.contains("package.json 中的 check 脚本已变化"));
let unsupported =
resolve_project_verification_spec_at(&root, "deploy", "node scripts/deploy.mjs", 30)
.expect_err("deploy is outside verification allowlist");
assert!(unsupported.contains("只允许验证类脚本"));
let empty_suffix = resolve_project_verification_spec_at(&root, "test:", "node --test", 30)
.expect_err("named script suffix must not be empty");
assert!(empty_suffix.contains("只允许验证类脚本"));
let deploy_family = resolve_project_verification_spec_at(
&root,
"deploy:production",
"node scripts/deploy.mjs",
30,
)
.expect_err("deploy family stays outside verification allowlist");
assert!(deploy_family.contains("只允许验证类脚本"));
for unsafe_script in [
"test::unit",
"test:unit fast",
"test:../unit",
"pretest:unit",
"posttest:unit",
] {
let error = resolve_project_verification_spec_at(&root, unsafe_script, "node --test", 30)
.expect_err("unsafe or lifecycle-like script name must fail closed");
assert!(error.contains("只允许验证类脚本"));
}
let overlong_script = format!("test:{}", "a".repeat(161));
let error = resolve_project_verification_spec_at(&root, &overlong_script, "node --test", 30)
.expect_err("overlong named script must fail closed");
assert!(error.contains("总长度不能超过"));
fs::remove_dir_all(root).ok();
}
#[test]
fn project_verification_output_redacts_secrets_and_keeps_failure_tail() {
let secret = concat!("sk-", "1234567890abcdefghijkl");
let github_secret = format!("ghp_{}", "a".repeat(36));
let output = format!(
"head\n{}\nprovider-error({secret});\ngithub={github_secret}\nVERIFY_FAILURE_TAIL",
"x".repeat(PROJECT_VERIFICATION_OUTPUT_MAX_BYTES * 2),
);
let sanitized = sanitize_project_verification_output(&output);
assert!(sanitized.len() <= PROJECT_VERIFICATION_OUTPUT_MAX_BYTES + 256);
assert!(sanitized.contains("