refactor/整理现有的string-typed资源类型 #388

Merged
k88936 merged 80 commits from feat/agc-organize-assetkind into master 2026-09-19 03:51:33 +08:00
129 changed files with 4273 additions and 2216 deletions
+3
View File
@@ -23,3 +23,6 @@
*.meta text
*.anim text
*.controller text
# Rust ts-rs 生成的共享契约:保留在仓库中供 TS 消费,但不作为手写源文件统计。
packages/shared/src/contracts/generated/** linguist-generated=true
+10 -1
View File
@@ -1,5 +1,14 @@
{
"singleQuote": true,
"semi": true,
"trailingComma": "all"
"trailingComma": "all",
"overrides": [
{
"files": "packages/shared/src/contracts/generated/**/*.ts",
"options": {
"printWidth": 1000,
"singleQuote": false
}
}
]
}
@@ -676,11 +676,11 @@ async function runSelfTest() {
designFoundationAssetCall?.arguments?.input?.outputPath ===
'assets/ui-prototype.png' &&
designFoundationAssetCall.arguments.input.aspectRatio === '16:9' &&
designFoundationAssetCall.arguments.input.assetKind === 'ui-prototype' &&
designFoundationAssetCall.arguments.input.assetKind === 'ui-design' &&
artAssetPlanAssetCall?.arguments?.input?.outputPath ===
'assets/art-spritesheet.png' &&
artAssetPlanAssetCall.arguments.input.aspectRatio === '1:1' &&
artAssetPlanAssetCall.arguments.input.assetKind === 'art-spritesheet',
artAssetPlanAssetCall.arguments.input.assetKind === 'icon-spritesheet',
'self-test-visual-assets-invalid',
);
@@ -70,7 +70,7 @@ const requiredFormalArtifactSpecs = [
{ path: 'game/balance.json', kind: 'json' },
{ path: 'assets/manifest.art.json', kind: 'json' },
{ path: 'assets/manifest.audio.json', kind: 'json' },
{ path: 'game/index.html', kind: 'game-entry' },
{ path: 'game/index.html', kind: 'file' },
{ path: 'exports/README.md', kind: 'file' },
];
const editorImageArtifactSpecs = [
@@ -290,7 +290,7 @@ function deterministicArtManifest(
{
id: 'garden-guardians-spritesheet',
path: 'assets/art-spritesheet.png',
kind: 'art-spritesheet',
kind: 'icon-spritesheet',
usage: ['defenders', 'enemies', 'battlefield-ui'],
source: 'canvas',
status: 'ready',
@@ -710,7 +710,7 @@ function canvasAssetCall(agentId) {
outputPath: 'assets/ui-prototype.png',
aspectRatio: '16:9',
imageSize: '2K',
assetKind: 'ui-prototype',
assetKind: 'ui-design',
assetLabel: '游戏横屏界面原型图',
replaceExisting: false,
});
@@ -721,7 +721,7 @@ function canvasAssetCall(agentId) {
outputPath: 'assets/art-spritesheet.png',
aspectRatio: '1:1',
imageSize: '1K',
assetKind: 'art-spritesheet',
assetKind: 'icon-spritesheet',
assetLabel: '游戏首版核心美术素材',
replaceExisting: false,
sliceMode: 'connected-components',
@@ -830,12 +830,12 @@ function missingGeneratedVisualAssetObservation(context, agentId) {
},
'design-foundation': {
path: 'assets/ui-prototype.png',
kind: 'ui-prototype',
kind: 'ui-design',
summary: '策划界面原型图尚未按正式视觉流程生成并登记,不能完成任务',
},
'art-asset-plan': {
path: 'assets/art-spritesheet.png',
kind: 'art-spritesheet',
kind: 'icon-spritesheet',
summary: '首版美术素材图尚未按正式视觉流程生成并登记,不能完成任务',
},
}[agentId];
@@ -2667,7 +2667,7 @@ function createDeterministicCanvasFixture(apiKey) {
asset: {
assetId: `asset-${sliceImageId}`,
assetObjectId: sliceAssetObjectId,
assetKind: 'art-spritesheet-slice',
assetKind: 'icon',
projectId,
taskId,
},
@@ -2707,7 +2707,7 @@ function createDeterministicCanvasFixture(apiKey) {
spritesheetAsset: {
assetId: `asset-${imageId}`,
assetObjectId,
assetKind: 'art-spritesheet',
assetKind: 'icon-spritesheet',
projectId,
taskId,
},
@@ -2749,7 +2749,7 @@ function createDeterministicCanvasFixture(apiKey) {
const assetObjectId = `asset-object-${imageId}`;
const resourceId = `resource-${imageId}`;
const assetKind =
typeof body?.assetKind === 'string' ? body.assetKind : 'game-art';
typeof body?.assetKind === 'string' ? body.assetKind : 'image';
images.set(imageId, {
...image,
objectKey,
+1
View File
@@ -5016,6 +5016,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"ts-rs",
]
[[package]]
@@ -56,7 +56,7 @@ platform-agent = { path = "../../../server-rs/crates/platform-agent" }
portable-pty = "0.9"
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "native-tls", "stream"] }
regex = "1"
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false, features = ["ts-bindings"] }
tauri = { version = "2.11.2", features = [] }
tauri-plugin-dialog = "2.7.1"
tauri-plugin-http = { version = "2.5.9", default-features = false, features = ["charset", "cookies", "http2", "rustls-tls"] }
@@ -63,7 +63,7 @@
"agents/openai.yaml",
"references/platform-art-contract.md"
],
"sha256": "47ac742d9b88e5d6cd9833484ab212152578e58ae27f7add312fd1d78183385c"
"sha256": "6668bf1aa69601bcc65c97fdcd879c699c3139befcd70805d95614997f6e44e9"
},
{
"name": "agc-web-game-development",
@@ -19,13 +19,13 @@ image, UI design image, or publication material; use `agc_edit_image` for an
edit of an existing registered image; use `taonier_prepare_game_art` only for
the complete game-art package and its canonical slices.
With `agc_generate_image`, `kind="character"` and `kind="art-spritesheet"`
With `agc_generate_image`, `kind="character"` and `kind="icon-spritesheet"`
generate the subject on a solid-colour background and automatically matte it
away afterwards, producing transparent-background results; write the prompt
for the subject only, never for a scene. `kind="image"` keeps the rendered
frame without extra processing.
When `agc_generate_image` is used with `kind="art-spritesheet"`, `sliceMode` is
When `agc_generate_image` is used with `kind="icon-spritesheet"`, `sliceMode` is
required and has no default, so decide it explicitly:
- Use `sliceMode="grid"` with `gridX` and `gridY` (1-32 each) only when the user
@@ -1198,7 +1198,7 @@ mod tests {
"tool": "agc_generate_image",
"arguments": {
"prompt": prompt,
"kind": "art-spritesheet",
"kind": "icon-spritesheet",
"sliceMode": "connected-components",
"sliceCount": 8,
"screenColor": "#CFEFFF"
@@ -1,6 +1,10 @@
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem};
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageItem};
use super::validation::validate_direct_codex_user_item;
use crate::agent::{read_manifest_for_project, sanitize_attachment_local_path};
use crate::ui_editor::persistence::{
generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND,
UI_DESIGN_DOC_MEDIA_TYPE,
};
use serde_json::Value;
use std::path::Path;
@@ -111,29 +115,165 @@ pub(crate) fn direct_codex_user_item_to_prompt(
item: &DirectCodexUserItem,
) -> Result<String, String> {
let wire = direct_codex_user_item_to_wire_input(root, item)?;
wire.as_array()
let DirectCodexUserItem::Message(message) = item;
let mut prompt = wire
.as_array()
.ok_or_else(|| "DirectProject user item wire input 不是数组".to_string())
.map(|parts| {
.and_then(|parts| {
parts
.iter()
.filter_map(|part| part.get("text").and_then(Value::as_str))
.collect::<String>()
})
.and_then(|prompt| {
if prompt.trim().is_empty() {
Err("DirectProject user item 不能转换为空 prompt".to_string())
} else {
Ok(prompt)
.map(|part| {
part.get("text")
.and_then(Value::as_str)
.ok_or_else(|| "DirectProject user item wire part 缺少 text".to_string())
})
.collect::<Result<String, String>>()
})?;
if let Some(code_context) = render_ui_design_code_context(root, message)? {
prompt.push('\n');
prompt.push_str(&code_context);
}
if prompt.trim().is_empty() {
return Err("DirectProject user item 不能转换为空 prompt".to_string());
}
Ok(prompt)
}
/// 本轮 prompt 的 UI 设计文档引用上下文:复用 UI Editor 代码导出,把带文档注释的
/// JS 片段路径交给模型;生成失败只追加原始错误,不阻断本轮引用,其它引用继续处理。
///
/// 只在生成本轮 prompt 时展开:历史 item 回读走 `direct_codex_user_item_to_response_item`
/// 的纯投影,不得在这里产生项目写副作用。
fn render_ui_design_code_context(
root: &Path,
message: &DirectCodexUserMessageItem,
) -> Result<Option<String>, String> {
let referenced_ids = message
.content
.iter()
.filter_map(|part| match part {
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
Some(resource_id.trim())
}
_ => None,
})
.collect::<Vec<_>>();
if referenced_ids.is_empty() {
return Ok(None);
}
let manifest = read_manifest_for_project(root)?;
let mut lines = Vec::new();
for resource_id in referenced_ids {
let is_ui_design_doc = manifest.assets.iter().any(|asset| {
asset.id == resource_id
&& asset.kind == UI_DESIGN_DOC_ASSET_KIND
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
});
if !is_ui_design_doc {
continue;
}
lines.push(
match generate_ui_design_code_at(GenerateUiDesignCodeInput {
project_path: root.to_string_lossy().into_owned(),
expected_project_id: manifest.project_id.clone(),
asset_id: resource_id.to_string(),
}) {
Ok(result) => format!("请先阅读生成的带有文档的代码片段: {}", result.relative_path),
Err(error) => format!("生成代码遇到错误{error}"),
},
);
}
if lines.is_empty() {
return Ok(None);
}
Ok(Some(lines.join("\n")))
}
#[cfg(test)]
mod tests {
use super::direct_codex_user_item_to_response_item;
use super::{direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item};
use crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE;
use serde_json::json;
use shared_contracts::game_creation_app::{
GameCreationAppAssetKind, GameCreationAppAssetSource, GameCreationAppAssetSourceKind,
};
use std::path::Path;
const PROMPT_CONTEXT_PROJECT_ID: &str = "direct-prompt-context-project";
fn prompt_context_project() -> tempfile::TempDir {
let directory = tempfile::tempdir().expect("temp project");
crate::init_local_game_project_at(
directory.path(),
PROMPT_CONTEXT_PROJECT_ID,
"Direct prompt 上下文测试",
)
.expect("init project");
directory
}
fn register_fixture_asset(
root: &Path,
relative_path: &str,
kind: GameCreationAppAssetKind,
media_type: &str,
) -> String {
let absolute_path = crate::resolve_local_project_path(root, relative_path)
.expect("resolve fixture asset path");
std::fs::create_dir_all(absolute_path.parent().expect("fixture asset parent"))
.expect("create fixture asset parent");
std::fs::write(&absolute_path, b"{}").expect("write fixture asset file");
crate::assets::register_local_asset_at(
root,
relative_path,
kind,
media_type,
"prompt-context-test",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Generated,
canvas_project_id: None,
resource_id: Some("prompt-context".to_string()),
asset_object_id: None,
task_id: None,
prompt: None,
model: None,
generation_route: None,
generation_kind: None,
reference_resource_ids: Vec::new(),
},
)
.expect("register fixture asset")
.id
}
fn ui_design_doc_fixture(initialize_state: bool) -> (tempfile::TempDir, String) {
let directory = prompt_context_project();
let asset_id = register_fixture_asset(
directory.path(),
"ui/design.json",
GameCreationAppAssetKind::UiDesignDoc,
UI_DESIGN_DOC_MEDIA_TYPE,
);
if initialize_state {
crate::ui_editor::persistence::initialize_ui_design_state_at(
directory.path(),
PROMPT_CONTEXT_PROJECT_ID,
&asset_id,
)
.expect("initialize UI design state");
}
(directory, asset_id)
}
fn user_item_with_resource_reference(asset_id: &str) -> serde_json::Value {
json!({
"type": "message",
"role": "user",
"id": "turn-1:user",
"content": [{"type": "agc_resource_reference", "resourceId": asset_id}],
})
}
#[test]
fn standard_response_item_passes_through_without_agc_private_parts() {
let item = json!({
@@ -174,4 +314,81 @@ mod tests {
.expect_err("history item without type must fail");
assert!(error.contains("缺少 type"), "{error}");
}
#[test]
fn ui_design_doc_reference_appends_generated_code_context() {
let (project, asset_id) = ui_design_doc_fixture(true);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(
prompt.contains(&format!(
"[素材引用 resourceId={asset_id};项目路径=ui/design.json]"
)),
"{prompt}"
);
assert!(
prompt.contains("请先阅读生成的带有文档的代码片段: ui/generated-"),
"{prompt}"
);
}
#[test]
fn history_projection_never_writes_generated_ui_design_code() {
let (project, asset_id) = ui_design_doc_fixture(true);
let item = user_item_with_resource_reference(&asset_id);
direct_codex_user_item_to_response_item(project.path(), &item).expect("history projection");
let generated_root =
crate::resolve_local_project_path(project.path(), "ui").expect("resolve ui directory");
let generated_files = std::fs::read_dir(generated_root)
.expect("read ui directory")
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.file_name()
.to_string_lossy()
.starts_with("generated-")
})
.count();
assert_eq!(
generated_files, 0,
"历史回读只做纯投影,不得生成 UI 设计代码"
);
}
#[test]
fn ui_design_generation_failure_keeps_reference_and_reports_error() {
let (project, asset_id) = ui_design_doc_fixture(false);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
assert!(prompt.contains("生成代码遇到错误"), "{prompt}");
}
#[test]
fn other_asset_kind_reference_does_not_generate_ui_design_code() {
let project = prompt_context_project();
let asset_id = register_fixture_asset(
project.path(),
"assets/hero.png",
GameCreationAppAssetKind::Character,
"image/png",
);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
assert!(
!prompt.contains("请先阅读生成的带有文档的代码片段"),
"{prompt}"
);
assert!(!prompt.contains("生成代码遇到错误"), "{prompt}");
}
}
File diff suppressed because it is too large Load Diff
@@ -1197,9 +1197,9 @@ fn bridge_registered_resource(
"mediaType": asset.media_type,
// Agent 与 UI 必须看到同一个口径:UI 栏目走 TS 的 `gameCreationAppAssetCategory`
// (落盘值 + 按 kind 派生 + 读时自愈),这里走 Rust 的同构实现。
// 直接透传落盘 `asset.category` 会让 `kind:"ui"` 的资产在 UI 显示「UI 交互」、
// 直接透传落盘 `asset.category` 会让 UI 设计资产在 UI 显示「UI 交互」、
// 在 Agent 侧读到 `unclassified`(真机 55 条分歧)。
"category": game_creation_app_asset_effective_category(&asset.kind, asset.category),
"category": game_creation_app_asset_effective_category(asset.kind, asset.category),
"tags": asset.tags,
"canvasProjectId": asset.source.canvas_project_id,
"resourceId": asset.source.resource_id,
@@ -1240,7 +1240,9 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
arguments,
"kind",
DIRECT_TOOL_BRIDGE_MAX_RESOURCE_KIND_CHARS,
)?;
)?
.map(|kind| bridge_asset_list_kind_filter(&kind))
.transpose()?;
let asset_id = bridge_optional_bounded_string(
arguments,
"assetId",
@@ -1260,7 +1262,7 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
let mut assets = manifest
.assets
.iter()
.filter(|asset| kind.as_ref().is_none_or(|kind| asset.kind == *kind))
.filter(|asset| kind.is_none_or(|kind| asset.kind == kind))
.filter(|asset| {
asset_id
.as_ref()
@@ -1326,6 +1328,24 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
}
}
/// `asset.list` 的 `kind` 过滤:只接受 canonical 值,认不出的值直接报错。
///
/// 以前认不出的值会收口成 `unknown` 再参与等值过滤,结果是**静默返回空列表**:调用方(模型)
/// 会以为项目里没有这类资产,而不是"你传的 kind 不合法",于是继续按错误前提往下走。
/// `unknown` 本身仍是合法输入(确有 kind 未知的资产),只拒绝"既不是 canonical、也不是字面
/// `unknown`"的原值。
fn bridge_asset_list_kind_filter(raw: &str) -> Result<GameCreationAppAssetKind, String> {
let kind = GameCreationAppAssetKind::parse_with_context(raw, "asset.list.kind");
if kind == GameCreationAppAssetKind::Unknown
&& raw.trim() != GameCreationAppAssetKind::Unknown.as_str()
{
return Err(format!(
"kind 不是已知的 manifest 资源 kind{raw};请改用 canonical kind(如 image、scene、character、icon、icon-spritesheet、character-animation、audio、video、document"
));
}
Ok(kind)
}
fn bridge_project_file_class(path: &str) -> (&'static str, Option<&'static str>) {
let extension = Path::new(path)
.extension()
@@ -1896,7 +1916,7 @@ async fn bridge_create_or_derive_resource(
source_asset_id: source_asset.as_ref().map(|asset| asset.id.clone()),
source_path: source_asset.as_ref().map(|asset| asset.local_path.clone()),
source_media_type: source_asset.as_ref().map(|asset| asset.media_type.clone()),
source_subtype: source_asset.as_ref().map(|asset| asset.kind.clone()),
source_subtype: source_asset.as_ref().map(|asset| asset.kind.to_string()),
producer_task_id: source_asset
.as_ref()
.and_then(|asset| asset.source.task_id.clone()),
@@ -2006,7 +2026,7 @@ async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Val
source_asset_id: Some(source_asset_id.clone()),
source_path: Some(source_asset.local_path.clone()),
source_media_type: Some(source_asset.media_type.clone()),
source_subtype: Some(source_asset.kind.clone()),
source_subtype: Some(source_asset.kind.to_string()),
producer_task_id: source_asset.source.task_id.clone(),
source_version_id: None,
prompt: "去除背景".to_string(),
@@ -2174,35 +2194,37 @@ async fn bridge_prepare_game_art(state: &DirectToolBridgeState, arguments: &Valu
result
}
fn bridge_image_generation_kind(arguments: &Value) -> Result<String, String> {
fn bridge_image_generation_kind(arguments: &Value) -> Result<GameCreationAppAssetKind, String> {
let kind = arguments
.get("kind")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("image");
normalize_platform_art_asset_generation_kind(kind)
.map(str::to_string)
.ok_or_else(|| {
format!(
"工具参数 kind 只允许 {}",
PLATFORM_ART_ASSET_GENERATION_KINDS.join("")
)
})
normalize_platform_art_asset_generation_kind(kind).ok_or_else(|| {
format!(
"工具参数 kind 只允许 {}",
PLATFORM_ART_ASSET_GENERATION_KINDS
.iter()
.map(|kind| kind.as_str())
.collect::<Vec<_>>()
.join("")
)
})
}
/// 切分模式没有默认值:图集必须显式声明,且声明必须与 kind 和网格参数自洽。
fn validate_generate_image_slice_declaration(
kind: &str,
kind: GameCreationAppAssetKind,
slice_mode: Option<&str>,
grid_x: Option<u32>,
grid_y: Option<u32>,
slice_count: Option<usize>,
) -> Result<(), String> {
if kind == "art-spritesheet" {
if kind == GameCreationAppAssetKind::IconSpritesheet {
if slice_mode.is_none() {
return Err(
"kind=art-spritesheet 必须显式声明 sliceMode,没有默认值:需求要求等分网格、固定槽位或指定行列数时传 sliceMode=grid 并提供 gridX/gridY;自由排布、数量不定或只要求一张图集时传 sliceMode=connected-components"
"kind=icon-spritesheet 必须显式声明 sliceMode,没有默认值:需求要求等分网格、固定槽位或指定行列数时传 sliceMode=grid 并提供 gridX/gridY;自由排布、数量不定或只要求一张图集时传 sliceMode=connected-components"
.to_string(),
);
}
@@ -2215,19 +2237,19 @@ fn validate_generate_image_slice_declaration(
}
if slice_mode.is_some() || grid_x.is_some() || grid_y.is_some() || slice_count.is_some() {
return Err(format!(
"工具参数 sliceMode/gridX/gridY/sliceCount 仅对 kind=art-spritesheet 生效,当前 kind={kind}"
"工具参数 sliceMode/gridX/gridY/sliceCount 仅对 kind=icon-spritesheet 生效,当前 kind={kind}"
));
}
Ok(())
}
/// 抠图纯色背景只服务 character 与 art-spritesheet 链路;格式校验收口为
/// 抠图纯色背景只服务 character 与 icon-spritesheet 链路;格式校验收口为
/// `auto` 或 `#RRGGBB`(服务端另有支持色板,客户端不复制),`auto`/空串归一为
/// None(服务端自动决策),hex 统一大写后透传。其它 kind 携带该字段直接拒绝,
/// 避免服务端静默忽略造成“已生效”的误解。
fn normalize_generate_image_screen_color(
arguments: &Value,
kind: &str,
kind: GameCreationAppAssetKind,
) -> Result<Option<String>, String> {
let Some(value) = arguments.get("screenColor") else {
return Ok(None);
@@ -2235,9 +2257,12 @@ fn normalize_generate_image_screen_color(
if value.is_null() {
return Ok(None);
}
if !matches!(kind, "character" | "art-spritesheet") {
if !matches!(
kind,
GameCreationAppAssetKind::Character | GameCreationAppAssetKind::IconSpritesheet
) {
return Err(format!(
"工具参数 screenColor 仅对 kind=character 和 kind=art-spritesheet 生效,当前 kind={kind}"
"工具参数 screenColor 仅对 kind=character 和 kind=icon-spritesheet 生效,当前 kind={kind}"
));
}
let raw = value
@@ -2364,18 +2389,18 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value)
})
.transpose()?;
validate_generate_image_slice_declaration(
kind.as_str(),
kind,
slice_mode.as_deref(),
grid_x,
grid_y,
slice_count,
)?;
let screen_color = normalize_generate_image_screen_color(arguments, kind.as_str())?;
let screen_color = normalize_generate_image_screen_color(arguments, kind)?;
let options = PlatformArtAssetGenerationOptions {
output_path,
aspect_ratio,
image_size,
asset_kind: kind.clone(),
asset_kind: kind,
asset_label: asset_name.clone(),
replace_existing: false,
slice_count,
@@ -2864,14 +2889,19 @@ pub(crate) async fn start_direct_tool_bridge(
mod tests {
#[test]
fn generate_image_slice_declaration_is_explicit_and_self_consistent() {
let missing =
validate_generate_image_slice_declaration("art-spritesheet", None, None, None, None)
.expect_err("art-spritesheet without sliceMode must fail closed");
let missing = validate_generate_image_slice_declaration(
GameCreationAppAssetKind::IconSpritesheet,
None,
None,
None,
None,
)
.expect_err("icon-spritesheet without sliceMode must fail closed");
assert!(missing.contains("没有默认值"), "{missing}");
assert!(missing.contains("connected-components"), "{missing}");
assert!(validate_generate_image_slice_declaration(
"art-spritesheet",
GameCreationAppAssetKind::IconSpritesheet,
Some("connected-components"),
None,
None,
@@ -2879,7 +2909,7 @@ mod tests {
)
.is_ok());
assert!(validate_generate_image_slice_declaration(
"art-spritesheet",
GameCreationAppAssetKind::IconSpritesheet,
Some("grid"),
Some(3),
Some(2),
@@ -2887,7 +2917,7 @@ mod tests {
)
.is_ok());
let grid_with_count = validate_generate_image_slice_declaration(
"art-spritesheet",
GameCreationAppAssetKind::IconSpritesheet,
Some("grid"),
Some(2),
Some(2),
@@ -2897,58 +2927,80 @@ mod tests {
assert!(grid_with_count.contains("gridX×gridY"), "{grid_with_count}");
let wrong_kind = validate_generate_image_slice_declaration(
"image",
GameCreationAppAssetKind::Image,
Some("connected-components"),
None,
None,
None,
)
.expect_err("slice declaration must stay scoped to art-spritesheet");
.expect_err("slice declaration must stay scoped to icon-spritesheet");
assert!(
wrong_kind.contains("仅对 kind=art-spritesheet 生效"),
wrong_kind.contains("仅对 kind=icon-spritesheet 生效"),
"{wrong_kind}"
);
let wrong_kind_count =
validate_generate_image_slice_declaration("image", None, None, None, Some(8))
.expect_err("sliceCount-only violation must be rejected");
let wrong_kind_count = validate_generate_image_slice_declaration(
GameCreationAppAssetKind::Image,
None,
None,
None,
Some(8),
)
.expect_err("sliceCount-only violation must be rejected");
assert!(
wrong_kind_count.contains("sliceCount"),
"sliceCount-only violation must name sliceCount: {wrong_kind_count}"
);
assert!(validate_generate_image_slice_declaration("image", None, None, None, None).is_ok());
assert!(validate_generate_image_slice_declaration(
GameCreationAppAssetKind::Image,
None,
None,
None,
None
)
.is_ok());
}
#[test]
fn generate_image_screen_color_is_normalized_and_kind_gated() {
// 省略与显式 null 等价,且不触发 kind 门禁。
assert_eq!(
normalize_generate_image_screen_color(&json!({}), "image").expect("omitted"),
normalize_generate_image_screen_color(&json!({}), GameCreationAppAssetKind::Image)
.expect("omitted"),
None
);
assert_eq!(
normalize_generate_image_screen_color(&json!({"screenColor": null}), "image")
.expect("null"),
normalize_generate_image_screen_color(
&json!({"screenColor": null}),
GameCreationAppAssetKind::Image
)
.expect("null"),
None
);
// auto 家族归一为 None(服务端自动决策),大小写与空白不敏感。
for raw in ["auto", "AUTO", " auto ", ""] {
assert_eq!(
normalize_generate_image_screen_color(&json!({"screenColor": raw}), "character")
.expect("auto variants"),
normalize_generate_image_screen_color(
&json!({"screenColor": raw}),
GameCreationAppAssetKind::Character
)
.expect("auto variants"),
None,
"{raw}"
);
}
// hex 统一大写透传;色板白名单由服务端权威校验,客户端只守格式。
assert_eq!(
normalize_generate_image_screen_color(&json!({"screenColor": "#cfefff"}), "character")
.expect("lowercase hex"),
normalize_generate_image_screen_color(
&json!({"screenColor": "#cfefff"}),
GameCreationAppAssetKind::Character
)
.expect("lowercase hex"),
Some("#CFEFFF".to_string())
);
assert_eq!(
normalize_generate_image_screen_color(
&json!({"screenColor": " #A0BBA0 "}),
"art-spritesheet"
GameCreationAppAssetKind::IconSpritesheet
)
.expect("padded hex"),
Some("#A0BBA0".to_string())
@@ -2956,19 +3008,24 @@ mod tests {
// 非 auto/非 hex、非字符串一律拒绝。
for bad in [json!("green"), json!("#GGGGGG"), json!("#FFF"), json!(12)] {
assert!(
normalize_generate_image_screen_color(&json!({"screenColor": bad}), "character")
.is_err(),
normalize_generate_image_screen_color(
&json!({"screenColor": bad}),
GameCreationAppAssetKind::Character
)
.is_err(),
"{bad}"
);
}
// 其它 kind 携带该字段直接拒绝,即使取值合法。
let gated =
normalize_generate_image_screen_color(&json!({"screenColor": "#CFEFFF"}), "image")
.expect_err("screenColor must stay scoped to character/art-spritesheet");
let gated = normalize_generate_image_screen_color(
&json!({"screenColor": "#CFEFFF"}),
GameCreationAppAssetKind::Image,
)
.expect_err("screenColor must stay scoped to character/icon-spritesheet");
assert!(gated.contains("kind=character"), "{gated}");
assert!(normalize_generate_image_screen_color(
&json!({"screenColor": "auto"}),
"ui-prototype"
GameCreationAppAssetKind::UiDesign
)
.is_err());
}
@@ -3737,12 +3794,14 @@ mod tests {
fn bridge_art_resource_exposes_only_the_safe_identity_projection() {
let asset = GameCreationAppAssetManifestEntry {
id: "local-art-1".to_string(),
kind: "art-spritesheet-slice".to_string(),
kind: GameCreationAppAssetKind::Icon,
media_type: "image/png".to_string(),
local_path: "assets/art-spritesheet-slices/player.png".to_string(),
image_sequence_frames: None,
image_sequence_duration_ms: None,
category: game_creation_app_asset_category_for_kind("art-spritesheet-slice"),
category: game_creation_app_asset_category_for_kind(
GameCreationAppAssetKind::IconSpritesheet,
),
tags: Vec::new(),
source: GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Canvas,
@@ -3794,7 +3853,7 @@ mod tests {
fn bridge_registered_resource_exposes_formal_sequence_without_private_generation_fields() {
let asset = GameCreationAppAssetManifestEntry {
id: "animation-1".to_string(),
kind: "character-animation".to_string(),
kind: GameCreationAppAssetKind::CharacterAnimation,
media_type: "video/mp4".to_string(),
local_path: "assets/edits/animation.mp4".to_string(),
image_sequence_frames: Some(vec![
@@ -3807,7 +3866,9 @@ mod tests {
},
]),
image_sequence_duration_ms: Some(4_000),
category: game_creation_app_asset_category_for_kind("character-animation"),
category: game_creation_app_asset_category_for_kind(
GameCreationAppAssetKind::CharacterAnimation,
),
tags: Vec::new(),
source: GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Canvas,
@@ -3845,7 +3906,7 @@ mod tests {
fn bridge_registered_resource_keeps_explicit_manifest_classification() {
let asset = GameCreationAppAssetManifestEntry {
id: "spec-1".to_string(),
kind: "spec".to_string(),
kind: GameCreationAppAssetKind::Spec,
media_type: "application/json".to_string(),
local_path: "assets/specs/hero.json".to_string(),
image_sequence_frames: None,
@@ -3881,13 +3942,13 @@ mod tests {
///
/// UI 栏目走 TS 的 `gameCreationAppAssetCategory`(落盘值 + 按 kind 派生 + 读时自愈),
/// Agent 走 Rust 的同构实现 `game_creation_app_asset_effective_category`。直接透传落盘
/// `category` 会让 `kind:"ui"` 的资产在 UI 显示「UI 交互」、在 Agent 侧读到
/// `unclassified`——真机 122 条资产里有 55 条这样分叉
/// `category` 会让 `ui-design` 的资产在 UI 显示「UI 交互」、在 Agent 侧读到
/// `unclassified`,两侧分类口径必须一致
#[test]
fn bridge_registered_resource_projects_effective_category_not_raw_persisted_value() {
let asset = GameCreationAppAssetManifestEntry {
id: "ui-1".to_string(),
kind: "ui".to_string(),
id: "ui-design-1".to_string(),
kind: GameCreationAppAssetKind::UiDesign,
media_type: "application/json".to_string(),
local_path: "assets/UI 设计 1.json".to_string(),
image_sequence_frames: None,
@@ -3910,7 +3971,7 @@ mod tests {
};
let projection = bridge_registered_resource(&asset, false);
assert_eq!(projection["kind"], "ui");
assert_eq!(projection["kind"], "ui-design");
assert_eq!(projection["category"], "ui-interaction");
}
@@ -247,7 +247,7 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
"type": "string",
"enum": PLATFORM_ART_ASSET_GENERATION_KINDS,
"default": "image",
"description": "image=普通新图(不做额外处理),character=角色图(纯色底生成后自动抠图,产出透明背景立绘,prompt 只描述角色主体),spec/icon-spec=统一视觉规范图(spec 是服务端同义词,客户端统一登记为 icon-spec),ui-prototype=完整 UI 设计图,art-spritesheet=透明游戏素材图集(纯色底生成后自动抠图并切片,项目须已有 icon-spec 规范图),publication-material=发布宣传图"
"description": "image=普通新图(不做额外处理),character=角色图(纯色底生成后自动抠图,产出透明背景立绘,prompt 只描述角色主体),icon-spec=统一视觉规范图ui-design=完整 UI 设计图,icon-spritesheet=透明游戏素材图集(纯色底生成后自动抠图并切片,项目须已有 icon-spec 规范图),publication-material=发布宣传图"
},
"aspectRatio": {
"type": "string",
@@ -273,7 +273,7 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
"sliceMode": {
"type": "string",
"enum": ["connected-components", "grid"],
"description": "仅 kind=art-spritesheet 生效,且必填、没有默认值:需求明确要求等分网格、固定槽位或指定行列数时传 grid,并用 gridX/gridY 传入来自需求本身的行列数;自由排布、数量不定或只要求一张图集时传 connected-components,需要约束素材张数时用 sliceCount。省略、与 kind 不匹配或与 gridX/gridY 互相矛盾时客户端直接拒绝,不会替你选择"
"description": "仅 kind=icon-spritesheet 生效,且必填、没有默认值:需求明确要求等分网格、固定槽位或指定行列数时传 grid,并用 gridX/gridY 传入来自需求本身的行列数;自由排布、数量不定或只要求一张图集时传 connected-components,需要约束素材张数时用 sliceCount。省略、与 kind 不匹配或与 gridX/gridY 互相矛盾时客户端直接拒绝,不会替你选择"
},
"gridX": {
"type": "integer",
@@ -291,11 +291,11 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
"type": "integer",
"minimum": 1,
"maximum": 256,
"description": "只与 kind=art-spritesheet 且 sliceMode=connected-components 同时提供,用于约束目标素材张数;省略时按图像内容自动识别"
"description": "只与 kind=icon-spritesheet 且 sliceMode=connected-components 同时提供,用于约束目标素材张数;省略时按图像内容自动识别"
},
"screenColor": {
"type": "string",
"description": "抠图纯色背景,仅 kind=character(角色形象)和 kind=art-spritesheet(图标素材)生效,其它 kind 携带会被拒绝。生成时把主体置于该纯色背景上,回图后据此抠除背景。取值只能是 auto 或下列色板 hex 之一,传值只填 hex 本身、不要附带色名:#CFEFFF(浅雾蓝)、#B0C2E0(浅钢蓝)、#FFD6C2(暖浅桃色)、#E6D8FF(淡薰衣草紫)、#F4D8E8(浅粉灰)、#7FB3FF(中度天蓝)、#FFF2A8(浅柠黄)、#CFFFE1(淡薄荷绿)、#D8DEE8(浅中性灰)、#D8D2E8(淡灰紫)、#A8F7F0(高对比浅青)、#A0BBA0(灰竹绿);auto 时由服务端自动选色。手动指定时不能与角色或素材本体的颜色接近"
"description": "抠图纯色背景,仅 kind=character(角色形象)和 kind=icon-spritesheet(图标素材)生效,其它 kind 携带会被拒绝。生成时把主体置于该纯色背景上,回图后据此抠除背景。取值只能是 auto 或下列色板 hex 之一,传值只填 hex 本身、不要附带色名:#CFEFFF(浅雾蓝)、#B0C2E0(浅钢蓝)、#FFD6C2(暖浅桃色)、#E6D8FF(淡薰衣草紫)、#F4D8E8(浅粉灰)、#7FB3FF(中度天蓝)、#FFF2A8(浅柠黄)、#CFFFE1(淡薄荷绿)、#D8DEE8(浅中性灰)、#D8D2E8(淡灰紫)、#A8F7F0(高对比浅青)、#A0BBA0(灰竹绿);auto 时由服务端自动选色。手动指定时不能与角色或素材本体的颜色接近"
}
},
"required": ["prompt"],
@@ -341,7 +341,7 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
"type": "string",
"minLength": 1,
"maxLength": 80,
"description": "可选的 manifest 资源 kind 精确过滤,例如 video、character-animation、sound-effect、background-music 或 art-spritesheet-slice"
"description": "可选的 manifest 资源 kind 精确过滤,必须是 canonical kind例如 video、character-animation、sound-effect、background-music、icon-spritesheet);传非 canonical 值会被拒绝并报错,不会静默返回空列表"
},
"assetId": {
"type": "string",
@@ -2523,7 +2523,7 @@ mod tests {
// 目录外的 kind 必须在本地拒绝:既不下发 bridge,也不产生任何计费副作用。
let unsupported_kind = call_agc_generate_image(&json!({
"prompt": "像素月光主角",
"kind": "game-art"
"kind": "future-kind"
}))
.await;
assert_eq!(unsupported_kind["isError"], json!(true));
@@ -2533,7 +2533,7 @@ mod tests {
let oversized_prompt = call_agc_generate_image(&json!({
"prompt": "x".repeat(DIRECT_TOOLS_MCP_MAX_IMAGE_PROMPT_CHARS + 1),
"kind": "art-spritesheet"
"kind": "icon-spritesheet"
}))
.await;
assert_eq!(oversized_prompt["isError"], json!(true));
@@ -2897,12 +2897,12 @@ mod tests {
fn tool_chain_image_asset(id: &str, local_path: &str) -> GameCreationAppAssetManifestEntry {
GameCreationAppAssetManifestEntry {
id: id.to_string(),
kind: "image".to_string(),
kind: GameCreationAppAssetKind::Image,
media_type: "image/png".to_string(),
local_path: local_path.to_string(),
image_sequence_frames: None,
image_sequence_duration_ms: None,
category: game_creation_app_asset_category_for_kind("image"),
category: game_creation_app_asset_category_for_kind(GameCreationAppAssetKind::Image),
tags: Vec::new(),
source: GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Canvas,
File diff suppressed because it is too large Load Diff
@@ -31,9 +31,9 @@ fn is_direct_platform_art_generation_runtime_context(
context: &PlatformArtGenerationRuntimeContext,
) -> bool {
let asset_kind = match context.run_id.as_str() {
"art-spec" => "icon-spec",
"game-background" => "game-background",
"art-spritesheet" => "art-spritesheet",
"art-spec" => GameCreationAppAssetKind::IconSpec,
"game-background" => GameCreationAppAssetKind::Scene,
"art-spritesheet" => GameCreationAppAssetKind::IconSpritesheet,
_ => return false,
};
context.agent_id == "direct-codex-art"
@@ -42,7 +42,11 @@ fn is_direct_platform_art_generation_runtime_context(
&& context.source == "direct-codex"
&& context.action_id == format!("direct-taonier-{}", context.run_id)
&& context.action_fingerprint
== format!("direct-taonier-art-v1:{asset_kind}:{}", context.run_id)
== format!(
"direct-taonier-art-v1:{}:{}",
asset_kind.as_str(),
context.run_id
)
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -98,7 +98,7 @@ pub(crate) fn render_local_asset_prompt_context(root: &Path) -> Result<String, S
output.push_str("- ");
output.push_str(&asset.id);
output.push_str(": ");
output.push_str(&asset.kind);
output.push_str(asset.kind.as_str());
output.push_str(" / ");
output.push_str(&asset.media_type);
output.push_str(" / ");
@@ -396,7 +396,7 @@ fn agent_runtime_action_receipt_safe_detail_with_owner(
let inspection_kind = detail
.get("inspectionKind")
.and_then(serde_json::Value::as_str)
.filter(|value| *value == AGENT_RUNTIME_UI_PROTOTYPE_INSPECTION_KIND);
.filter(|value| *value == AGENT_RUNTIME_UI_DESIGN_INSPECTION_KIND);
let validation_profile = detail
.get("validationProfile")
.and_then(serde_json::Value::as_str)
@@ -408,7 +408,7 @@ fn agent_runtime_action_receipt_safe_detail_with_owner(
)
});
let (passed, checks, issues) = if validation_profile.is_some() {
if inspection_kind != Some(AGENT_RUNTIME_UI_PROTOTYPE_INSPECTION_KIND) {
if inspection_kind != Some(AGENT_RUNTIME_UI_DESIGN_INSPECTION_KIND) {
return None;
}
let passed = detail.get("passed")?.as_bool()?;
@@ -2061,17 +2061,23 @@ mod tests {
.expect("resume first manifest task fixture");
for (local_path, kind) in [
("assets/art-spec.png", "icon-spec"),
("assets/ui-prototype.png", "ui-prototype"),
("assets/art-spritesheet.png", "art-spritesheet"),
("assets/art-spec.png", GameCreationAppAssetKind::IconSpec),
(
"assets/ui-prototype.png",
GameCreationAppAssetKind::UiDesign,
),
(
"assets/art-spritesheet.png",
GameCreationAppAssetKind::IconSpritesheet,
),
] {
let (generation_route, generation_kind, reference_resource_ids) = match kind {
"icon-spec" => (
GameCreationAppAssetKind::IconSpec => (
"/api/external/v1/editor/images/generations",
"spec",
Vec::new(),
),
"ui-prototype" => (
GameCreationAppAssetKind::UiDesign => (
"/api/external/v1/editor/images/generations",
"ui-design",
vec!["resource-icon-spec".to_string()],
@@ -2082,7 +2088,7 @@ mod tests {
vec!["resource-icon-spec".to_string()],
),
};
let bytes = if kind == "art-spritesheet" {
let bytes = if kind == GameCreationAppAssetKind::IconSpritesheet {
use image::ImageEncoder;
let mut bytes = Vec::new();
image::codecs::png::PngEncoder::new(&mut bytes)
@@ -1136,7 +1136,7 @@ pub(in crate::agent) fn ui_prototype_visual_inspection_blocker_detail_at_locked(
&& record
.get("inspectionKind")
.and_then(serde_json::Value::as_str)
== Some(AGENT_RUNTIME_UI_PROTOTYPE_INSPECTION_KIND)
== Some(AGENT_RUNTIME_UI_DESIGN_INSPECTION_KIND)
&& record
.get("validationProfile")
.and_then(serde_json::Value::as_str)
@@ -1216,11 +1216,19 @@ pub(in crate::agent) fn visual_asset_completion_blocker_at_locked(
return None;
}
let (expected_path, expected_kind, label) = match agent_id {
"art-director" => (AGENT_RUNTIME_ART_SPEC_PATH, "icon-spec", "统一视觉规范图"),
"design-foundation" => ("assets/ui-prototype.png", "ui-prototype", "策划界面原型图"),
"art-director" => (
AGENT_RUNTIME_ART_SPEC_PATH,
GameCreationAppAssetKind::IconSpec,
"统一视觉规范图",
),
"design-foundation" => (
"assets/ui-prototype.png",
GameCreationAppAssetKind::UiDesign,
"策划界面原型图",
),
"art-asset-plan" => (
"assets/art-spritesheet.png",
"art-spritesheet",
GameCreationAppAssetKind::IconSpritesheet,
"首版美术素材图",
),
_ => return None,
@@ -336,7 +336,11 @@ fn build_game_creator_agent_background_tool_plan_request_at(
render_game_creator_agent_runtime_steers_for_prompt(root, agent_id, session_id, run_id)?;
let loop_index = loop_index.saturating_add(1);
let context_preload_notice = game_creator_agent_context_preload_notice(agent_id);
let canvas_asset_kind_catalog = AGENT_RUNTIME_CANVAS_ASSET_KINDS.join("|");
let canvas_asset_kind_catalog = AGENT_RUNTIME_CANVAS_ASSET_KINDS
.iter()
.map(|kind| kind.as_str())
.collect::<Vec<_>>()
.join("|");
let limited_command_contract = if runtime_owner_artifact_validation_available {
"当前固定 owner 不得调用 command.run_limited 或 project.verify;完成固定正式产物后直接交付,由 Runtime 在收束门内执行结构验证。".to_string()
} else {
@@ -1,7 +1,7 @@
use super::*;
pub(crate) const AGENT_RUNTIME_CANVAS_ASSET_KINDS: &[&str] =
&["game-art", "icon-spec", "ui-prototype", "art-spritesheet"];
pub(crate) const AGENT_RUNTIME_CANVAS_ASSET_KINDS: &[GameCreationAppAssetKind] =
GameCreationAppAssetKind::CANVAS_ASSET_KINDS;
#[cfg(test)]
mod canvas_asset_kind_contract_tests {
@@ -11,7 +11,12 @@ mod canvas_asset_kind_contract_tests {
fn canvas_asset_kind_catalog_preserves_authoritative_contract() {
assert_eq!(
AGENT_RUNTIME_CANVAS_ASSET_KINDS,
&["game-art", "icon-spec", "ui-prototype", "art-spritesheet"]
&[
GameCreationAppAssetKind::Image,
GameCreationAppAssetKind::IconSpec,
GameCreationAppAssetKind::UiDesign,
GameCreationAppAssetKind::IconSpritesheet,
]
);
}
}
@@ -16,7 +16,7 @@ pub(in crate::agent) fn art_manifest_content() -> String {
"status": "generated",
"assets": [{
"path": ART_SPRITESHEET_PATH,
"kind": "art-spritesheet",
"kind": GameCreationAppAssetKind::IconSpritesheet.as_str(),
"usage": REQUIRED_SLICE_USAGES,
}],
"sliceManifest": ART_SLICE_MANIFEST_PATH,
@@ -8,12 +8,12 @@ pub(in crate::agent) fn autonomous_registered_derived_visuals_need_repair_at(roo
(
"design-foundation",
"assets/ui-prototype.png",
"ui-prototype",
GameCreationAppAssetKind::UiDesign,
),
(
"art-asset-plan",
"assets/art-spritesheet.png",
"art-spritesheet",
GameCreationAppAssetKind::IconSpritesheet,
),
]
.into_iter()
@@ -644,9 +644,9 @@ fn standalone_platform_art_generation_ledger_identity_is_valid(
};
let direct_identity_is_valid = if agent_id == "direct-codex-art" {
let expected_asset_kind = match run_id {
"art-spec" => Some("icon-spec"),
"game-background" => Some("game-background"),
"art-spritesheet" => Some("art-spritesheet"),
"art-spec" => Some(GameCreationAppAssetKind::IconSpec),
"game-background" => Some(GameCreationAppAssetKind::Scene),
"art-spritesheet" => Some(GameCreationAppAssetKind::IconSpritesheet),
_ => None,
};
expected_asset_kind.is_some_and(|asset_kind| {
@@ -655,7 +655,9 @@ fn standalone_platform_art_generation_ledger_identity_is_valid(
&& string_field("source") == Some("direct-codex")
&& string_field("actionId") == Some(format!("direct-taonier-{run_id}").as_str())
&& string_field("actionFingerprint")
== Some(format!("direct-taonier-art-v1:{asset_kind}:{run_id}").as_str())
== Some(
format!("direct-taonier-art-v1:{}:{run_id}", asset_kind.as_str()).as_str(),
)
})
} else {
false
@@ -1561,10 +1563,14 @@ mod orphaned_external_generation_recovery_tests {
fs::create_dir_all(workflow_path.parent().expect("workflow parent"))
.expect("workflow directory");
let rollback_assets = [
("assets/art-spec.png", "icon-spec", b"old-spec".as_slice()),
(
"assets/art-spec.png",
GameCreationAppAssetKind::IconSpec,
b"old-spec".as_slice(),
),
(
"assets/direct-game-background.png",
"game-background",
GameCreationAppAssetKind::Scene,
b"old-background".as_slice(),
),
]
@@ -1702,7 +1708,7 @@ mod orphaned_external_generation_recovery_tests {
write_complete_stage(serde_json::json!({
"prompt": "Direct art spec",
"kind": "spec",
"assetKind": "game-background",
"assetKind": "scene",
"projectId": "canvas-project",
"assetFolderId": "canvas-assets",
"generationInputs": {

Some files were not shown because too many files have changed in this diff Show More