模板库假数据注入改为 Cargo feature 控制
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m37s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m59s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m50s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m58s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m36s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m56s
Project CI / Frontend tests (pull_request) Failing after 4m59s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m56s
Project CI / Native shell tests (pull_request) Successful in 8m58s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m37s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m59s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m50s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m58s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m36s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m56s
Project CI / Frontend tests (pull_request) Failing after 4m59s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m56s
Project CI / Native shell tests (pull_request) Successful in 8m58s
- 新增 feature template-library-fixtures(默认关闭):关闭时 apply_template_library_fixtures 为恒等透传,正式产物不含注入分支 - 注入实现下沉为 cfg 模块:按真实清单循环补齐假数据(唯一 id/标题/封面地址,标签追加批次分组,安装态 1/3 混合),条数由 AGC_TEMPLATE_LIBRARY_SYNTHETIC_COUNT 控制(默认 1000) - 两种编译模式各补一条用例:默认构建必须恒等透传,开启 feature 必须补齐到配置条数;另补假数据识别/计数解析单测 - 前端补 1000 条渲染回归:1000 张卡片渲染 + 已安装/标签/关键词过滤数量自洽 - 技术方案补「本地压测假数据注入」与 1000 条实测结论(标签条膨胀、一次性渲染 1000 卡片与封面请求需后续收口)
This commit is contained in:
@@ -6,6 +6,9 @@ publish = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# 模板库假数据注入(仅本地页面压测/演示用):只有显式开启该 feature 才会编译并在读取清单后
|
||||
# 把条目循环补齐成假数据;计数由 AGC_TEMPLATE_LIBRARY_SYNTHETIC_COUNT 控制(默认 1000)。
|
||||
template-library-fixtures = []
|
||||
cocos-editor = ["cocos-editor-bridge/process-discovery"]
|
||||
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
|
||||
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
|
||||
|
||||
@@ -359,6 +359,153 @@ fn to_entry(
|
||||
})
|
||||
}
|
||||
|
||||
/// 本地假数据注入:只在 `template-library-fixtures` feature(或测试构建)下编译。
|
||||
///
|
||||
/// 开启时把真实清单循环补齐成假数据(条数见 `fixtures::synthetic_template_count`);
|
||||
/// 未开启时是恒等透传,正式构建里没有任何注入分支。
|
||||
fn apply_template_library_fixtures(entries: Vec<GameTemplateEntry>) -> Vec<GameTemplateEntry> {
|
||||
#[cfg(feature = "template-library-fixtures")]
|
||||
{
|
||||
return fixtures::pad_synthetic_templates(entries, fixtures::synthetic_template_count());
|
||||
}
|
||||
#[cfg(not(feature = "template-library-fixtures"))]
|
||||
{
|
||||
entries
|
||||
}
|
||||
}
|
||||
|
||||
/// 本地假数据注入实现:只在 `template-library-fixtures` feature(或测试构建)下编译。
|
||||
///
|
||||
/// 位置刻意放在 Rust 侧:模板库的清单校验、安装状态与建项目都在这一侧,TS 只消费快照做渲染;
|
||||
/// 在这里注入才能压到与真实一致的整条链路。正式构建不含该模块,因此不存在误触发路径。
|
||||
#[cfg(any(test, feature = "template-library-fixtures"))]
|
||||
pub(crate) mod fixtures {
|
||||
use super::*;
|
||||
|
||||
pub(crate) const SYNTHETIC_COUNT_ENV: &str = "AGC_TEMPLATE_LIBRARY_SYNTHETIC_COUNT";
|
||||
pub(crate) const DEFAULT_SYNTHETIC_COUNT: usize = 1_000;
|
||||
const MAX_SYNTHETIC_COUNT: usize = 20_000;
|
||||
|
||||
/// 假数据条数:环境变量优先,缺省 1000;0 表示不注入。
|
||||
pub(crate) fn synthetic_template_count() -> usize {
|
||||
parse_synthetic_count(std::env::var(SYNTHETIC_COUNT_ENV).ok().as_deref())
|
||||
}
|
||||
|
||||
fn parse_synthetic_count(raw: Option<&str>) -> usize {
|
||||
raw.and_then(|value| value.trim().parse::<usize>().ok())
|
||||
.unwrap_or(DEFAULT_SYNTHETIC_COUNT)
|
||||
.min(MAX_SYNTHETIC_COUNT)
|
||||
}
|
||||
|
||||
/// 把真实清单循环复制成指定条数:id/标题/封面地址唯一,安装态按 1/3 混合。
|
||||
pub(crate) fn pad_synthetic_templates(
|
||||
entries: Vec<GameTemplateEntry>,
|
||||
target: usize,
|
||||
) -> Vec<GameTemplateEntry> {
|
||||
if target <= entries.len() || entries.is_empty() {
|
||||
return entries;
|
||||
}
|
||||
let base = entries.clone();
|
||||
let mut padded = entries;
|
||||
let mut index = padded.len();
|
||||
while padded.len() < target {
|
||||
let source = &base[index % base.len()];
|
||||
let installed = index % 3 == 0;
|
||||
let mut next = source.clone();
|
||||
next.id = format!("{}-{:04}", source.id, index);
|
||||
next.title = format!("{} · 假数据 {index:04}", source.title);
|
||||
let mut tags = source.tags.clone();
|
||||
tags.push(format!("批次-{:02}", index % 20));
|
||||
next.tags = tags;
|
||||
next.cover_url = format!("{}?synthetic={index}", source.cover_url);
|
||||
next.installed = installed;
|
||||
next.installed_version = installed.then(|| source.template_version.clone());
|
||||
next.installed_at_millis = installed.then(|| now_millis());
|
||||
padded.push(next);
|
||||
index += 1;
|
||||
}
|
||||
padded
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn base_entry(id: &str, tag: &str) -> GameTemplateEntry {
|
||||
GameTemplateEntry {
|
||||
id: id.to_string(),
|
||||
title: format!("模板 {id}"),
|
||||
summary: "假数据基础条目".to_string(),
|
||||
tags: vec![tag.to_string()],
|
||||
runtime: "html".to_string(),
|
||||
engine: "phaser".to_string(),
|
||||
engine_version: "4.2.1".to_string(),
|
||||
template_version: "0.1.0".to_string(),
|
||||
updated_at: "2026-09-17T00:00:00Z".to_string(),
|
||||
entry: "game/index.html".to_string(),
|
||||
zip_url: format!("https://oss.example/templates/v1/{id}/template.zip"),
|
||||
zip_size_bytes: 1024,
|
||||
zip_sha256: "a".repeat(64),
|
||||
cover_url: format!("https://oss.example/templates/v1/{id}/cover.svg"),
|
||||
cover_width: 960,
|
||||
cover_height: 540,
|
||||
installed: false,
|
||||
installed_version: None,
|
||||
installed_at_millis: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_synthetic_count_from_env_value() {
|
||||
assert_eq!(parse_synthetic_count(None), DEFAULT_SYNTHETIC_COUNT);
|
||||
assert_eq!(parse_synthetic_count(Some(" 250 ")), 250);
|
||||
assert_eq!(parse_synthetic_count(Some("0")), 0);
|
||||
assert_eq!(
|
||||
parse_synthetic_count(Some("not-a-number")),
|
||||
DEFAULT_SYNTHETIC_COUNT
|
||||
);
|
||||
assert_eq!(parse_synthetic_count(Some("999999")), MAX_SYNTHETIC_COUNT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pads_entries_with_unique_identity_and_mixed_install_state() {
|
||||
let base = vec![
|
||||
base_entry("blank-web", "空白"),
|
||||
base_entry("blank-2d", "2d"),
|
||||
];
|
||||
let padded = pad_synthetic_templates(base.clone(), 9);
|
||||
assert_eq!(padded.len(), 9);
|
||||
// 真实条目保持原样排在最前。
|
||||
assert_eq!(padded[0].id, "blank-web");
|
||||
assert_eq!(padded[1].id, "blank-2d");
|
||||
let ids = padded
|
||||
.iter()
|
||||
.map(|entry| entry.id.clone())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(ids.len(), 9, "假数据 id 必须唯一");
|
||||
let covers = padded
|
||||
.iter()
|
||||
.map(|entry| entry.cover_url.clone())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(covers.len(), 9, "假数据封面地址必须唯一");
|
||||
assert!(padded.iter().any(|entry| entry.installed));
|
||||
assert!(padded.iter().any(|entry| !entry.installed));
|
||||
assert!(padded
|
||||
.iter()
|
||||
.skip(2)
|
||||
.all(|entry| entry.tags.iter().any(|tag| tag.starts_with("批次-"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padding_is_a_no_op_without_room_to_fill() {
|
||||
let base = vec![base_entry("blank-web", "空白")];
|
||||
assert_eq!(pad_synthetic_templates(base.clone(), 1).len(), 1);
|
||||
assert_eq!(pad_synthetic_templates(base.clone(), 0).len(), 1);
|
||||
assert!(pad_synthetic_templates(Vec::new(), 10).is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_template_library_client() -> reqwest::Client {
|
||||
reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
@@ -624,6 +771,7 @@ pub(crate) async fn fetch_game_template_library(
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let entries = apply_template_library_fixtures(entries);
|
||||
Ok(GameTemplateLibrarySnapshot {
|
||||
schema_version: header.schema_version,
|
||||
library: header.library,
|
||||
@@ -1013,6 +1161,38 @@ mod tests {
|
||||
assert!(error.contains("模板尚未安装"), "{error}");
|
||||
}
|
||||
|
||||
/// 正式构建(未开 feature)必须恒等透传:注入路径不能出现在默认产物里。
|
||||
#[cfg(not(feature = "template-library-fixtures"))]
|
||||
#[test]
|
||||
fn fixtures_are_inert_without_the_feature() {
|
||||
let index_body = include_str!("../tests/fixtures/agc-template-library-index.json");
|
||||
let (_, templates) =
|
||||
parse_game_template_library_index(index_body).expect("parse fixture index");
|
||||
let entries = templates
|
||||
.iter()
|
||||
.map(|summary| to_entry(summary, None).expect("entry"))
|
||||
.collect::<Vec<_>>();
|
||||
let original = entries.len();
|
||||
let applied = apply_template_library_fixtures(entries);
|
||||
assert_eq!(applied.len(), original, "默认构建不应注入假数据");
|
||||
}
|
||||
|
||||
/// 开启 feature 后同一次调用必须补齐到配置条数。
|
||||
#[cfg(feature = "template-library-fixtures")]
|
||||
#[test]
|
||||
fn fixtures_expand_entries_when_the_feature_is_enabled() {
|
||||
let index_body = include_str!("../tests/fixtures/agc-template-library-index.json");
|
||||
let (_, templates) =
|
||||
parse_game_template_library_index(index_body).expect("parse fixture index");
|
||||
let entries = templates
|
||||
.iter()
|
||||
.map(|summary| to_entry(summary, None).expect("entry"))
|
||||
.collect::<Vec<_>>();
|
||||
let applied = apply_template_library_fixtures(entries);
|
||||
assert_eq!(applied.len(), fixtures::synthetic_template_count());
|
||||
assert!(applied.len() > templates.len());
|
||||
}
|
||||
|
||||
/// 可选的真连检查:`cargo test --bin genarrative-ai-game-creator-shell template_library -- --ignored`。
|
||||
/// 默认跳过,避免离线环境因网络失败误报。
|
||||
#[tokio::test]
|
||||
|
||||
@@ -6,6 +6,11 @@ import type {
|
||||
GameTemplateEntry,
|
||||
TemplateLibraryFilters,
|
||||
} from '../src/features/template-library/templateLibraryModel';
|
||||
import {
|
||||
collectGameTemplateTags,
|
||||
EMPTY_TEMPLATE_LIBRARY_FILTERS,
|
||||
filterGameTemplates,
|
||||
} from '../src/features/template-library/templateLibraryModel';
|
||||
import type { TemplateLibraryController } from '../src/features/template-library/useTemplateLibrary';
|
||||
import TemplateRecommendations from '../src/view/home/TemplateRecommendations';
|
||||
import TemplateLibraryView from '../src/view/template-library';
|
||||
@@ -327,6 +332,62 @@ describe('TemplateLibraryView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('大库量渲染(1000 条假数据)', () => {
|
||||
const bulk = Array.from({ length: 1000 }, (_, index) =>
|
||||
template({
|
||||
id: `bulk-${index}`,
|
||||
title: `批量模板 ${index}`,
|
||||
summary: '压测条目',
|
||||
tags: ['起步工程', `批次-${String(index % 20).padStart(2, '0')}`],
|
||||
installed: index % 3 === 0,
|
||||
installedVersion: index % 3 === 0 ? '0.1.0' : null,
|
||||
}),
|
||||
);
|
||||
|
||||
it('renders every card and keeps filters consistent at 1000 entries', () => {
|
||||
const { container } = render(
|
||||
<TemplateLibraryView
|
||||
controller={controller({
|
||||
templates: bulk,
|
||||
visibleTemplates: bulk,
|
||||
installedCount: bulk.filter((entry) => entry.installed).length,
|
||||
tagOptions: collectGameTemplateTags(bulk),
|
||||
})}
|
||||
onBack={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll('article')).toHaveLength(1000);
|
||||
expect(screen.getByText('共 1000 个模板 · 已下载 334 个')).toBeTruthy();
|
||||
// 标签筛选条会随库量膨胀,这里先记录当前聚合出来的规模(1000 条 × 批次标签)。
|
||||
const tagButtons = screen
|
||||
.getAllByRole('button')
|
||||
.filter((button) =>
|
||||
button.getAttribute('aria-label')?.startsWith('标签筛选'),
|
||||
);
|
||||
expect(tagButtons.length).toBeGreaterThan(20);
|
||||
|
||||
// 纯前端筛选在大库量下仍然是 O(n) 的一遍过滤,数量与已安装态自洽。
|
||||
const installedOnly = filterGameTemplates(bulk, {
|
||||
...EMPTY_TEMPLATE_LIBRARY_FILTERS,
|
||||
installedOnly: true,
|
||||
});
|
||||
expect(installedOnly).toHaveLength(334);
|
||||
expect(
|
||||
filterGameTemplates(bulk, {
|
||||
...EMPTY_TEMPLATE_LIBRARY_FILTERS,
|
||||
tags: ['批次-07'],
|
||||
}),
|
||||
).toHaveLength(50);
|
||||
expect(
|
||||
filterGameTemplates(bulk, {
|
||||
...EMPTY_TEMPLATE_LIBRARY_FILTERS,
|
||||
query: '批量模板 999',
|
||||
}).map((entry) => entry.id),
|
||||
).toEqual(['bulk-999']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TemplateRecommendations', () => {
|
||||
it('renders the recommended templates and opens the library', () => {
|
||||
const onOpenLibrary = vi.fn();
|
||||
|
||||
@@ -75,6 +75,32 @@ templates/
|
||||
|
||||
## 验收与验证
|
||||
|
||||
## 本地压测假数据注入(feature 控制)
|
||||
|
||||
模板库的数据源在 Rust 侧(清单校验、安装状态、下载与建项目都在这里),TS 只消费快照做渲染,所以假数据注入也放在 Rust 侧,走与真实完全一致的链路。
|
||||
|
||||
- 开关:Cargo feature `template-library-fixtures`(**默认关闭**)。关闭时 `apply_template_library_fixtures` 是恒等透传,正式产物里不存在注入分支,并有单测保证这一点。
|
||||
- 条数:环境变量 `AGC_TEMPLATE_LIBRARY_SYNTHETIC_COUNT`(默认 1000;`0` 表示不注入;上限 20000)。
|
||||
- 假数据特征:真实条目保留在最前,其余按真实条目循环复制;`id`/标题唯一,封面地址追加 `?synthetic=N`(强制逐张请求,模拟“每个模板各自封面”);标签追加 `批次-00..19`;安装态按 1/3 混合。
|
||||
- 运行方式:
|
||||
|
||||
```bash
|
||||
# 本机 dev 客户端(保留 Windows 默认 feature)
|
||||
AGC_DEV_CARGO_FEATURES=cocos-editor-execute,template-library-fixtures npm run dev
|
||||
# 直接跑二进制
|
||||
cargo run --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --features template-library-fixtures
|
||||
# 覆盖条数
|
||||
AGC_TEMPLATE_LIBRARY_SYNTHETIC_COUNT=300 AGC_DEV_CARGO_FEATURES=template-library-fixtures npm run dev
|
||||
```
|
||||
|
||||
两种编译模式都要过模板库单测:默认构建跑「恒等透传」用例,`--features template-library-fixtures` 跑「补齐到配置条数」用例。
|
||||
|
||||
### 1000 条实测结论
|
||||
|
||||
- 页面能正常渲染 1000 张卡片(头部显示「共 1000 个模板 · 已下载 335 个」),并且滚动容器生效(窗口高度压到 430px 时右侧出现滚动条,页面内容被裁切而不是溢出到窗口外)。
|
||||
- 需要后续收口的两点(本次未改):① 标签筛选条随库量膨胀——1000 条时聚合出 35 个标签、占三行;② 一次性渲染 1000 个卡片节点并触发 1000 次封面请求。建议标签只展示 Top N + 「更多」,卡片列表加分页或虚拟滚动。
|
||||
- 前端回归:1000 条渲染 + 已安装过滤(334)/标签过滤(50)/关键词过滤数量自洽,见 `apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx`。
|
||||
|
||||
```bash
|
||||
# 模板库单测(清单校验、键安全、解压路径逃逸、安装与建项目)
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell template_library
|
||||
|
||||
Reference in New Issue
Block a user