Merge pull request '修复客户端常用设置读写卡顿' (#307) from fix/config_sl into master
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/307 Reviewed-by: 段舒康 <kdletters@qq.com>
This commit was merged in pull request #307.
This commit is contained in:
@@ -722,7 +722,7 @@ function runAgent() {
|
|||||||
stderr += chunk.toString();
|
stderr += chunk.toString();
|
||||||
});
|
});
|
||||||
child.on('error', reject);
|
child.on('error', reject);
|
||||||
child.on('close', (code) => {
|
child.on('close', (code, signal) => {
|
||||||
const output = `${stdout}${stderr}`;
|
const output = `${stdout}${stderr}`;
|
||||||
if (previewReadError) {
|
if (previewReadError) {
|
||||||
reject(previewReadError);
|
reject(previewReadError);
|
||||||
@@ -740,13 +740,24 @@ function runAgent() {
|
|||||||
previewDom,
|
previewDom,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(output || `agent run exited with ${code}`));
|
reject(
|
||||||
|
new Error(
|
||||||
|
`agent run failed: exitCode=${code}, signal=${signal ?? 'none'}\n` +
|
||||||
|
`stderr tail (last 8000 characters):\n${stderr.slice(-8000)}\n` +
|
||||||
|
`stdout tail (last 4000 characters):\n${stdout.slice(-4000)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedLocalAsset() {
|
async function seedLocalAsset() {
|
||||||
|
await fs.mkdir(path.join(projectRoot, 'game'), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(projectRoot, 'game/index.html'),
|
||||||
|
'<!doctype html><html lang="zh-CN"><meta charset="UTF-8"><body>还没有生成游戏</body></html>',
|
||||||
|
);
|
||||||
await fs.mkdir(path.join(projectRoot, 'assets/uploads'), { recursive: true });
|
await fs.mkdir(path.join(projectRoot, 'assets/uploads'), { recursive: true });
|
||||||
await fs.mkdir(path.join(projectRoot, '.agent'), { recursive: true });
|
await fs.mkdir(path.join(projectRoot, '.agent'), { recursive: true });
|
||||||
await seedConversationContext();
|
await seedConversationContext();
|
||||||
|
|||||||
@@ -4710,10 +4710,10 @@ mod tests {
|
|||||||
assert!(!prompt.contains("你是 Codex"));
|
assert!(!prompt.contains("你是 Codex"));
|
||||||
assert!(prompt.contains("不要等待 Supervisor"));
|
assert!(prompt.contains("不要等待 Supervisor"));
|
||||||
assert!(prompt.contains("提示词与技能"));
|
assert!(prompt.contains("提示词与技能"));
|
||||||
assert!(prompt.contains("AGC 工程合同(仅说明项目边界,不是流程门槛)"));
|
assert!(prompt.contains("AGC 工程合同:当前 cwd 是用户选择的项目目录"));
|
||||||
assert!(prompt.contains("客户端扩展列表中用户已启用的第三方 MCP"));
|
assert!(prompt.contains("客户端扩展列表中用户已启用的第三方 MCP"));
|
||||||
assert!(prompt.contains("用户明确指定第三方 MCP Server 或工具时"));
|
assert!(prompt.contains("用户明确指定第三方 MCP Server 或工具时"));
|
||||||
assert!(prompt.contains("先按需读取当前 cwd 下适用的 `AGENTS.md`"));
|
assert!(prompt.contains("先读取当前 cwd 下适用的 AGENTS.md、README 或项目说明"));
|
||||||
assert!(prompt.contains("agc_write_file"));
|
assert!(prompt.contains("agc_write_file"));
|
||||||
assert!(prompt.contains("content 必须是目标文件的完整原始 UTF-8 正文"));
|
assert!(prompt.contains("content 必须是目标文件的完整原始 UTF-8 正文"));
|
||||||
assert!(prompt.contains("不得把 command.exec 的 Exit code、Wall time、Output 包装"));
|
assert!(prompt.contains("不得把 command.exec 的 Exit code、Wall time、Output 包装"));
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ pub(in crate::agent) use canvas_generation::{
|
|||||||
commit_prepared_platform_art_asset_at, commit_prepared_platform_art_asset_strict_slices_at,
|
commit_prepared_platform_art_asset_at, commit_prepared_platform_art_asset_strict_slices_at,
|
||||||
generate_platform_art_asset_with_retained_runtime_options_at,
|
generate_platform_art_asset_with_retained_runtime_options_at,
|
||||||
generate_platform_art_asset_with_runtime_options_at,
|
generate_platform_art_asset_with_runtime_options_at,
|
||||||
platform_art_generation_error_result_unknown, register_existing_platform_art_slices_at,
|
platform_art_generation_error_result_unknown, recover_persisted_visual_generation_options,
|
||||||
request_platform_art_asset_with_runtime_options_at, restore_platform_art_asset_bytes_at,
|
register_existing_platform_art_slices_at, request_platform_art_asset_with_runtime_options_at,
|
||||||
|
restore_platform_art_asset_bytes_at,
|
||||||
retained_platform_art_generation_runtime_spritesheet_identity_at,
|
retained_platform_art_generation_runtime_spritesheet_identity_at,
|
||||||
retained_platform_art_generation_runtime_state_matches_direct_stage_at,
|
retained_platform_art_generation_runtime_state_matches_direct_stage_at,
|
||||||
validate_platform_art_png_bytes_with_limits,
|
validate_platform_art_png_bytes_with_limits,
|
||||||
|
|||||||
@@ -429,6 +429,87 @@ impl Default for PlatformArtAssetGenerationOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(in crate::agent) fn recover_persisted_visual_generation_options(
|
||||||
|
root: &Path,
|
||||||
|
pending: &AgentRuntimePendingToolAction,
|
||||||
|
prompt: &str,
|
||||||
|
requested: &PlatformArtAssetGenerationOptions,
|
||||||
|
) -> Result<Option<PlatformArtAssetGenerationOptions>, String> {
|
||||||
|
let context = platform_art_generation_runtime_context_from_pending(pending);
|
||||||
|
let Some(state) = read_platform_art_generation_runtime_state(root, &context)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let (path, ratio, size, kind, label, generation_kind) = match context.agent_id.as_str() {
|
||||||
|
"art-director" => (
|
||||||
|
"assets/art-spec.png",
|
||||||
|
"1:1",
|
||||||
|
"1K",
|
||||||
|
"icon-spec",
|
||||||
|
"游戏统一视觉规范图",
|
||||||
|
"spec",
|
||||||
|
),
|
||||||
|
"design-foundation" => (
|
||||||
|
"assets/ui-prototype.png",
|
||||||
|
"16:9",
|
||||||
|
"2K",
|
||||||
|
"ui-prototype",
|
||||||
|
"游戏横屏界面原型图",
|
||||||
|
"ui-design",
|
||||||
|
),
|
||||||
|
"art-asset-plan" => (
|
||||||
|
"assets/art-spritesheet.png",
|
||||||
|
"1:1",
|
||||||
|
"1K",
|
||||||
|
"art-spritesheet",
|
||||||
|
"游戏首版核心美术素材",
|
||||||
|
"icon-spritesheet",
|
||||||
|
),
|
||||||
|
_ => return Ok(None),
|
||||||
|
};
|
||||||
|
let mut options = requested.clone();
|
||||||
|
let label = if context.agent_id == "design-foundation"
|
||||||
|
&& options
|
||||||
|
.output_path
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(design_foundation_ui_page_output_path_is_valid)
|
||||||
|
{
|
||||||
|
"游戏功能页面设计图"
|
||||||
|
} else {
|
||||||
|
label
|
||||||
|
};
|
||||||
|
if options.output_path.is_none() {
|
||||||
|
options.output_path = Some(path.to_string());
|
||||||
|
}
|
||||||
|
for (value, default) in [
|
||||||
|
(&mut options.aspect_ratio, ratio),
|
||||||
|
(&mut options.image_size, size),
|
||||||
|
(&mut options.asset_kind, kind),
|
||||||
|
(&mut options.asset_label, label),
|
||||||
|
] {
|
||||||
|
if value.is_empty() {
|
||||||
|
*value = default.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let snapshot = platform_art_generation_runtime_request_snapshot(&state)?;
|
||||||
|
// 只恢复经过身份校验的已有请求;显式改参和新请求仍走当前合同。
|
||||||
|
if snapshot.generation_kind == generation_kind
|
||||||
|
&& snapshot.generation_prompt == build_platform_art_asset_prompt(prompt, &[], &options)
|
||||||
|
&& options.asset_kind == kind
|
||||||
|
&& options.aspect_ratio == ratio
|
||||||
|
&& options.image_size == size
|
||||||
|
&& (options.output_path.as_deref() == Some(path)
|
||||||
|
|| (context.agent_id == "design-foundation"
|
||||||
|
&& options
|
||||||
|
.output_path
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(design_foundation_ui_page_output_path_is_valid)))
|
||||||
|
{
|
||||||
|
Ok(Some(options))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
struct ExistingPlatformArtAssetFingerprint {
|
struct ExistingPlatformArtAssetFingerprint {
|
||||||
local_path: String,
|
local_path: String,
|
||||||
@@ -8895,13 +8976,13 @@ mod canvas_generation_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strict_spritesheet_contract_requires_the_grid_2x2_provider_receipt() {
|
fn strict_spritesheet_contract_accepts_variable_slices_without_fixed_layout() {
|
||||||
let canvas_context = ExternalCanvasGenerationContext {
|
let canvas_context = ExternalCanvasGenerationContext {
|
||||||
project_id: "canvas-project".to_string(),
|
project_id: "canvas-project".to_string(),
|
||||||
asset_folder_id: "asset-folder".to_string(),
|
asset_folder_id: "asset-folder".to_string(),
|
||||||
canvas_name: "contract-test".to_string(),
|
canvas_name: "contract-test".to_string(),
|
||||||
};
|
};
|
||||||
let slices = (0..4)
|
let slices = (0..3)
|
||||||
.map(|index| {
|
.map(|index| {
|
||||||
let download = rgba_test_png(100 + index);
|
let download = rgba_test_png(100 + index);
|
||||||
let validated = validate_platform_art_png_bytes_with_limits(
|
let validated = validate_platform_art_png_bytes_with_limits(
|
||||||
@@ -8927,7 +9008,7 @@ mod canvas_generation_tests {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let error = validate_strict_platform_art_spritesheet_contract(
|
validate_strict_platform_art_spritesheet_contract(
|
||||||
&slices,
|
&slices,
|
||||||
&canvas_context,
|
&canvas_context,
|
||||||
Some("canvas-project"),
|
Some("canvas-project"),
|
||||||
@@ -8941,9 +9022,7 @@ mod canvas_generation_tests {
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.expect_err("missing fixed-layout proof must not be accepted");
|
.expect("valid slice identities and pixel evidence do not require a fixed layout");
|
||||||
|
|
||||||
assert!(error.contains("grid-2x2"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -10168,7 +10247,8 @@ mod canvas_generation_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn accepted_runtime_generation_rejects_changed_intent_before_operation_get() {
|
async fn accepted_runtime_generation_checks_running_operation_before_rejecting_changed_intent()
|
||||||
|
{
|
||||||
let temporary = tempfile::tempdir().expect("create changed-intent recovery project");
|
let temporary = tempfile::tempdir().expect("create changed-intent recovery project");
|
||||||
let root = temporary.path();
|
let root = temporary.path();
|
||||||
init_local_game_project_at(root, "changed-intent", "旧异步生成意图隔离")
|
init_local_game_project_at(root, "changed-intent", "旧异步生成意图隔离")
|
||||||
@@ -10188,6 +10268,31 @@ mod canvas_generation_tests {
|
|||||||
.set_nonblocking(true)
|
.set_nonblocking(true)
|
||||||
.expect("set changed intent fixture nonblocking");
|
.expect("set changed intent fixture nonblocking");
|
||||||
let base_url = format!("http://{}", listener.local_addr().expect("fixture address"));
|
let base_url = format!("http://{}", listener.local_addr().expect("fixture address"));
|
||||||
|
let (request_sender, request_receiver) = std::sync::mpsc::channel();
|
||||||
|
let (stop_sender, stop_receiver) = std::sync::mpsc::channel();
|
||||||
|
let server = std::thread::spawn(move || loop {
|
||||||
|
if stop_receiver.try_recv().is_ok() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let (mut stream, _) = match listener.accept() {
|
||||||
|
Ok(connection) => connection,
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(error) => panic!("accept changed intent request: {error}"),
|
||||||
|
};
|
||||||
|
request_sender
|
||||||
|
.send(read_test_http_request(&mut stream))
|
||||||
|
.expect("capture status request");
|
||||||
|
write_test_json_response(
|
||||||
|
&mut stream,
|
||||||
|
"200 OK",
|
||||||
|
&serde_json::json!({
|
||||||
|
"data": {"operationId": "old-operation", "status": "running"}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
let runtime_context = PlatformArtGenerationRuntimeContext {
|
let runtime_context = PlatformArtGenerationRuntimeContext {
|
||||||
agent_id: "art-director".to_string(),
|
agent_id: "art-director".to_string(),
|
||||||
task_id: "art-director".to_string(),
|
task_id: "art-director".to_string(),
|
||||||
@@ -10253,10 +10358,15 @@ mod canvas_generation_tests {
|
|||||||
error.contains("当前生成意图与已持久化请求快照不一致"),
|
error.contains("当前生成意图与已持久化请求快照不一致"),
|
||||||
"{error}"
|
"{error}"
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
stop_sender.send(()).expect("stop changed intent fixture");
|
||||||
listener.accept(),
|
server.join().expect("join changed intent fixture");
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock
|
let requests = request_receiver.try_iter().collect::<Vec<_>>();
|
||||||
));
|
assert_eq!(
|
||||||
|
requests.len(),
|
||||||
|
1,
|
||||||
|
"only the existing operation may be queried"
|
||||||
|
);
|
||||||
|
assert!(requests[0].starts_with("GET /api/external/v1/generations/old-operation "));
|
||||||
assert!(game_creator_agent_runtime_external_generation_exists(
|
assert!(game_creator_agent_runtime_external_generation_exists(
|
||||||
root,
|
root,
|
||||||
&runtime_context.agent_id,
|
&runtime_context.agent_id,
|
||||||
@@ -10265,7 +10375,8 @@ mod canvas_generation_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn accepted_derived_generation_rejects_changed_art_spec_before_operation_get() {
|
async fn accepted_derived_generation_checks_running_operation_before_rejecting_changed_art_spec(
|
||||||
|
) {
|
||||||
let temporary = tempfile::tempdir().expect("create changed reference project");
|
let temporary = tempfile::tempdir().expect("create changed reference project");
|
||||||
let root = temporary.path();
|
let root = temporary.path();
|
||||||
init_local_game_project_at(root, "changed-reference", "规范图身份隔离")
|
init_local_game_project_at(root, "changed-reference", "规范图身份隔离")
|
||||||
@@ -10310,6 +10421,31 @@ mod canvas_generation_tests {
|
|||||||
.set_nonblocking(true)
|
.set_nonblocking(true)
|
||||||
.expect("set changed reference fixture nonblocking");
|
.expect("set changed reference fixture nonblocking");
|
||||||
let base_url = format!("http://{}", listener.local_addr().expect("fixture address"));
|
let base_url = format!("http://{}", listener.local_addr().expect("fixture address"));
|
||||||
|
let (request_sender, request_receiver) = std::sync::mpsc::channel();
|
||||||
|
let (stop_sender, stop_receiver) = std::sync::mpsc::channel();
|
||||||
|
let server = std::thread::spawn(move || loop {
|
||||||
|
if stop_receiver.try_recv().is_ok() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let (mut stream, _) = match listener.accept() {
|
||||||
|
Ok(connection) => connection,
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(error) => panic!("accept changed reference request: {error}"),
|
||||||
|
};
|
||||||
|
request_sender
|
||||||
|
.send(read_test_http_request(&mut stream))
|
||||||
|
.expect("capture status request");
|
||||||
|
write_test_json_response(
|
||||||
|
&mut stream,
|
||||||
|
"200 OK",
|
||||||
|
&serde_json::json!({
|
||||||
|
"data": {"operationId": "old-background-operation", "status": "running"}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
let runtime_context = PlatformArtGenerationRuntimeContext {
|
let runtime_context = PlatformArtGenerationRuntimeContext {
|
||||||
agent_id: "direct-codex-art".to_string(),
|
agent_id: "direct-codex-art".to_string(),
|
||||||
task_id: "direct-codex-art-game-background".to_string(),
|
task_id: "direct-codex-art-game-background".to_string(),
|
||||||
@@ -10412,10 +10548,19 @@ mod canvas_generation_tests {
|
|||||||
"{error}"
|
"{error}"
|
||||||
);
|
);
|
||||||
assert!(error.contains("当前规范图身份"), "{error}");
|
assert!(error.contains("当前规范图身份"), "{error}");
|
||||||
assert!(matches!(
|
stop_sender
|
||||||
listener.accept(),
|
.send(())
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock
|
.expect("stop changed reference fixture");
|
||||||
));
|
server.join().expect("join changed reference fixture");
|
||||||
|
let requests = request_receiver.try_iter().collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
requests.len(),
|
||||||
|
1,
|
||||||
|
"only the existing operation may be queried"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
requests[0].starts_with("GET /api/external/v1/generations/old-background-operation ")
|
||||||
|
);
|
||||||
assert!(game_creator_agent_runtime_external_generation_exists(
|
assert!(game_creator_agent_runtime_external_generation_exists(
|
||||||
root,
|
root,
|
||||||
&runtime_context.agent_id,
|
&runtime_context.agent_id,
|
||||||
@@ -10809,6 +10954,38 @@ mod canvas_generation_tests {
|
|||||||
mark_platform_art_generation_runtime_accepted(root, state, "test-operation-id", 0)
|
mark_platform_art_generation_runtime_accepted(root, state, "test-operation-id", 0)
|
||||||
.expect("accept current generation fixture");
|
.expect("accept current generation fixture");
|
||||||
|
|
||||||
|
let sparse_options = PlatformArtAssetGenerationOptions {
|
||||||
|
output_path: Some(AGENT_RUNTIME_ART_SPEC_PATH.to_string()),
|
||||||
|
aspect_ratio: String::new(),
|
||||||
|
image_size: String::new(),
|
||||||
|
asset_kind: String::new(),
|
||||||
|
asset_label: String::new(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
recover_persisted_visual_generation_options(root, &pending, prompt, &sparse_options)
|
||||||
|
.expect("recover persisted defaults"),
|
||||||
|
Some(options.clone())
|
||||||
|
);
|
||||||
|
assert!(recover_persisted_visual_generation_options(
|
||||||
|
root,
|
||||||
|
&pending,
|
||||||
|
"显式改变生成意图",
|
||||||
|
&sparse_options,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.is_none());
|
||||||
|
let mut explicit_options = sparse_options.clone();
|
||||||
|
explicit_options.asset_kind = "game-background".to_string();
|
||||||
|
assert!(recover_persisted_visual_generation_options(
|
||||||
|
root,
|
||||||
|
&pending,
|
||||||
|
prompt,
|
||||||
|
&explicit_options,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.is_none());
|
||||||
|
|
||||||
resume_game_creator_agent_background_tasks_at(root)
|
resume_game_creator_agent_background_tasks_at(root)
|
||||||
.expect("resume accepted generation through recovery scan");
|
.expect("resume accepted generation through recovery scan");
|
||||||
let first = request_receiver
|
let first = request_receiver
|
||||||
@@ -11063,19 +11240,18 @@ mod canvas_generation_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn canonical_art_spritesheet_request_has_exactly_four_ordered_categories() {
|
fn canonical_art_spritesheet_request_preserves_project_requirements() {
|
||||||
let descriptions = canonical_art_spritesheet_icon_descriptions("原创收集玩法");
|
let descriptions = canonical_art_spritesheet_icon_descriptions("原创收集玩法");
|
||||||
assert_eq!(descriptions.len(), 4);
|
assert_eq!(descriptions.len(), 1);
|
||||||
for (index, description) in descriptions.iter().enumerate() {
|
assert!(descriptions[0].contains("原创收集玩法"));
|
||||||
assert!(description.starts_with(&format!("第 {} 类", index + 1)));
|
assert!(descriptions[0].contains("数量、类别、排列和切片方式由本次需求决定"));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn canonical_art_spritesheet_descriptions_obey_external_editor_item_limit() {
|
fn canonical_art_spritesheet_descriptions_obey_external_editor_item_limit() {
|
||||||
let descriptions = canonical_art_spritesheet_icon_descriptions(&"原创玩法需求".repeat(128));
|
let descriptions = canonical_art_spritesheet_icon_descriptions(&"原创玩法需求".repeat(128));
|
||||||
|
|
||||||
assert_eq!(descriptions.len(), 4);
|
assert!(!descriptions.is_empty());
|
||||||
assert!(descriptions
|
assert!(descriptions
|
||||||
.iter()
|
.iter()
|
||||||
.all(|description| description.chars().count() <= 200));
|
.all(|description| description.chars().count() <= 200));
|
||||||
@@ -11466,7 +11642,7 @@ mod canvas_generation_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strict_slice_commit_rejects_non_four_slice_results_before_replacing_the_sheet() {
|
fn strict_slice_commit_rejects_empty_slice_results_before_replacing_the_sheet() {
|
||||||
let temporary = tempfile::tempdir().expect("create strict slice project");
|
let temporary = tempfile::tempdir().expect("create strict slice project");
|
||||||
let root = temporary.path();
|
let root = temporary.path();
|
||||||
init_local_game_project_at(root, "strict-slices", "严格切片测试")
|
init_local_game_project_at(root, "strict-slices", "严格切片测试")
|
||||||
@@ -11481,9 +11657,9 @@ mod canvas_generation_tests {
|
|||||||
&replacement_options(),
|
&replacement_options(),
|
||||||
|_| Ok(()),
|
|_| Ok(()),
|
||||||
)
|
)
|
||||||
.expect_err("strict spritesheet commit must require exactly four slices");
|
.expect_err("strict spritesheet commit must require at least one slice");
|
||||||
|
|
||||||
assert!(error.contains("恰好包含 4 个独立切片"));
|
assert!(error.contains("至少需要一个独立切片"));
|
||||||
assert_eq!(fs::read(path).expect("read preserved sheet"), b"old-image");
|
assert_eq!(fs::read(path).expect("read preserved sheet"), b"old-image");
|
||||||
assert!(!root
|
assert!(!root
|
||||||
.join("assets/art-spritesheet-slices/manifest.json")
|
.join("assets/art-spritesheet-slices/manifest.json")
|
||||||
|
|||||||
@@ -1790,103 +1790,62 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn agent_prompt_design_foundation_forbids_implementation_and_browser_validation() {
|
fn agent_prompt_design_foundation_keeps_its_role_boundary() {
|
||||||
for editor_api_key_is_configured in [false, true] {
|
for editor_api_key_is_configured in [false, true] {
|
||||||
let prompt = game_creator_design_foundation_tool_plan_prompt(
|
let prompt = game_creator_design_foundation_tool_plan_prompt(
|
||||||
"shared runtime contract",
|
"shared runtime contract",
|
||||||
editor_api_key_is_configured,
|
editor_api_key_is_configured,
|
||||||
);
|
);
|
||||||
|
assert!(prompt.contains("只负责玩法规格、界面建议和视觉工具使用指导"));
|
||||||
assert!(prompt.contains("项目文件写入只允许 memory/project.md 与 game/game_design.md"));
|
assert!(prompt.contains("项目文件与图片输出必须服从当前任务明确要求"));
|
||||||
assert!(prompt.contains("不得创建、修改、删除或补丁 game/index.html"));
|
assert!(prompt.contains("不修改 game/index.html,不启动预览或试玩"));
|
||||||
assert!(prompt.contains("由 Runtime 在收束门内验证本人 owner 产物"));
|
|
||||||
assert!(
|
|
||||||
prompt.contains("不得调用 project.verify、command.run_limited、game.static_smoke")
|
|
||||||
);
|
|
||||||
assert!(prompt.contains("preview.start 或 preview.validate"));
|
|
||||||
assert!(prompt.contains("最终静态验收仍属于 preview-readiness"));
|
|
||||||
assert!(prompt.contains("浏览器验收仍属于 preview-playtest"));
|
|
||||||
assert!(prompt.contains(
|
|
||||||
"不得通过 command.exec、command.start 或其他工具启动本地预览服务、浏览器、Playwright"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn agent_prompt_design_foundation_preserves_the_canvas_specific_contract() {
|
fn agent_prompt_design_foundation_describes_available_deliveries() {
|
||||||
let without_canvas =
|
let without_canvas =
|
||||||
game_creator_design_foundation_tool_plan_prompt("shared runtime contract", false);
|
game_creator_design_foundation_tool_plan_prompt("shared runtime contract", false);
|
||||||
assert!(without_canvas.contains("不调用 canvas.asset_generate"));
|
assert!(without_canvas.contains("不调用 canvas.asset_generate"));
|
||||||
assert!(without_canvas.contains("不伪造 assets/ui-prototype.png"));
|
assert!(without_canvas.contains("memory/project.md 与 game/game_design.md"));
|
||||||
assert!(without_canvas.contains("每个功能页面各写一行 @genarrative-ui-page"));
|
assert!(without_canvas.contains("每个功能页面各写一行 @genarrative-ui-page"));
|
||||||
|
|
||||||
let with_canvas =
|
let with_canvas =
|
||||||
game_creator_design_foundation_tool_plan_prompt("shared runtime contract", true);
|
game_creator_design_foundation_tool_plan_prompt("shared runtime contract", true);
|
||||||
|
assert!(with_canvas.contains("根据当前玩法需求编写规格和界面建议"));
|
||||||
|
assert!(with_canvas.contains("用途、数量、输出路径、尺寸、参考资源和是否需要 spritesheet"));
|
||||||
assert!(with_canvas.contains("再调用 canvas.asset_generate"));
|
assert!(with_canvas.contains("再调用 canvas.asset_generate"));
|
||||||
assert!(with_canvas.contains("assets/art-spec.png"));
|
|
||||||
assert!(with_canvas.contains("referenceImageSrcs 第一项"));
|
|
||||||
assert!(with_canvas.contains("assets/ui-prototype.png"));
|
|
||||||
assert!(with_canvas.contains("调用 image.inspect"));
|
|
||||||
assert!(with_canvas.contains("ui-prototype.v2"));
|
|
||||||
assert!(with_canvas.contains("成功只表示候选图片已生成并登记,不等于视觉验收完成"));
|
|
||||||
assert!(with_canvas.contains("由 Runtime 在收束门内同时核对固定 owner 文档"));
|
|
||||||
assert!(!with_canvas.contains("成功动作本身就是当前 revision 的验证"));
|
|
||||||
assert!(with_canvas.contains("调用 ui.workflow.run"));
|
|
||||||
assert!(with_canvas.contains("visual-binding 最终编辑器路由"));
|
|
||||||
assert!(with_canvas.contains("每个功能页面各写一行 @genarrative-ui-page"));
|
|
||||||
assert!(with_canvas.contains("ui.workflow.run 的 discover"));
|
|
||||||
assert!(with_canvas.contains("assets/ui-pages/{pageId}.png"));
|
|
||||||
assert!(with_canvas.contains("informationHud"));
|
|
||||||
assert!(with_canvas.contains("failureRestartFlow"));
|
|
||||||
assert!(with_canvas.contains("不得假设为塔防"));
|
|
||||||
assert!(with_canvas
|
|
||||||
.contains("POST /api/external/v1/editor/images/generations(kind=ui-design)"));
|
|
||||||
assert!(with_canvas
|
|
||||||
.contains("不得误用 POST /api/external/v1/editor/ui-designs/assets/extractions"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn agent_prompt_art_director_switches_between_read_only_and_conditional_canvas_owner() {
|
fn agent_prompt_art_director_describes_credentials_and_resource_checks() {
|
||||||
let without_canvas =
|
let without_canvas =
|
||||||
game_creator_art_director_tool_plan_prompt("shared runtime contract", false);
|
game_creator_art_director_tool_plan_prompt("shared runtime contract", false);
|
||||||
assert!(without_canvas.contains("这是只读协调任务"));
|
assert!(without_canvas.contains("这是只读协调任务"));
|
||||||
assert!(without_canvas.contains("只完成正式 director 结论并直接交付"));
|
assert!(without_canvas.contains("只完成正式 director 结论并直接交付"));
|
||||||
assert!(without_canvas.contains("不调用 canvas.asset_generate"));
|
assert!(without_canvas.contains("不调用 canvas.asset_generate"));
|
||||||
assert!(without_canvas.contains("图片产物与验收条款在本轮不适用"));
|
|
||||||
|
|
||||||
let with_canvas =
|
let with_canvas =
|
||||||
game_creator_art_director_tool_plan_prompt("shared runtime contract", true);
|
game_creator_art_director_tool_plan_prompt("shared runtime contract", true);
|
||||||
assert!(with_canvas.contains("outputPath=assets/art-spec.png"));
|
assert!(with_canvas
|
||||||
assert!(with_canvas.contains("assetKind=icon-spec"));
|
.contains("canvas.asset_generate 的 assetKind、outputPath、尺寸、比例和提示词"));
|
||||||
assert!(with_canvas.contains("成功只表示固定候选已生成并登记,不等于视觉门已经通过"));
|
assert!(with_canvas.contains("需要参考图时使用已登记资源 ID"));
|
||||||
assert!(with_canvas.contains("由 Runtime 在收束时核对当前 revision"));
|
assert!(with_canvas.contains("生成后核对返回资源、权限、计费和登记状态"));
|
||||||
assert!(!with_canvas.contains("成功会为本人当前 revision 形成验证凭证"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn agent_prompt_art_asset_plan_uses_fixed_transparent_spritesheet_route_and_warnings() {
|
fn agent_prompt_art_asset_plan_describes_resource_discovery_and_generation() {
|
||||||
let without_canvas =
|
let without_canvas =
|
||||||
game_creator_art_asset_plan_tool_plan_prompt("shared runtime contract", false);
|
game_creator_art_asset_plan_tool_plan_prompt("shared runtime contract", false);
|
||||||
assert!(without_canvas.contains("不调用 canvas.asset_generate"));
|
assert!(without_canvas.contains("不调用 canvas.asset_generate"));
|
||||||
assert!(without_canvas.contains("不伪造 assets/art-spritesheet.png"));
|
assert!(without_canvas.contains("写入可解析的 assets/manifest.art.json"));
|
||||||
|
|
||||||
let with_canvas =
|
let with_canvas =
|
||||||
game_creator_art_asset_plan_tool_plan_prompt("shared runtime contract", true);
|
game_creator_art_asset_plan_tool_plan_prompt("shared runtime contract", true);
|
||||||
assert!(with_canvas.contains("用 asset.list 确认 assets/art-spec.png 已登记"));
|
assert!(with_canvas.contains("使用 asset.list 了解已有资源"));
|
||||||
assert!(with_canvas.contains("由 Runtime 形成 iconDescriptions"));
|
assert!(with_canvas.contains("再按需调用 canvas.asset_generate"));
|
||||||
assert!(with_canvas.contains("玩家主体及朝向/状态"));
|
assert!(with_canvas.contains("spritesheet 可通过 sliceCount 指定切片数量"));
|
||||||
assert!(with_canvas.contains("不得假设为塔防"));
|
assert!(with_canvas.contains("生成后核对资源登记、透明度、警告和实际使用情况"));
|
||||||
assert!(with_canvas.contains("权威 resourceId 作为 referenceId"));
|
|
||||||
assert!(with_canvas.contains("POST /api/external/v1/editor/icon-spritesheets/generations"));
|
|
||||||
assert!(with_canvas.contains("不得回退普通生图或 UI extraction"));
|
|
||||||
assert!(with_canvas.contains("screenColor=auto"));
|
|
||||||
assert!(with_canvas.contains("缺少规范图时必须等待 art-director"));
|
|
||||||
assert!(with_canvas.contains("不得回退普通生图"));
|
|
||||||
assert!(with_canvas.contains("真实 alpha"));
|
|
||||||
assert!(with_canvas.contains("warning.code=postprocess-failed-source-preserved"));
|
|
||||||
assert!(with_canvas.contains("不得登记、验收或自动重试"));
|
|
||||||
assert!(with_canvas.contains("仅 sliceWarning"));
|
|
||||||
assert!(with_canvas.contains("由 Runtime 在收束门内验证本人固定 manifest 产物"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -2011,17 +2011,6 @@ mod tests {
|
|||||||
assert!(error.contains("不得启动 isolated child"));
|
assert!(error.contains("不得启动 isolated child"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn autonomous_initial_collaboration_requires_leader_artifacts() {
|
|
||||||
let mut plan = autonomous_initial_leader_plan();
|
|
||||||
plan.actions[1] = autonomous_initial_delegate("art-director", &[]);
|
|
||||||
|
|
||||||
let error = validate_agent_runtime_autonomous_initial_collaboration_contract(&plan)
|
|
||||||
.expect_err("art artifact must be required");
|
|
||||||
|
|
||||||
assert!(error.contains("expectedArtifacts 必须包含 assets/art-spec.png"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn autonomous_manifest_dag_waits_only_after_seed_execution_starts() {
|
fn autonomous_manifest_dag_waits_only_after_seed_execution_starts() {
|
||||||
let temporary = tempfile::tempdir().expect("create manifest DAG policy root");
|
let temporary = tempfile::tempdir().expect("create manifest DAG policy root");
|
||||||
|
|||||||
@@ -1923,8 +1923,8 @@ mod tests {
|
|||||||
assert!(!publish_prompt.contains("验证本人固定 owner 产物"));
|
assert!(!publish_prompt.contains("验证本人固定 owner 产物"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
async fn art_director_ready_task_is_read_only_without_key_and_canvas_owner_with_key() {
|
fn art_director_ready_task_is_read_only_without_credentials() {
|
||||||
let task = seed_task("art-director");
|
let task = seed_task("art-director");
|
||||||
{
|
{
|
||||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||||
@@ -1940,42 +1940,5 @@ mod tests {
|
|||||||
&prompt
|
&prompt
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// debug 构建下 editor_api_mode() 恒为 PlatformAccount,配置里的
|
|
||||||
// editorApi.apiKey 会被 editor_api_key_is_configured 完全忽略,只有凭据
|
|
||||||
// override 或平台会话才算「已配置」。用支持的 override 钩子模拟。
|
|
||||||
crate::assets::with_external_editor_api_credentials(
|
|
||||||
crate::assets::external_editor_api_credentials_for_test(
|
|
||||||
"https://editor.test".to_string(),
|
|
||||||
"art-director-ready-task-key".to_string(),
|
|
||||||
),
|
|
||||||
async {
|
|
||||||
let prompt = render_autonomous_manifest_ready_task_background_prompt(&task);
|
|
||||||
assert!(prompt.contains("非只读视觉规范生成任务"));
|
|
||||||
assert!(prompt.contains(
|
|
||||||
crate::agent::AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER
|
|
||||||
));
|
|
||||||
assert!(prompt.contains("生成并登记 assets/art-spec.png"));
|
|
||||||
assert!(prompt.contains("会同时提交当前 run 的 mutation 与验证凭证"));
|
|
||||||
assert!(prompt.contains("Runtime 会在子 Run 终态后幂等投影 manifest"));
|
|
||||||
assert!(!prompt.contains("无生图凭据只读协调任务"));
|
|
||||||
assert!(autonomous_manifest_ready_task_requires_visual_asset(
|
|
||||||
"art-director"
|
|
||||||
));
|
|
||||||
assert!(!agent_runtime_task_requires_read_only_delivery(
|
|
||||||
"art-director",
|
|
||||||
&prompt
|
|
||||||
));
|
|
||||||
let art_asset_prompt = render_autonomous_manifest_ready_task_background_prompt(
|
|
||||||
&seed_task("art-asset-plan"),
|
|
||||||
);
|
|
||||||
assert!(art_asset_prompt.contains("canvas.asset_generate"));
|
|
||||||
assert!(art_asset_prompt.contains("asset.list"));
|
|
||||||
assert!(art_asset_prompt.contains("file.write 写入 assets/manifest.art.json"));
|
|
||||||
assert!(art_asset_prompt.contains("不要调用 image.inspect"));
|
|
||||||
assert!(art_asset_prompt.contains("把结构化计划最后一步标记 completed"));
|
|
||||||
assert!(!art_asset_prompt.contains("按现有 visual gate 生成、登记并验收"));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ mod goal_contract;
|
|||||||
mod helpers;
|
mod helpers;
|
||||||
mod isolated_joins;
|
mod isolated_joins;
|
||||||
mod media;
|
mod media;
|
||||||
|
pub(in crate::agent) use media::design_foundation_ui_page_output_path_is_valid;
|
||||||
mod policy;
|
mod policy;
|
||||||
mod preview;
|
mod preview;
|
||||||
mod process_ops;
|
mod process_ops;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ fn agent_runtime_canvas_asset_kind_is_supported(asset_kind: &str) -> bool {
|
|||||||
AGENT_RUNTIME_CANVAS_ASSET_KINDS.contains(&asset_kind)
|
AGENT_RUNTIME_CANVAS_ASSET_KINDS.contains(&asset_kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn design_foundation_ui_page_output_path_is_valid(path: &str) -> bool {
|
pub(in crate::agent) fn design_foundation_ui_page_output_path_is_valid(path: &str) -> bool {
|
||||||
let Some(page_id) = path
|
let Some(page_id) = path
|
||||||
.strip_prefix("assets/ui-pages/")
|
.strip_prefix("assets/ui-pages/")
|
||||||
.and_then(|value| value.strip_suffix(".png"))
|
.and_then(|value| value.strip_suffix(".png"))
|
||||||
@@ -545,7 +545,7 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
|
|||||||
.or_else(|| input.get("slice_count"))
|
.or_else(|| input.get("slice_count"))
|
||||||
.and_then(serde_json::Value::as_u64)
|
.and_then(serde_json::Value::as_u64)
|
||||||
.map(|value| value as usize);
|
.map(|value| value as usize);
|
||||||
let requested_options = PlatformArtAssetGenerationOptions {
|
let mut requested_options = PlatformArtAssetGenerationOptions {
|
||||||
output_path: (!output_path.trim().is_empty()).then_some(output_path),
|
output_path: (!output_path.trim().is_empty()).then_some(output_path),
|
||||||
aspect_ratio,
|
aspect_ratio,
|
||||||
image_size,
|
image_size,
|
||||||
@@ -554,6 +554,25 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
|
|||||||
replace_existing,
|
replace_existing,
|
||||||
slice_count,
|
slice_count,
|
||||||
};
|
};
|
||||||
|
if let Some(pending) = pending_action {
|
||||||
|
match recover_persisted_visual_generation_options(
|
||||||
|
root,
|
||||||
|
pending,
|
||||||
|
&prompt,
|
||||||
|
&requested_options,
|
||||||
|
) {
|
||||||
|
Ok(Some(recovered)) => requested_options = recovered,
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(error) => {
|
||||||
|
return AgentRuntimeToolObservation {
|
||||||
|
tool: "canvas.asset_generate".to_string(),
|
||||||
|
status: AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION.to_string(),
|
||||||
|
summary: redact_agent_runtime_project_paths(root, &error, 240),
|
||||||
|
detail: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut options = {
|
let mut options = {
|
||||||
let defaults = PlatformArtAssetGenerationOptions::default();
|
let defaults = PlatformArtAssetGenerationOptions::default();
|
||||||
PlatformArtAssetGenerationOptions {
|
PlatformArtAssetGenerationOptions {
|
||||||
|
|||||||
@@ -2007,10 +2007,10 @@ pub(crate) fn write_game_creator_app_config(
|
|||||||
let _guard = GAME_CREATOR_CONFIG_WRITE_LOCK
|
let _guard = GAME_CREATOR_CONFIG_WRITE_LOCK
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|_| "配置写入锁不可用")?;
|
.map_err(|_| "配置写入锁不可用")?;
|
||||||
let stored = load_game_creator_app_config()?;
|
let (current, overlays) = load_game_creator_app_config_for_write()?;
|
||||||
config.selected_model_id = stored.selected_model_id;
|
config.selected_model_id = current.selected_model_id;
|
||||||
config.selected_model_is_default = stored.selected_model_is_default;
|
config.selected_model_is_default = current.selected_model_is_default;
|
||||||
persist_game_creator_app_config(config)
|
persist_game_creator_app_config(config, overlays, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -2029,20 +2029,67 @@ pub(crate) fn select_game_creator_model(
|
|||||||
{
|
{
|
||||||
return Err("模型标识无效".into());
|
return Err("模型标识无效".into());
|
||||||
}
|
}
|
||||||
let mut config = load_game_creator_app_config()?;
|
let (mut config, overlays) = load_game_creator_app_config_for_write()?;
|
||||||
config.selected_model_id = model_id;
|
config.selected_model_id = model_id;
|
||||||
config.selected_model_is_default = is_default;
|
config.selected_model_is_default = is_default;
|
||||||
persist_game_creator_app_config(config)
|
persist_game_creator_app_config(config, overlays, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn persist_game_creator_app_config(
|
fn persist_game_creator_app_config(
|
||||||
config: GameCreatorAppConfig,
|
config: GameCreatorAppConfig,
|
||||||
|
overlays: Vec<(PathBuf, serde_json::Value)>,
|
||||||
|
model_only: bool,
|
||||||
) -> Result<GameCreatorAppConfigView, String> {
|
) -> Result<GameCreatorAppConfigView, String> {
|
||||||
let config = normalize_game_creator_app_config(config)?;
|
let config = normalize_game_creator_app_config(config)?;
|
||||||
let path = writable_game_creator_config_path()?;
|
let path = writable_game_creator_config_path()?;
|
||||||
let content = serialize_game_creator_app_config_for_renderer_write(&config)?;
|
let content = serialize_game_creator_app_config_for_renderer_write(&config)?;
|
||||||
write_game_creator_config_atomically(&path, &format!("{content}\n"))?;
|
let mut writes = vec![(path, format!("{content}\n"))];
|
||||||
game_creator_app_config_view(load_game_creator_app_config()?)
|
let saved: serde_json::Value = serde_json::from_str(&content)
|
||||||
|
.map_err(|error| format!("解析已序列化客户端配置失败:{error}"))?;
|
||||||
|
for (overlay_path, mut overlay) in overlays {
|
||||||
|
let previous = overlay.clone();
|
||||||
|
if let Some(fields) = overlay.as_object_mut() {
|
||||||
|
for (key, value) in fields.iter_mut() {
|
||||||
|
if matches!(key.as_str(), "selectedModelId" | "selectedModelIsDefault")
|
||||||
|
== model_only
|
||||||
|
{
|
||||||
|
if let Some(saved_value) = saved.get(key) {
|
||||||
|
// 仅同步已有覆盖项;其它字段继续保留原有覆盖语义。
|
||||||
|
if key == "agentLlm" {
|
||||||
|
*value = saved_value.clone();
|
||||||
|
} else {
|
||||||
|
update_existing_config_overlay_fields(value, saved_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if overlay != previous {
|
||||||
|
let content = serde_json::to_string_pretty(&overlay)
|
||||||
|
.map_err(|error| format!("序列化客户端覆盖配置失败:{error}"))?;
|
||||||
|
writes.push((overlay_path, format!("{content}\n")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::config::write_game_creator_config_batch(&writes)?;
|
||||||
|
// `config` is already normalized and is exactly what was persisted.
|
||||||
|
// Avoid reloading it here: a reload repeats the Windows private-path and
|
||||||
|
// ACL checks and made saving the settings panel appear to hang.
|
||||||
|
game_creator_app_config_view(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_existing_config_overlay_fields(
|
||||||
|
overlay: &mut serde_json::Value,
|
||||||
|
saved: &serde_json::Value,
|
||||||
|
) {
|
||||||
|
if let (Some(fields), Some(saved_fields)) = (overlay.as_object_mut(), saved.as_object()) {
|
||||||
|
for (key, value) in fields {
|
||||||
|
if let Some(saved_value) = saved_fields.get(key) {
|
||||||
|
update_existing_config_overlay_fields(value, saved_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !overlay.is_null() {
|
||||||
|
*overlay = saved.clone();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -3272,9 +3272,10 @@ pub(crate) fn configure_game_creator_runtime_config_dir(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates a persisted AGC config file without following links. Existing
|
/// Validates a persisted AGC config file without following links.
|
||||||
/// regular files are tightened through the same Windows owner/DACL gate used
|
///
|
||||||
/// by credential files before any read is allowed.
|
/// Reading configuration must remain read-only. ACL hardening is performed
|
||||||
|
/// when the file is created or replaced, not on every settings-panel read.
|
||||||
fn validate_game_creator_config_file_entry(path: &Path) -> Result<bool, String> {
|
fn validate_game_creator_config_file_entry(path: &Path) -> Result<bool, String> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
validate_game_creator_private_path_ancestors_with_auto_elevation(path, "客户端配置文件")?;
|
validate_game_creator_private_path_ancestors_with_auto_elevation(path, "客户端配置文件")?;
|
||||||
@@ -3293,8 +3294,6 @@ fn validate_game_creator_config_file_entry(path: &Path) -> Result<bool, String>
|
|||||||
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
||||||
return Err("客户端配置文件必须是普通文件,不能是链接或其他对象".to_string());
|
return Err("客户端配置文件必须是普通文件,不能是链接或其他对象".to_string());
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
|
||||||
secure_windows_game_creator_path_for_current_user_with_auto_elevation(path, false, true)?;
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3352,6 +3351,31 @@ pub(crate) fn load_game_creator_app_config() -> Result<GameCreatorAppConfig, Str
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_game_creator_app_config_for_write(
|
||||||
|
) -> Result<(GameCreatorAppConfig, Vec<(PathBuf, serde_json::Value)>), String> {
|
||||||
|
let writable_path = writable_game_creator_config_path()?;
|
||||||
|
let mut config = GameCreatorAppConfig::default();
|
||||||
|
let mut overlays = Vec::new();
|
||||||
|
let mut after_writable = false;
|
||||||
|
for path in game_creator_config_paths() {
|
||||||
|
if path == writable_path {
|
||||||
|
after_writable = true;
|
||||||
|
}
|
||||||
|
if let Some(content) = read_game_creator_config_file(&path)? {
|
||||||
|
merge_game_creator_config_content(&mut config, &path, &content)?;
|
||||||
|
if after_writable && path != writable_path {
|
||||||
|
let value = serde_json::from_str(&content)
|
||||||
|
.map_err(|error| format!("解析客户端覆盖配置失败:{error}"))?;
|
||||||
|
overlays.push((path, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if game_creator_official_llm_route_locked() {
|
||||||
|
lock_game_creator_app_config_to_official_route(&mut config);
|
||||||
|
}
|
||||||
|
Ok((config, overlays))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn scrub_locked_game_creator_config_file(config: &mut GameCreatorAppConfigFile) -> bool {
|
pub(crate) fn scrub_locked_game_creator_config_file(config: &mut GameCreatorAppConfigFile) -> bool {
|
||||||
let mut changed = config.agent_mode.as_deref()
|
let mut changed = config.agent_mode.as_deref()
|
||||||
!= Some(GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER)
|
!= Some(GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER)
|
||||||
@@ -3588,6 +3612,13 @@ pub(crate) fn merge_game_creator_config_file(
|
|||||||
config: &mut GameCreatorAppConfig,
|
config: &mut GameCreatorAppConfig,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
if let Some(content) = read_game_creator_config_file(path)? {
|
||||||
|
merge_game_creator_config_content(config, path, &content)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_game_creator_config_file(path: &Path) -> Result<Option<String>, String> {
|
||||||
let backup_path = game_creator_config_backup_path(path);
|
let backup_path = game_creator_config_backup_path(path);
|
||||||
let path_exists = validate_game_creator_config_file_entry(path)?;
|
let path_exists = validate_game_creator_config_file_entry(path)?;
|
||||||
let read_path = if path_exists {
|
let read_path = if path_exists {
|
||||||
@@ -3595,11 +3626,19 @@ pub(crate) fn merge_game_creator_config_file(
|
|||||||
} else if validate_game_creator_config_file_entry(&backup_path)? {
|
} else if validate_game_creator_config_file_entry(&backup_path)? {
|
||||||
backup_path.as_path()
|
backup_path.as_path()
|
||||||
} else {
|
} else {
|
||||||
return Ok(());
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let content = read_game_creator_private_file_to_string(read_path, "客户端配置", 256 * 1024)?;
|
let content = read_game_creator_private_file_to_string(read_path, "客户端配置", 256 * 1024)?;
|
||||||
let file_config = serde_json::from_str::<GameCreatorAppConfigFile>(&content)
|
Ok(Some(content))
|
||||||
.map_err(|error| format!("解析客户端配置失败:{}: {error}", read_path.display()))?;
|
}
|
||||||
|
|
||||||
|
fn merge_game_creator_config_content(
|
||||||
|
config: &mut GameCreatorAppConfig,
|
||||||
|
path: &Path,
|
||||||
|
content: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let file_config = serde_json::from_str::<GameCreatorAppConfigFile>(content)
|
||||||
|
.map_err(|error| format!("解析客户端配置失败:{}: {error}", path.display()))?;
|
||||||
if let Some(agent_mode) = file_config.agent_mode {
|
if let Some(agent_mode) = file_config.agent_mode {
|
||||||
config.agent_mode = agent_mode;
|
config.agent_mode = agent_mode;
|
||||||
}
|
}
|
||||||
@@ -3642,6 +3681,48 @@ fn game_creator_config_backup_path(path: &Path) -> PathBuf {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_game_creator_config_batch(writes: &[(PathBuf, String)]) -> Result<(), String> {
|
||||||
|
if writes.len() == 1 {
|
||||||
|
return write_game_creator_config_atomically(&writes[0].0, &writes[0].1);
|
||||||
|
}
|
||||||
|
let originals = writes
|
||||||
|
.iter()
|
||||||
|
.map(|(path, _)| read_game_creator_config_file(path))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
for (index, (path, content)) in writes.iter().enumerate() {
|
||||||
|
if let Err(mut error) = write_game_creator_config_atomically(path, content) {
|
||||||
|
// 写入可能在替换后的权限检查失败,因此失败目标也需要核对并恢复。
|
||||||
|
for rollback_index in (0..=index).rev() {
|
||||||
|
let path = &writes[rollback_index].0;
|
||||||
|
let original = &originals[rollback_index];
|
||||||
|
if read_game_creator_config_file(path).ok().as_ref() == Some(original) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let restored = match original {
|
||||||
|
Some(content) => write_game_creator_config_atomically(path, content),
|
||||||
|
None => fs::remove_file(path)
|
||||||
|
.or_else(|error| {
|
||||||
|
if error.kind() == std::io::ErrorKind::NotFound {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|error| error.to_string()),
|
||||||
|
};
|
||||||
|
if let Err(restore_error) = restored {
|
||||||
|
error.push_str(&format!(
|
||||||
|
";恢复配置失败:{}: {restore_error}",
|
||||||
|
path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn write_game_creator_config_atomically(
|
pub(crate) fn write_game_creator_config_atomically(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
content: &str,
|
content: &str,
|
||||||
|
|||||||
@@ -1132,225 +1132,6 @@ async fn background_agent_runtime_delegate_respects_project_policy() {
|
|||||||
fs::remove_dir_all(root).ok();
|
fs::remove_dir_all(root).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn visual_specialists_reject_overriding_their_fixed_image_contract() {
|
|
||||||
let root = unique_project_path();
|
|
||||||
init_local_game_project_at(&root, "project-1", "视觉固定输出合同测试").expect("project init");
|
|
||||||
write_project_permission_policy_at(
|
|
||||||
&root,
|
|
||||||
ProjectPermissionPolicy {
|
|
||||||
denied_commands: Vec::new(),
|
|
||||||
confirm_commands: Vec::new(),
|
|
||||||
agent_policies: BTreeMap::new(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("allow canvas generation");
|
|
||||||
let action = AgentRuntimeToolAction {
|
|
||||||
tool: "canvas.asset_generate".to_string(),
|
|
||||||
reason: Some("尝试覆盖策划固定图片合同".to_string()),
|
|
||||||
input: serde_json::json!({
|
|
||||||
"prompt": "生成横屏界面原型",
|
|
||||||
"outputPath": "assets/wrong-prototype.png",
|
|
||||||
"aspectRatio": "1:1",
|
|
||||||
"imageSize": "2K",
|
|
||||||
"assetKind": "game-art",
|
|
||||||
"assetLabel": "错误标签",
|
|
||||||
"replaceExisting": false
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
let observation = execute_game_creator_agent_runtime_tool_action_with_action_id(
|
|
||||||
&root,
|
|
||||||
"design-foundation",
|
|
||||||
"visual-fixed-contract-run",
|
|
||||||
"必须交付固定原型图",
|
|
||||||
&action,
|
|
||||||
Some("visual-fixed-contract-action"),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(observation.status, "failed");
|
|
||||||
for expected in [
|
|
||||||
"不能覆盖固定输出合同",
|
|
||||||
"outputPath=assets/ui-prototype.png",
|
|
||||||
"aspectRatio=16:9",
|
|
||||||
"imageSize=2K",
|
|
||||||
"assetKind=ui-prototype",
|
|
||||||
"assetLabel=游戏横屏界面原型图",
|
|
||||||
] {
|
|
||||||
assert!(observation.summary.contains(expected));
|
|
||||||
}
|
|
||||||
assert_eq!(observation.detail, None);
|
|
||||||
assert!(!root.join("assets/wrong-prototype.png").exists());
|
|
||||||
assert!(read_manifest_for_project(&root)
|
|
||||||
.expect("manifest after rejected override")
|
|
||||||
.assets
|
|
||||||
.is_empty());
|
|
||||||
|
|
||||||
let one_k_action = AgentRuntimeToolAction {
|
|
||||||
tool: "canvas.asset_generate".to_string(),
|
|
||||||
reason: Some("尝试使用会返回 3:2 文件的 1K 规格".to_string()),
|
|
||||||
input: serde_json::json!({
|
|
||||||
"prompt": "生成横屏界面原型",
|
|
||||||
"outputPath": "assets/ui-prototype.png",
|
|
||||||
"aspectRatio": "16:9",
|
|
||||||
"imageSize": "1K",
|
|
||||||
"assetKind": "ui-prototype",
|
|
||||||
"assetLabel": "游戏横屏界面原型图",
|
|
||||||
"replaceExisting": false
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
let one_k_observation = execute_game_creator_agent_runtime_tool_action_with_action_id(
|
|
||||||
&root,
|
|
||||||
"design-foundation",
|
|
||||||
"visual-fixed-contract-run",
|
|
||||||
"UI 原型必须使用真正的 16:9 输出规格",
|
|
||||||
&one_k_action,
|
|
||||||
Some("visual-fixed-contract-1k-action"),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(one_k_observation.status, "failed");
|
|
||||||
assert!(one_k_observation.summary.contains("不能覆盖固定输出合同"));
|
|
||||||
|
|
||||||
fs::remove_dir_all(root).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn design_foundation_rejects_scene_image_stale_run_and_stale_sha_visual_proofs() {
|
|
||||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
|
||||||
"visual-semantic-gate-test-user",
|
|
||||||
"visual-semantic-gate-test-key",
|
|
||||||
"https://dev.genarrative.world",
|
|
||||||
);
|
|
||||||
let _config_guard = crate::tests::write_test_local_config(
|
|
||||||
r#"{"editorApi":{"apiKey":"visual-semantic-gate-test-key"}}"#.to_string(),
|
|
||||||
);
|
|
||||||
let root = unique_project_path();
|
|
||||||
init_local_game_project_at(&root, "project-1", "UI 原型语义完成门禁测试")
|
|
||||||
.expect("project init");
|
|
||||||
register_canvas_visual_asset_fixture(&root, "assets/art-spec.png", "icon-spec");
|
|
||||||
register_canvas_visual_asset_fixture(&root, AGENT_RUNTIME_UI_PROTOTYPE_PATH, "ui-prototype");
|
|
||||||
let run_id = "design-ui-semantic-gate-run";
|
|
||||||
let state = start_game_creator_agent_runtime_task_at(
|
|
||||||
&root,
|
|
||||||
"design-foundation",
|
|
||||||
"交付真正可实现的 UI 原型",
|
|
||||||
run_id,
|
|
||||||
"agent-background-task",
|
|
||||||
"准备完成",
|
|
||||||
vec!["生成并检查 UI 原型".to_string()],
|
|
||||||
)
|
|
||||||
.expect("start UI prototype runtime");
|
|
||||||
let revision = read_game_creator_agent_runtime_project_revision(&root)
|
|
||||||
.expect("read UI prototype revision")
|
|
||||||
.revision;
|
|
||||||
|
|
||||||
let uninspected = finish_game_creator_agent_background_runtime_turn_at(
|
|
||||||
&root,
|
|
||||||
state.clone(),
|
|
||||||
"只有文件登记不能证明它是真正的 UI 原型。",
|
|
||||||
revision,
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.expect("missing visual verdict remains recoverable");
|
|
||||||
let uninspected_blocker = match uninspected {
|
|
||||||
AgentBackgroundFinalizationOutcome::Stale(blocker) => blocker,
|
|
||||||
_ => panic!("uninspected image must not complete design-foundation"),
|
|
||||||
};
|
|
||||||
assert!(uninspected_blocker
|
|
||||||
.detail
|
|
||||||
.as_deref()
|
|
||||||
.is_some_and(|detail| detail.contains("requiredInspection=image.inspect")));
|
|
||||||
|
|
||||||
append_ui_prototype_inspection_fixture_with_profile(&root, run_id, true, "ui-prototype.v1");
|
|
||||||
let legacy_v1 = finish_game_creator_agent_background_runtime_turn_at(
|
|
||||||
&root,
|
|
||||||
state.clone(),
|
|
||||||
"历史 v1 审计不能放行当前 run。",
|
|
||||||
revision,
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.expect("legacy v1 remains recoverable but not authoritative");
|
|
||||||
assert!(matches!(
|
|
||||||
legacy_v1,
|
|
||||||
AgentBackgroundFinalizationOutcome::Stale(ref blocker)
|
|
||||||
if blocker.tool == "runtime.visual_asset"
|
|
||||||
));
|
|
||||||
|
|
||||||
append_ui_prototype_inspection_fixture(&root, run_id, false);
|
|
||||||
let scene_blocked = finish_game_creator_agent_background_runtime_turn_at(
|
|
||||||
&root,
|
|
||||||
state.clone(),
|
|
||||||
"场景图不能冒充 UI 原型。",
|
|
||||||
revision,
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.expect("scene verdict remains recoverable");
|
|
||||||
let scene_blocker = match scene_blocked {
|
|
||||||
AgentBackgroundFinalizationOutcome::Stale(blocker) => blocker,
|
|
||||||
_ => panic!("scene image must not complete design-foundation"),
|
|
||||||
};
|
|
||||||
assert_eq!(scene_blocker.tool, "runtime.visual_asset");
|
|
||||||
assert!(scene_blocker.summary.contains("尚未通过结构化 UI 视觉检查"));
|
|
||||||
assert!(scene_blocker
|
|
||||||
.detail
|
|
||||||
.as_deref()
|
|
||||||
.is_some_and(|detail| detail.contains("informationHud=false")));
|
|
||||||
|
|
||||||
append_ui_prototype_inspection_fixture(&root, "another-design-run", true);
|
|
||||||
let wrong_run = finish_game_creator_agent_background_runtime_turn_at(
|
|
||||||
&root,
|
|
||||||
state.clone(),
|
|
||||||
"其他 run 的证据不能放行。",
|
|
||||||
revision,
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.expect("wrong run verdict remains recoverable");
|
|
||||||
assert!(matches!(
|
|
||||||
wrong_run,
|
|
||||||
AgentBackgroundFinalizationOutcome::Stale(ref blocker)
|
|
||||||
if blocker.tool == "runtime.visual_asset"
|
|
||||||
));
|
|
||||||
|
|
||||||
append_ui_prototype_inspection_fixture(&root, run_id, true);
|
|
||||||
let mut replacement = valid_test_png_bytes();
|
|
||||||
replacement.extend_from_slice(b"changed-ui-prototype");
|
|
||||||
fs::write(root.join(AGENT_RUNTIME_UI_PROTOTYPE_PATH), replacement)
|
|
||||||
.expect("replace UI prototype fixture");
|
|
||||||
let stale_sha = finish_game_creator_agent_background_runtime_turn_at(
|
|
||||||
&root,
|
|
||||||
state.clone(),
|
|
||||||
"旧图片 SHA 的证据不能放行。",
|
|
||||||
revision,
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.expect("stale sha verdict remains recoverable");
|
|
||||||
let stale_sha_blocker = match stale_sha {
|
|
||||||
AgentBackgroundFinalizationOutcome::Stale(blocker) => blocker,
|
|
||||||
_ => panic!("stale image proof must not complete design-foundation"),
|
|
||||||
};
|
|
||||||
assert!(stale_sha_blocker
|
|
||||||
.detail
|
|
||||||
.as_deref()
|
|
||||||
.is_some_and(|detail| detail.contains("requiredInspection=image.inspect")));
|
|
||||||
|
|
||||||
append_ui_prototype_inspection_fixture(&root, run_id, true);
|
|
||||||
let completed = finish_game_creator_agent_background_runtime_turn_at(
|
|
||||||
&root,
|
|
||||||
state,
|
|
||||||
"当前图片已通过全部八项 UI 原型检查。",
|
|
||||||
revision,
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.expect("current passed UI proof allows finalization");
|
|
||||||
assert!(matches!(
|
|
||||||
completed,
|
|
||||||
AgentBackgroundFinalizationOutcome::Completed(_)
|
|
||||||
));
|
|
||||||
|
|
||||||
fs::remove_dir_all(root).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn supervisor_collaboration_canvas_asset_generate_rechecks_after_project_lock() {
|
async fn supervisor_collaboration_canvas_asset_generate_rechecks_after_project_lock() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
@@ -1804,6 +1585,8 @@ async fn canvas_replacement_rejects_parent_run_that_terminates_during_external_r
|
|||||||
"替换不符合原创要求的正式图片",
|
"替换不符合原创要求的正式图片",
|
||||||
&serde_json::json!({
|
&serde_json::json!({
|
||||||
"prompt": "生成原创晶体与潮汐构装体图集",
|
"prompt": "生成原创晶体与潮汐构装体图集",
|
||||||
|
"outputPath": "assets/art-spritesheet.png",
|
||||||
|
"assetKind": "art-spritesheet",
|
||||||
"replaceExisting": true
|
"replaceExisting": true
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -2181,61 +1964,21 @@ fn gui_and_cli_autonomous_game_build_runs_still_allow_publish_delegates() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn visual_specialist_delegations_require_image_artifacts_but_read_only_work_allows_none() {
|
fn read_only_delegation_persists_acceptance_criteria_without_artifacts() {
|
||||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
|
||||||
"visual-delegation-contract-user",
|
|
||||||
"visual-delegation-contract-key",
|
|
||||||
"https://dev.genarrative.world",
|
|
||||||
);
|
|
||||||
let _config_guard = crate::tests::write_test_local_config(
|
|
||||||
r#"{"editorApi":{"apiKey":"visual-delegation-contract-key"}}"#.to_string(),
|
|
||||||
);
|
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "视觉委派合同测试").expect("project init");
|
init_local_game_project_at(&root, "project-1", "只读委派合同测试").expect("project init");
|
||||||
let parent_run_id = "visual-delegate-contract-parent-run";
|
let parent_run_id = "visual-delegate-contract-parent-run";
|
||||||
start_game_creator_agent_runtime_task_at(
|
start_game_creator_agent_runtime_task_at(
|
||||||
&root,
|
&root,
|
||||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||||
"校验视觉专业任务合同",
|
"校验只读任务合同",
|
||||||
parent_run_id,
|
parent_run_id,
|
||||||
"agent-chat",
|
"agent-chat",
|
||||||
"准备委派",
|
"准备委派",
|
||||||
vec!["拒绝用文本回执冒充图片".to_string()],
|
vec!["保存只读验收标准".to_string()],
|
||||||
)
|
)
|
||||||
.expect("start supervisor parent runtime");
|
.expect("start supervisor parent runtime");
|
||||||
|
|
||||||
for (agent_id, required_path) in [
|
|
||||||
("design-foundation", "assets/ui-prototype.png"),
|
|
||||||
("art-asset-plan", "assets/art-spritesheet.png"),
|
|
||||||
] {
|
|
||||||
let action_id = format!("reject-empty-visual-{agent_id}");
|
|
||||||
let observation = observe_agent_runtime_agent_delegate(
|
|
||||||
&root,
|
|
||||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
||||||
parent_run_id,
|
|
||||||
Some(&action_id),
|
|
||||||
&serde_json::json!({
|
|
||||||
"agentId": agent_id,
|
|
||||||
"task": "交付真实图片",
|
|
||||||
"acceptanceCriteria": ["必须能看到图片"],
|
|
||||||
"expectedArtifacts": [],
|
|
||||||
"repairOfDelegationId": null,
|
|
||||||
"runId": null
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
assert_eq!(observation.status, "failed");
|
|
||||||
assert!(observation.summary.contains(required_path));
|
|
||||||
let delegation_id = agent_runtime_delegation_id(
|
|
||||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
|
||||||
parent_run_id,
|
|
||||||
agent_id,
|
|
||||||
&action_id,
|
|
||||||
);
|
|
||||||
assert!(read_static_delegate_delivery_at(&root, &delegation_id)
|
|
||||||
.expect("read rejected visual delivery")
|
|
||||||
.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
let target_agent_id = "quality-review";
|
let target_agent_id = "quality-review";
|
||||||
let action_id = "allow-read-only-empty-artifacts";
|
let action_id = "allow-read-only-empty-artifacts";
|
||||||
let target_lock = try_acquire_game_creator_agent_runtime_task_lock(&root, target_agent_id)
|
let target_lock = try_acquire_game_creator_agent_runtime_task_lock(&root, target_agent_id)
|
||||||
|
|||||||
@@ -653,7 +653,7 @@ fn parallel_read_batch_finishes_before_concurrent_steer_is_accepted() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn provider_action_batch_verification_then_parallel_reads_share_current_gate_snapshot() {
|
async fn provider_action_batch_verification_then_parallel_reads_share_current_gate_snapshot() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(
|
init_existing_html_project_at(
|
||||||
&root,
|
&root,
|
||||||
"project-provider-batch-verification-parallel-read",
|
"project-provider-batch-verification-parallel-read",
|
||||||
"Provider 批次验证后并行读取测试",
|
"Provider 批次验证后并行读取测试",
|
||||||
|
|||||||
@@ -929,117 +929,6 @@ fn project_supervisor_runtime_id_is_normalized_and_collected() {
|
|||||||
fs::remove_dir_all(root).ok();
|
fs::remove_dir_all(root).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn visual_specialist_prompts_require_real_registered_image_deliveries() {
|
|
||||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
|
||||||
"visual-prompt-test-user",
|
|
||||||
"visual-prompt-test-key",
|
|
||||||
"https://dev.genarrative.world",
|
|
||||||
);
|
|
||||||
let _config_guard = crate::tests::write_test_local_config(
|
|
||||||
r#"{"editorApi":{"apiKey":"visual-prompt-test-key"}}"#.to_string(),
|
|
||||||
);
|
|
||||||
let design_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
|
||||||
"design-foundation",
|
|
||||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
|
||||||
);
|
|
||||||
for expected in [
|
|
||||||
"文本策划只是中间结果",
|
|
||||||
"canvas.asset_generate",
|
|
||||||
"16:9",
|
|
||||||
"2K",
|
|
||||||
"assets/ui-prototype.png",
|
|
||||||
"assetKind=ui-prototype",
|
|
||||||
"asset.list",
|
|
||||||
"image.inspect",
|
|
||||||
"ui-prototype.v2",
|
|
||||||
"informationHud",
|
|
||||||
"gameplaySurface",
|
|
||||||
"objectiveEntities",
|
|
||||||
"failureRestartFlow",
|
|
||||||
"responsiveLayout",
|
|
||||||
"不得假设为塔防",
|
|
||||||
"原创标题、实体、资源、目标名称与视觉语言",
|
|
||||||
"assets/art-spec.png",
|
|
||||||
"icon-spec",
|
|
||||||
"referenceImageSrcs 第一项",
|
|
||||||
"POST /api/external/v1/editor/images/generations(kind=ui-design)",
|
|
||||||
"不得误用 POST /api/external/v1/editor/ui-designs/assets/extractions",
|
|
||||||
"canvas.asset_generate 成功只表示候选图片已生成并登记,不等于视觉验收完成",
|
|
||||||
"由 Runtime 在收束门内同时核对固定 owner 文档",
|
|
||||||
"已有同路径画布资产时先核对登记",
|
|
||||||
"检查已通过时不得重复生成或再次扣费",
|
|
||||||
"纯场景图",
|
|
||||||
"不得把计划写完当成 completed",
|
|
||||||
] {
|
|
||||||
assert!(
|
|
||||||
design_prompt.contains(expected),
|
|
||||||
"design visual prompt missing {expected}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let director_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
|
||||||
"art-director",
|
|
||||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
|
||||||
);
|
|
||||||
for expected in [
|
|
||||||
"assets/art-spec.png",
|
|
||||||
"assetKind=icon-spec",
|
|
||||||
"POST /api/external/v1/editor/images/generations(kind=spec)",
|
|
||||||
"后续 UI 和透明图集共同引用",
|
|
||||||
"不得用 generationInputs.artSpec JSON",
|
|
||||||
"成功只表示固定候选已生成并登记,不等于视觉门已经通过",
|
|
||||||
"由 Runtime 在收束时核对当前 revision",
|
|
||||||
"缺少 resourceId 时不得提交最终回复",
|
|
||||||
] {
|
|
||||||
assert!(
|
|
||||||
director_prompt.contains(expected),
|
|
||||||
"art director prompt missing {expected}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let art_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
|
||||||
"art-asset-plan",
|
|
||||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
|
||||||
);
|
|
||||||
for expected in [
|
|
||||||
"资产清单和美术计划只是中间结果",
|
|
||||||
"canvas.asset_generate",
|
|
||||||
"assets/manifest.art.json",
|
|
||||||
"assets/art-spritesheet.png",
|
|
||||||
"固定使用 1:1、1K",
|
|
||||||
"assetKind=art-spritesheet",
|
|
||||||
"用 asset.list 确认 assets/art-spec.png 已登记",
|
|
||||||
"由 Runtime 形成 iconDescriptions",
|
|
||||||
"权威 resourceId 作为 referenceId",
|
|
||||||
"POST /api/external/v1/editor/icon-spritesheets/generations",
|
|
||||||
"screenColor=auto",
|
|
||||||
"不得回退普通生图",
|
|
||||||
"回读 observation 与 asset.list",
|
|
||||||
"真实 alpha",
|
|
||||||
"warning.code=postprocess-failed-source-preserved",
|
|
||||||
"不得登记、验收或自动重试",
|
|
||||||
"仅 sliceWarning",
|
|
||||||
"已有有效同路径资产时不得重复生成或扣费",
|
|
||||||
"asset.list",
|
|
||||||
"不得运行 game.static_smoke 或 preview.validate",
|
|
||||||
"不得编辑 game/index.html",
|
|
||||||
"透明证据不足时不得提交最终回复",
|
|
||||||
] {
|
|
||||||
assert!(
|
|
||||||
art_prompt.contains(expected),
|
|
||||||
"art visual prompt missing {expected}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ordinary_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
|
||||||
"quality-review",
|
|
||||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
|
||||||
);
|
|
||||||
assert!(!ordinary_prompt.contains("assets/ui-prototype.png"));
|
|
||||||
assert!(!ordinary_prompt.contains("assets/art-spritesheet.png"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn visual_prompts_degrade_to_text_contracts_without_editor_api_key() {
|
fn visual_prompts_degrade_to_text_contracts_without_editor_api_key() {
|
||||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||||
|
|||||||
@@ -2330,7 +2330,7 @@ async fn executed_project_verification_audit_failure_requires_reconciliation() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn project_verification_gate_accepts_only_exact_game_static_smoke() {
|
fn project_verification_gate_accepts_only_exact_game_static_smoke() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "精确静态验证项目").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "精确静态验证项目").expect("project init");
|
||||||
advance_project_revision_for_test(&root, "playtest", "playtest-run", "file.write");
|
advance_project_revision_for_test(&root, "playtest", "playtest-run", "file.write");
|
||||||
fs::write(
|
fs::write(
|
||||||
root.join("game/index.html"),
|
root.join("game/index.html"),
|
||||||
@@ -2536,7 +2536,7 @@ fn agent_runtime_project_verify_summary_hashes_command_without_head_or_tail() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn limited_local_command_runs_static_game_smoke_and_writes_log() {
|
fn limited_local_command_runs_static_game_smoke_and_writes_log() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
fs::write(
|
fs::write(
|
||||||
root.join("game/index.html"),
|
root.join("game/index.html"),
|
||||||
fake_llm_game_draft().game_html,
|
fake_llm_game_draft().game_html,
|
||||||
@@ -2866,7 +2866,7 @@ async fn project_verification_runs_without_prepost_and_records_failure_and_timeo
|
|||||||
#[test]
|
#[test]
|
||||||
fn limited_local_command_appends_playtest_to_existing_trace() {
|
fn limited_local_command_appends_playtest_to_existing_trace() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
fs::write(
|
fs::write(
|
||||||
root.join("game/index.html"),
|
root.join("game/index.html"),
|
||||||
fake_llm_game_draft().game_html,
|
fake_llm_game_draft().game_html,
|
||||||
@@ -2915,7 +2915,7 @@ fn limited_local_command_appends_playtest_to_existing_trace() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn limited_local_command_rejects_placeholder_game_smoke() {
|
fn limited_local_command_rejects_placeholder_game_smoke() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
|
|
||||||
let error = run_limited_local_command_at(&root, "game.static_smoke")
|
let error = run_limited_local_command_at(&root, "game.static_smoke")
|
||||||
.expect_err("placeholder game should fail smoke");
|
.expect_err("placeholder game should fail smoke");
|
||||||
@@ -2927,7 +2927,7 @@ fn limited_local_command_rejects_placeholder_game_smoke() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn limited_local_command_rejects_forbidden_runtime_apis() {
|
fn limited_local_command_rejects_forbidden_runtime_apis() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
let html = fake_llm_game_draft()
|
let html = fake_llm_game_draft()
|
||||||
.game_html
|
.game_html
|
||||||
.replace("const marker =", "fetch('/secret');\n const marker =");
|
.replace("const marker =", "fetch('/secret');\n const marker =");
|
||||||
@@ -2943,7 +2943,7 @@ fn limited_local_command_rejects_forbidden_runtime_apis() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn limited_local_command_rejects_blank_canvas_game_smoke() {
|
fn limited_local_command_rejects_blank_canvas_game_smoke() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
let html = r#"<!doctype html>
|
let html = r#"<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<body>
|
<body>
|
||||||
@@ -2970,7 +2970,8 @@ fn limited_local_command_rejects_blank_canvas_game_smoke() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn limited_local_command_rejects_invalid_javascript_before_surface_checks() {
|
fn limited_local_command_rejects_invalid_javascript_before_surface_checks() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "脚本语法优先验证测试").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "脚本语法优先验证测试")
|
||||||
|
.expect("project init");
|
||||||
let html = r#"<!doctype html>
|
let html = r#"<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<body>
|
<body>
|
||||||
@@ -2994,7 +2995,7 @@ fn limited_local_command_rejects_invalid_javascript_before_surface_checks() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn limited_local_command_rejects_token_rich_truncated_script() {
|
fn limited_local_command_rejects_token_rich_truncated_script() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "截断游戏入口测试").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "截断游戏入口测试").expect("project init");
|
||||||
let html = r#"<!doctype html>
|
let html = r#"<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -776,6 +776,7 @@ fn app_config_commands_write_runtime_config_file() {
|
|||||||
.expect("read persisted runtime config");
|
.expect("read persisted runtime config");
|
||||||
assert!(!persisted.contains("editorApi"));
|
assert!(!persisted.contains("editorApi"));
|
||||||
assert!(!persisted.contains("editor-key"));
|
assert!(!persisted.contains("editor-key"));
|
||||||
|
assert!(!root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME).exists());
|
||||||
|
|
||||||
let read_back = read_game_creator_app_config().expect("read runtime config");
|
let read_back = read_game_creator_app_config().expect("read runtime config");
|
||||||
assert_eq!(read_back.config.llm.model, "runtime-model");
|
assert_eq!(read_back.config.llm.model, "runtime-model");
|
||||||
@@ -807,6 +808,99 @@ fn app_config_commands_write_runtime_config_file() {
|
|||||||
fs::remove_dir_all(root).expect("cleanup runtime config dir");
|
fs::remove_dir_all(root).expect("cleanup runtime config dir");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn app_config_batch_restores_main_when_overlay_write_fails() {
|
||||||
|
for main_exists in [false, true] {
|
||||||
|
let root = unique_project_path();
|
||||||
|
fs::create_dir_all(&root).expect("config dir");
|
||||||
|
let main = root.join(GAME_CREATOR_CONFIG_FILE_NAME);
|
||||||
|
let overlay = root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME);
|
||||||
|
let original = "{\"llm\":{\"stream\":false}}\n";
|
||||||
|
if main_exists {
|
||||||
|
fs::write(&main, original).expect("main config");
|
||||||
|
}
|
||||||
|
fs::write(&overlay, original).expect("overlay config");
|
||||||
|
// 普通目录占据备份路径,让覆盖文件在替换前失败。
|
||||||
|
fs::create_dir(root.join(format!(".{}.previous", GAME_CREATOR_LOCAL_CONFIG_FILE_NAME)))
|
||||||
|
.expect("block overlay replacement");
|
||||||
|
crate::config::write_game_creator_config_batch(&[
|
||||||
|
(main.clone(), "{\"llm\":{\"stream\":true}}\n".to_string()),
|
||||||
|
(overlay.clone(), "{\"llm\":{\"stream\":true}}\n".to_string()),
|
||||||
|
])
|
||||||
|
.expect_err("overlay replacement must fail");
|
||||||
|
if main_exists {
|
||||||
|
assert_eq!(fs::read_to_string(&main).expect("restored main"), original);
|
||||||
|
} else {
|
||||||
|
assert!(!main.exists());
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(&overlay).expect("unchanged overlay"),
|
||||||
|
original
|
||||||
|
);
|
||||||
|
fs::remove_dir_all(root).expect("cleanup config dir");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn app_config_save_updates_conflicting_local_overlay() {
|
||||||
|
let root = unique_project_path();
|
||||||
|
fs::create_dir_all(&root).expect("config dir");
|
||||||
|
let _guard = use_test_runtime_config_dir(root.clone());
|
||||||
|
let overlay_path = root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME);
|
||||||
|
fs::write(
|
||||||
|
&overlay_path,
|
||||||
|
r#"{"llm":{"reasoningEffort":"low"},"selectedModelId":"existing","selectedModelIsDefault":true,"custom":{"keep":true}}"#,
|
||||||
|
)
|
||||||
|
.expect("write overlay");
|
||||||
|
let mut config = load_game_creator_app_config().expect("load config");
|
||||||
|
config.llm.reasoning_effort = "high".to_string();
|
||||||
|
config.selected_model_is_default = false;
|
||||||
|
let saved = write_game_creator_app_config(config).expect("save config");
|
||||||
|
let effective = load_game_creator_app_config().expect("effective config");
|
||||||
|
assert_eq!(saved.config.llm.reasoning_effort, "high");
|
||||||
|
assert_eq!(effective.llm.reasoning_effort, "high");
|
||||||
|
assert!(saved.config.selected_model_is_default);
|
||||||
|
assert!(effective.selected_model_is_default);
|
||||||
|
let overlay: serde_json::Value =
|
||||||
|
serde_json::from_str(&fs::read_to_string(&overlay_path).expect("read overlay"))
|
||||||
|
.expect("parse overlay");
|
||||||
|
assert_eq!(overlay["selectedModelId"], "existing");
|
||||||
|
assert_eq!(overlay["selectedModelIsDefault"], true);
|
||||||
|
assert_eq!(
|
||||||
|
overlay["llm"],
|
||||||
|
serde_json::json!({"reasoningEffort": "high"})
|
||||||
|
);
|
||||||
|
assert_eq!(overlay["custom"]["keep"], true);
|
||||||
|
fs::remove_dir_all(root).expect("cleanup config dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn app_config_model_selection_only_updates_model_overlay() {
|
||||||
|
let root = unique_project_path();
|
||||||
|
fs::create_dir_all(&root).expect("config dir");
|
||||||
|
let _guard = use_test_runtime_config_dir(root.clone());
|
||||||
|
let overlay_path = root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME);
|
||||||
|
fs::write(
|
||||||
|
&overlay_path,
|
||||||
|
r#"{"selectedModelId":"old","selectedModelIsDefault":false,"llm":{"reasoningEffort":"low"}}"#,
|
||||||
|
)
|
||||||
|
.expect("write overlay");
|
||||||
|
let saved = select_game_creator_model("new-model".to_string(), true).expect("select model");
|
||||||
|
let effective = load_game_creator_app_config().expect("effective config");
|
||||||
|
assert_eq!(saved.config.selected_model_id, "new-model");
|
||||||
|
assert_eq!(effective.selected_model_id, "new-model");
|
||||||
|
assert!(saved.config.selected_model_is_default);
|
||||||
|
assert!(effective.selected_model_is_default);
|
||||||
|
let overlay: serde_json::Value =
|
||||||
|
serde_json::from_str(&fs::read_to_string(&overlay_path).expect("read overlay"))
|
||||||
|
.expect("parse overlay");
|
||||||
|
assert_eq!(
|
||||||
|
overlay["llm"],
|
||||||
|
serde_json::json!({"reasoningEffort": "low"})
|
||||||
|
);
|
||||||
|
fs::remove_dir_all(root).expect("cleanup config dir");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn model_selection_default_flag_round_trips() {
|
fn model_selection_default_flag_round_trips() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
|
|||||||
@@ -1944,7 +1944,7 @@ fn validate_llm_game_draft_accepts_win_condition_as_goal() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn agent_run_resume_restarts_generation_from_latest_goal() {
|
async fn agent_run_resume_restarts_generation_from_latest_goal() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
write_agent_run_trace(
|
write_agent_run_trace(
|
||||||
&root,
|
&root,
|
||||||
"run-control-resume",
|
"run-control-resume",
|
||||||
|
|||||||
@@ -299,6 +299,22 @@ impl Drop for TestRuntimeConfigDirGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_existing_html_project_at(
|
||||||
|
root: &Path,
|
||||||
|
project_id: &str,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<InitLocalProjectResult, String> {
|
||||||
|
// 已有单 HTML 项目在初始化前就有入口,不能使用新建 npm 项目的脚手架。
|
||||||
|
let game_dir = root.join("game");
|
||||||
|
fs::create_dir_all(&game_dir).expect("create existing HTML fixture directory");
|
||||||
|
fs::write(
|
||||||
|
game_dir.join("index.html"),
|
||||||
|
"<!doctype html><html lang=\"zh-CN\"><meta charset=\"UTF-8\"><body>还没有生成游戏</body></html>",
|
||||||
|
)
|
||||||
|
.expect("write existing HTML fixture entry");
|
||||||
|
init_local_game_project_at(root, project_id, name)
|
||||||
|
}
|
||||||
|
|
||||||
fn unique_project_path() -> PathBuf {
|
fn unique_project_path() -> PathBuf {
|
||||||
let millis = SystemTime::now()
|
let millis = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -3706,52 +3722,6 @@ fn ui_prototype_assessment_fixture(passed: bool) -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_ui_prototype_inspection_fixture(root: &Path, run_id: &str, passed: bool) {
|
|
||||||
append_ui_prototype_inspection_fixture_with_profile(
|
|
||||||
root,
|
|
||||||
run_id,
|
|
||||||
passed,
|
|
||||||
AGENT_RUNTIME_UI_PROTOTYPE_VALIDATION_PROFILE,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_ui_prototype_inspection_fixture_with_profile(
|
|
||||||
root: &Path,
|
|
||||||
run_id: &str,
|
|
||||||
passed: bool,
|
|
||||||
validation_profile: &str,
|
|
||||||
) {
|
|
||||||
let image_bytes =
|
|
||||||
fs::read(root.join(AGENT_RUNTIME_UI_PROTOTYPE_PATH)).expect("read UI prototype fixture");
|
|
||||||
let image_sha256 = format!("{:x}", Sha256::digest(&image_bytes));
|
|
||||||
let issues = if passed {
|
|
||||||
Vec::<String>::new()
|
|
||||||
} else {
|
|
||||||
vec!["只有战场场景和来袭箭头,缺少资源栏、单位卡槽、波次状态与主要控件".to_string()]
|
|
||||||
};
|
|
||||||
append_agent_db_record(
|
|
||||||
root,
|
|
||||||
serde_json::json!({
|
|
||||||
"recordType": "agent.runtime.image.inspect",
|
|
||||||
"agentId": "design-foundation",
|
|
||||||
"runId": run_id,
|
|
||||||
"images": [{
|
|
||||||
"path": AGENT_RUNTIME_UI_PROTOTYPE_PATH,
|
|
||||||
"sha256": image_sha256,
|
|
||||||
"bytes": image_bytes.len(),
|
|
||||||
}],
|
|
||||||
"responseId": "resp_ui_prototype_fixture",
|
|
||||||
"conclusionChars": 20,
|
|
||||||
"inspectionKind": AGENT_RUNTIME_UI_PROTOTYPE_INSPECTION_KIND,
|
|
||||||
"validationProfile": validation_profile,
|
|
||||||
"passed": passed,
|
|
||||||
"checks": ui_prototype_checks_fixture(passed),
|
|
||||||
"issues": issues,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.expect("append UI prototype inspection fixture");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supervisor_collaboration_delegate_action_for_test(
|
fn supervisor_collaboration_delegate_action_for_test(
|
||||||
agent_id: &str,
|
agent_id: &str,
|
||||||
repair_of_delegation_id: Option<&str>,
|
repair_of_delegation_id: Option<&str>,
|
||||||
|
|||||||
@@ -550,7 +550,7 @@ fn canonical_visual_completion_requires_persisted_route_kind_and_current_spec_re
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn background_agent_runtime_can_start_local_preview() {
|
async fn background_agent_runtime_can_start_local_preview() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||||
fs::write(
|
fs::write(
|
||||||
root.join("game/index.html"),
|
root.join("game/index.html"),
|
||||||
fake_llm_game_draft().game_html,
|
fake_llm_game_draft().game_html,
|
||||||
@@ -1289,6 +1289,7 @@ async fn platform_art_external_request_does_not_hold_project_lock_or_overwrite_m
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn generate_local_game_draft_fails_after_max_passes_without_final_artifacts() {
|
async fn generate_local_game_draft_fails_after_max_passes_without_final_artifacts() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
|
init_existing_html_project_at(&root, "project-1", "HTML fixture").expect("project init");
|
||||||
let mut invalid_draft = fake_llm_game_draft();
|
let mut invalid_draft = fake_llm_game_draft();
|
||||||
invalid_draft.game_html = r#"<!doctype html>
|
invalid_draft.game_html = r#"<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
@@ -2590,7 +2591,7 @@ fn local_project_file_commands_read_write_list_and_delete_text_files() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn local_project_export_package_uses_runtime_whitelist_and_records() {
|
fn local_project_export_package_uses_runtime_whitelist_and_records() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html)
|
write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html)
|
||||||
.expect("write playable html");
|
.expect("write playable html");
|
||||||
write_local_project_file_at(&root, "assets/hero.txt", "hero asset").expect("write asset");
|
write_local_project_file_at(&root, "assets/hero.txt", "hero asset").expect("write asset");
|
||||||
@@ -2709,7 +2710,7 @@ fn local_project_export_package_list_skips_symlink_packages() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn local_project_export_package_rejects_symlink_assets() {
|
fn local_project_export_package_rejects_symlink_assets() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html)
|
write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html)
|
||||||
.expect("write playable html");
|
.expect("write playable html");
|
||||||
write_local_project_file_at(&root, "exports/README.md", "playtest notes")
|
write_local_project_file_at(&root, "exports/README.md", "playtest notes")
|
||||||
@@ -2731,7 +2732,7 @@ fn local_project_export_package_rejects_symlink_assets() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn local_project_export_package_rejects_symlink_runtime_dirs_and_readme() {
|
fn local_project_export_package_rejects_symlink_runtime_dirs_and_readme() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html)
|
write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html)
|
||||||
.expect("write playable html");
|
.expect("write playable html");
|
||||||
write_local_project_file_at(&root, "memory/project.md", "private memory")
|
write_local_project_file_at(&root, "memory/project.md", "private memory")
|
||||||
@@ -2763,7 +2764,7 @@ fn local_project_export_package_rejects_symlink_runtime_dirs_and_readme() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn local_project_export_package_requires_playable_html_and_readme() {
|
fn local_project_export_package_requires_playable_html_and_readme() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
let missing_readme =
|
let missing_readme =
|
||||||
export_local_project_package_at(&root).expect_err("default html is not playable");
|
export_local_project_package_at(&root).expect_err("default html is not playable");
|
||||||
assert!(missing_readme.contains("游戏入口必须包含可渲染画布"));
|
assert!(missing_readme.contains("游戏入口必须包含可渲染画布"));
|
||||||
@@ -3640,7 +3641,7 @@ fn cli_agent_resume_without_appdata_fails_before_runtime_dispatch() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn local_preview_server_serves_game_index() {
|
fn local_preview_server_serves_game_index() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
fs::write(root.join("assets/player.png"), b"PNGDATA").expect("asset");
|
fs::write(root.join("assets/player.png"), b"PNGDATA").expect("asset");
|
||||||
|
|
||||||
let (preview, stop) = start_local_game_preview_for_project(&root).expect("preview start");
|
let (preview, stop) = start_local_game_preview_for_project(&root).expect("preview start");
|
||||||
@@ -4619,7 +4620,7 @@ fn local_preview_bridge_injection_stays_outside_unclosed_inert_html_contexts() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn local_preview_server_drains_split_browser_headers_before_response() {
|
fn local_preview_server_drains_split_browser_headers_before_response() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "鍍忕礌鍔ㄤ綔鍘熷瀷").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "鍍忕礌鍔ㄤ綔鍘熷瀷").expect("project init");
|
||||||
|
|
||||||
let (preview, stop) = start_local_game_preview_for_project(&root).expect("preview start");
|
let (preview, stop) = start_local_game_preview_for_project(&root).expect("preview start");
|
||||||
let mut stream = TcpStream::connect(("127.0.0.1", preview.port)).expect("preview connect");
|
let mut stream = TcpStream::connect(("127.0.0.1", preview.port)).expect("preview connect");
|
||||||
@@ -4699,7 +4700,7 @@ fn preview_content_type_covers_common_game_assets() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn local_preview_head_preserves_asset_content_length() {
|
fn local_preview_head_preserves_asset_content_length() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||||
fs::write(root.join("assets/player.png"), b"PNGDATA").expect("asset");
|
fs::write(root.join("assets/player.png"), b"PNGDATA").expect("asset");
|
||||||
|
|
||||||
let response = build_preview_response(&root, "HEAD", "/assets/player.png");
|
let response = build_preview_response(&root, "HEAD", "/assets/player.png");
|
||||||
@@ -4741,7 +4742,7 @@ fn local_preview_project_revision_reports_the_current_atomic_sidecar() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn local_preview_start_rejects_a_stale_validated_revision_atomically() {
|
fn local_preview_start_rejects_a_stale_validated_revision_atomically() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "预览 revision 启动门禁测试")
|
init_existing_html_project_at(&root, "project-1", "预览 revision 启动门禁测试")
|
||||||
.expect("project init");
|
.expect("project init");
|
||||||
let revision = advance_project_revision_for_test(
|
let revision = advance_project_revision_for_test(
|
||||||
&root,
|
&root,
|
||||||
@@ -4768,7 +4769,7 @@ fn local_preview_start_rejects_a_stale_validated_revision_atomically() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn stale_preview_cleanup_does_not_stop_a_newer_matching_project_server() {
|
fn stale_preview_cleanup_does_not_stop_a_newer_matching_project_server() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "预览原子停止测试").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "预览原子停止测试").expect("project init");
|
||||||
let registry = PreviewRegistry::default();
|
let registry = PreviewRegistry::default();
|
||||||
let (first, first_stop) =
|
let (first, first_stop) =
|
||||||
start_local_game_preview_for_project(&root).expect("first preview start");
|
start_local_game_preview_for_project(&root).expect("first preview start");
|
||||||
@@ -4799,7 +4800,8 @@ fn stale_preview_cleanup_does_not_stop_a_newer_matching_project_server() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn stale_preview_cleanup_cannot_be_blocked_by_project_stop_policy() {
|
fn stale_preview_cleanup_cannot_be_blocked_by_project_stop_policy() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "预览补偿清理策略测试").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "预览补偿清理策略测试")
|
||||||
|
.expect("project init");
|
||||||
write_project_permission_policy_at(
|
write_project_permission_policy_at(
|
||||||
&root,
|
&root,
|
||||||
ProjectPermissionPolicy {
|
ProjectPermissionPolicy {
|
||||||
@@ -4826,6 +4828,7 @@ fn stale_preview_cleanup_cannot_be_blocked_by_project_stop_policy() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn local_preview_serves_generated_playable_game() {
|
fn local_preview_serves_generated_playable_game() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
|
init_existing_html_project_at(&root, "project-1", "HTML fixture").expect("project init");
|
||||||
write_local_game_draft_at(&root, "像素风横版动作", &fake_llm_game_draft())
|
write_local_game_draft_at(&root, "像素风横版动作", &fake_llm_game_draft())
|
||||||
.expect("draft should generate");
|
.expect("draft should generate");
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ async fn request_llm_game_draft_uses_openai_compatible_provider_output() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn generate_local_game_draft_sends_asset_context_to_llm() {
|
async fn generate_local_game_draft_sends_asset_context_to_llm() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
|
init_existing_html_project_at(&root, "project-1", "HTML fixture").expect("project init");
|
||||||
let uploaded = upload_local_asset_at(&root, "../角色.png", "image/png", b"fake-png")
|
let uploaded = upload_local_asset_at(&root, "../角色.png", "image/png", b"fake-png")
|
||||||
.expect("asset upload");
|
.expect("asset upload");
|
||||||
append_local_conversation_message_at(
|
append_local_conversation_message_at(
|
||||||
@@ -1344,7 +1345,7 @@ async fn provider_action_batch_agent_db_failure_does_not_advance_cursor() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn provider_action_batch_mutation_then_verification_rolls_forward_gate() {
|
async fn provider_action_batch_mutation_then_verification_rolls_forward_gate() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(
|
init_existing_html_project_at(
|
||||||
&root,
|
&root,
|
||||||
"project-provider-batch-mutation-verification",
|
"project-provider-batch-mutation-verification",
|
||||||
"Provider 批次修改后验证测试",
|
"Provider 批次修改后验证测试",
|
||||||
@@ -5864,7 +5865,7 @@ async fn background_agent_runtime_cancellation_wins_over_inflight_llm_error() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn agent_loop_uses_per_agent_llm_overrides() {
|
async fn agent_loop_uses_per_agent_llm_overrides() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "local-project-draft", "未命名游戏原型")
|
init_existing_html_project_at(&root, "local-project-draft", "未命名游戏原型")
|
||||||
.expect("init project");
|
.expect("init project");
|
||||||
let (planner_sender, planner_receiver) = mpsc::channel();
|
let (planner_sender, planner_receiver) = mpsc::channel();
|
||||||
let planner_base_url = spawn_mock_llm_server_responses_with_capture(
|
let planner_base_url = spawn_mock_llm_server_responses_with_capture(
|
||||||
@@ -6081,7 +6082,7 @@ async fn agent_role_briefs_run_same_wave_llm_agents_in_parallel() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn agent_loop_writes_spec_findings_and_retries_generator() {
|
async fn agent_loop_writes_spec_findings_and_retries_generator() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||||
let mut first_draft = fake_llm_game_draft();
|
let mut first_draft = fake_llm_game_draft();
|
||||||
first_draft.game_html = r#"<!doctype html>
|
first_draft.game_html = r#"<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|||||||
@@ -980,180 +980,6 @@ async fn provider_retry_final_reply_thinking_only_response_retries_before_handof
|
|||||||
fs::remove_dir_all(config_dir).ok();
|
fs::remove_dir_all(config_dir).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn visual_specialist_finalization_requires_existing_registered_canvas_image() {
|
|
||||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
|
||||||
"visual-finalization-test-user",
|
|
||||||
"visual-finalization-test-key",
|
|
||||||
"https://dev.genarrative.world",
|
|
||||||
);
|
|
||||||
let _config_guard = crate::tests::write_test_local_config(
|
|
||||||
r#"{"editorApi":{"apiKey":"visual-finalization-test-key"}}"#.to_string(),
|
|
||||||
);
|
|
||||||
for (agent_id, local_path, kind) in [
|
|
||||||
(
|
|
||||||
"design-foundation",
|
|
||||||
"assets/ui-prototype.png",
|
|
||||||
"ui-prototype",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"art-asset-plan",
|
|
||||||
"assets/art-spritesheet.png",
|
|
||||||
"art-spritesheet",
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
let root = unique_project_path();
|
|
||||||
init_local_game_project_at(&root, "project-1", "视觉 Runtime 完成门禁测试")
|
|
||||||
.expect("project init");
|
|
||||||
register_canvas_visual_asset_fixture(&root, "assets/art-spec.png", "icon-spec");
|
|
||||||
let run_id = format!("visual-finalization-{agent_id}");
|
|
||||||
let state = start_game_creator_agent_runtime_task_at(
|
|
||||||
&root,
|
|
||||||
agent_id,
|
|
||||||
"必须交付真实图片",
|
|
||||||
&run_id,
|
|
||||||
"agent-background-task",
|
|
||||||
"准备完成",
|
|
||||||
vec!["生成并登记图片".to_string()],
|
|
||||||
)
|
|
||||||
.expect("start visual runtime");
|
|
||||||
let revision = read_game_creator_agent_runtime_project_revision(&root)
|
|
||||||
.expect("read visual revision")
|
|
||||||
.revision;
|
|
||||||
|
|
||||||
let missing_file = finish_game_creator_agent_background_runtime_turn_at(
|
|
||||||
&root,
|
|
||||||
state.clone(),
|
|
||||||
"不能在缺图时完成",
|
|
||||||
revision,
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.expect("missing image is recoverable blocker");
|
|
||||||
let blocker = match missing_file {
|
|
||||||
AgentBackgroundFinalizationOutcome::Stale(blocker) => blocker,
|
|
||||||
_ => panic!("missing image must block finalization"),
|
|
||||||
};
|
|
||||||
assert_eq!(blocker.tool, "runtime.visual_asset");
|
|
||||||
assert!(blocker.summary.contains("不能完成任务"));
|
|
||||||
assert!(blocker
|
|
||||||
.detail
|
|
||||||
.as_deref()
|
|
||||||
.is_some_and(|detail| detail.contains(local_path)));
|
|
||||||
|
|
||||||
let absolute_path = root.join(local_path);
|
|
||||||
fs::create_dir_all(absolute_path.parent().expect("visual parent"))
|
|
||||||
.expect("create unregistered visual directory");
|
|
||||||
fs::write(
|
|
||||||
&absolute_path,
|
|
||||||
if kind == "art-spritesheet" {
|
|
||||||
transparent_test_png_bytes()
|
|
||||||
} else {
|
|
||||||
valid_test_png_bytes()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("write unregistered visual image");
|
|
||||||
let unregistered = finish_game_creator_agent_background_runtime_turn_at(
|
|
||||||
&root,
|
|
||||||
state.clone(),
|
|
||||||
"不能在图片未登记时完成",
|
|
||||||
revision,
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.expect("unregistered image is recoverable blocker");
|
|
||||||
assert!(matches!(
|
|
||||||
unregistered,
|
|
||||||
AgentBackgroundFinalizationOutcome::Stale(ref blocker)
|
|
||||||
if blocker.tool == "runtime.visual_asset"
|
|
||||||
));
|
|
||||||
|
|
||||||
register_local_asset_at(
|
|
||||||
&root,
|
|
||||||
local_path,
|
|
||||||
kind,
|
|
||||||
"image/png",
|
|
||||||
"canvas",
|
|
||||||
GameCreationAppAssetSource {
|
|
||||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
|
||||||
canvas_project_id: Some("canvas-project-1".to_string()),
|
|
||||||
resource_id: Some(format!("resource-{kind}")),
|
|
||||||
asset_object_id: Some(format!("asset-object-{kind}")),
|
|
||||||
task_id: Some(format!("task-{kind}")),
|
|
||||||
prompt: Some("测试视觉资产".to_string()),
|
|
||||||
model: Some("gpt-image-2".to_string()),
|
|
||||||
generation_route: None,
|
|
||||||
generation_kind: None,
|
|
||||||
reference_resource_ids: Vec::new(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("register legacy canvas image");
|
|
||||||
let legacy = finish_game_creator_agent_background_runtime_turn_at(
|
|
||||||
&root,
|
|
||||||
state.clone(),
|
|
||||||
"旧文件不能冒充正式视觉产物",
|
|
||||||
revision,
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.expect("legacy image is a recoverable blocker");
|
|
||||||
assert!(matches!(
|
|
||||||
legacy,
|
|
||||||
AgentBackgroundFinalizationOutcome::Stale(ref blocker)
|
|
||||||
if blocker.tool == "runtime.visual_asset"
|
|
||||||
&& blocker.detail.as_deref().is_some_and(|detail| detail.contains("legacy"))
|
|
||||||
));
|
|
||||||
register_local_asset_at(
|
|
||||||
&root,
|
|
||||||
local_path,
|
|
||||||
kind,
|
|
||||||
"image/png",
|
|
||||||
"canvas",
|
|
||||||
GameCreationAppAssetSource {
|
|
||||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
|
||||||
canvas_project_id: Some("canvas-project-1".to_string()),
|
|
||||||
resource_id: Some(format!("resource-{kind}")),
|
|
||||||
asset_object_id: Some(format!("asset-object-{kind}")),
|
|
||||||
task_id: Some(format!("task-{kind}")),
|
|
||||||
prompt: Some("测试视觉资产".to_string()),
|
|
||||||
model: Some("gpt-image-2".to_string()),
|
|
||||||
generation_route: Some(
|
|
||||||
if kind == "ui-prototype" {
|
|
||||||
"/api/external/v1/editor/images/generations"
|
|
||||||
} else {
|
|
||||||
"/api/external/v1/editor/icon-spritesheets/generations"
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
generation_kind: Some(
|
|
||||||
if kind == "ui-prototype" {
|
|
||||||
"ui-design"
|
|
||||||
} else {
|
|
||||||
"icon-spritesheet"
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
reference_resource_ids: vec!["resource-icon-spec".to_string()],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("register provenanced canvas image");
|
|
||||||
if agent_id == "design-foundation" {
|
|
||||||
append_ui_prototype_inspection_fixture(&root, &run_id, true);
|
|
||||||
}
|
|
||||||
let completed = finish_game_creator_agent_background_runtime_turn_at(
|
|
||||||
&root,
|
|
||||||
state,
|
|
||||||
"真实图片已经生成并登记。",
|
|
||||||
revision,
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.expect("registered image allows finalization");
|
|
||||||
assert!(matches!(
|
|
||||||
completed,
|
|
||||||
AgentBackgroundFinalizationOutcome::Completed(_)
|
|
||||||
));
|
|
||||||
|
|
||||||
fs::remove_dir_all(root).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn background_finalization_rechecks_stale_credential_before_assistant_persistence() {
|
fn background_finalization_rechecks_stale_credential_before_assistant_persistence() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::support::*;
|
use super::support::*;
|
||||||
|
use crate::tests::init_existing_html_project_at;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn agent_runtime_agent_message_uses_durable_semantic_idempotency() {
|
fn agent_runtime_agent_message_uses_durable_semantic_idempotency() {
|
||||||
@@ -539,7 +540,7 @@ async fn background_agent_runtime_can_read_other_agent_status() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn background_agent_runtime_can_run_limited_static_smoke() {
|
async fn background_agent_runtime_can_run_limited_static_smoke() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
init_existing_html_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||||
let playable_game_html = fake_llm_game_draft().game_html;
|
let playable_game_html = fake_llm_game_draft().game_html;
|
||||||
write_project_permission_policy_at(
|
write_project_permission_policy_at(
|
||||||
&root,
|
&root,
|
||||||
@@ -1549,49 +1550,6 @@ async fn background_agent_runtime_deletes_file_then_verifies_before_completion()
|
|||||||
fs::remove_dir_all(root).ok();
|
fs::remove_dir_all(root).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn art_asset_plan_cannot_delete_registered_fixed_visual_asset() {
|
|
||||||
let root = unique_project_path();
|
|
||||||
init_local_game_project_at(&root, "project-1", "固定美术资产删除门禁").expect("project init");
|
|
||||||
register_canvas_visual_asset_fixture(&root, "assets/art-spritesheet.png", "art-spritesheet");
|
|
||||||
write_project_permission_policy_at(
|
|
||||||
&root,
|
|
||||||
ProjectPermissionPolicy {
|
|
||||||
denied_commands: Vec::new(),
|
|
||||||
confirm_commands: Vec::new(),
|
|
||||||
agent_policies: BTreeMap::new(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("allow delete tools");
|
|
||||||
|
|
||||||
let action = AgentRuntimeToolAction {
|
|
||||||
tool: "file.delete".to_string(),
|
|
||||||
reason: Some("尝试删除已登记的固定美术素材".to_string()),
|
|
||||||
input: serde_json::json!({ "path": "assets/art-spritesheet.png" }),
|
|
||||||
};
|
|
||||||
let observation = execute_game_creator_agent_runtime_tool_action_with_action_id(
|
|
||||||
&root,
|
|
||||||
"art-asset-plan",
|
|
||||||
"art-delete-protection-run",
|
|
||||||
"已登记的固定美术素材必须复用",
|
|
||||||
&action,
|
|
||||||
Some("art-delete-protection-action"),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(observation.status, "blocked");
|
|
||||||
assert!(observation.summary.contains("禁止删除固定正式产物"));
|
|
||||||
assert!(root.join("assets/art-spritesheet.png").is_file());
|
|
||||||
assert_eq!(
|
|
||||||
read_game_creator_agent_runtime_project_revision(&root)
|
|
||||||
.expect("read revision")
|
|
||||||
.revision,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
fs::remove_dir_all(root).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn design_foundation_file_tools_cannot_modify_the_game_entry() {
|
async fn design_foundation_file_tools_cannot_modify_the_game_entry() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
|
|||||||
@@ -3989,137 +3989,6 @@ async fn background_task_recovers_when_assistant_message_cannot_persist() {
|
|||||||
fs::remove_dir_all(root).ok();
|
fs::remove_dir_all(root).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn task_update_requires_registered_visual_asset_before_completion() {
|
|
||||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
|
||||||
"visual-task-update-test-user",
|
|
||||||
"visual-task-update-test-key",
|
|
||||||
"https://dev.genarrative.world",
|
|
||||||
);
|
|
||||||
let _config_guard = crate::tests::write_test_local_config(
|
|
||||||
r#"{"editorApi":{"apiKey":"visual-task-update-test-key"}}"#.to_string(),
|
|
||||||
);
|
|
||||||
for (task_id, local_path, kind, missing_summary) in [
|
|
||||||
(
|
|
||||||
"art-director",
|
|
||||||
"assets/art-spec.png",
|
|
||||||
"icon-spec",
|
|
||||||
"统一视觉规范图尚未按正式视觉流程生成并登记",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"design-foundation",
|
|
||||||
"assets/ui-prototype.png",
|
|
||||||
"ui-prototype",
|
|
||||||
"策划界面原型图尚未按正式视觉流程生成并登记",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"art-asset-plan",
|
|
||||||
"assets/art-spritesheet.png",
|
|
||||||
"art-spritesheet",
|
|
||||||
"首版美术素材图尚未按正式视觉流程生成并登记",
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
let root = unique_project_path();
|
|
||||||
init_local_game_project_at(&root, "project-1", "视觉任务完成门禁测试")
|
|
||||||
.expect("project init");
|
|
||||||
write_project_permission_policy_at(
|
|
||||||
&root,
|
|
||||||
ProjectPermissionPolicy {
|
|
||||||
denied_commands: Vec::new(),
|
|
||||||
confirm_commands: Vec::new(),
|
|
||||||
agent_policies: BTreeMap::new(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("allow task update");
|
|
||||||
let run_id = format!("visual-task-update-{task_id}");
|
|
||||||
let action = AgentRuntimeToolAction {
|
|
||||||
tool: "task.update".to_string(),
|
|
||||||
reason: Some("标记视觉任务完成".to_string()),
|
|
||||||
input: serde_json::json!({
|
|
||||||
"taskId": task_id,
|
|
||||||
"status": "completed"
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
let missing = execute_game_creator_agent_runtime_tool_action_with_action_id(
|
|
||||||
&root,
|
|
||||||
task_id,
|
|
||||||
&run_id,
|
|
||||||
"完成视觉任务",
|
|
||||||
&action,
|
|
||||||
Some("visual-task-update-missing"),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(missing.status, "failed");
|
|
||||||
assert!(missing.summary.contains(missing_summary));
|
|
||||||
let manifest = read_manifest_for_project(&root).expect("manifest after rejected update");
|
|
||||||
assert_eq!(
|
|
||||||
manifest
|
|
||||||
.tasks
|
|
||||||
.iter()
|
|
||||||
.find(|task| task.id == task_id)
|
|
||||||
.expect("visual task")
|
|
||||||
.status,
|
|
||||||
GameCreationAppTaskStatus::Pending
|
|
||||||
);
|
|
||||||
|
|
||||||
if task_id != "art-director" {
|
|
||||||
register_canvas_visual_asset_fixture(&root, "assets/art-spec.png", "icon-spec");
|
|
||||||
}
|
|
||||||
if task_id == "art-asset-plan" {
|
|
||||||
register_canvas_visual_asset_fixture(&root, "assets/ui-prototype.png", "ui-prototype");
|
|
||||||
}
|
|
||||||
register_canvas_visual_asset_fixture(&root, local_path, kind);
|
|
||||||
if task_id == "design-foundation" {
|
|
||||||
append_ui_prototype_inspection_fixture(&root, &run_id, false);
|
|
||||||
let scene_rejected = execute_game_creator_agent_runtime_tool_action_with_action_id(
|
|
||||||
&root,
|
|
||||||
task_id,
|
|
||||||
&run_id,
|
|
||||||
"拒绝用场景图完成 UI 原型任务",
|
|
||||||
&action,
|
|
||||||
Some("visual_task_update_scene_rejected"),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(scene_rejected.status, "failed");
|
|
||||||
assert!(scene_rejected.summary.contains("结构化 UI 视觉检查"));
|
|
||||||
let manifest =
|
|
||||||
read_manifest_for_project(&root).expect("manifest after rejected scene image");
|
|
||||||
assert_eq!(
|
|
||||||
manifest
|
|
||||||
.tasks
|
|
||||||
.iter()
|
|
||||||
.find(|task| task.id == task_id)
|
|
||||||
.expect("design visual task")
|
|
||||||
.status,
|
|
||||||
GameCreationAppTaskStatus::Pending
|
|
||||||
);
|
|
||||||
append_ui_prototype_inspection_fixture(&root, &run_id, true);
|
|
||||||
}
|
|
||||||
let completed = execute_game_creator_agent_runtime_tool_action_with_action_id(
|
|
||||||
&root,
|
|
||||||
task_id,
|
|
||||||
&run_id,
|
|
||||||
"完成视觉任务",
|
|
||||||
&action,
|
|
||||||
Some("visual-task-update-completed"),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(completed.status, "ok", "{completed:?}");
|
|
||||||
let manifest = read_manifest_for_project(&root).expect("manifest after visual completion");
|
|
||||||
assert_eq!(
|
|
||||||
manifest
|
|
||||||
.tasks
|
|
||||||
.iter()
|
|
||||||
.find(|task| task.id == task_id)
|
|
||||||
.expect("visual task")
|
|
||||||
.status,
|
|
||||||
GameCreationAppTaskStatus::Completed
|
|
||||||
);
|
|
||||||
|
|
||||||
fs::remove_dir_all(root).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn schedule_ready_manifest_tasks_command_requires_auto_policy() {
|
fn schedule_ready_manifest_tasks_command_requires_auto_policy() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
|
|||||||
@@ -675,6 +675,7 @@ async fn background_agent_runtime_file_and_memory_writes_respect_project_policy(
|
|||||||
#[test]
|
#[test]
|
||||||
fn generate_local_game_draft_writes_memory_design_and_game() {
|
fn generate_local_game_draft_writes_memory_design_and_game() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
|
init_existing_html_project_at(&root, "project-1", "HTML fixture").expect("project init");
|
||||||
let draft = fake_llm_game_draft();
|
let draft = fake_llm_game_draft();
|
||||||
let result = write_local_game_draft_at(&root, "像素风横版动作 </script><script>", &draft)
|
let result = write_local_game_draft_at(&root, "像素风横版动作 </script><script>", &draft)
|
||||||
.expect("draft should generate");
|
.expect("draft should generate");
|
||||||
@@ -761,6 +762,7 @@ fn generate_local_game_draft_writes_memory_design_and_game() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn generate_local_game_draft_appends_short_and_long_memory() {
|
fn generate_local_game_draft_appends_short_and_long_memory() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
|
init_existing_html_project_at(&root, "project-1", "HTML fixture").expect("project init");
|
||||||
let draft = fake_llm_game_draft();
|
let draft = fake_llm_game_draft();
|
||||||
|
|
||||||
write_local_game_draft_at(&root, "第一版横版动作", &draft).expect("first draft");
|
write_local_game_draft_at(&root, "第一版横版动作", &draft).expect("first draft");
|
||||||
|
|||||||
@@ -11091,7 +11091,6 @@ export function App({
|
|||||||
{runtimeConfigOpen ? (
|
{runtimeConfigOpen ? (
|
||||||
<RuntimeConfigDialog
|
<RuntimeConfigDialog
|
||||||
projectPath={localProject?.projectPath}
|
projectPath={localProject?.projectPath}
|
||||||
llmConfigStatus={llmConfigStatus}
|
|
||||||
onClose={() => setRuntimeConfigOpen(false)}
|
onClose={() => setRuntimeConfigOpen(false)}
|
||||||
onLog={(entry) => setCommandLog((current) => [...current, entry])}
|
onLog={(entry) => setCommandLog((current) => [...current, entry])}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user