删除资源 kind 兼容白名单
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m4s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m4s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
服务端图片快速编辑改用共享枚举静态图片判定 移除退役 kind 的兼容放行与冗余回归测试 清理客户端恒假 art-spritesheet 判断并统一远端 kind 解析 同步 AGC kind 里程碑、实施计划与决策记录
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import type { FileManagerFile } from '@cubone/react-file-manager';
|
||||
import type { ComponentType, ReactNode } from 'react';
|
||||
|
||||
import type { GameCreationAppAssetKind } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import {
|
||||
type GameCreationAppAssetKind,
|
||||
parseGameCreationAppAssetKind,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
|
||||
export type ImportedAsset = {
|
||||
id: string;
|
||||
@@ -374,7 +377,13 @@ export function buildRemoteFolderFiles(
|
||||
asset.imageSrc ||
|
||||
'platform-image',
|
||||
localPath,
|
||||
assetKind: asset.assetKind ?? null,
|
||||
assetKind:
|
||||
asset.assetKind == null
|
||||
? null
|
||||
: parseGameCreationAppAssetKind(
|
||||
asset.assetKind,
|
||||
'asset-importer.remote.assetKind',
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ import type {
|
||||
import { GAME_CREATION_APP_UI_DESIGN_ASSET_KIND } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import {
|
||||
isGameCreationAppAssetKind,
|
||||
isGameCreationAppAssetVisualKind,
|
||||
isGameCreationAppUiDesignDocAsset,
|
||||
parseGameCreationAppAssetKind,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
@@ -3398,7 +3399,8 @@ export default function ProjectDevelopmentView({
|
||||
);
|
||||
const hasRegisteredArtImageAssets = manifest.assets.some(
|
||||
(asset) =>
|
||||
asset.kind === 'art-spritesheet' && asset.mediaType.startsWith('image/'),
|
||||
asset.mediaType.startsWith('image/') &&
|
||||
isGameCreationAppAssetVisualKind(asset.kind),
|
||||
);
|
||||
const allAgentSummaries = [
|
||||
summarizeAgent(manifest, 'design', '设计实现 Agent'),
|
||||
|
||||
@@ -26,12 +26,13 @@
|
||||
- `packages/shared/src/contracts/generated/` 作为受控生成目录在 `.prettierrc.json` 中使用与 ts-rs 原始输出一致的格式选项,并在 `.gitattributes` 标记为 `linguist-generated`;生成文件仍参与 Prettier/ESLint 检查,`cargo test export_bindings` 可幂等复现且不再污染工作树。
|
||||
- `Unknown` 表示解析边界无法判定的输入;写入方可以保留该结果,但必须明确决定拒绝写入或交由后续归类,不得把它误写成某个具体 kind。
|
||||
- shell TS 资源引用边界消费生成绑定,资源 kind 集合从生成的 union 穷举派生,并为未知值保留原始值与上下文日志。
|
||||
- api-server 图片快速编辑来源校验改为严格解析 `GameCreationAppAssetKind`,只允许共享枚举的静态图片成员;删除原有 legacy/兼容字符串白名单,退役值不再放行。
|
||||
|
||||
明确不修改:
|
||||
|
||||
- `category`、`mediaType`、`source.kind`、`source.generationKind`;
|
||||
- api-server、SpacetimeDB schema、OpenAPI;
|
||||
- shared-contracts 的 canonical alias 表与读时自愈:本次不新增、不扩展,留给独立词汇收敛切片;
|
||||
- api-server 的其他资源 taxonomy、SpacetimeDB schema、OpenAPI;本切片只改图片快速编辑的 GameCreationApp manifest 来源校验。
|
||||
- shared-contracts 的 canonical alias 表与读时自愈:alias 表已删除,严格解析和 `Unknown` 口径保持不变;
|
||||
- `packages/shared` 中重复的资源 kind 手写契约与其余前端裸 `string` 字段(后续切片)。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- server-side 其他 `asset_kind`、External API DTO、SpacetimeDB schema。
|
||||
- server-side 其他 `asset_kind`、External API DTO、SpacetimeDB schema;仅纳入 api-server 图片快速编辑对 GameCreationApp manifest 来源的严格校验。
|
||||
- `source.kind`、`generationKind`、MIME 和文件格式。
|
||||
- 其他领域字段为配合本里程碑进行“顺手枚举化”。
|
||||
|
||||
@@ -37,4 +37,4 @@
|
||||
- [x] `font`、`audio`、`sound-effect`、`background-music` 均有真实生产路径(字体导入、上传音频推断、资源编辑音频派生)。
|
||||
- [x] ts-rs 绑定由 `cargo test export_bindings` 生成,生成目录加入 `.prettierignore` / eslint `ignorePatterns`,可幂等复现且不污染工作树。
|
||||
- [x] 退役的 `BindingDTO` / `BindingChange` 不再导出,手写的 `exports_ui_editor_types` 已删除。
|
||||
- [ ] 旧 alias 不再由共享契约把非 canonical 输入收口成 canonical(该表属读边界,本次未新增也未扩展,留给独立词汇收敛切片)。
|
||||
- [x] 旧 alias 不再由共享契约把非 canonical 输入收口成 canonical;api-server 图片快速编辑不再保留 legacy 兼容白名单,非 canonical 来源直接失败。
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
|
||||
## 范围
|
||||
|
||||
- 审计 shell 内所有可能表示 GameCreationApp 资源 kind 的字段、构造器、写入边界、筛选器、资源投影、Agent 资源引用和测试 fixture。
|
||||
- 审计 shell 内所有可能表示 GameCreationApp 资源 kind 的字段、构造器、写入边界、筛选器、资源投影、Agent 资源引用和测试 fixture,并收紧 api-server 图片快速编辑的 manifest 来源边界。
|
||||
- 以 api-server 当前仍有效的同语义 kind 作为命名与 serde 值参考,并评估 shell 当前实际生产 kind;排除 api-server legacy、billing/storage taxonomy 和其他开放领域。
|
||||
- 确认 Rust enum、`Unknown`、kebab-case JSON 和 ts-rs 生成文件的归属、生成命令与校验门禁。
|
||||
- 形成逐项 todo:当前值、真实语义、生产/测试状态、目标 enum、是否改写、验证方式。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- 不扫描或修改 server-side 代码;api-server 只提供词汇参考。
|
||||
- 不扫描或修改 api-server 其他领域的 `asset_kind`;图片快速编辑来源校验属于本次 GameCreationApp manifest 边界,必须复用共享枚举且不得保留 legacy 白名单。
|
||||
- 不改 `mediaType`、MIME、文件扩展名、`source.kind`、`source.generationKind`、任务/工作流 kind、路径名或 tracking scope。
|
||||
- 不在本里程碑修改 manifest writer、前端消费方或公开 API。
|
||||
- 不实现旧 manifest migration、alias 表或 fallback 清理。
|
||||
- 不在本里程碑修改 manifest writer;前端消费方仅清理退役 kind 的恒假判断,不扩展公开契约。
|
||||
- 不实现旧 manifest migration;旧值只在共享解析边界收口为 `Unknown`,在图片快速编辑请求边界被拒绝,不增加 alias 表或 fallback。
|
||||
|
||||
## 依赖与前置条件
|
||||
|
||||
@@ -34,10 +34,10 @@
|
||||
## 验收标准
|
||||
|
||||
- [ ] shell 内 manifest 资源 kind 的生产写入点、解析点、投影点和测试 fixture 有完整清单。
|
||||
- [ ] 每个旧值均已标注为:正式 GameCreationApp kind、其他领域字符串、legacy/错误 fallback;没有只按 grep 结果猜测的条目。
|
||||
- [x] 每个旧值均已标注为:正式 GameCreationApp kind、其他领域字符串、legacy/错误 fallback;没有只按 grep 结果猜测的条目。
|
||||
- [ ] api-server 有效值、shell 扩展值和排除值各有明确依据。
|
||||
- [ ] 生成架构不会要求 server-side 默认构建安装 ts-rs。
|
||||
- [ ] todo 清单的每项都有关闭条件和对应验证证据。
|
||||
- [x] todo 清单的每项都有关闭条件和对应验证证据;图片快速编辑不再维护兼容白名单,退役 kind 直接失败。
|
||||
|
||||
## 证据要求
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
- 决策(判据不许散落):切片残留登记只看 `assets/art-spritesheet-slices/` 路径(不再附带 `kind == Icon`);sprite 身份比较忽略随 kind 派生的 `metadata.asset_type`;TS 侧「UI 编辑器文档资产」判据只留 `isGameCreationAppUiDesignDocAsset()` 一份,资源画布入口 / UI 编辑器桥接 / 资源引用缩略图统一调用。
|
||||
- 决策(已知代价,不补救):既有项目里 legacy kind 读入即 `unknown`,依赖 kind 等值比较的运行门禁会按"缺少该资源"处理。这是「不迁移、不 fallback」的必然结果,本次明确不为存量数据做迁移;将来若要迁就必须单独立项(一次性迁移或读侧白名单),不得把别名表加回解析路径。
|
||||
- 决策(留痕必须真的落地):原实现用 `tracing::warn!`,而 AGC 壳没有 tracing subscriber,等于没有日志。现在 `shared-contracts` 只暴露可注册回调 `set_non_canonical_asset_kind_reporter()`,AGC 壳在 `main()` 里接到 `app_log!`,日志同时含原始输入串与调用上下文;`kind-observability` feature 与 `tracing` 依赖一并删除。TS 侧对应 `parseGameCreationAppAssetKind()` 的 `console.warn`。
|
||||
- 决策(平台/画板词汇表):`AGENT_RUNTIME_CANVAS_ASSET_KINDS`(`canvas.asset_generate.assetKind` 的 enum)与 `platform_art_asset_manifest_kind()` 是「平台词汇 → manifest kind」的唯一映射点,保留原词汇但把认不出的原值改为留痕收口;对外 API 的 `EDITOR_IMAGE_EDIT_STATIC_IMAGE_ASSET_KINDS` 白名单保持原样(API 兼容面,不代表客户端归一规则)。
|
||||
- 决策(平台/画板词汇表):`AGENT_RUNTIME_CANVAS_ASSET_KINDS`(`canvas.asset_generate.assetKind` 的 enum)与 `platform_art_asset_manifest_kind()` 是「平台词汇 → manifest kind」的唯一映射点,保留原词汇但把认不出的原值改为留痕收口;图片快速编辑来源直接解析 `GameCreationAppAssetKind` 的 canonical 静态图片成员并要求 `mediaType=image`,原 `EDITOR_IMAGE_EDIT_STATIC_IMAGE_ASSET_KINDS` 兼容白名单已删除,退役 kind 不再作为 GameCreationApp manifest 来源放行。
|
||||
- 验证:`cargo test -p shared-contracts`(含词汇表唯一性、严格性与留痕用例)、`cargo test --locked -p shared-contracts --features ts-bindings export_bindings` 后 `git diff` 为空、AGC bin 定向用例(`platform_art_asset_manifest_kind_maps_platform_vocabulary_explicitly`、`derived_asset_manifest_kind_is_never_unknown_for_text_derivatives`、`non_canonical_manifest_asset_kinds_report_raw_values_only`)、`npx vitest run packages/shared/src/contracts/gameCreationApp.test.ts apps/ai-game-creator-shell/tests/uiDesignResourceBridge.test.ts apps/ai-game-creator-shell/tests/appSurface.test.ts`。
|
||||
- 关联文档:[AI 游戏创作智能体 App 实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md) 的 2026-09-17 节。
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- **严格口径的已知代价**:既有项目 manifest 里若存的是 legacy kind(`"UI"`、`"ui-prototype"`、`"art-spritesheet"`、`"game-background"`…),读入后就是 `unknown`,依赖 kind 等值比较的运行门禁会按「缺少该资源」处理。这是「不迁移、不 fallback」的必然结果,由项目决策接受;若将来要迁就存量数据,必须单独立项(一次性迁移或读侧白名单),不得把别名表加回解析路径。
|
||||
- **留痕走 `app_log!`**:`shared-contracts` 不再依赖 `tracing`(`kind-observability` feature 删除),改为暴露可注册回调 `set_non_canonical_asset_kind_reporter()`;AGC 壳在 `main()` 里注册成 `app_log!`,日志同时含**原始输入串**与调用上下文,用于回查还有谁在写 legacy kind。TS 侧对应 `parseGameCreationAppAssetKind()` 的 `console.warn`。
|
||||
- **分类映射**:`GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND` 改为按 `GameCreationAppAssetKind` 变体穷举(含 `unknown → unclassified`),与 `GameCreationAppAssetKind::ALL` 的对齐由单测 `asset_category_mapping_covers_every_kind` 守住;TS 侧同表按生成 union 穷举。
|
||||
- **画布生成 kind 共用枚举**:`canvas.asset_generate` 的 `assetKind` 白名单直接复用 `GameCreationAppAssetKind::CANVAS_ASSET_KINDS`(当前为 `image / icon-spec / ui-design / icon-spritesheet`),工具 schema、prompt 和执行前校验均从该枚举派生,不再维护 `&[&str]` 字符串目录。平台适配层若仍需要旧的外部请求字面值,只能在发送边界显式映射;manifest 始终写入枚举的 canonical 值。对外 API 的 `EDITOR_IMAGE_EDIT_STATIC_IMAGE_ASSET_KINDS` 白名单保持原样,属 API 兼容面,不代表客户端 kind 归一规则。
|
||||
- **画布生成与图片快速编辑共用枚举**:`canvas.asset_generate` 的 `assetKind` 白名单直接复用 `GameCreationAppAssetKind::CANVAS_ASSET_KINDS`(当前为 `image / icon-spec / ui-design / icon-spritesheet`),工具 schema、prompt 和执行前校验均从该枚举派生,不再维护 `&[&str]` 字符串目录。图片快速编辑来源同样严格解析 `GameCreationAppAssetKind`,只允许 `is_static_image()` 的 canonical 成员并要求 `mediaType=image`;原有 `EDITOR_IMAGE_EDIT_STATIC_IMAGE_ASSET_KINDS` 兼容白名单已删除,退役值不再放行。平台适配层若仍需要其他领域的外部请求字面值,只能在发送边界显式映射;manifest 始终写入枚举的 canonical 值。
|
||||
- **版本素材替换**:`subtypeEqual` 改为枚举相等(不再经别名字符串归一),`categoryEqual` 仍用 `game_creation_app_asset_effective_category` 的读时口径。
|
||||
|
||||
## 2026-09-15 GameCreationApp 资源 kind 枚举化(当前权威口径)
|
||||
|
||||
@@ -38,6 +38,7 @@ use shared_contracts::editor_generation::{
|
||||
editor_generation_stable_asset_id, editor_generation_stable_asset_object_id,
|
||||
editor_generation_stable_resource_id,
|
||||
};
|
||||
use shared_contracts::game_creation_app::GameCreationAppAssetKind;
|
||||
use shared_kernel::build_prefixed_uuid_id;
|
||||
use spacetime_client::editor_project::{
|
||||
EditorGenerationResultPersistItemRecordInput, EditorGenerationResultPersistRecord,
|
||||
@@ -4110,52 +4111,19 @@ fn align_editor_image_edit_dimension(value: u32) -> u32 {
|
||||
value.saturating_add(15) / 16 * 16
|
||||
}
|
||||
|
||||
/// 图片快速编辑端点允许的**静态图**来源类型。
|
||||
///
|
||||
/// 口径:以「媒体类型为静态图的 canonical 类型」为权威集合,并额外收下平台侧按本地
|
||||
/// manifest 原始类型登记来源资源时会出现的等价写法。kind 词汇表本身以
|
||||
/// `server-rs/crates/shared-contracts/src/game_creation_app/asset_kind.rs` 的
|
||||
/// `GameCreationAppAssetKind` 为唯一真源(严格解析、无别名表),这里保持的是**对外 API 的
|
||||
/// 兼容白名单**(按库原始值放行),不参与、也不代表客户端的 kind 归一规则。
|
||||
///
|
||||
/// 必须与 `media_type == image` 一起构成 AND 门:视频、音频、序列帧等非静态媒体即使挂着
|
||||
/// 图片类 assetKind 也照旧拒绝。
|
||||
pub(crate) const EDITOR_IMAGE_EDIT_STATIC_IMAGE_ASSET_KINDS: [&str; 18] = [
|
||||
// canonical 静态图类型
|
||||
"image",
|
||||
"scene",
|
||||
"character",
|
||||
"icon",
|
||||
"icon-spritesheet",
|
||||
"icon-spec",
|
||||
"ui-design",
|
||||
"publication-material",
|
||||
"spec",
|
||||
// 指向上述 canonical 类型的合法 legacy 类型
|
||||
"game-background",
|
||||
"character-art",
|
||||
"game-art",
|
||||
"illustration",
|
||||
"art-spritesheet",
|
||||
"art-spritesheet-slice",
|
||||
"ui-prototype",
|
||||
// AGC 本地 manifest 等在用的等价静态图类型
|
||||
"ui",
|
||||
// `register_local_asset_entry`(assets.rs)空 kind 兜底写的是 `"asset"`:
|
||||
// 未归类上传的 PNG 会以 `kind: "asset"` 登记,而共享契约把 `"asset"` canonical 化成
|
||||
// `"image"`,漏掉它就等于把「未归类静态图」挡在快速编辑之外(400)。
|
||||
// 白名单必须在 canonical 静态图别名下封闭。
|
||||
"asset",
|
||||
];
|
||||
|
||||
pub(crate) fn ensure_editor_image_edit_source_kind_allowed(
|
||||
asset_kind: Option<&str>,
|
||||
media_type: Option<&str>,
|
||||
) -> Result<(), AppError> {
|
||||
let asset_kind = asset_kind.map(str::trim).filter(|value| !value.is_empty());
|
||||
let asset_kind = asset_kind.filter(|value| !value.trim().is_empty());
|
||||
let media_type = media_type.map(str::trim).filter(|value| !value.is_empty());
|
||||
let asset_kind_allowed = asset_kind
|
||||
.is_none_or(|asset_kind| EDITOR_IMAGE_EDIT_STATIC_IMAGE_ASSET_KINDS.contains(&asset_kind));
|
||||
let asset_kind_allowed = asset_kind.is_none_or(|asset_kind| {
|
||||
GameCreationAppAssetKind::parse_with_context(
|
||||
asset_kind,
|
||||
"editor-image-edit.source.asset_kind",
|
||||
)
|
||||
.is_static_image()
|
||||
});
|
||||
let media_type_allowed = matches!(media_type, None | Some("image"));
|
||||
if asset_kind_allowed && media_type_allowed {
|
||||
return Ok(());
|
||||
@@ -20749,7 +20717,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_image_edit_source_kind_whitelist_covers_static_image_kinds_only() {
|
||||
fn editor_image_edit_source_kind_policy_covers_static_image_kinds_only() {
|
||||
let cases = [
|
||||
(None, None, true),
|
||||
(None, Some("image"), true),
|
||||
@@ -20762,16 +20730,6 @@ mod tests {
|
||||
(Some("publication-material"), Some("image"), true),
|
||||
(Some("ui-design"), Some("image"), true),
|
||||
(Some("scene"), Some("image"), true),
|
||||
// 对外 API 兼容白名单里的平台原始写法:平台按本地 manifest 原始类型登记来源资源时
|
||||
// 会出现这些值,实测 `art-spritesheet` / `ui` 曾整片 400。
|
||||
(Some("art-spritesheet"), Some("image"), true),
|
||||
(Some("art-spritesheet-slice"), Some("image"), true),
|
||||
(Some("ui"), Some("image"), true),
|
||||
(Some("ui-prototype"), Some("image"), true),
|
||||
(Some("game-art"), Some("image"), true),
|
||||
(Some("game-background"), Some("image"), true),
|
||||
(Some("character-art"), Some("image"), true),
|
||||
(Some("illustration"), Some("image"), true),
|
||||
// 非静态媒体与非静态类型必须继续拒绝。
|
||||
(Some("character-animation"), Some("image-sequence"), false),
|
||||
(Some("sound-effect"), Some("audio"), false),
|
||||
@@ -20783,8 +20741,6 @@ mod tests {
|
||||
(Some("future-asset-kind"), Some("image"), false),
|
||||
(None, Some("future-media-type"), false),
|
||||
// mediaType 是 AND 门:图片类 assetKind 挂非静态媒体照旧拒绝。
|
||||
(Some("art-spritesheet"), Some("video"), false),
|
||||
(Some("ui"), Some("audio"), false),
|
||||
(Some("icon"), Some("image-sequence"), false),
|
||||
(Some("scene"), Some("image-sequence"), false),
|
||||
];
|
||||
@@ -20793,13 +20749,13 @@ mod tests {
|
||||
assert_eq!(
|
||||
result.is_ok(),
|
||||
expected_allowed,
|
||||
"unexpected whitelist result for assetKind={asset_kind:?}, mediaType={media_type:?}",
|
||||
"unexpected source-kind policy result for assetKind={asset_kind:?}, mediaType={media_type:?}",
|
||||
);
|
||||
}
|
||||
|
||||
let error =
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("art-spritesheet"), Some("video"))
|
||||
.expect_err("非静态媒体即使挂着图片类 assetKind 也不得支持快速编辑");
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("future-kind"), Some("image"))
|
||||
.expect_err("未知 kind 不得作为快速编辑来源");
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("message")),
|
||||
@@ -20912,10 +20868,6 @@ mod tests {
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("icon"), Some("image")).is_ok(),
|
||||
"icon 是共享契约里的静态图 canonical 类型,与 generationInputs.source 无关"
|
||||
);
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("art-spritesheet"), Some("image"))
|
||||
.is_ok()
|
||||
);
|
||||
assert!(ensure_editor_image_edit_source_kind_allowed(Some("icon"), Some("video")).is_err());
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("future-kind"), Some("image"))
|
||||
@@ -20923,132 +20875,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 实测 400×5 的回归用例:AGC「图片快速编辑」发出的请求,来源资源的 assetKind 是本地
|
||||
/// manifest 原始类型 `art-spritesheet` / `ui` / `asset`(mediaType=image)。这条路径必须
|
||||
/// 放行;非静态媒体(video/audio/image-sequence)必须继续拒绝。
|
||||
///
|
||||
/// 夹具必须真的走进被测路径,不能是自洽空断言:请求体先反序列化成
|
||||
/// `EditorImageEditRequest`,再从**请求体自己的** `generationInputs.assetKind` 取类型喂给
|
||||
/// 放行判据;并用源码级闸门断言钉住
|
||||
/// `resolve_editor_image_edit_source` / `ensure_editor_image_edit_target_matches_source`
|
||||
/// 两个函数体内不得再出现 `generation_inputs`(历史上 `generationInputs.source` 参与过
|
||||
/// gating,把 AGC 请求整体挡成 400;解析与对账需要 DB/OSS,源码级断言是无需基础设施
|
||||
/// 也能钉住这条回归的唯一位置)。
|
||||
#[test]
|
||||
fn game_creator_client_quick_edit_accepts_local_manifest_static_image_kinds() {
|
||||
// 与客户端 `submit_resource_edit_remote` 发送的 JSON 同形。
|
||||
let agc_payload: EditorImageEditRequest = serde_json::from_str(
|
||||
r#"{
|
||||
"prompt": "把这张图改成夜间配色",
|
||||
"sourceReferenceId": "resource-art-spritesheet",
|
||||
"generationInputs": {
|
||||
"source": "ai-game-creator-client",
|
||||
"assetKind": "art-spritesheet"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("AGC quick edit payload should deserialize into the request DTO");
|
||||
assert_eq!(
|
||||
agc_payload.source_reference_id, "resource-art-spritesheet",
|
||||
"回归夹具必须保持 AGC 客户端实际发送的请求形状"
|
||||
);
|
||||
assert_eq!(
|
||||
agc_payload.target_layer_id, None,
|
||||
"AGC 快速编辑不带 targetLayerId,走的是「主来源即目标」分支"
|
||||
);
|
||||
// 关键:放行判据只吃来源资源的权威 assetKind 与 mediaType。这里用请求体自己声明的
|
||||
// assetKind 驱动判据——请求体里同时带着 `generationInputs.source`,它不得参与 gating。
|
||||
let requested_kind = agc_payload
|
||||
.generation_inputs
|
||||
.as_ref()
|
||||
.and_then(|inputs| inputs.pointer("/assetKind"))
|
||||
.and_then(Value::as_str);
|
||||
assert_eq!(
|
||||
requested_kind,
|
||||
Some("art-spritesheet"),
|
||||
"回归夹具必须保持 AGC 客户端实际发送的请求形状"
|
||||
);
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed(
|
||||
normalize_editor_image_edit_resolved_source_kind(requested_kind).as_deref(),
|
||||
Some("image"),
|
||||
)
|
||||
.is_ok(),
|
||||
"AGC 快速编辑请求不得因为 generationInputs.source 被拒"
|
||||
);
|
||||
|
||||
// 源码级闸门:请求解析与目标对账都不得重新引入 generationInputs 门槛。
|
||||
let source = include_str!("editor_project.rs");
|
||||
assert_function_not_contains(
|
||||
source,
|
||||
"async fn resolve_editor_image_edit_source(",
|
||||
"fn ensure_editor_image_edit_source_snapshot_matches(",
|
||||
&["generation_inputs", "generationInputs"],
|
||||
);
|
||||
assert_function_not_contains(
|
||||
source,
|
||||
"fn ensure_editor_image_edit_target_matches_source(",
|
||||
"async fn resolve_editor_image_edit_source(",
|
||||
&["generation_inputs", "generationInputs"],
|
||||
);
|
||||
|
||||
for (asset_kind, media_type) in [
|
||||
("art-spritesheet", "image"),
|
||||
("ui", "image"),
|
||||
("asset", "image"),
|
||||
("ui-prototype", "image"),
|
||||
("game-art", "image"),
|
||||
("game-background", "image"),
|
||||
("icon", "image"),
|
||||
] {
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed(Some(asset_kind), Some(media_type))
|
||||
.is_ok(),
|
||||
"AGC 快速编辑必须放行 {asset_kind}/{media_type}"
|
||||
);
|
||||
}
|
||||
|
||||
for (asset_kind, media_type) in [
|
||||
("art-spritesheet", "video"),
|
||||
("ui", "audio"),
|
||||
("asset", "video"),
|
||||
("video", "video"),
|
||||
("sound-effect", "audio"),
|
||||
("background-music", "audio"),
|
||||
("character-animation", "image-sequence"),
|
||||
("future-kind", "image"),
|
||||
] {
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed(Some(asset_kind), Some(media_type))
|
||||
.is_err(),
|
||||
"非静态来源必须继续拒绝:{asset_kind}/{media_type}"
|
||||
);
|
||||
}
|
||||
|
||||
// 400 错误体必须带上真实原因,客户端才能诊断(对应 AGC 的
|
||||
// `editor_api_rejection_reason` 取值顺序修正)。
|
||||
let error =
|
||||
ensure_editor_image_edit_source_kind_allowed(Some("future-kind"), Some("image"))
|
||||
.expect_err("未知类型必须拒绝");
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("provider")),
|
||||
Some(&json!("editor-image-edit"))
|
||||
);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("message")),
|
||||
Some(&json!("当前素材类型不支持图片快速编辑"))
|
||||
);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("assetKind")),
|
||||
Some(&json!("future-kind"))
|
||||
);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("mediaType")),
|
||||
Some(&json!("image"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_image_edit_target_binding_prefers_object_id_and_fails_closed() {
|
||||
let target = EditorImageEditTargetLayerSource {
|
||||
|
||||
@@ -111,6 +111,22 @@ macro_rules! game_creation_app_asset_kinds {
|
||||
)
|
||||
}
|
||||
|
||||
/// 可作为单张静态图片来源的资源族。
|
||||
pub const fn is_static_image(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Image
|
||||
| Self::Scene
|
||||
| Self::Character
|
||||
| Self::Icon
|
||||
| Self::IconSpritesheet
|
||||
| Self::IconSpec
|
||||
| Self::UiDesign
|
||||
| Self::PublicationMaterial
|
||||
| Self::Spec
|
||||
)
|
||||
}
|
||||
|
||||
/// 严格解析:只认 canonical 值,其余(含大小写、空白变体)一律 `Unknown`。
|
||||
///
|
||||
/// 私有实现细节,**不是**解析入口:外部一律走 [`Self::parse_with_context`],
|
||||
@@ -320,7 +336,7 @@ mod tests {
|
||||
#[test]
|
||||
fn unknown_wire_values_are_closed_over_unknown() {
|
||||
let _guard = record_reports();
|
||||
for raw in ["asset", "ui", "game-background", "future-kind"] {
|
||||
for raw in ["future-kind", "not-a-kind", "IMAGE", " image "] {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<GameCreationAppAssetKind>(&format!("\"{raw}\"")).unwrap(),
|
||||
GameCreationAppAssetKind::Unknown
|
||||
@@ -381,15 +397,15 @@ mod tests {
|
||||
GameCreationAppAssetKind::parse_with_context("unknown", "unit-test.vocabulary"),
|
||||
GameCreationAppAssetKind::Unknown
|
||||
);
|
||||
for raw in ["UI", "ui", " image ", "Image", "art-spritesheet-slice"] {
|
||||
for raw in ["IMAGE", " image ", "Image", "future-kind"] {
|
||||
assert_eq!(
|
||||
GameCreationAppAssetKind::parse_with_context(raw, "unit-test.vocabulary"),
|
||||
GameCreationAppAssetKind::Unknown,
|
||||
"{raw} 不许命中 canonical 值"
|
||||
);
|
||||
}
|
||||
// 每个非 canonical 变体都要留痕(5 条大小写/空白/legacy 变体都算异常)。
|
||||
assert_eq!(recorded_reports().len(), 5);
|
||||
// 每个非 canonical 变体都要留痕。
|
||||
assert_eq!(recorded_reports().len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -426,5 +442,25 @@ mod tests {
|
||||
GameCreationAppAssetKind::UiDesign,
|
||||
]
|
||||
);
|
||||
|
||||
let static_images = GameCreationAppAssetKind::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|kind| kind.is_static_image())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
static_images,
|
||||
vec![
|
||||
GameCreationAppAssetKind::Image,
|
||||
GameCreationAppAssetKind::Scene,
|
||||
GameCreationAppAssetKind::Character,
|
||||
GameCreationAppAssetKind::Icon,
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
GameCreationAppAssetKind::UiDesign,
|
||||
GameCreationAppAssetKind::PublicationMaterial,
|
||||
GameCreationAppAssetKind::Spec,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user