合并主分支游戏创作更新
同步 AI 游戏创作 Runtime、资源图与工作台主线改动 保留画布编辑器生成配方恢复改动
This commit is contained in:
@@ -138,10 +138,10 @@ Carry the current art spec in `generationInputs.artSpec` and reflect important c
|
||||
}
|
||||
```
|
||||
|
||||
The top-level `style` field is not the art spec's visual-style prose. It controls deterministic post-processing:
|
||||
The top-level `style` field is not the art spec's visual-style prose. It appends a short server-side clause to the prompt sent to the provider and enables deterministic post-processing:
|
||||
|
||||
- Omitted, `null`, empty string, or `"none"`: disable post-processing without warning.
|
||||
- `"pixelArt"`: enable pixel-art snapping for ordinary image generation, `kind: "character"`, and icon spritesheet generation.
|
||||
- Omitted, `null`, empty string, or `"none"`: no clause is appended and no post-processing runs, without warning.
|
||||
- `"pixelArt"`: append one short pixel-art line to the end of the prompt sent to the provider, and enable pixel-art snapping, for ordinary image generation, `kind: "character"`, and icon spritesheet generation. The line is appended, not substituted — the rest of your prompt is unchanged. For the exact per-kind wording, read the `style` field description in the OpenAPI document; it is the contract, and this guide deliberately does not copy it.
|
||||
- Unknown strings, or `"pixelArt"` on unsupported kinds such as `spec`, `quick-edit`, `ui-design`, or `publication-material`: continue without style processing and return `warning.code: "unsupported-image-style"`.
|
||||
- Non-string JSON values: malformed request, HTTP `400`.
|
||||
|
||||
|
||||
@@ -161,9 +161,6 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check server-rs boundaries
|
||||
run: npm run check:server-rs-ddd
|
||||
|
||||
- name: Prepare server-rs Rust dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -181,6 +178,9 @@ jobs:
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
|
||||
- name: Check server-rs boundaries
|
||||
run: npm run check:server-rs-ddd
|
||||
|
||||
- name: Run server-rs workspace tests
|
||||
run: cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml
|
||||
|
||||
|
||||
@@ -885,6 +885,10 @@ function readBrowserDom(url) {
|
||||
|
||||
function resolveChromeBin() {
|
||||
for (const candidate of [
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
||||
'/opt/google/chrome/chrome',
|
||||
'/usr/bin/google-chrome',
|
||||
'/usr/bin/google-chrome-stable',
|
||||
'/usr/bin/chromium',
|
||||
|
||||
@@ -5708,7 +5708,7 @@ mod canvas_generation_tests {
|
||||
|
||||
fn read_test_http_request(stream: &mut std::net::TcpStream) -> String {
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.set_read_timeout(Some(Duration::from_secs(10)))
|
||||
.expect("set request read timeout");
|
||||
let mut bytes = Vec::new();
|
||||
let mut buffer = [0_u8; 4096];
|
||||
@@ -6702,7 +6702,7 @@ mod canvas_generation_tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn recovery_scan_resumes_accepted_generation_on_default_worker_stack() {
|
||||
let temporary = tempfile::tempdir().expect("create accepted scan project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("accepted-generation-scan-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "accepted-scan", "恢复扫描测试")
|
||||
.expect("init accepted scan project");
|
||||
|
||||
+2
-2
@@ -838,7 +838,7 @@ mod external_generation_state_tests {
|
||||
|
||||
#[test]
|
||||
fn prepared_generation_state_reuses_identity_and_transitions_to_accepted() {
|
||||
let temporary = tempfile::tempdir().expect("create generation ledger project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("external-generation-ledger-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "generation-ledger", "生成账本测试")
|
||||
.expect("init project");
|
||||
@@ -955,7 +955,7 @@ mod external_generation_state_tests {
|
||||
|
||||
#[test]
|
||||
fn legacy_completed_generation_persists_only_allowlisted_safe_download_fields() {
|
||||
let temporary = tempfile::tempdir().expect("create legacy generation ledger project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("legacy-generation-ledger-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "legacy-generation-ledger", "旧同步生成账本测试")
|
||||
.expect("init project");
|
||||
|
||||
@@ -106,6 +106,7 @@ pub(crate) use project_gates::{
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use project_gates::{
|
||||
ensure_current_autonomous_ready_child_mutation_at_locked,
|
||||
supervisor_collaboration_policy_completion_blocker_for_test_at,
|
||||
supervisor_orchestrator_mutation_block_after_dispatch_for_test,
|
||||
};
|
||||
|
||||
@@ -50,12 +50,17 @@ pub(crate) fn append_agent_runtime_tool_call_record(
|
||||
action: &AgentRuntimeToolAction,
|
||||
observation: &AgentRuntimeToolObservation,
|
||||
action_id: Option<&str>,
|
||||
action_fingerprint: Option<&str>,
|
||||
) {
|
||||
let record = AgentRuntimeToolCallRecord {
|
||||
action_id: action_id.map(ToString::to_string),
|
||||
tool: observation.tool.clone(),
|
||||
status: observation.status.clone(),
|
||||
action_fingerprint: Some(agent_runtime_tool_action_fingerprint(action, task)),
|
||||
action_fingerprint: Some(
|
||||
action_fingerprint
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| agent_runtime_tool_action_fingerprint(action, task)),
|
||||
),
|
||||
input_summary: agent_runtime_tool_action_input_summary(root, action),
|
||||
reason: action
|
||||
.reason
|
||||
|
||||
@@ -132,7 +132,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
false,
|
||||
|| observe_agent_runtime_memory(root, agent_id, &action.input),
|
||||
),
|
||||
"memory.write" => observe_agent_runtime_memory_write(root, agent_id, &action.input),
|
||||
"memory.write" => observe_agent_runtime_memory_write(root, agent_id, run_id, &action.input),
|
||||
"conversation.read" => observe_agent_runtime_project_snapshot_with_lock(
|
||||
root,
|
||||
agent_id,
|
||||
@@ -285,8 +285,8 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
false,
|
||||
|| observe_agent_runtime_task_list(root, agent_id, run_id),
|
||||
),
|
||||
"task.create" => observe_agent_runtime_task_create(root, agent_id, &action.input),
|
||||
"task.update" => observe_agent_runtime_task_update(root, agent_id, &action.input),
|
||||
"task.create" => observe_agent_runtime_task_create(root, agent_id, run_id, &action.input),
|
||||
"task.update" => observe_agent_runtime_task_update(root, agent_id, run_id, &action.input),
|
||||
"command.exec" => {
|
||||
observe_agent_runtime_command_exec(
|
||||
root,
|
||||
@@ -386,7 +386,9 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
)
|
||||
.await
|
||||
}
|
||||
"blackboard.write" => observe_agent_runtime_blackboard_write(root, agent_id, &action.input),
|
||||
"blackboard.write" => {
|
||||
observe_agent_runtime_blackboard_write(root, agent_id, run_id, &action.input)
|
||||
}
|
||||
"agent.message" => {
|
||||
observe_agent_runtime_agent_message(root, agent_id, run_id, &action.input)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ pub(in crate::agent) fn persist_game_creator_agent_user_input_wait_at(
|
||||
&pending.action,
|
||||
&waiting_observation,
|
||||
Some(&pending.action_id),
|
||||
Some(&pending.action_fingerprint),
|
||||
);
|
||||
runtime.pending_tool_action = Some(pending.summary());
|
||||
runtime.status = "waiting-for-user-input".to_string();
|
||||
|
||||
+53
-35
@@ -536,34 +536,13 @@ pub(in crate::agent) fn autonomous_manifest_dag_in_progress_at(
|
||||
root: &Path,
|
||||
) -> Result<bool, String> {
|
||||
let manifest = read_manifest_for_project(root)?;
|
||||
let source = read_game_creator_agent_runtime_at(root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
.ok()
|
||||
.filter(|runtime| {
|
||||
runtime.state.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& agent_runtime_supervisor_source_is_trusted(&runtime.state.source)
|
||||
})
|
||||
.map(|runtime| runtime.state.source)
|
||||
.or_else(|| {
|
||||
let path = game_creator_agent_runtime_task_path(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
);
|
||||
read_all_game_creator_agent_runtime_tasks(&path)
|
||||
.ok()
|
||||
.map(latest_game_creator_agent_runtime_tasks)
|
||||
.and_then(|records| {
|
||||
records.into_iter().rev().find_map(|record| {
|
||||
(record.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& record.parent_run_id.is_none()
|
||||
&& agent_runtime_supervisor_source_is_trusted(&record.source))
|
||||
.then_some(record.source)
|
||||
})
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
"无法解析当前自主构建根 Run 的可信 source,拒绝按 GUI 完整 DAG 回退".to_string()
|
||||
})?;
|
||||
let seed_task_ids = autonomous_manifest_seed_tasks_for_source(&source)
|
||||
let current_root = current_autonomous_game_build_root_task_at(root)?.ok_or_else(|| {
|
||||
"无法解析当前自主构建根 Run 的可信 source,拒绝按 GUI 完整 DAG 回退".to_string()
|
||||
})?;
|
||||
if !autonomous_game_build_root_task_is_active(¤t_root) {
|
||||
return Ok(false);
|
||||
}
|
||||
let seed_task_ids = autonomous_manifest_seed_tasks_for_source(¤t_root.source)
|
||||
.into_iter()
|
||||
.map(|task| task.id)
|
||||
.collect::<BTreeSet<_>>();
|
||||
@@ -572,19 +551,58 @@ pub(in crate::agent) fn autonomous_manifest_dag_in_progress_at(
|
||||
.iter()
|
||||
.filter(|task| seed_task_ids.contains(&task.id))
|
||||
.collect::<Vec<_>>();
|
||||
let started = seed_tasks
|
||||
.iter()
|
||||
.any(|task| task.status != GameCreationAppTaskStatus::Pending);
|
||||
let running = seed_tasks
|
||||
.iter()
|
||||
.any(|task| task.status == GameCreationAppTaskStatus::Running);
|
||||
let completed = seed_tasks
|
||||
.iter()
|
||||
.all(|task| task.status == GameCreationAppTaskStatus::Completed);
|
||||
let failed = seed_tasks
|
||||
.iter()
|
||||
.any(|task| task.status == GameCreationAppTaskStatus::Failed);
|
||||
Ok(started && running && !completed && !failed)
|
||||
let active_child = autonomous_manifest_parent_has_active_ready_task_at(
|
||||
root,
|
||||
¤t_root.run_id,
|
||||
&seed_task_ids,
|
||||
)?;
|
||||
Ok((running || active_child) && !failed)
|
||||
}
|
||||
|
||||
fn autonomous_manifest_parent_has_active_ready_task_at(
|
||||
root: &Path,
|
||||
parent_run_id: &str,
|
||||
seed_task_ids: &BTreeSet<String>,
|
||||
) -> Result<bool, String> {
|
||||
for task_id in seed_task_ids {
|
||||
let records =
|
||||
latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(
|
||||
&game_creator_agent_runtime_task_path(root, task_id),
|
||||
)?);
|
||||
for record in records {
|
||||
if record.source != "agent-ready-task-scheduler"
|
||||
|| record.parent_agent_id.as_deref()
|
||||
!= Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
|| record.parent_run_id.as_deref() != Some(parent_run_id)
|
||||
|| game_creator_agent_runtime_terminal_status(&record).is_some()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if record.run_id != autonomous_manifest_ready_task_run_id(parent_run_id, task_id) {
|
||||
return Err(format!(
|
||||
"当前自主构建父 Run 的活跃 child runId 不符合确定性绑定:taskId={task_id}"
|
||||
));
|
||||
}
|
||||
let state = agent_runtime_state_from_task_record(&record);
|
||||
let binding = autonomous_manifest_ready_task_parent_binding_for_state_at(root, &state)?
|
||||
.ok_or_else(|| {
|
||||
format!("当前自主构建父 Run 的活跃 child 缺少父绑定:taskId={task_id}")
|
||||
})?;
|
||||
if binding.root_run_id != parent_run_id {
|
||||
return Err(format!(
|
||||
"当前自主构建父 Run 的活跃 child rootRunId 不一致:taskId={task_id}"
|
||||
));
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_agent_runtime_autonomous_plan_liveness(
|
||||
|
||||
@@ -776,6 +776,7 @@ pub(in crate::agent) fn project_game_creator_agent_runtime_parallel_read_batch(
|
||||
&pending.action,
|
||||
&observation,
|
||||
Some(&pending.action_id),
|
||||
Some(&pending.action_fingerprint),
|
||||
);
|
||||
runtime.pending_tool_action = None;
|
||||
runtime.status = "running".to_string();
|
||||
|
||||
+1
-1
@@ -790,7 +790,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn generation_cleanup_failure_preserves_pending_identity_anchor() {
|
||||
let temporary = tempfile::tempdir().expect("create pending cleanup project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("pending-generation-cleanup-");
|
||||
let root = temporary.path();
|
||||
let run_id = "generation-cleanup-order-run";
|
||||
init_local_game_project_at(root, "generation-cleanup-order", "生成账本清理顺序测试")
|
||||
|
||||
@@ -142,6 +142,7 @@ pub(crate) fn prepare_agent_runtime_project_mutation_locked(
|
||||
blocker.detail.unwrap_or_default()
|
||||
));
|
||||
}
|
||||
ensure_current_autonomous_ready_child_mutation_at_locked(root, agent_id, run_id)?;
|
||||
let mut revision = read_game_creator_agent_runtime_project_revision(root)?;
|
||||
let mut gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?;
|
||||
let next_revision = revision
|
||||
@@ -176,6 +177,159 @@ pub(crate) fn prepare_agent_runtime_project_mutation_locked(
|
||||
Ok(next_revision)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_current_autonomous_ready_child_mutation_at_locked(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
) -> Result<(), String> {
|
||||
let normalized_agent_id = match normalize_game_creator_runtime_agent_id(agent_id) {
|
||||
Ok(agent_id) => agent_id,
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
let binding =
|
||||
read_game_creator_agent_runtime_run_profile_binding(root, &normalized_agent_id, run_id)?;
|
||||
let Some(binding) = binding else {
|
||||
let task = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
&normalized_agent_id,
|
||||
run_id,
|
||||
)?;
|
||||
if task
|
||||
.as_ref()
|
||||
.is_some_and(|task| task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD)
|
||||
{
|
||||
return Err("autonomous Run 项目修改缺少 Run Profile binding,已失败关闭".to_string());
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
if binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||
return Ok(());
|
||||
}
|
||||
if binding.agent_id != normalized_agent_id
|
||||
|| binding.run_id != run_id
|
||||
|| binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
{
|
||||
return Err("autonomous Run 项目修改的 Run Profile 绑定身份不一致".to_string());
|
||||
}
|
||||
let task =
|
||||
read_latest_game_creator_agent_runtime_task_by_run_id(root, &normalized_agent_id, run_id)?
|
||||
.ok_or_else(|| {
|
||||
"autonomous Run 项目修改缺少 durable task journal,已失败关闭".to_string()
|
||||
})?;
|
||||
if task.agent_id != binding.agent_id
|
||||
|| task.run_id != binding.run_id
|
||||
|| task.source != binding.source
|
||||
|| task.run_profile != binding.profile
|
||||
|| task.run_profile_binding_fingerprint != binding.binding_fingerprint
|
||||
|| task.parent_agent_id != binding.parent_agent_id
|
||||
|| task.parent_run_id != binding.parent_run_id
|
||||
{
|
||||
return Err("autonomous Run 项目修改的 durable task journal 与绑定不一致".to_string());
|
||||
}
|
||||
let is_root = binding.agent_id == binding.root_agent_id
|
||||
&& binding.run_id == binding.root_run_id
|
||||
&& binding.parent_agent_id.is_none()
|
||||
&& binding.parent_run_id.is_none();
|
||||
if is_root {
|
||||
if task.parent_agent_id.is_some()
|
||||
|| task.parent_run_id.is_some()
|
||||
|| !agent_runtime_supervisor_source_is_trusted(&task.source)
|
||||
{
|
||||
return Err("autonomous 根 Run 项目修改的 durable identity 不一致".to_string());
|
||||
}
|
||||
} else {
|
||||
let parent_agent_id = binding
|
||||
.parent_agent_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少 parentAgentId".to_string())?;
|
||||
let parent_run_id = binding
|
||||
.parent_run_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少 parentRunId".to_string())?;
|
||||
let parent_binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
parent_agent_id,
|
||||
parent_run_id,
|
||||
)?
|
||||
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少父 Run Profile binding".to_string())?;
|
||||
if binding.parent_binding_fingerprint.as_deref()
|
||||
!= Some(parent_binding.binding_fingerprint.as_str())
|
||||
|| parent_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
|| parent_binding.root_agent_id != binding.root_agent_id
|
||||
|| parent_binding.root_run_id != binding.root_run_id
|
||||
{
|
||||
return Err(
|
||||
"autonomous 派生 Run 项目修改的父 binding 或 root identity 不一致".to_string(),
|
||||
);
|
||||
}
|
||||
if binding.source == "agent-ready-task-scheduler" {
|
||||
let state = agent_runtime_state_from_task_record(&task);
|
||||
let ready_binding =
|
||||
autonomous_manifest_ready_task_parent_binding_for_state_at(root, &state)?
|
||||
.ok_or_else(|| {
|
||||
"autonomous ready-task 项目修改缺少确定性父 Run 绑定".to_string()
|
||||
})?;
|
||||
if ready_binding != binding
|
||||
|| state.run_id
|
||||
!= autonomous_manifest_ready_task_run_id(
|
||||
&binding.root_run_id,
|
||||
&normalized_agent_id,
|
||||
)
|
||||
{
|
||||
return Err("autonomous ready-task 项目修改的确定性父子身份不一致".to_string());
|
||||
}
|
||||
} else if binding.source == "agent-delegate"
|
||||
&& task
|
||||
.delegation_id
|
||||
.as_deref()
|
||||
.is_none_or(|delegation_id| delegation_id.trim().is_empty())
|
||||
{
|
||||
return Err("autonomous agent-delegate 项目修改缺少 delegationId".to_string());
|
||||
}
|
||||
}
|
||||
if task.status != "running" || game_creator_agent_runtime_terminal_status(&task).is_some() {
|
||||
return Err("autonomous Run 项目修改要求当前 durable task 仍为 running".to_string());
|
||||
}
|
||||
let current_root = current_autonomous_game_build_root_task_at(root)?
|
||||
.ok_or_else(|| "autonomous Run 项目修改时当前根 Run 已不存在".to_string())?;
|
||||
if current_root.run_id != binding.root_run_id {
|
||||
return Err(format!(
|
||||
"autonomous Run 已被更新根 Run 取代:currentRunId={}",
|
||||
current_root.run_id
|
||||
));
|
||||
}
|
||||
let current_root_binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
¤t_root.agent_id,
|
||||
¤t_root.run_id,
|
||||
)?
|
||||
.ok_or_else(|| "autonomous Run 当前根缺少 Run Profile binding".to_string())?;
|
||||
if current_root.agent_id != binding.root_agent_id
|
||||
|| current_root.source != current_root_binding.source
|
||||
|| current_root.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
|| current_root.parent_agent_id.is_some()
|
||||
|| current_root.parent_run_id.is_some()
|
||||
|| current_root.delegation_id.is_some()
|
||||
|| current_root_binding.agent_id != binding.root_agent_id
|
||||
|| current_root_binding.run_id != binding.root_run_id
|
||||
|| current_root_binding.root_agent_id != current_root_binding.agent_id
|
||||
|| current_root_binding.root_run_id != current_root_binding.run_id
|
||||
|| current_root_binding.parent_agent_id.is_some()
|
||||
|| current_root_binding.parent_run_id.is_some()
|
||||
|| current_root_binding.binding_fingerprint != current_root.run_profile_binding_fingerprint
|
||||
|| (is_root && current_root_binding.binding_fingerprint != binding.binding_fingerprint)
|
||||
{
|
||||
return Err("autonomous Run 当前根 journal 与 binding 不一致".to_string());
|
||||
}
|
||||
if !autonomous_game_build_root_task_is_active(¤t_root) {
|
||||
return Err(format!(
|
||||
"autonomous Run 当前根已不再活跃:status={} phase={}",
|
||||
current_root.status, current_root.phase
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn begin_agent_runtime_project_verification_locked(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
|
||||
+2
@@ -689,6 +689,7 @@ pub(in crate::agent) fn persist_game_creator_agent_runtime_provider_batch_waitin
|
||||
&pending.action,
|
||||
observation,
|
||||
Some(&pending.action_id),
|
||||
Some(&pending.action_fingerprint),
|
||||
);
|
||||
complete_agent_runtime_active_plan_step(
|
||||
runtime,
|
||||
@@ -788,6 +789,7 @@ pub(in crate::agent) fn project_game_creator_agent_runtime_provider_batch_abort(
|
||||
&pending.action,
|
||||
&observation,
|
||||
Some(&pending.action_id),
|
||||
Some(&pending.action_fingerprint),
|
||||
);
|
||||
complete_agent_runtime_active_plan_step(runtime, "failed", &observation_summary);
|
||||
runtime.pending_tool_action = None;
|
||||
|
||||
+4
-3
@@ -463,7 +463,8 @@ mod tests {
|
||||
root_source: &str,
|
||||
suffix: &str,
|
||||
) -> String {
|
||||
let temporary = tempfile::tempdir().expect("temporary project root");
|
||||
let temporary =
|
||||
crate::tests::canonical_test_tempdir(&format!("provider-role-overlay-{suffix}-"));
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, &format!("overlay-{suffix}"), "role overlay test")
|
||||
.expect("project init");
|
||||
@@ -549,7 +550,7 @@ mod tests {
|
||||
const ORDINARY_NOTICE: &str = "除下方有界仓库启动上下文、当前 Session 未压缩对话尾部或历史压缩摘要外,项目记忆、资产和源码正文不会预加载";
|
||||
const MEMORY_MARKER: &str = "supervisor-preloaded-context-marker";
|
||||
|
||||
let directory = tempfile::tempdir().expect("temp project directory");
|
||||
let directory = crate::tests::canonical_test_tempdir("provider-request-project-");
|
||||
let root = directory.path().join("project");
|
||||
init_local_game_project_at(&root, "project-1", "项目总控预加载说明测试")
|
||||
.expect("project init");
|
||||
@@ -806,7 +807,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn planning_request_advertises_only_native_mcp_functions() {
|
||||
let directory = tempfile::tempdir().expect("temp project directory");
|
||||
let directory = crate::tests::canonical_test_tempdir("native-mcp-prompt-");
|
||||
let root = directory.path().join("project");
|
||||
init_local_game_project_at(&root, "project-mcp", "MCP 原生函数说明测试")
|
||||
.expect("project init");
|
||||
|
||||
@@ -2,6 +2,12 @@ use super::*;
|
||||
|
||||
pub(super) static GAME_CREATOR_AGENT_RUNTIME_UPDATE_APP_HANDLE: OnceLock<tauri::AppHandle> =
|
||||
OnceLock::new();
|
||||
pub(super) static GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINK: OnceLock<
|
||||
std::sync::Mutex<Option<GameCreatorManifestInvalidationEventSink>>,
|
||||
> = OnceLock::new();
|
||||
#[cfg(test)]
|
||||
pub(super) static GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINK_TEST_LOCK: std::sync::Mutex<()> =
|
||||
std::sync::Mutex::new(());
|
||||
pub(super) static STATIC_DELEGATE_PARENT_WAKE_SINGLEFLIGHT: OnceLock<
|
||||
std::sync::Mutex<std::collections::BTreeMap<String, bool>>,
|
||||
> = OnceLock::new();
|
||||
@@ -101,6 +107,8 @@ pub(crate) const AGENT_RUNTIME_ISOLATED_JOIN_SOURCE: &str = "agent-isolated-join
|
||||
pub(crate) const AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE: &str = "project-supervisor-gui";
|
||||
pub(crate) const AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE: &str = "project-supervisor-cli";
|
||||
pub(crate) const AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE: &str = "project-supervisor-game-chat";
|
||||
pub(super) const GAME_CHAT_FIXED_TASK_GRAPH_STALLED_ERROR: &str =
|
||||
"game-chat 首版固定任务图无法继续推进,拒绝回退到普通 Provider 协作波";
|
||||
|
||||
pub(crate) fn agent_runtime_supervisor_source_is_trusted(source: &str) -> bool {
|
||||
matches!(
|
||||
@@ -234,15 +242,21 @@ pub(in crate::agent) use recovery_scan::*;
|
||||
pub(in crate::agent) use task_queue::*;
|
||||
pub(in crate::agent) use task_start::*;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use entrypoints::acquire_game_creator_manifest_invalidation_event_sink_test_guard;
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use entrypoints::{
|
||||
chat_with_game_creator_agent_at, chat_with_game_creator_role_agent_at,
|
||||
chat_with_game_creator_role_agent_for_session_at, chat_with_game_creator_role_agent_runtime_at,
|
||||
chat_with_game_creator_role_agent_runtime_for_session_at,
|
||||
chat_with_game_creator_role_agent_stream_at,
|
||||
chat_with_game_creator_role_agent_stream_for_session_at, generate_local_game_draft_at,
|
||||
read_game_creator_agent_runtime_at, read_game_creator_agent_runtime_for_session_at,
|
||||
read_game_creator_agent_runtimes_at, set_game_creator_agent_runtime_update_app_handle,
|
||||
chat_with_game_creator_role_agent_stream_for_session_at,
|
||||
configure_game_creator_manifest_invalidation_event_sink,
|
||||
emit_game_creator_agent_runtime_update, game_creator_agent_runtime_update_event,
|
||||
generate_local_game_draft_at, read_game_creator_agent_runtime_at,
|
||||
read_game_creator_agent_runtime_for_session_at, read_game_creator_agent_runtimes_at,
|
||||
set_game_creator_agent_runtime_update_app_handle,
|
||||
start_game_creator_manifest_invalidation_event_sink,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use finalization::resume_game_creator_agent_finalization_for_test_at;
|
||||
@@ -277,8 +291,12 @@ pub(crate) use provider_recovery::{
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use provider_recovery::{
|
||||
drive_waiting_autonomous_manifest_parent_wake_budget_for_test,
|
||||
ensure_waiting_provider_retry_records_for_test,
|
||||
mark_autonomous_manifest_parent_wake_needs_reconciliation_for_test,
|
||||
prepare_waiting_autonomous_manifest_parent_for_test,
|
||||
probe_static_delegate_parent_wake_singleflight_coalescing,
|
||||
repair_autonomous_manifest_parent_wake_reconciliation_projection_for_test,
|
||||
};
|
||||
pub(crate) use recovery_scan::{
|
||||
cleanup_game_creator_agent_runtime_completed_finalizations_at,
|
||||
@@ -293,10 +311,12 @@ pub(crate) use task_queue::{
|
||||
run_game_creator_agent_background_task_with_context,
|
||||
spawn_next_game_creator_agent_background_task_drain,
|
||||
spawn_next_game_creator_agent_background_task_drain_with_lock,
|
||||
spawn_started_game_creator_agent_background_task_drain_with_lock,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use task_start::start_game_creator_agent_background_task_with_session_lane_hook_at;
|
||||
pub(crate) use task_start::{
|
||||
autonomous_game_build_root_task_is_active, current_autonomous_game_build_root_task_at,
|
||||
notify_external_agent_runner_after_background_task_enqueue,
|
||||
project_autonomous_manifest_ready_task_terminal_at,
|
||||
schedule_autonomous_game_build_ready_tasks_at, schedule_game_creator_agent_ready_tasks_at,
|
||||
|
||||
@@ -1,30 +1,174 @@
|
||||
use super::*;
|
||||
|
||||
const GAME_CREATOR_MANIFEST_INVALIDATION_RELAY_MAX_BYTES: u64 = 64 * 1024;
|
||||
|
||||
fn lock_game_creator_manifest_invalidation_event_sink(
|
||||
) -> std::sync::MutexGuard<'static, Option<GameCreatorManifestInvalidationEventSink>> {
|
||||
GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINK
|
||||
.get_or_init(|| Mutex::new(None))
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
}
|
||||
|
||||
pub(crate) fn set_game_creator_agent_runtime_update_app_handle(app: tauri::AppHandle) {
|
||||
let _ = GAME_CREATOR_AGENT_RUNTIME_UPDATE_APP_HANDLE.set(app);
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn emit_game_creator_agent_runtime_update(root: &Path, agent_id: &str) {
|
||||
pub(crate) fn start_game_creator_manifest_invalidation_event_sink(
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<GameCreatorManifestInvalidationEventSink, String> {
|
||||
let listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))
|
||||
.map_err(|error| format!("绑定 manifest 失效事件接收端失败:{error}"))?;
|
||||
let port = listener
|
||||
.local_addr()
|
||||
.map_err(|error| format!("读取 manifest 失效事件接收端失败:{error}"))?
|
||||
.port();
|
||||
let token = format!(
|
||||
"{}{}",
|
||||
uuid::Uuid::new_v4().simple(),
|
||||
uuid::Uuid::new_v4().simple()
|
||||
);
|
||||
let expected_token = token.clone();
|
||||
thread::Builder::new()
|
||||
.name("manifest-invalidation-event-sink".to_string())
|
||||
.spawn(move || {
|
||||
for incoming in listener.incoming() {
|
||||
let Ok(mut stream) = incoming else {
|
||||
continue;
|
||||
};
|
||||
let _ = stream.set_read_timeout(Some(Duration::from_millis(250)));
|
||||
let mut payload = Vec::new();
|
||||
let mut limited =
|
||||
(&mut stream).take(GAME_CREATOR_MANIFEST_INVALIDATION_RELAY_MAX_BYTES + 1);
|
||||
if limited.read_to_end(&mut payload).is_err()
|
||||
|| payload.len() as u64 > GAME_CREATOR_MANIFEST_INVALIDATION_RELAY_MAX_BYTES
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Ok(envelope) = serde_json::from_slice::<
|
||||
GameCreatorManifestInvalidationRelayEnvelope,
|
||||
>(&payload) else {
|
||||
continue;
|
||||
};
|
||||
if envelope.token != expected_token {
|
||||
continue;
|
||||
}
|
||||
let _ = app.emit("game-creator-manifest-invalidated", envelope.event);
|
||||
}
|
||||
})
|
||||
.map_err(|error| format!("启动 manifest 失效事件接收端失败:{error}"))?;
|
||||
Ok(GameCreatorManifestInvalidationEventSink { port, token })
|
||||
}
|
||||
|
||||
pub(crate) fn configure_game_creator_manifest_invalidation_event_sink(
|
||||
port: u16,
|
||||
token: &str,
|
||||
) -> Result<(), String> {
|
||||
if port == 0 {
|
||||
return Err("manifest 失效事件接收端口无效".to_string());
|
||||
}
|
||||
let token = token.trim();
|
||||
if token.len() != 64 || !token.bytes().all(|byte| byte.is_ascii_hexdigit()) {
|
||||
return Err("manifest 失效事件接收令牌无效".to_string());
|
||||
}
|
||||
*lock_game_creator_manifest_invalidation_event_sink() =
|
||||
Some(GameCreatorManifestInvalidationEventSink {
|
||||
port,
|
||||
token: token.to_string(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) struct GameCreatorManifestInvalidationEventSinkTestGuard {
|
||||
_isolation: std::sync::MutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl GameCreatorManifestInvalidationEventSinkTestGuard {
|
||||
pub(crate) fn configure(&self, port: u16, token: &str) -> Result<(), String> {
|
||||
configure_game_creator_manifest_invalidation_event_sink(port, token)
|
||||
}
|
||||
|
||||
pub(crate) fn configured_sink(&self) -> Option<GameCreatorManifestInvalidationEventSink> {
|
||||
lock_game_creator_manifest_invalidation_event_sink().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Drop for GameCreatorManifestInvalidationEventSinkTestGuard {
|
||||
fn drop(&mut self) {
|
||||
*lock_game_creator_manifest_invalidation_event_sink() = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn acquire_game_creator_manifest_invalidation_event_sink_test_guard(
|
||||
) -> GameCreatorManifestInvalidationEventSinkTestGuard {
|
||||
let isolation = GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINK_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
GameCreatorManifestInvalidationEventSinkTestGuard {
|
||||
_isolation: isolation,
|
||||
}
|
||||
}
|
||||
|
||||
fn relay_game_creator_manifest_invalidation(root: &Path, agent_id: &str) -> Result<(), String> {
|
||||
let sink = lock_game_creator_manifest_invalidation_event_sink().clone();
|
||||
let Some(sink) = sink else {
|
||||
return Ok(());
|
||||
};
|
||||
let envelope = GameCreatorManifestInvalidationRelayEnvelope {
|
||||
token: sink.token,
|
||||
event: GameCreatorManifestInvalidatedEvent {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
agent_id: agent_id.to_string(),
|
||||
},
|
||||
};
|
||||
let payload = serde_json::to_vec(&envelope)
|
||||
.map_err(|error| format!("序列化 manifest 失效事件失败:{error}"))?;
|
||||
if payload.len() as u64 > GAME_CREATOR_MANIFEST_INVALIDATION_RELAY_MAX_BYTES {
|
||||
return Err("manifest 失效事件超过大小上限".to_string());
|
||||
}
|
||||
let address = std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST, sink.port).into();
|
||||
let mut stream = TcpStream::connect_timeout(&address, Duration::from_millis(100))
|
||||
.map_err(|error| format!("连接 manifest 失效事件接收端失败:{error}"))?;
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_millis(100)))
|
||||
.map_err(|error| format!("配置 manifest 失效事件发送超时失败:{error}"))?;
|
||||
stream
|
||||
.write_all(&payload)
|
||||
.map_err(|error| format!("发送 manifest 失效事件失败:{error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_agent_runtime_update_event(
|
||||
root: &Path,
|
||||
runtime: AgentRuntimeResult,
|
||||
) -> GameCreatorAgentRuntimeUpdateEvent {
|
||||
GameCreatorAgentRuntimeUpdateEvent {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
agent_id: runtime.state.agent_id.clone(),
|
||||
run_id: runtime.state.run_id.clone(),
|
||||
status: runtime.state.status.clone(),
|
||||
phase: runtime.state.phase.clone(),
|
||||
manifest_invalidated: true,
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn emit_game_creator_agent_runtime_update(root: &Path, agent_id: &str) {
|
||||
if GAME_CREATOR_AGENT_RUNTIME_UPDATE_APP_HANDLE.get().is_none() {
|
||||
let _ = relay_game_creator_manifest_invalidation(root, agent_id);
|
||||
}
|
||||
let Some(app) = GAME_CREATOR_AGENT_RUNTIME_UPDATE_APP_HANDLE.get() else {
|
||||
return;
|
||||
};
|
||||
let Ok(runtime) = read_game_creator_agent_runtime_at(root, agent_id) else {
|
||||
return;
|
||||
};
|
||||
let agent_id = runtime.state.agent_id.clone();
|
||||
let run_id = runtime.state.run_id.clone();
|
||||
let status = runtime.state.status.clone();
|
||||
let phase = runtime.state.phase.clone();
|
||||
let _ = app.emit(
|
||||
"game-creator-agent-runtime-update",
|
||||
GameCreatorAgentRuntimeUpdateEvent {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
agent_id,
|
||||
run_id,
|
||||
status,
|
||||
phase,
|
||||
runtime,
|
||||
},
|
||||
game_creator_agent_runtime_update_event(root, runtime),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -584,8 +728,14 @@ pub(in crate::agent) fn read_game_creator_agent_runtime_with_session_filter_at(
|
||||
}
|
||||
let recent_events =
|
||||
read_recent_game_creator_agent_runtime_events_for_session(&event_path, session_id)?;
|
||||
let task_snapshot =
|
||||
read_game_creator_agent_runtime_task_snapshot_for_session(&task_path, session_id)?;
|
||||
let task_snapshot = read_game_creator_agent_runtime_task_snapshot_for_session(
|
||||
&task_path,
|
||||
session_id,
|
||||
(!state.run_id.trim().is_empty()).then_some(state.run_id.as_str()),
|
||||
)?;
|
||||
if state.started_at == 0 {
|
||||
state.started_at = task_snapshot.run_started_at.unwrap_or(state.updated_at);
|
||||
}
|
||||
state.task_queue = task_snapshot.task_queue.clone();
|
||||
let response_stream =
|
||||
visible_game_creator_agent_runtime_response_stream_at(root, &state).unwrap_or(None);
|
||||
|
||||
+359
-1
@@ -12,6 +12,7 @@ pub(crate) const GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX: &str =
|
||||
"game-chat-first-playable-hard-budget-exhausted";
|
||||
|
||||
const FALLBACK_THEME_MARKER: &str = "__GAME_CHAT_THEME__";
|
||||
pub(super) const GAME_CHAT_CODE_COMPLETION_REPAIR_STEP: &str = "修复 Runtime 完成门诊断并重新验证";
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct GameChatFastPathBudget {
|
||||
@@ -313,6 +314,134 @@ fn game_chat_fast_path_has_art_manifest(root: &Path) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
fn game_chat_english_words(task: &str) -> Vec<&str> {
|
||||
task.split(|character: char| !character.is_ascii_alphanumeric())
|
||||
.filter(|word| !word.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn game_chat_english_words_contain_phrase(words: &[&str], phrase: &[&str]) -> bool {
|
||||
words
|
||||
.windows(phrase.len())
|
||||
.any(|candidate| candidate == phrase)
|
||||
}
|
||||
|
||||
fn game_chat_english_application_is_negated(words: &[&str], application_index: usize) -> bool {
|
||||
let prefix = &words[application_index.saturating_sub(4)..application_index];
|
||||
prefix
|
||||
.iter()
|
||||
.any(|word| matches!(*word, "not" | "never" | "dont"))
|
||||
|| game_chat_english_words_contain_phrase(prefix, &["don", "t"])
|
||||
|| ["refuse", "refuses", "refused"]
|
||||
.iter()
|
||||
.any(|refusal| prefix.ends_with(&[*refusal]) || prefix.ends_with(&[*refusal, "to"]))
|
||||
}
|
||||
|
||||
fn game_chat_chinese_application_is_negated(clause: &str, application_index: usize) -> bool {
|
||||
let prefix = clause[..application_index].trim_end();
|
||||
[
|
||||
"不要", "不", "别", "勿", "请勿", "禁止", "拒绝", "避免", "无需", "无须", "不能", "不可",
|
||||
"不得", "不应",
|
||||
]
|
||||
.iter()
|
||||
.any(|negation| prefix.ends_with(negation))
|
||||
}
|
||||
|
||||
fn game_chat_chinese_clause_requests_existing_art_application(clause: &str) -> bool {
|
||||
let names_art = ["美术资源", "美术素材", "已有素材", "现有素材"]
|
||||
.iter()
|
||||
.any(|marker| clause.contains(marker));
|
||||
if !names_art {
|
||||
return false;
|
||||
}
|
||||
|
||||
let explicitly_names_existing_art = ["已有美术", "现有美术", "已有素材", "现有素材"]
|
||||
.iter()
|
||||
.any(|marker| clause.contains(marker));
|
||||
let requests_new_art = [
|
||||
"全新美术",
|
||||
"新的美术",
|
||||
"新美术",
|
||||
"全新素材",
|
||||
"新的素材",
|
||||
"新素材",
|
||||
"重新生成美术",
|
||||
"重做美术",
|
||||
]
|
||||
.iter()
|
||||
.any(|marker| clause.contains(marker));
|
||||
if requests_new_art && !explicitly_names_existing_art {
|
||||
return false;
|
||||
}
|
||||
|
||||
["替换", "换成", "接入", "使用", "应用", "复用"]
|
||||
.iter()
|
||||
.any(|application| {
|
||||
clause.match_indices(application).any(|(index, _)| {
|
||||
// “换成” is also a suffix of “替换成”; the latter must be
|
||||
// judged once at the beginning of the complete action.
|
||||
!(*application == "换成" && clause[..index].ends_with('替'))
|
||||
&& !game_chat_chinese_application_is_negated(clause, index)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn game_chat_explicit_existing_art_reuse_intent(task: &str) -> bool {
|
||||
let normalized = task.trim().to_ascii_lowercase();
|
||||
let english_words = game_chat_english_words(&normalized);
|
||||
let requests_existing_art_in_chinese = normalized
|
||||
.split(|character: char| {
|
||||
matches!(
|
||||
character,
|
||||
',' | '。' | ';' | ';' | ',' | '.' | '!' | '!' | '?' | '?' | '\n' | '\r'
|
||||
)
|
||||
})
|
||||
.any(game_chat_chinese_clause_requests_existing_art_application);
|
||||
let requests_new_art_in_english = [["new", "art"], ["fresh", "art"], ["regenerate", "art"]]
|
||||
.iter()
|
||||
.any(|phrase| game_chat_english_words_contain_phrase(&english_words, phrase));
|
||||
let requests_application_in_english = english_words.iter().enumerate().any(|(index, word)| {
|
||||
matches!(*word, "replace" | "use" | "apply" | "reuse")
|
||||
&& !game_chat_english_application_is_negated(&english_words, index)
|
||||
});
|
||||
let names_art_in_english = [
|
||||
&["art", "asset"][..],
|
||||
&["art", "assets"][..],
|
||||
&["spritesheet"][..],
|
||||
&["spritesheets"][..],
|
||||
&["sprite", "sheet"][..],
|
||||
&["sprite", "sheets"][..],
|
||||
]
|
||||
.iter()
|
||||
.any(|phrase| game_chat_english_words_contain_phrase(&english_words, phrase));
|
||||
requests_existing_art_in_chinese
|
||||
|| (!requests_new_art_in_english && requests_application_in_english && names_art_in_english)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn game_chat_existing_art_reuse_refinement_intent_at(
|
||||
root: &Path,
|
||||
task: &str,
|
||||
) -> Result<bool, String> {
|
||||
if !game_chat_explicit_existing_art_reuse_intent(task)
|
||||
|| game_chat_fallback_targets_initial_placeholder(root)?
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn game_chat_existing_art_reuse_refinement_is_valid_at(
|
||||
root: &Path,
|
||||
task: &str,
|
||||
) -> Result<bool, String> {
|
||||
if !game_chat_existing_art_reuse_refinement_intent_at(root, task)? {
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(game_chat_fast_path_has_visual_asset(root, "art-director")
|
||||
&& game_chat_fast_path_has_visual_asset(root, "art-asset-plan")
|
||||
&& game_chat_fast_path_has_art_manifest(root))
|
||||
}
|
||||
|
||||
pub(crate) fn game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
@@ -727,6 +856,165 @@ fn game_chat_fast_path_verified_delivery_plan(
|
||||
}
|
||||
}
|
||||
|
||||
fn game_chat_fast_path_completion_repair_plan(
|
||||
runtime: &AgentRuntimeState,
|
||||
blocker: &AgentRuntimeToolObservation,
|
||||
) -> Option<AgentRuntimeToolPlan> {
|
||||
if !agent_runtime_has_structured_plan(runtime)
|
||||
|| runtime
|
||||
.plan_steps
|
||||
.iter()
|
||||
.any(|step| step.title == GAME_CHAT_CODE_COMPLETION_REPAIR_STEP)
|
||||
|| runtime
|
||||
.plan_steps
|
||||
.iter()
|
||||
.any(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_FAILED)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let first_non_terminal_index = runtime.plan_steps.iter().position(|step| {
|
||||
step.status != AGENT_RUNTIME_PLAN_STATUS_COMPLETED
|
||||
&& step.status != AGENT_RUNTIME_PLAN_STATUS_FAILED
|
||||
});
|
||||
if first_non_terminal_index.is_none()
|
||||
&& runtime.plan_steps.len() >= AGENT_RUNTIME_PLAN_STEP_LIMIT
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut repair_inserted = false;
|
||||
let mut steps = runtime
|
||||
.plan_steps
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, step)| {
|
||||
if Some(index) == first_non_terminal_index {
|
||||
repair_inserted = true;
|
||||
return AgentRuntimePlanUpdateStep {
|
||||
step: GAME_CHAT_CODE_COMPLETION_REPAIR_STEP.to_string(),
|
||||
status: AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS.to_string(),
|
||||
};
|
||||
}
|
||||
AgentRuntimePlanUpdateStep {
|
||||
step: step.title.clone(),
|
||||
status: if step.status == AGENT_RUNTIME_PLAN_STATUS_COMPLETED {
|
||||
AGENT_RUNTIME_PLAN_STATUS_COMPLETED
|
||||
} else {
|
||||
AGENT_RUNTIME_PLAN_STATUS_PENDING
|
||||
}
|
||||
.to_string(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !repair_inserted {
|
||||
steps.push(AgentRuntimePlanUpdateStep {
|
||||
step: GAME_CHAT_CODE_COMPLETION_REPAIR_STEP.to_string(),
|
||||
status: AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS.to_string(),
|
||||
});
|
||||
}
|
||||
Some(AgentRuntimeToolPlan {
|
||||
thinking_summary: "静态检查已通过,但 Runtime 完成门仍有明确诊断;重新打开修复计划并交给 Code Agent 处理。"
|
||||
.to_string(),
|
||||
plan_update: Some(AgentRuntimePlanUpdate {
|
||||
explanation: format!(
|
||||
"{}:{}",
|
||||
blocker.summary,
|
||||
blocker.detail.as_deref().unwrap_or("请按完成门诊断继续修复")
|
||||
),
|
||||
steps,
|
||||
}),
|
||||
plan: Vec::new(),
|
||||
actions: Vec::new(),
|
||||
response: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn game_chat_fast_path_external_repair_observation_at(
|
||||
root: &Path,
|
||||
runtime: &AgentRuntimeState,
|
||||
) -> Option<AgentRuntimeToolObservation> {
|
||||
if runtime.agent_id != "code-prototype"
|
||||
|| !agent_runtime_has_structured_plan(runtime)
|
||||
|| runtime
|
||||
.plan_steps
|
||||
.iter()
|
||||
.any(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_FAILED)
|
||||
|| (!runtime.plan_steps.iter().any(|step| {
|
||||
step.title == GAME_CHAT_CODE_COMPLETION_REPAIR_STEP
|
||||
|| step
|
||||
.title
|
||||
.starts_with(&format!("{GAME_CHAT_CODE_COMPLETION_REPAIR_STEP}("))
|
||||
}) && runtime.plan_steps.iter().any(|step| {
|
||||
step.status != AGENT_RUNTIME_PLAN_STATUS_COMPLETED
|
||||
&& step.status != AGENT_RUNTIME_PLAN_STATUS_FAILED
|
||||
}))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
autonomous_game_build_completion_blocker_at_locked(root, runtime).map(|mut blocker| {
|
||||
blocker.summary = format!(
|
||||
"{};结构化计划窗口已终态,进入外部 repair lane,只执行读取、实际 mutation 与重新验证",
|
||||
blocker.summary
|
||||
);
|
||||
blocker
|
||||
})
|
||||
}
|
||||
|
||||
fn game_chat_fast_path_current_run_owns_mutation(
|
||||
root: &Path,
|
||||
runtime: &AgentRuntimeState,
|
||||
gate: &AgentRuntimeVerificationGate,
|
||||
revision: u64,
|
||||
) -> Result<bool, String> {
|
||||
let Some(tool) = gate
|
||||
.last_mutation_tool
|
||||
.as_deref()
|
||||
.filter(|_| gate.mutation_revision == Some(revision))
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let Some(last_mutation_call) = runtime
|
||||
.recent_tool_calls
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|call| call.tool == tool)
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let (Some(action_id), Some(action_fingerprint)) = (
|
||||
last_mutation_call.action_id.as_deref(),
|
||||
last_mutation_call.action_fingerprint.as_deref(),
|
||||
) else {
|
||||
return Ok(false);
|
||||
};
|
||||
if last_mutation_call.status != "ok"
|
||||
|| !is_valid_agent_runtime_action_id(action_id)
|
||||
|| !is_valid_agent_runtime_action_fingerprint(action_fingerprint)
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let (records, _) =
|
||||
read_agent_db_records_bounded(root, AGENT_RUNTIME_ACTION_HISTORY_MAX_DB_BYTES)?;
|
||||
Ok(records.iter().rev().any(|record| {
|
||||
record.get("recordType").and_then(serde_json::Value::as_str)
|
||||
== Some(AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE)
|
||||
&& record.get("agentId").and_then(serde_json::Value::as_str)
|
||||
== Some(runtime.agent_id.as_str())
|
||||
&& record.get("taskId").and_then(serde_json::Value::as_str)
|
||||
== Some(runtime.task_id.as_str())
|
||||
&& record.get("sessionId").and_then(serde_json::Value::as_str)
|
||||
== Some(runtime.session_id.as_str())
|
||||
&& record.get("runId").and_then(serde_json::Value::as_str)
|
||||
== Some(runtime.run_id.as_str())
|
||||
&& record.get("actionId").and_then(serde_json::Value::as_str) == Some(action_id)
|
||||
&& record
|
||||
.get("actionFingerprint")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some(action_fingerprint)
|
||||
&& record.get("tool").and_then(serde_json::Value::as_str) == Some(tool)
|
||||
&& record.get("status").and_then(serde_json::Value::as_str) == Some("ok")
|
||||
}))
|
||||
}
|
||||
|
||||
fn game_chat_fast_path_current_revision_is_verified(
|
||||
root: &Path,
|
||||
runtime: &AgentRuntimeState,
|
||||
@@ -898,13 +1186,29 @@ pub(crate) fn game_chat_fast_path_plan_at(
|
||||
}
|
||||
}
|
||||
"code-prototype" => {
|
||||
if agent_runtime_has_structured_plan(runtime)
|
||||
&& runtime
|
||||
.plan_steps
|
||||
.iter()
|
||||
.any(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_FAILED)
|
||||
{
|
||||
return Err(
|
||||
"game-chat code-prototype 结构化计划已包含 failed 步骤,拒绝继续空转"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
let revision = read_game_creator_agent_runtime_project_revision(root)?;
|
||||
let gate = read_game_creator_agent_runtime_verification_gate(
|
||||
root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
)?;
|
||||
let owns_current_mutation = gate.mutation_revision == Some(revision.revision);
|
||||
let owns_current_mutation = game_chat_fast_path_current_run_owns_mutation(
|
||||
root,
|
||||
runtime,
|
||||
&gate,
|
||||
revision.revision,
|
||||
)?;
|
||||
let current_revision_verified = owns_current_mutation
|
||||
&& gate.verified_revision == Some(revision.revision)
|
||||
&& gate.last_verification_status.as_deref()
|
||||
@@ -914,6 +1218,13 @@ pub(crate) fn game_chat_fast_path_plan_at(
|
||||
== Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED);
|
||||
|
||||
if current_revision_verified {
|
||||
if let Some(blocker) =
|
||||
autonomous_game_build_completion_blocker_at_locked(root, runtime)
|
||||
{
|
||||
return Ok(game_chat_fast_path_completion_repair_plan(
|
||||
runtime, &blocker,
|
||||
));
|
||||
}
|
||||
return Ok(Some(game_chat_fast_path_verified_delivery_plan(
|
||||
runtime,
|
||||
"首个可玩版本代码已生成并通过静态自检。",
|
||||
@@ -1650,6 +1961,53 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_art_reuse_intent_requires_whole_english_action_words_and_rejects_negation() {
|
||||
let accepted = [
|
||||
"Use existing art assets in the current game.",
|
||||
"Please REUSE the existing art assets.",
|
||||
"Replace placeholders with current art assets.",
|
||||
"Apply the existing spritesheet to the UI.",
|
||||
"Reuse the sprite-sheet for the falling blocks.",
|
||||
"Use existing art assets; do not generate new ones.",
|
||||
"使用现有素材,不要重新生成。",
|
||||
"请复用已有美术资源。",
|
||||
"不要重新生成美术,继续接入现有素材。",
|
||||
];
|
||||
for task in accepted {
|
||||
assert!(
|
||||
game_chat_explicit_existing_art_reuse_intent(task),
|
||||
"expected existing-art reuse intent: {task}"
|
||||
);
|
||||
}
|
||||
|
||||
let rejected = [
|
||||
"Do not use existing art assets.",
|
||||
"Do NOT use the existing art assets.",
|
||||
"Don't reuse existing art assets.",
|
||||
"Never apply the existing spritesheet.",
|
||||
"Do not replace the UI with existing art assets.",
|
||||
"We refuse to use existing art assets.",
|
||||
"Refuse art assets.",
|
||||
"Misuse art assets.",
|
||||
"These are useful art assets.",
|
||||
"Discuss art assets because they exist.",
|
||||
"Create new art assets.",
|
||||
"Regenerate art assets.",
|
||||
"不要复用已有素材。",
|
||||
"不要接入现有素材。",
|
||||
"不应用已有美术资源。",
|
||||
"别替换成现有素材。",
|
||||
"不要使用现有素材,改为重做美术。",
|
||||
];
|
||||
for task in rejected {
|
||||
assert!(
|
||||
!game_chat_explicit_existing_art_reuse_intent(task),
|
||||
"expected no existing-art reuse intent: {task}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_slice_completion_validation_rejects_tampering_and_duplicate_pixels() {
|
||||
let temporary = tempfile::tempdir().expect("create slice validation project");
|
||||
|
||||
@@ -891,6 +891,7 @@ pub(in crate::agent) fn advance_game_creator_agent_runtime_provider_batch_gate(
|
||||
&next_pending.action,
|
||||
&observation,
|
||||
Some(&next_pending.action_id),
|
||||
Some(&next_pending.action_fingerprint),
|
||||
);
|
||||
activate_agent_runtime_plan_step(
|
||||
runtime,
|
||||
|
||||
@@ -112,9 +112,7 @@ async fn request_game_creator_agent_tool_plan_with_game_chat_budget_at(
|
||||
.map(RequestedAgentRuntimeToolPlanOutcome::Ready);
|
||||
}
|
||||
if runtime.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
return Err(
|
||||
"game-chat 首版固定任务图无法继续推进,拒绝回退到普通 Provider 协作波".to_string(),
|
||||
);
|
||||
return Err(GAME_CHAT_FIXED_TASK_GRAPH_STALLED_ERROR.to_string());
|
||||
}
|
||||
let Some(timeout) = game_chat_fast_path_provider_timeout(&budget) else {
|
||||
if runtime.agent_id == "code-prototype" {
|
||||
@@ -127,13 +125,17 @@ async fn request_game_creator_agent_tool_plan_with_game_chat_budget_at(
|
||||
}
|
||||
return Err("game-chat 首版软预算已耗尽,拒绝继续请求 Provider".to_string());
|
||||
};
|
||||
let mut provider_observations = observations.to_vec();
|
||||
if let Some(blocker) = game_chat_fast_path_external_repair_observation_at(root, runtime) {
|
||||
provider_observations.push(blocker);
|
||||
}
|
||||
let provider_request = request_game_creator_agent_background_tool_plan_at(
|
||||
root,
|
||||
&runtime.agent_id,
|
||||
&runtime.session_id,
|
||||
&runtime.run_id,
|
||||
task,
|
||||
observations,
|
||||
&provider_observations,
|
||||
loop_index,
|
||||
runtime.applied_steer_cursor,
|
||||
);
|
||||
@@ -714,10 +716,10 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let autonomous_manifest_can_wait = agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
let autonomous_manifest_parent_can_wait = agent_id
|
||||
== GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& !game_chat_hard_budget_expired
|
||||
&& !autonomous_registered_derived_visuals_need_repair_at(&root)
|
||||
&& !game_creator_agent_runtime_provider_action_batch_exists(
|
||||
&root,
|
||||
&agent_id,
|
||||
@@ -732,32 +734,47 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
&& isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id).is_none()
|
||||
&& static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
|
||||
.is_none();
|
||||
if autonomous_manifest_can_wait {
|
||||
if let Err(error) = schedule_autonomous_game_build_ready_tasks_at(
|
||||
&root,
|
||||
&agent_id,
|
||||
&runtime.run_id,
|
||||
3,
|
||||
) {
|
||||
return fail_game_creator_agent_background_context_at(
|
||||
&root,
|
||||
&agent_id,
|
||||
&session_id,
|
||||
runtime,
|
||||
&format!("调度自主构建 manifest 任务失败:{error}"),
|
||||
);
|
||||
}
|
||||
let manifest_in_progress = match autonomous_manifest_dag_in_progress_at(&root) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
return fail_game_creator_agent_background_context_at(
|
||||
if autonomous_manifest_parent_can_wait {
|
||||
// 已登记但损坏的派生视觉需要先由父 Run 规划修复,因此此时不再调度新的
|
||||
// manifest child;但已经持久化并运行的 child 仍是当前 DAG 的活跃工作,
|
||||
// 父 Run 必须继续等待,不能提前落入 game-chat fixed-graph-stalled。
|
||||
let scheduled_ready_tasks =
|
||||
if autonomous_registered_derived_visuals_need_repair_at(&root) {
|
||||
Vec::new()
|
||||
} else {
|
||||
match schedule_autonomous_game_build_ready_tasks_at(
|
||||
&root,
|
||||
&agent_id,
|
||||
&session_id,
|
||||
runtime,
|
||||
&format!("读取自主构建 manifest 等待屏障失败:{error}"),
|
||||
);
|
||||
&runtime.run_id,
|
||||
3,
|
||||
) {
|
||||
Ok(tasks) => tasks,
|
||||
Err(error) => {
|
||||
return fail_game_creator_agent_background_context_at(
|
||||
&root,
|
||||
&agent_id,
|
||||
&session_id,
|
||||
runtime,
|
||||
&format!("调度自主构建 manifest 任务失败:{error}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
let manifest_in_progress = if scheduled_ready_tasks.is_empty() {
|
||||
match autonomous_manifest_dag_in_progress_at(&root) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
return fail_game_creator_agent_background_context_at(
|
||||
&root,
|
||||
&agent_id,
|
||||
&session_id,
|
||||
runtime,
|
||||
&format!("读取自主构建 manifest 等待屏障失败:{error}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if manifest_in_progress {
|
||||
let blocker =
|
||||
@@ -1264,9 +1281,12 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
&runtime.run_id,
|
||||
);
|
||||
let verified_delivery = match verification_gate {
|
||||
Ok(gate) => agent_runtime_autonomous_verified_delivery_allows_plan_completion(
|
||||
&agent_id, &gate,
|
||||
),
|
||||
Ok(gate) => {
|
||||
agent_runtime_autonomous_verified_delivery_allows_plan_completion(
|
||||
&agent_id, &gate,
|
||||
) && autonomous_game_build_completion_blocker_at_locked(&root, &runtime)
|
||||
.is_none()
|
||||
}
|
||||
Err(error) => {
|
||||
return fail_game_creator_agent_background_context_at(
|
||||
&root,
|
||||
@@ -2700,6 +2720,9 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
observation_action_identity
|
||||
.as_ref()
|
||||
.map(|identity| identity.0.as_str()),
|
||||
observation_action_identity
|
||||
.as_ref()
|
||||
.map(|identity| identity.1.as_str()),
|
||||
);
|
||||
if observation.is_waiting_for_confirmation() {
|
||||
let mut pending_action = durable_action
|
||||
|
||||
+4
-16
@@ -64,14 +64,8 @@ async fn game_chat_absolute_deadline_returns_an_in_flight_result_before_expiry()
|
||||
|
||||
#[tokio::test]
|
||||
async fn game_chat_absolute_deadline_preserves_external_generation_for_same_action_resume() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"genarrative-game-chat-deadline-reconciliation-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("system clock")
|
||||
.as_nanos()
|
||||
));
|
||||
let temporary = crate::tests::canonical_test_tempdir("game-chat-deadline-reconciliation-");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "deadline-reconciliation", "硬截止收尾测试")
|
||||
.expect("project init");
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
@@ -239,14 +233,8 @@ async fn game_chat_absolute_deadline_preserves_external_generation_for_same_acti
|
||||
|
||||
#[test]
|
||||
fn game_chat_absolute_deadline_still_cleans_local_action_recovery() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"genarrative-game-chat-deadline-local-cleanup-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("system clock")
|
||||
.as_nanos()
|
||||
));
|
||||
let temporary = crate::tests::canonical_test_tempdir("game-chat-deadline-local-cleanup-");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "deadline-local-cleanup", "硬截止本地清理测试")
|
||||
.expect("project init");
|
||||
let mut runtime = start_game_creator_agent_runtime_task_at(
|
||||
|
||||
+1219
-23
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,6 @@ where
|
||||
.await
|
||||
.expect("pending continuation task must exist")
|
||||
}
|
||||
|
||||
async fn run_after_pending_stack_boundary<T>(
|
||||
future: std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'static>>,
|
||||
) -> T
|
||||
@@ -150,7 +149,6 @@ fn persist_game_creator_agent_runtime_continuation_reconciliation_emergency_at(
|
||||
);
|
||||
emit_game_creator_agent_runtime_update(root, &runtime.agent_id);
|
||||
}
|
||||
|
||||
pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
root: PathBuf,
|
||||
agent_id: String,
|
||||
@@ -606,6 +604,7 @@ async fn continue_game_creator_agent_pending_tool_action_within_stack_boundary(
|
||||
&action,
|
||||
&observation,
|
||||
Some(&pending.action_id),
|
||||
Some(&pending.action_fingerprint),
|
||||
);
|
||||
complete_agent_runtime_active_plan_step(
|
||||
&mut runtime,
|
||||
@@ -662,6 +661,7 @@ async fn continue_game_creator_agent_pending_tool_action_within_stack_boundary(
|
||||
&action,
|
||||
&observation,
|
||||
Some(&pending.action_id),
|
||||
Some(&pending.action_fingerprint),
|
||||
);
|
||||
complete_agent_runtime_active_plan_step(
|
||||
&mut runtime,
|
||||
@@ -1093,6 +1093,7 @@ pub(in crate::agent) fn mark_game_creator_agent_runtime_tool_observation_needs_r
|
||||
&pending.action,
|
||||
observation,
|
||||
Some(&pending.action_id),
|
||||
Some(&pending.action_fingerprint),
|
||||
);
|
||||
// needs-reconciliation 是外部结果未知边界,不是结构化计划步骤的确定失败。
|
||||
// 保持 active,后续同一 action 对账成功时才能完成该步骤,并让持久 context
|
||||
|
||||
@@ -1332,7 +1332,7 @@ mod pending_recovery_tests {
|
||||
|
||||
#[test]
|
||||
fn observed_unknown_canvas_generation_returns_to_same_approved_action() {
|
||||
let temporary = tempfile::tempdir().expect("create prepared pending project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("prepared-pending-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "prepared-pending", "原俄罗斯方块项目")
|
||||
.expect("init prepared pending project");
|
||||
@@ -1413,7 +1413,7 @@ mod pending_recovery_tests {
|
||||
|
||||
#[test]
|
||||
fn legacy_executing_canvas_generation_returns_to_same_approved_action() {
|
||||
let temporary = tempfile::tempdir().expect("create executing prepared project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("executing-prepared-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "executing-prepared", "旧版俄罗斯方块项目")
|
||||
.expect("init executing prepared project");
|
||||
@@ -1490,7 +1490,7 @@ mod pending_recovery_tests {
|
||||
|
||||
#[test]
|
||||
fn observed_postprocessing_failure_resumes_from_accepted_generation() {
|
||||
let temporary = tempfile::tempdir().expect("create accepted recovery project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("accepted-recovery-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "accepted-recovery", "俄罗斯方块素材后处理恢复")
|
||||
.expect("init accepted recovery project");
|
||||
@@ -1560,7 +1560,7 @@ mod pending_recovery_tests {
|
||||
|
||||
#[test]
|
||||
fn canvas_reconciliation_keeps_the_context_plan_step_active() {
|
||||
let temporary = tempfile::tempdir().expect("create reconciliation context project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("reconciliation-context-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "reconciliation-context", "俄罗斯方块恢复上下文")
|
||||
.expect("init reconciliation context project");
|
||||
|
||||
+917
-52
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user