5db407c3dc
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m12s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m18s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m25s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m22s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m49s
Project CI / AI game creator shell Rust crates (push) Successful in 2m36s
Project CI / Repository checks (push) Failing after 1m11s
Project CI / Frontend tests (push) Failing after 4m37s
Project CI / AI game creator shell web tests (push) Failing after 4m6s
Project CI / Native shell tests (push) Successful in 9m39s
Project CI / Backend tests (push) Successful in 10m12s
## 交付范围 - OSS `agc-dev` bucket 的 `templates/` 前缀作为 AGC 游戏模板库(**去掉 `agc/` 这一层**;`agc/` 继续只放客户端安装包与更新清单)。 - 模板正文改为 zip(zip 根 == AGC 项目根,如 `game/index.html`);清单补齐 `tags`、封面字段与包大小/摘要,`templateVersion` 用于判断是否需要重下。 - 客户端:Rust 模板库模块、模板库全屏页、首页模板推荐位、左侧导航入口。 ## 客户端行为 - `fetch_game_template_library`:读取并校验清单(≤4 MiB),缓存到 `<app_data>/templates/index.json`;网络失败回退本机缓存并标注 `source=cache`。 - `download_game_template`:按清单下载 zip(≤512 MiB),校验字节数与 SHA-256 后解压到 `<app_data>/templates/installed/<id>/<version>/`,最后写 `installed.json` 作为安装完成标记。 - `create_automatic_local_game_project_from_template`:先把模板文件铺进新项目目录,再走既有 `init_local_game_project_at` 补 `.agent` 清单与标准目录;失败删除半成品目录。 - 解压只接受普通文件与目录(拒绝绝对路径、`..`、盘符、符号链接),安装目录名由标识符白名单拼出;远端只信「受信任 OSS 主机 + 对象键」,清单里的地址字段不参与请求。 - UI:搜索(空白分隔多关键词「与」)、标签/运行时筛选、仅看已下载、封面卡片与已下载徽标;「使用模板」在版本落后时先重下再建项。 ## 文档与脚本 - 主规范 `docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md`;里程碑与实施计划见 `docs/project-memory/plans/`;`decision-log.md` 记录库路径、清单 schema、安装缓存目录与 CSP 约定。 - 发布脚本 `scripts/agc-template-library-publish.mjs`(`--dry-run` 校验 → 上传 → 回读校验)。 - 首页「灵感推荐」替换为模板库推荐位,删除本机灵感图目录与 `InspirationGallery.tsx`;`tauri.conf.json` 的 `img-src` 放行受信任 OSS 主机用于加载封面。 ## 验证 - `cargo test --bin genarrative-ai-game-creator-shell template_library`:9 passed;`--ignored` 真连检查 `fetches_the_live_template_library_index` 通过。 - `npx vitest run src/features/template-library`:9 passed。 - `npm run typecheck`、`cargo fmt --check`、`npm run check:encoding`、`node scripts/check-doc-index.mjs`、`git diff --check` 均通过。 - OSS 侧匿名复核:`templates/index.json`、各 `cover.png` 与 `template.zip` 均返回 200。 ## 未做 / 待确认 - 本轮未做客户端点击级人工验收(下一步单独验证并把结果补进此 PR)。 - 模板封面目前是占位标题卡,正式封面由后续模板包提供。 --------- Co-authored-by: kdletters <61648117+kdletters@users.noreply.github.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/396
109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
buildTemplateRows,
|
||
computeTemplateGridColumns,
|
||
computeTemplateGridLayout,
|
||
computeTemplateRowHeight,
|
||
TEMPLATE_CARD_GAP,
|
||
TEMPLATE_CARD_MIN_WIDTH,
|
||
TEMPLATE_CARD_TEXT_HEIGHT,
|
||
} from '../src/features/template-library/templateLibraryGrid';
|
||
import type { GameTemplateEntry } from '../src/features/template-library/templateLibraryModel';
|
||
|
||
function entry(id: string): GameTemplateEntry {
|
||
return {
|
||
id,
|
||
title: id,
|
||
summary: '',
|
||
tags: [],
|
||
runtime: 'html',
|
||
engine: 'phaser',
|
||
engineVersion: '4.2.1',
|
||
templateVersion: '0.1.0',
|
||
updatedAt: '2026-09-17T00:00:00Z',
|
||
entry: 'game/index.html',
|
||
zipUrl: `https://oss.example/templates/v1/${id}/template.zip`,
|
||
zipSizeBytes: 1024,
|
||
zipSha256: 'a'.repeat(64),
|
||
coverUrl: `https://oss.example/templates/v1/${id}/cover.svg`,
|
||
coverWidth: 960,
|
||
coverHeight: 540,
|
||
installed: false,
|
||
installedVersion: null,
|
||
installedAtMillis: null,
|
||
};
|
||
}
|
||
|
||
describe('computeTemplateGridColumns', () => {
|
||
it('fits as many min-width columns as the container allows', () => {
|
||
expect(computeTemplateGridColumns(0)).toBe(1);
|
||
expect(computeTemplateGridColumns(200)).toBe(1);
|
||
expect(computeTemplateGridColumns(TEMPLATE_CARD_MIN_WIDTH)).toBe(1);
|
||
// 两列边界:2*250 + 14 = 514
|
||
const twoColumnWidth = 2 * TEMPLATE_CARD_MIN_WIDTH + TEMPLATE_CARD_GAP;
|
||
expect(computeTemplateGridColumns(twoColumnWidth)).toBe(2);
|
||
expect(computeTemplateGridColumns(twoColumnWidth - 1)).toBe(1);
|
||
// 1200px:4 列((1200+14)/(250+14) = 4.59)
|
||
expect(computeTemplateGridColumns(1200)).toBe(4);
|
||
});
|
||
});
|
||
|
||
describe('computeTemplateRowHeight', () => {
|
||
it('keeps cover ratio + fixed text block', () => {
|
||
// 列宽 300 → 卡片 286 → 封面 286*9/16 = 160.875 → 161
|
||
expect(computeTemplateRowHeight(300)).toBe(
|
||
161 + TEMPLATE_CARD_TEXT_HEIGHT + TEMPLATE_CARD_GAP,
|
||
);
|
||
// 极窄时按最小卡片宽度兜底,避免行高被压成 0
|
||
expect(computeTemplateRowHeight(10)).toBeGreaterThan(
|
||
TEMPLATE_CARD_TEXT_HEIGHT,
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('computeTemplateGridLayout', () => {
|
||
it('derives columns, row height and row count for a big library', () => {
|
||
const layout = computeTemplateGridLayout({
|
||
containerWidth: 1200,
|
||
itemCount: 1000,
|
||
});
|
||
expect(layout.columnCount).toBe(4);
|
||
expect(layout.columnWidth).toBe(300);
|
||
expect(layout.rowHeight).toBe(
|
||
161 + TEMPLATE_CARD_TEXT_HEIGHT + TEMPLATE_CARD_GAP,
|
||
);
|
||
expect(layout.rowCount).toBe(250);
|
||
// 虚拟列表只渲染可视行,滚动高度仍由总行数决定
|
||
expect(layout.rowCount * layout.rowHeight).toBeGreaterThan(80000);
|
||
});
|
||
|
||
it('handles inline and empty libraries', () => {
|
||
expect(
|
||
computeTemplateGridLayout({ containerWidth: 0, itemCount: 5 }),
|
||
).toEqual({
|
||
columnCount: 1,
|
||
columnWidth: 1,
|
||
rowHeight: computeTemplateRowHeight(1),
|
||
rowCount: 5,
|
||
});
|
||
expect(
|
||
computeTemplateGridLayout({ containerWidth: 1200, itemCount: 0 })
|
||
.rowCount,
|
||
).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('buildTemplateRows', () => {
|
||
it('chunks entries per row and pads the tail with nulls', () => {
|
||
const rows = buildTemplateRows([entry('a'), entry('b'), entry('c')], 2);
|
||
expect(rows).toHaveLength(2);
|
||
expect(rows[0]?.map((item) => item?.id)).toEqual(['a', 'b']);
|
||
expect(rows[1]?.map((item) => item?.id ?? null)).toEqual(['c', null]);
|
||
});
|
||
|
||
it('returns no rows for an invalid column count', () => {
|
||
expect(buildTemplateRows([entry('a')], 0)).toEqual([]);
|
||
});
|
||
});
|