修复游戏创作工作流与UI图集事务
将固定图片规则降为默认合同并允许Supervisor持久委派扩展UI图集 修复条件Graph路由、终态投影、steer窗口和manifest只读刷新 将UI主图、切片、manifest与Agent DB审计收口为可恢复事务 加固跨平台句柄、journal重放、残留隔离和Agent DB容量门禁 补充工作流、事务恢复、Provider与项目夹具回归 同步技术方案与团队共享决策
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -519,7 +519,7 @@ fn game_creator_art_asset_plan_tool_plan_prompt(
|
||||
);
|
||||
}
|
||||
format!(
|
||||
"{prompt}\n\n你负责首版美术素材实际生成。资产清单和美术计划只是中间结果;最终必须调用 canvas.asset_generate 生成并登记 assets/art-spritesheet.png,固定使用 1:1、1K、assetKind=art-spritesheet、assetLabel=游戏首版核心美术素材、replaceExisting=false,并写入可解析的 assets/manifest.art.json。调用前必须用 asset.list 确认 assets/art-spec.png 已登记为当前项目的 icon-spec 画布资源,并依据当前任务、game/game_design.md 与 manifest 逐项说明真实需要的玩家主体及朝向/状态、目标或收集物、障碍/场景元素和反馈特效,由 Runtime 形成 iconDescriptions;不得假设为塔防或加入合同中不存在的单位、敌人、波次、卡牌。Runtime 固定以规范图的权威 resourceId 作为 referenceImageSrc,调用 POST /api/external/v1/editor/icon-spritesheets/generations,并用 screenColor=auto 完成透明后处理;不得把 UI 原型、Data URL、Blob URL、本地路径或结构化 JSON 冒充规范图引用,不得回退普通生图或 UI extraction。缺少规范图时必须等待 art-director 依赖并如实阻塞。成功后回读 observation 与 asset.list,核对服务端返回的透明 spritesheet、真实 alpha、warning 和 sliceWarning。warning.code=postprocess-failed-source-preserved 时没有透明图集,不得登记、验收或自动重试;仅 sliceWarning 时可保留完整透明图集,但不得声称独立切片已生成。已有有效同路径资产时不得重复生成或扣费;只有带 repairOfDelegationId 的唯一返工轮可 replaceExisting=true 原位替换。不得运行 game.static_smoke 或 preview.validate,也不得编辑 game/index.html。图片生成未配置、待确认、失败或透明证据不足时不得提交最终回复。"
|
||||
"{prompt}\n\n你负责首版美术素材实际生成。资产清单和美术计划只是中间结果;最终必须调用 canvas.asset_generate 生成并登记 assets/art-spritesheet.png,固定使用 1:1、1K、assetKind=art-spritesheet、assetLabel=游戏首版核心美术素材、replaceExisting=false,并写入可解析的 assets/manifest.art.json。调用前必须用 asset.list 确认 assets/art-spec.png 已登记为当前项目的 icon-spec 画布资源,并依据当前任务、game/game_design.md 与 manifest 逐项说明真实需要的玩家主体及朝向/状态、目标或收集物、障碍/场景元素和反馈特效,由 Runtime 形成 iconDescriptions;不得假设为塔防或加入合同中不存在的单位、敌人、波次、卡牌。Runtime 固定以规范图的权威 resourceId 作为 referenceImageSrc,调用 POST /api/external/v1/editor/icon-spritesheets/generations,并用 screenColor=auto 完成透明后处理;不得把 UI 原型、Data URL、Blob URL、本地路径或结构化 JSON 冒充规范图引用,不得回退普通生图或 UI extraction。缺少规范图时必须等待 art-director 依赖并如实阻塞。成功后回读 observation 与 asset.list,核对服务端返回的透明 spritesheet、真实 alpha、warning 和 sliceWarning。warning.code=postprocess-failed-source-preserved 时没有透明图集,不得登记、验收或自动重试;仅 sliceWarning 时可保留完整透明图集,但不得声称独立切片已生成。已有有效同路径资产时不得重复生成或扣费;只有带 repairOfDelegationId 的唯一返工轮可 replaceExisting=true 原位替换。当且仅当当前 Project Supervisor 的静态委派 expectedArtifacts 同时保留 assets/art-spritesheet.png 并声明另一个 UI 图集 PNG 时,可按委派语义额外生成该路径,固定使用 1:1、1K、assetKind=ui-spritesheet 和贴合任务的 assetLabel;初次生成 replaceExisting=false,只有完整继承同一 expectedArtifacts 的唯一返工委派可设 replaceExisting=true。不得自行发明额外路径。不得运行 game.static_smoke 或 preview.validate,也不得编辑 game/index.html。图片生成未配置、待确认、失败或透明证据不足时不得提交最终回复。"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1234,6 +1234,9 @@ mod tests {
|
||||
assert!(with_canvas.contains("warning.code=postprocess-failed-source-preserved"));
|
||||
assert!(with_canvas.contains("不得登记、验收或自动重试"));
|
||||
assert!(with_canvas.contains("仅 sliceWarning"));
|
||||
assert!(with_canvas.contains("Project Supervisor 的静态委派 expectedArtifacts"));
|
||||
assert!(with_canvas.contains("assetKind=ui-spritesheet"));
|
||||
assert!(with_canvas.contains("不得自行发明额外路径"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -629,6 +629,11 @@ pub(crate) fn validate_agent_runtime_autonomous_plan_liveness(
|
||||
.actions
|
||||
.iter()
|
||||
.any(|action| action.tool.trim() == "agent.delegate");
|
||||
let has_code_asset_route = agent_id == "code-director"
|
||||
&& plan
|
||||
.actions
|
||||
.iter()
|
||||
.any(|action| action.tool.trim() == "agent.route_manifest");
|
||||
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
if let Some(failed_playtest_revision) = verification_gate.failed_playtest_revision {
|
||||
if project_revision < failed_playtest_revision {
|
||||
@@ -765,6 +770,7 @@ pub(crate) fn validate_agent_runtime_autonomous_plan_liveness(
|
||||
|| mutation_revision.is_some()
|
||||
|| !plan.response.trim().is_empty()
|
||||
|| has_mutation
|
||||
|| has_code_asset_route
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
@@ -813,6 +819,7 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_read_only_delivery_pla
|
||||
| "command.output_read"
|
||||
| "command.poll"
|
||||
| "image.inspect" => false,
|
||||
"agent.route_manifest" => agent_id != "code-director",
|
||||
"preview.validate" => agent_id != "preview-playtest",
|
||||
"command.run_limited" => {
|
||||
agent_id != "preview-readiness"
|
||||
@@ -1771,6 +1778,13 @@ mod tests {
|
||||
serde_json::json!({"commandId": "game.static_smoke"}),
|
||||
);
|
||||
let preview = plan_for("preview.validate", serde_json::json!({}));
|
||||
let route_manifest = plan_for(
|
||||
"agent.route_manifest",
|
||||
serde_json::json!({
|
||||
"strategy": "use-existing-art",
|
||||
"missingAssetSlots": [],
|
||||
}),
|
||||
);
|
||||
|
||||
assert!(validate_agent_runtime_autonomous_read_only_delivery_plan(
|
||||
"preview-readiness",
|
||||
@@ -1784,9 +1798,41 @@ mod tests {
|
||||
&preview,
|
||||
)
|
||||
.is_ok());
|
||||
assert!(validate_agent_runtime_autonomous_read_only_delivery_plan(
|
||||
"code-director",
|
||||
true,
|
||||
&route_manifest,
|
||||
)
|
||||
.is_ok());
|
||||
let verification_gate = AgentRuntimeVerificationGate {
|
||||
schema_version: "test".to_string(),
|
||||
project_id: "test".to_string(),
|
||||
agent_id: "code-director".to_string(),
|
||||
run_id: "test".to_string(),
|
||||
requires_verification: false,
|
||||
mutation_revision: None,
|
||||
verified_revision: None,
|
||||
last_mutation_tool: None,
|
||||
last_verification_tool: None,
|
||||
last_verification_status: None,
|
||||
failed_playtest_revision: None,
|
||||
updated_at: 0,
|
||||
};
|
||||
assert!(validate_agent_runtime_autonomous_plan_liveness(
|
||||
"code-director",
|
||||
AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT + 1,
|
||||
0,
|
||||
&verification_gate,
|
||||
&[],
|
||||
&route_manifest,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.is_ok());
|
||||
for (agent_id, plan) in [
|
||||
("quality-review", &smoke),
|
||||
("quality-review", &preview),
|
||||
("quality-review", &route_manifest),
|
||||
("preview-readiness", &preview),
|
||||
("preview-playtest", &smoke),
|
||||
] {
|
||||
|
||||
+133
-2
@@ -19,12 +19,22 @@ pub(in crate::agent) fn validate_agent_runtime_pending_tool_action_content(
|
||||
"taskContext": task,
|
||||
}))
|
||||
.map_err(|error| format!("序列化待确认工具动作失败:{error}"))?;
|
||||
validate_agent_runtime_pending_serialized_content(root, &content)
|
||||
validate_agent_runtime_pending_sensitive_serialized_content(&content)?;
|
||||
let action_content = serde_json::to_string(action)
|
||||
.map_err(|error| format!("序列化待确认工具动作失败:{error}"))?;
|
||||
validate_agent_runtime_pending_project_path_content(root, &action_content)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn validate_agent_runtime_pending_serialized_content(
|
||||
root: &Path,
|
||||
content: &str,
|
||||
) -> Result<(), String> {
|
||||
validate_agent_runtime_pending_sensitive_serialized_content(content)?;
|
||||
validate_agent_runtime_pending_project_path_content(root, content)
|
||||
}
|
||||
|
||||
fn validate_agent_runtime_pending_sensitive_serialized_content(
|
||||
content: &str,
|
||||
) -> Result<(), String> {
|
||||
let lower = content.to_ascii_lowercase();
|
||||
let sensitive_rule = [
|
||||
@@ -46,6 +56,13 @@ pub(in crate::agent) fn validate_agent_runtime_pending_serialized_content(
|
||||
"待确认工具输入命中敏感规则 #{rule},Runtime 已拒绝持久化"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_agent_runtime_pending_project_path_content(
|
||||
root: &Path,
|
||||
content: &str,
|
||||
) -> Result<(), String> {
|
||||
let root_display = root.to_string_lossy();
|
||||
if !root_display.is_empty() && content.contains(root_display.as_ref()) {
|
||||
return Err("待确认工具输入包含项目绝对路径,Runtime 已拒绝持久化".to_string());
|
||||
@@ -374,7 +391,12 @@ pub(in crate::agent) fn validate_agent_runtime_pending_tool_action_record(
|
||||
}
|
||||
let serialized = serde_json::to_string(pending)
|
||||
.map_err(|error| format!("序列化 Agent Runtime 待确认动作失败:{error}"))?;
|
||||
validate_agent_runtime_pending_serialized_content(root, &serialized)?;
|
||||
validate_agent_runtime_pending_sensitive_serialized_content(&serialized)?;
|
||||
let mut project_path_record = pending.clone();
|
||||
project_path_record.task.clear();
|
||||
let project_path_serialized = serde_json::to_string(&project_path_record)
|
||||
.map_err(|error| format!("序列化 Agent Runtime 待确认路径校验记录失败:{error}"))?;
|
||||
validate_agent_runtime_pending_project_path_content(root, &project_path_serialized)?;
|
||||
let action_fingerprint = agent_runtime_pending_tool_action_fingerprint(
|
||||
&pending.action,
|
||||
&pending.task,
|
||||
@@ -762,6 +784,115 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_content_allows_project_root_in_task_context_when_action_is_relative() {
|
||||
let root = Path::new("/data/dsk/games/game01");
|
||||
let action = AgentRuntimeToolAction {
|
||||
tool: "file.list".to_string(),
|
||||
reason: Some("核对当前资产".to_string()),
|
||||
input: serde_json::json!({ "path": "assets" }),
|
||||
};
|
||||
|
||||
validate_agent_runtime_pending_tool_action_content(
|
||||
root,
|
||||
&action,
|
||||
"继续修复 /data/dsk/games/game01 中的现有项目",
|
||||
)
|
||||
.expect("task context may identify the current project while tool input stays relative");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_content_rejects_project_root_in_tool_action() {
|
||||
let root = Path::new("/data/dsk/games/game01");
|
||||
let action = AgentRuntimeToolAction {
|
||||
tool: "file.read".to_string(),
|
||||
reason: Some("读取当前入口".to_string()),
|
||||
input: serde_json::json!({
|
||||
"path": "/data/dsk/games/game01/game/index.html"
|
||||
}),
|
||||
};
|
||||
|
||||
let error =
|
||||
validate_agent_runtime_pending_tool_action_content(root, &action, "继续修复当前项目")
|
||||
.expect_err("tool action must keep using a project-relative path");
|
||||
assert!(error.contains("项目绝对路径"), "{error}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_batch_round_trips_when_only_task_identifies_project_root() {
|
||||
let temporary = crate::tests::canonical_test_tempdir("pending-task-project-root-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "pending-task-project-root", "待确认路径批次测试")
|
||||
.expect("init project");
|
||||
let task = format!("继续修复 {} 中的现有项目", root.display());
|
||||
let mut runtime = start_game_creator_agent_runtime_task_at(
|
||||
root,
|
||||
"art-director",
|
||||
&task,
|
||||
"pending-task-project-root-run",
|
||||
"agent-ready-task-scheduler",
|
||||
"核对当前项目",
|
||||
vec!["核对当前项目".to_string()],
|
||||
)
|
||||
.expect("start runtime");
|
||||
runtime.loop_iteration = 1;
|
||||
let plan = AgentRuntimeToolPlan {
|
||||
thinking_summary: "核对相对路径项目上下文".to_string(),
|
||||
plan_update: None,
|
||||
plan: vec!["核对资产".to_string(), "核对入口".to_string()],
|
||||
actions: vec![
|
||||
AgentRuntimeToolAction {
|
||||
tool: "file.list".to_string(),
|
||||
reason: Some("核对当前资产".to_string()),
|
||||
input: serde_json::json!({ "path": "assets" }),
|
||||
},
|
||||
AgentRuntimeToolAction {
|
||||
tool: "file.read".to_string(),
|
||||
reason: Some("核对当前入口".to_string()),
|
||||
input: serde_json::json!({
|
||||
"path": "game/index.html",
|
||||
"startLine": 1,
|
||||
"maxLines": 20
|
||||
}),
|
||||
},
|
||||
],
|
||||
response: String::new(),
|
||||
};
|
||||
let project_revision =
|
||||
read_game_creator_agent_runtime_project_revision(root).expect("read project revision");
|
||||
let repository_fingerprint = build_repository_startup_context_at(root)
|
||||
.expect("build repository context")
|
||||
.fingerprint;
|
||||
|
||||
let prepared = prepare_game_creator_agent_runtime_provider_action_batch(
|
||||
root,
|
||||
&runtime,
|
||||
&task,
|
||||
&plan,
|
||||
&[],
|
||||
&project_revision,
|
||||
&repository_fingerprint,
|
||||
)
|
||||
.await
|
||||
.expect("prepare provider batch");
|
||||
let prepared = match prepared {
|
||||
AgentRuntimeProviderActionBatchPreparation::Ready(batch) => batch,
|
||||
other => panic!("expected ready provider batch, got {other:?}"),
|
||||
};
|
||||
let persisted = read_game_creator_agent_runtime_provider_action_batch(
|
||||
root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
)
|
||||
.expect("read persisted provider batch");
|
||||
|
||||
assert_eq!(persisted.batch_id, prepared.batch_id);
|
||||
assert_eq!(persisted.actions.len(), 2);
|
||||
assert!(persisted.actions.iter().all(|pending| pending.task == task));
|
||||
assert_eq!(persisted.actions[0].action, plan.actions[0]);
|
||||
assert_eq!(persisted.actions[1].action, plan.actions[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_content_still_rejects_api_key_fields_and_secret_tokens() {
|
||||
let root = Path::new("C:\\workspace");
|
||||
|
||||
+14
-3
@@ -1,7 +1,12 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) const AGENT_RUNTIME_CANVAS_ASSET_KINDS: &[&str] =
|
||||
&["game-art", "icon-spec", "ui-prototype", "art-spritesheet"];
|
||||
pub(crate) const AGENT_RUNTIME_CANVAS_ASSET_KINDS: &[&str] = &[
|
||||
"game-art",
|
||||
"icon-spec",
|
||||
"ui-prototype",
|
||||
"art-spritesheet",
|
||||
"ui-spritesheet",
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod canvas_asset_kind_contract_tests {
|
||||
@@ -11,7 +16,13 @@ mod canvas_asset_kind_contract_tests {
|
||||
fn canvas_asset_kind_catalog_preserves_authoritative_contract() {
|
||||
assert_eq!(
|
||||
AGENT_RUNTIME_CANVAS_ASSET_KINDS,
|
||||
&["game-art", "icon-spec", "ui-prototype", "art-spritesheet"]
|
||||
&[
|
||||
"game-art",
|
||||
"icon-spec",
|
||||
"ui-prototype",
|
||||
"art-spritesheet",
|
||||
"ui-spritesheet",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -7495,6 +7495,47 @@ fn autonomous_ready_terminal_failures_are_projected_without_retry() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_scheduler_reprojects_a_recovered_terminal_child_before_returning() {
|
||||
let (_temporary, root, parent_state, _contract) =
|
||||
autonomous_fixture("做一个完整小游戏", "autonomous-recovered-terminal-parent");
|
||||
update_manifest_task_status_at(&root, "design-director", GameCreationAppTaskStatus::Running)
|
||||
.expect("mark recovered autonomous child running");
|
||||
let record = queue_autonomous_manifest_child_fixture(&root, &parent_state, "design-director");
|
||||
append_game_creator_agent_runtime_task_record(
|
||||
&root,
|
||||
&AgentRuntimeTaskRecord {
|
||||
status: "completed".to_string(),
|
||||
phase: "completed".to_string(),
|
||||
current_action: "recovered child already completed".to_string(),
|
||||
terminal_detail: Some("completed before scheduler recovery".to_string()),
|
||||
error: None,
|
||||
updated_at: unix_timestamp(),
|
||||
..record
|
||||
},
|
||||
)
|
||||
.expect("append recovered autonomous child terminal");
|
||||
|
||||
let scheduled = schedule_autonomous_game_build_ready_tasks_at(
|
||||
&root,
|
||||
&parent_state.agent_id,
|
||||
&parent_state.run_id,
|
||||
3,
|
||||
)
|
||||
.expect("recover terminal autonomous child");
|
||||
|
||||
assert_eq!(scheduled.len(), 1);
|
||||
assert_eq!(
|
||||
read_manifest_for_project(&root)
|
||||
.expect("read reprojected manifest")
|
||||
.tasks
|
||||
.iter()
|
||||
.find(|task| task.id == "design-director")
|
||||
.map(|task| &task.status),
|
||||
Some(&GameCreationAppTaskStatus::Completed),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn concurrent_autonomous_child_terminal_projection_preserves_all_manifest_updates() {
|
||||
let (_temporary, root, parent_state, _contract) =
|
||||
|
||||
@@ -705,6 +705,19 @@ pub(in crate::agent) fn continuation_for_game_creator_agent_runtime_steer(
|
||||
..AgentRuntimeContinuationContext::default()
|
||||
};
|
||||
context_tracker.apply_to_continuation(&mut continuation);
|
||||
if next_loop_index > 0
|
||||
&& next_loop_index % AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT == 0
|
||||
&& continuation.window_completed_loops > 0
|
||||
{
|
||||
continuation.window_completed_loops = 0;
|
||||
continuation.window_observation_fingerprints.clear();
|
||||
continuation.last_window_fingerprint =
|
||||
super::context_window::agent_runtime_context_window_fingerprint(
|
||||
&context_tracker.observation_signatures,
|
||||
)
|
||||
.or_else(|| context_tracker.last_window_fingerprint.clone());
|
||||
continuation.context_stalled = false;
|
||||
}
|
||||
continuation
|
||||
}
|
||||
|
||||
@@ -876,4 +889,41 @@ mod tests {
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steer_at_a_skipped_checkpoint_boundary_starts_a_fresh_window() {
|
||||
let temporary = crate::tests::canonical_test_tempdir("steer-window-boundary-");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "project-steer-window", "追加指令窗口边界")
|
||||
.expect("project init");
|
||||
let runtime = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
"code-prototype",
|
||||
"验证追加指令窗口边界",
|
||||
"steer-context-window-boundary-run",
|
||||
"agent-background-task",
|
||||
"追加指令窗口边界测试",
|
||||
vec!["恢复时保持 context bundle 有效".to_string()],
|
||||
)
|
||||
.expect("start steer window boundary runtime state");
|
||||
let mut tracker = AgentRuntimeContextWindowTracker {
|
||||
completed_loops: AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT - 1,
|
||||
..AgentRuntimeContextWindowTracker::default()
|
||||
};
|
||||
tracker
|
||||
.observation_signatures
|
||||
.insert("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string());
|
||||
|
||||
let continuation = continuation_for_game_creator_agent_runtime_steer(
|
||||
&runtime,
|
||||
&AgentRuntimeToolPlan::default(),
|
||||
&[],
|
||||
AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT,
|
||||
&tracker,
|
||||
);
|
||||
|
||||
assert_eq!(continuation.window_completed_loops, 0);
|
||||
assert!(continuation.window_observation_fingerprints.is_empty());
|
||||
assert!(!continuation.context_stalled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,11 @@ pub(in crate::agent) use run_status::*;
|
||||
pub(in crate::agent) use task_ops::*;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use media::validate_agent_runtime_canvas_replacement_authorization_at;
|
||||
pub(crate) use media::{
|
||||
resolve_agent_runtime_platform_art_generation_options_at,
|
||||
validate_agent_runtime_canvas_delegated_ui_spritesheet_authorization_at,
|
||||
validate_agent_runtime_canvas_replacement_authorization_at, AGENT_RUNTIME_ART_SPRITESHEET_PATH,
|
||||
};
|
||||
|
||||
pub(crate) use action_history::{
|
||||
is_valid_agent_runtime_action_id, observe_agent_runtime_action_history,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1257,7 +1257,6 @@ fn runtime_tool_input_schema(tool: &str) -> Value {
|
||||
"missingAssetSlots": {
|
||||
"type": "array",
|
||||
"maxItems": 2,
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "enum": ["art-spec", "core-spritesheet"] }
|
||||
}
|
||||
}
|
||||
@@ -1381,7 +1380,7 @@ mod tests {
|
||||
let Some(object) = schema.as_object() else {
|
||||
return;
|
||||
};
|
||||
for keyword in ["oneOf", "anyOf", "allOf", "not"] {
|
||||
for keyword in ["oneOf", "anyOf", "allOf", "not", "uniqueItems"] {
|
||||
if object.contains_key(keyword) {
|
||||
issues.push(format!(
|
||||
"strict schema contains unsupported {keyword} at {path}"
|
||||
|
||||
@@ -1026,9 +1026,131 @@ pub(crate) fn register_local_asset_entry(
|
||||
id_prefix: &str,
|
||||
source: GameCreationAppAssetSource,
|
||||
) -> Result<UploadLocalAssetResult, String> {
|
||||
let normalized_path = normalize_relative_path(local_path)?;
|
||||
let absolute_path = resolve_local_project_path(root, &normalized_path)?;
|
||||
let (manifest_path, mut manifest) = read_or_create_manifest(root)?;
|
||||
register_local_asset_entry_internal(root, local_path, kind, media_type, id_prefix, source, None)
|
||||
.map(|(registered, _)| registered)
|
||||
.map_err(LocalAssetRegistrationError::into_message)
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub(crate) struct CanvasAssetRegistrationAuditIdentity {
|
||||
pub(crate) asset_id: String,
|
||||
pub(crate) local_path: String,
|
||||
pub(crate) record_type: String,
|
||||
pub(crate) transaction_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub(crate) enum CanvasAssetRegistrationError {
|
||||
BeforeMutation(String),
|
||||
AuditAppendOutcomeUnknown {
|
||||
registered: UploadLocalAssetResult,
|
||||
audit: CanvasAssetRegistrationAuditIdentity,
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl CanvasAssetRegistrationError {
|
||||
pub(crate) fn audit_identity(&self) -> Option<&CanvasAssetRegistrationAuditIdentity> {
|
||||
match self {
|
||||
Self::BeforeMutation(_) => None,
|
||||
Self::AuditAppendOutcomeUnknown { audit, .. } => Some(audit),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CanvasAssetRegistrationError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BeforeMutation(message) => formatter.write_str(message),
|
||||
Self::AuditAppendOutcomeUnknown {
|
||||
audit, message, ..
|
||||
} => write!(
|
||||
formatter,
|
||||
"{message};素材登记审计追加结果未知:assetId={}, localPath={}, recordType={}, transactionId={}",
|
||||
audit.asset_id, audit.local_path, audit.record_type, audit.transaction_id
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_local_asset_entry_for_canvas_transaction(
|
||||
root: &Path,
|
||||
local_path: &str,
|
||||
kind: &str,
|
||||
media_type: &str,
|
||||
id_prefix: &str,
|
||||
source: GameCreationAppAssetSource,
|
||||
transaction_id: &str,
|
||||
) -> Result<(UploadLocalAssetResult, String), CanvasAssetRegistrationError> {
|
||||
let transaction_id = transaction_id.trim();
|
||||
validate_agent_db_canvas_asset_transaction_id(transaction_id)
|
||||
.map_err(CanvasAssetRegistrationError::BeforeMutation)?;
|
||||
register_local_asset_entry_internal(
|
||||
root,
|
||||
local_path,
|
||||
kind,
|
||||
media_type,
|
||||
id_prefix,
|
||||
source,
|
||||
Some(transaction_id),
|
||||
)
|
||||
.map_err(|error| error.into_canvas_error(transaction_id))
|
||||
}
|
||||
|
||||
enum LocalAssetRegistrationError {
|
||||
BeforeMutation(String),
|
||||
AuditAppendOutcomeUnknown {
|
||||
registered: UploadLocalAssetResult,
|
||||
record_type: String,
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl LocalAssetRegistrationError {
|
||||
fn into_message(self) -> String {
|
||||
match self {
|
||||
Self::BeforeMutation(message) | Self::AuditAppendOutcomeUnknown { message, .. } => {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_canvas_error(self, transaction_id: &str) -> CanvasAssetRegistrationError {
|
||||
match self {
|
||||
Self::BeforeMutation(message) => CanvasAssetRegistrationError::BeforeMutation(message),
|
||||
Self::AuditAppendOutcomeUnknown {
|
||||
registered,
|
||||
record_type,
|
||||
message,
|
||||
} => CanvasAssetRegistrationError::AuditAppendOutcomeUnknown {
|
||||
audit: CanvasAssetRegistrationAuditIdentity {
|
||||
asset_id: registered.id.clone(),
|
||||
local_path: registered.local_path.clone(),
|
||||
record_type,
|
||||
transaction_id: transaction_id.to_string(),
|
||||
},
|
||||
registered,
|
||||
message,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_local_asset_entry_internal(
|
||||
root: &Path,
|
||||
local_path: &str,
|
||||
kind: &str,
|
||||
media_type: &str,
|
||||
id_prefix: &str,
|
||||
source: GameCreationAppAssetSource,
|
||||
transaction_id: Option<&str>,
|
||||
) -> Result<(UploadLocalAssetResult, String), LocalAssetRegistrationError> {
|
||||
let normalized_path =
|
||||
normalize_relative_path(local_path).map_err(LocalAssetRegistrationError::BeforeMutation)?;
|
||||
let absolute_path = resolve_local_project_path(root, &normalized_path)
|
||||
.map_err(LocalAssetRegistrationError::BeforeMutation)?;
|
||||
let (manifest_path, mut manifest) =
|
||||
read_or_create_manifest(root).map_err(LocalAssetRegistrationError::BeforeMutation)?;
|
||||
let kind = if kind.is_empty() { "asset" } else { kind };
|
||||
let media_type = if media_type.is_empty() {
|
||||
"application/octet-stream"
|
||||
@@ -1062,25 +1184,49 @@ pub(crate) fn register_local_asset_entry(
|
||||
});
|
||||
(id, "asset.register")
|
||||
};
|
||||
write_manifest(&manifest_path, &manifest)?;
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": record_type,
|
||||
"assetId": id.clone(),
|
||||
"localPath": normalized_path.clone(),
|
||||
"kind": kind,
|
||||
"mediaType": media_type,
|
||||
"source": source_for_record,
|
||||
}),
|
||||
)?;
|
||||
|
||||
Ok(UploadLocalAssetResult {
|
||||
if let Some(transaction_id) = transaction_id {
|
||||
validate_agent_db_canvas_asset_audit_identity(transaction_id, &id, &normalized_path)
|
||||
.map_err(LocalAssetRegistrationError::BeforeMutation)?;
|
||||
}
|
||||
write_manifest(&manifest_path, &manifest)
|
||||
.map_err(LocalAssetRegistrationError::BeforeMutation)?;
|
||||
let mut audit = serde_json::json!({
|
||||
"recordType": record_type,
|
||||
"assetId": id.clone(),
|
||||
"localPath": normalized_path.clone(),
|
||||
"kind": kind,
|
||||
"mediaType": media_type,
|
||||
"source": source_for_record,
|
||||
});
|
||||
if let Some(transaction_id) = transaction_id {
|
||||
audit
|
||||
.as_object_mut()
|
||||
.expect("asset registration audit is an object")
|
||||
.insert(
|
||||
"transactionId".to_string(),
|
||||
serde_json::Value::String(transaction_id.to_string()),
|
||||
);
|
||||
}
|
||||
let registered = UploadLocalAssetResult {
|
||||
id,
|
||||
local_path: normalized_path.clone(),
|
||||
absolute_path: absolute_path.to_string_lossy().into_owned(),
|
||||
manifest_path: manifest_path.to_string_lossy().into_owned(),
|
||||
})
|
||||
};
|
||||
let append_result = if transaction_id.is_some() {
|
||||
append_agent_db_canvas_asset_transaction_audit_idempotent(root, audit).map(|_| ())
|
||||
} else {
|
||||
append_agent_db_record(root, audit)
|
||||
};
|
||||
if let Err(message) = append_result {
|
||||
return Err(LocalAssetRegistrationError::AuditAppendOutcomeUnknown {
|
||||
registered,
|
||||
record_type: record_type.to_string(),
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
Ok((registered, record_type.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1088,6 +1234,152 @@ mod tests {
|
||||
use super::*;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
fn unique_asset_registration_test_root(label: &str) -> PathBuf {
|
||||
std::env::temp_dir().join(format!(
|
||||
"genarrative-asset-registration-{label}-{}-{}",
|
||||
std::process::id(),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos()
|
||||
))
|
||||
}
|
||||
|
||||
fn canvas_registration_source() -> GameCreationAppAssetSource {
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: Some("canvas-project-1".to_string()),
|
||||
resource_id: Some("resource-1".to_string()),
|
||||
asset_object_id: Some("asset-object-1".to_string()),
|
||||
task_id: Some("task-1".to_string()),
|
||||
prompt: Some("test prompt".to_string()),
|
||||
model: Some("test-model".to_string()),
|
||||
generation_route: Some("test-route".to_string()),
|
||||
generation_kind: Some("test-kind".to_string()),
|
||||
reference_resource_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canvas_registration_sync_unknown_error_retains_complete_audit_identity() {
|
||||
let root = unique_asset_registration_test_root("post-sync-identity");
|
||||
init_local_game_project_at(&root, "asset-registration-test", "素材登记测试")
|
||||
.expect("initialize asset registration fixture");
|
||||
let local_path = "assets/ui-spritesheet.png";
|
||||
fs::write(root.join(local_path), b"test-image").expect("write asset registration fixture");
|
||||
fs::write(
|
||||
root.join(".agent/runtime/test-fail-after-agent-db-record-sync"),
|
||||
"asset.register",
|
||||
)
|
||||
.expect("inject post-sync Agent DB failure");
|
||||
|
||||
let error = register_local_asset_entry_for_canvas_transaction(
|
||||
&root,
|
||||
local_path,
|
||||
"ui-spritesheet",
|
||||
"image/png",
|
||||
"platform-art",
|
||||
canvas_registration_source(),
|
||||
"canvas-transaction-1",
|
||||
)
|
||||
.expect_err("post-sync append outcome must be surfaced as unknown");
|
||||
let audit = error
|
||||
.audit_identity()
|
||||
.expect("unknown append error carries audit identity");
|
||||
let registered = match &error {
|
||||
CanvasAssetRegistrationError::AuditAppendOutcomeUnknown { registered, .. } => {
|
||||
registered
|
||||
}
|
||||
CanvasAssetRegistrationError::BeforeMutation(_) => {
|
||||
panic!("post-sync error must carry registered asset")
|
||||
}
|
||||
};
|
||||
assert_eq!(audit.asset_id, registered.id);
|
||||
assert_eq!(audit.local_path, local_path);
|
||||
assert_eq!(audit.record_type, "asset.register");
|
||||
assert_eq!(audit.transaction_id, "canvas-transaction-1");
|
||||
assert!(error.to_string().contains("追加结果未知"));
|
||||
|
||||
let records = fs::read_to_string(root.join(".agent/agent.db"))
|
||||
.expect("read durably appended asset audit");
|
||||
let matching = records
|
||||
.lines()
|
||||
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
|
||||
.find(|record| {
|
||||
record.get("recordType").and_then(serde_json::Value::as_str)
|
||||
== Some("asset.register")
|
||||
&& record
|
||||
.get("transactionId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some("canvas-transaction-1")
|
||||
})
|
||||
.expect("post-sync failure occurs after the audit is durable");
|
||||
assert_eq!(matching["assetId"], audit.asset_id);
|
||||
assert_eq!(matching["localPath"], audit.local_path);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canvas_registration_rejects_non_compensatable_identity_before_manifest_mutation() {
|
||||
let cases = [
|
||||
(
|
||||
"unsafe-transaction",
|
||||
"assets/ui-spritesheet.png",
|
||||
"platform-art",
|
||||
"../unsafe",
|
||||
),
|
||||
(
|
||||
"outside-assets",
|
||||
"ui/ui-spritesheet.png",
|
||||
"platform-art",
|
||||
"canvas-transaction-2",
|
||||
),
|
||||
(
|
||||
"unsafe-asset-id",
|
||||
"assets/ui-spritesheet.png",
|
||||
"../unsafe",
|
||||
"canvas-transaction-3",
|
||||
),
|
||||
];
|
||||
for (label, local_path, id_prefix, transaction_id) in cases {
|
||||
let root = unique_asset_registration_test_root(label);
|
||||
init_local_game_project_at(&root, "asset-registration-test", "素材登记测试")
|
||||
.expect("initialize asset registration fixture");
|
||||
let absolute_path = root.join(local_path);
|
||||
fs::create_dir_all(
|
||||
absolute_path
|
||||
.parent()
|
||||
.expect("asset identity fixture has parent"),
|
||||
)
|
||||
.expect("create asset identity fixture parent");
|
||||
fs::write(&absolute_path, b"test-image").expect("write asset identity fixture");
|
||||
let (manifest_path, manifest_before) =
|
||||
read_or_create_manifest(&root).expect("read manifest before rejected registration");
|
||||
|
||||
let error = register_local_asset_entry_for_canvas_transaction(
|
||||
&root,
|
||||
local_path,
|
||||
"ui-spritesheet",
|
||||
"image/png",
|
||||
id_prefix,
|
||||
canvas_registration_source(),
|
||||
transaction_id,
|
||||
)
|
||||
.expect_err("non-compensatable Canvas identity must be rejected");
|
||||
assert!(
|
||||
matches!(error, CanvasAssetRegistrationError::BeforeMutation(_)),
|
||||
"{error}"
|
||||
);
|
||||
assert_eq!(
|
||||
read_manifest(&manifest_path).expect("read manifest after rejected registration"),
|
||||
manifest_before
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn read_asset_test_request(stream: &mut std::net::TcpStream) {
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
|
||||
@@ -193,7 +193,7 @@ pub(crate) fn get_local_game_manifest(
|
||||
return Err(format!("不支持通过 manifest 执行命令:{command_id}"));
|
||||
}
|
||||
enforce_project_permission_policy(root, command_id)?;
|
||||
read_manifest_for_project(root)
|
||||
read_existing_manifest_for_project(root)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -282,8 +282,11 @@ pub(crate) fn record_command_run(
|
||||
|
||||
pub(crate) fn read_manifest_for_project(root: &Path) -> Result<GameCreationAppManifest, String> {
|
||||
let (manifest_path, mut manifest) = read_or_create_manifest(root)?;
|
||||
let persisted = manifest.clone();
|
||||
ensure_manifest_seed_tasks(root, &mut manifest);
|
||||
write_manifest(&manifest_path, &manifest)?;
|
||||
if manifest != persisted {
|
||||
write_manifest(&manifest_path, &manifest)?;
|
||||
}
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,8 @@ fn manifest_read_and_project_write_recover_previous_file() {
|
||||
assert_eq!(recovered.project_id, "project-recovered");
|
||||
assert_eq!(recovered.goal.as_deref(), Some("保留恢复副本内容"));
|
||||
|
||||
let recovered = read_manifest_for_project(&root).expect("rewrite recovered manifest");
|
||||
write_manifest(&manifest_path, &recovered).expect("rewrite recovered manifest");
|
||||
let recovered = read_manifest(&manifest_path).expect("read installed manifest");
|
||||
assert_eq!(recovered.project_id, "project-recovered");
|
||||
assert!(manifest_path.is_file());
|
||||
assert!(!backup_path.exists());
|
||||
@@ -43,6 +44,20 @@ fn manifest_read_and_project_write_recover_previous_file() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_manifest_read_does_not_acquire_the_write_lock() {
|
||||
let root = unique_manifest_test_root("current-read-is-pure");
|
||||
let manifest_path = root.join(".agent/manifest.json");
|
||||
let manifest = new_game_creation_app_manifest("project-current", "当前项目");
|
||||
write_manifest(&manifest_path, &manifest).expect("write current manifest");
|
||||
|
||||
let _write_lock = acquire_manifest_write_lock(&manifest_path).expect("hold manifest lock");
|
||||
let read = read_manifest_for_project(&root).expect("read current manifest without writing");
|
||||
|
||||
assert_eq!(read, manifest);
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
fn version_fixture(
|
||||
version_id: &str,
|
||||
parent_version_id: Option<&str>,
|
||||
|
||||
@@ -1215,6 +1215,172 @@ async fn visual_specialists_reject_overriding_their_fixed_image_contract() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supervisor_delegation_can_extend_art_asset_plan_with_a_ui_spritesheet_contract() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(
|
||||
&root,
|
||||
"project-ui-spritesheet-contract",
|
||||
"UI 图集委派合同测试",
|
||||
)
|
||||
.expect("project init");
|
||||
let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID;
|
||||
let parent_session_id = "ui-spritesheet-parent-session";
|
||||
let parent_run_id = "ui-spritesheet-parent-run";
|
||||
let child_agent_id = "art-asset-plan";
|
||||
let child_session_id = "ui-spritesheet-child-session";
|
||||
let parent_action_id = "ui-spritesheet-delegate-action";
|
||||
let delegation_id = agent_runtime_delegation_id(
|
||||
parent_agent_id,
|
||||
parent_run_id,
|
||||
child_agent_id,
|
||||
parent_action_id,
|
||||
);
|
||||
let child_run_id = format!("delegated-{delegation_id}");
|
||||
let expected_artifacts = vec![
|
||||
AGENT_RUNTIME_ART_SPRITESHEET_PATH.to_string(),
|
||||
"assets/ui-spritesheet.png".to_string(),
|
||||
];
|
||||
let delivery = new_static_delegate_delivery_with_contract(
|
||||
parent_agent_id,
|
||||
parent_session_id,
|
||||
parent_run_id,
|
||||
parent_action_id,
|
||||
&delegation_id,
|
||||
child_agent_id,
|
||||
child_session_id,
|
||||
&child_run_id,
|
||||
&["保留核心图集并补齐运行时 UI 透明图集".to_string()],
|
||||
&expected_artifacts,
|
||||
None,
|
||||
);
|
||||
create_or_read_static_delegate_delivery_at(&root, &delivery)
|
||||
.expect("persist UI spritesheet delivery");
|
||||
let parent_task = AgentRuntimeTaskRecord {
|
||||
goal_id: None,
|
||||
goal_revision: 0,
|
||||
goal_status: None,
|
||||
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
|
||||
agent_id: parent_agent_id.to_string(),
|
||||
task_id: parent_agent_id.to_string(),
|
||||
session_id: parent_session_id.to_string(),
|
||||
run_id: parent_run_id.to_string(),
|
||||
source: AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE.to_string(),
|
||||
run_profile: default_agent_runtime_run_profile(),
|
||||
run_profile_binding_fingerprint: String::new(),
|
||||
parent_agent_id: None,
|
||||
parent_run_id: None,
|
||||
delegation_id: None,
|
||||
task: "安排补充 UI 图集".to_string(),
|
||||
status: "running".to_string(),
|
||||
phase: "waiting-for-delegate-receipts".to_string(),
|
||||
current_action: "等待 UI 图集".to_string(),
|
||||
terminal_detail: None,
|
||||
error: None,
|
||||
updated_at: unix_timestamp(),
|
||||
};
|
||||
write_agent_runtime_task_record_for_test(&root, &parent_task);
|
||||
write_agent_runtime_task_record_for_test(
|
||||
&root,
|
||||
&AgentRuntimeTaskRecord {
|
||||
goal_id: None,
|
||||
goal_revision: 0,
|
||||
goal_status: None,
|
||||
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
|
||||
agent_id: child_agent_id.to_string(),
|
||||
task_id: child_agent_id.to_string(),
|
||||
session_id: child_session_id.to_string(),
|
||||
run_id: child_run_id.clone(),
|
||||
source: "agent-delegate".to_string(),
|
||||
run_profile: default_agent_runtime_run_profile(),
|
||||
run_profile_binding_fingerprint: String::new(),
|
||||
parent_agent_id: Some(parent_agent_id.to_string()),
|
||||
parent_run_id: Some(parent_run_id.to_string()),
|
||||
delegation_id: Some(delegation_id),
|
||||
task: "生成运行时 UI 透明图集".to_string(),
|
||||
status: "running".to_string(),
|
||||
phase: "planning".to_string(),
|
||||
current_action: "准备生成 UI 图集".to_string(),
|
||||
terminal_detail: None,
|
||||
error: None,
|
||||
updated_at: unix_timestamp(),
|
||||
},
|
||||
);
|
||||
|
||||
let canonical = PlatformArtAssetGenerationOptions {
|
||||
output_path: Some(AGENT_RUNTIME_ART_SPRITESHEET_PATH.to_string()),
|
||||
aspect_ratio: "1:1".to_string(),
|
||||
image_size: "1K".to_string(),
|
||||
asset_kind: "art-spritesheet".to_string(),
|
||||
asset_label: "游戏首版核心美术素材".to_string(),
|
||||
replace_existing: false,
|
||||
};
|
||||
let requested = PlatformArtAssetGenerationOptions {
|
||||
output_path: Some("assets/ui-spritesheet.png".to_string()),
|
||||
aspect_ratio: "1:1".to_string(),
|
||||
image_size: "1K".to_string(),
|
||||
asset_kind: "ui-spritesheet".to_string(),
|
||||
asset_label: "晶穹叠阵运行时 UI 透明图集".to_string(),
|
||||
replace_existing: false,
|
||||
};
|
||||
let resolved = resolve_agent_runtime_platform_art_generation_options_at(
|
||||
&root,
|
||||
child_agent_id,
|
||||
&child_run_id,
|
||||
canonical.clone(),
|
||||
requested.clone(),
|
||||
)
|
||||
.expect("durable Supervisor delivery authorizes the declared UI spritesheet");
|
||||
assert_eq!(resolved, requested);
|
||||
assert!(resolve_agent_runtime_platform_art_generation_options_at(
|
||||
&root,
|
||||
child_agent_id,
|
||||
"ordinary-art-run",
|
||||
canonical.clone(),
|
||||
requested,
|
||||
)
|
||||
.expect_err("ordinary direct run cannot extend the fixed image contract")
|
||||
.contains("静态专业委派"));
|
||||
assert!(resolve_agent_runtime_platform_art_generation_options_at(
|
||||
&root,
|
||||
child_agent_id,
|
||||
&child_run_id,
|
||||
canonical.clone(),
|
||||
PlatformArtAssetGenerationOptions {
|
||||
output_path: Some(AGENT_RUNTIME_ART_SPRITESHEET_PATH.to_string()),
|
||||
asset_kind: "ui-spritesheet".to_string(),
|
||||
..canonical.clone()
|
||||
},
|
||||
)
|
||||
.expect_err("delegation cannot change the canonical core path semantics")
|
||||
.contains("不能覆盖固定输出合同"));
|
||||
|
||||
write_agent_runtime_task_record_for_test(
|
||||
&root,
|
||||
&AgentRuntimeTaskRecord {
|
||||
status: "failed".to_string(),
|
||||
phase: "failed".to_string(),
|
||||
current_action: "父 run 已失败".to_string(),
|
||||
terminal_detail: Some("父 run 已失败".to_string()),
|
||||
error: Some("parent-failed".to_string()),
|
||||
updated_at: unix_timestamp(),
|
||||
..parent_task
|
||||
},
|
||||
);
|
||||
assert!(
|
||||
validate_agent_runtime_canvas_delegated_ui_spritesheet_authorization_at(
|
||||
&root,
|
||||
child_agent_id,
|
||||
&child_run_id,
|
||||
"assets/ui-spritesheet.png",
|
||||
)
|
||||
.expect_err("terminal parent revokes delegated UI generation")
|
||||
.contains("未授权")
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_foundation_rejects_scene_image_stale_run_and_stale_sha_visual_proofs() {
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
|
||||
@@ -1849,6 +1849,8 @@ async fn platform_art_generation_step_falls_back_without_leaking_editor_key() {
|
||||
let root = unique_project_path();
|
||||
let config_dir = unique_project_path();
|
||||
let base_url = spawn_mock_external_canvas_generation_failure_server();
|
||||
init_local_game_project_at(&root, "project-art-fallback", "未命名游戏原型")
|
||||
.expect("initialize platform art fallback project");
|
||||
fs::create_dir_all(&config_dir).expect("runtime config dir");
|
||||
fs::write(
|
||||
config_dir.join(GAME_CREATOR_CONFIG_FILE_NAME),
|
||||
@@ -1888,7 +1890,7 @@ async fn platform_art_generation_step_falls_back_without_leaking_editor_key() {
|
||||
|
||||
assert_eq!(step.status, "failed");
|
||||
assert!(step.output_paths.is_empty());
|
||||
assert!(step.summary.contains("HTTP 500"));
|
||||
assert!(step.summary.contains("HTTP 500"), "{}", step.summary);
|
||||
assert!(!step.summary.contains("editor-fallback-secret"));
|
||||
assert!(read_manifest_for_project(&root).unwrap().assets.is_empty());
|
||||
|
||||
|
||||
@@ -7691,6 +7691,7 @@ fn agent_native_function_catalog_exposes_each_runtime_tool_with_core_schemas() {
|
||||
"icon-spec",
|
||||
"ui-prototype",
|
||||
"art-spritesheet",
|
||||
"ui-spritesheet",
|
||||
null
|
||||
])
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user