合并 Phaser 一键发布自动构建与打包
Project CI / AI game creator shell Rust crates (push) Successful in 1m36s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m9s
Project CI / Backend tests (push) Successful in 5m41s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 7m33s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m53s
Project CI / Native shell tests (push) Successful in 7m28s
Project CI / Frontend tests (push) Successful in 3m38s
Project CI / Repository checks (push) Successful in 3m36s
Project CI / AI game creator shell web tests (push) Successful in 2m55s
Project CI / AI game creator shell Rust crates (push) Successful in 1m36s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m9s
Project CI / Backend tests (push) Successful in 5m41s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 7m33s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m53s
Project CI / Native shell tests (push) Successful in 7m28s
Project CI / Frontend tests (push) Successful in 3m38s
Project CI / Repository checks (push) Successful in 3m36s
Project CI / AI game creator shell web tests (push) Successful in 2m55s
This commit was merged in pull request #481.
This commit is contained in:
@@ -4,10 +4,15 @@
|
||||
// E2E_ADMIN_USER=<管理员用户名> E2E_ADMIN_PASSWORD=<管理员密码> \
|
||||
// npm run check:game-distribution-media-e2e
|
||||
// E2E_API_BASE 可覆盖 api-server 地址(默认 http://127.0.0.1:12401)。
|
||||
// E2E_PACKAGE_ZIP 指向一个已经构建好的发行包(根目录含 index.html),例如真实
|
||||
// Phaser/Vite 工程 `game/dist/**` 打成的 ZIP;不传时使用脚本内置的最小 fixture。
|
||||
// E2E_GAME_TITLE 可覆盖游戏标题,便于在广场里认出这次验证。
|
||||
//
|
||||
// 覆盖:真实素材直传 OSS → 创建游戏(素材归属校验)→ 创建版本(资料冻结)→ 送审 →
|
||||
// 作者回读 frozenMetadata → 待审期间匿名不可见/不可读 → 管理员审核通过 → 公开投影
|
||||
// 暴露对象键且不泄露素材 ID → 匿名换签读封面与截图 → 发行网关可直接游玩。
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:12401';
|
||||
@@ -139,7 +144,39 @@ function gameMetadata(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const externalPackageZip = (process.env.E2E_PACKAGE_ZIP ?? '').trim();
|
||||
const gameTitleOverride = (process.env.E2E_GAME_TITLE ?? '').trim();
|
||||
|
||||
/** 返回待发布的发行包字节与条目数:优先使用调用方真实构建产物,否则用内置 fixture。 */
|
||||
async function buildZip() {
|
||||
if (externalPackageZip) {
|
||||
const bytes = await readFile(externalPackageZip);
|
||||
const archive = new JSZip();
|
||||
const parsed = await archive.loadAsync(bytes);
|
||||
const entryNames = Object.keys(parsed.files).filter(
|
||||
(name) => !parsed.files[name].dir,
|
||||
);
|
||||
if (!entryNames.includes('index.html')) {
|
||||
throw new Error(
|
||||
`E2E_PACKAGE_ZIP 根目录缺少 index.html:${externalPackageZip}`,
|
||||
);
|
||||
}
|
||||
// 真实构建产物(Phaser/Vite 等)资源名带哈希:从包内派生一个资源路径做网关断言。
|
||||
const assetPath =
|
||||
entryNames.find((name) => /^assets\/.+\.js$/u.test(name)) ??
|
||||
entryNames.find((name) => name.endsWith('.js'));
|
||||
if (!assetPath) {
|
||||
throw new Error(
|
||||
`E2E_PACKAGE_ZIP 内没有可断言的 JS 资源:${externalPackageZip}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
bytes: Buffer.from(bytes),
|
||||
fileCount: entryNames.length,
|
||||
assetPath,
|
||||
entryMarker: null,
|
||||
};
|
||||
}
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
'index.html',
|
||||
@@ -147,7 +184,12 @@ async function buildZip() {
|
||||
);
|
||||
zip.file('assets/app.js', 'document.documentElement.dataset.e2e="media";');
|
||||
const bytes = await zip.generateAsync({ type: 'uint8array' });
|
||||
return Buffer.from(bytes);
|
||||
return {
|
||||
bytes: Buffer.from(bytes),
|
||||
fileCount: 2,
|
||||
assetPath: 'assets/app.js',
|
||||
entryMarker: 'E2E-MEDIA-OK',
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -230,7 +272,7 @@ async function main() {
|
||||
|
||||
// 4. 创建游戏 + 版本(冻结资料)
|
||||
const metadata = gameMetadata({
|
||||
title: `分发媒体验证 ${id.slice(-6)}`,
|
||||
title: gameTitleOverride || `分发媒体验证 ${id.slice(-6)}`,
|
||||
coverAssetId: cover.assetObjectId,
|
||||
screenshots: [shot1.assetObjectId, shot2.assetObjectId],
|
||||
});
|
||||
@@ -284,7 +326,8 @@ async function main() {
|
||||
`status=${ghostVersion.status}`,
|
||||
);
|
||||
|
||||
const zipBytes = await buildZip();
|
||||
const built = await buildZip();
|
||||
const zipBytes = built.bytes;
|
||||
const crypto = await import('node:crypto');
|
||||
const sha256 = crypto.createHash('sha256').update(zipBytes).digest('hex');
|
||||
const version = await api(`/api/game-distribution/games/${gameId}/versions`, {
|
||||
@@ -294,7 +337,7 @@ async function main() {
|
||||
body: {
|
||||
packageSha256: sha256,
|
||||
packageBytes: zipBytes.length,
|
||||
packageFileCount: 2,
|
||||
packageFileCount: built.fileCount,
|
||||
packageEntryPath: 'index.html',
|
||||
gameMetadata: metadata,
|
||||
},
|
||||
@@ -423,7 +466,8 @@ async function main() {
|
||||
body: {
|
||||
decision: 'approve',
|
||||
expectedPublicationRevision: readback.data.version.publicationRevision,
|
||||
entryUrl: `${API}`,
|
||||
// 本地用发行网关路径当入口,让「审核通过 → 游玩」在本地也走真实网关。
|
||||
entryUrl: `${API}/api/game-distribution/releases/${gameId}/`,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -480,22 +524,27 @@ async function main() {
|
||||
`${API}/api/game-distribution/releases/${gameId}/index.html`,
|
||||
);
|
||||
const releaseBody = await release.text();
|
||||
const entryOk =
|
||||
release.status === 200 &&
|
||||
/<html|<!doctype html/iu.test(releaseBody) &&
|
||||
(built.entryMarker === null || releaseBody.includes(built.entryMarker));
|
||||
check(
|
||||
'发行网关返回游戏入口',
|
||||
release.status === 200 && releaseBody.includes('E2E-MEDIA-OK'),
|
||||
`status=${release.status}`,
|
||||
entryOk,
|
||||
`status=${release.status} bytes=${releaseBody.length}`,
|
||||
);
|
||||
check(
|
||||
'发行入口带 nosniff',
|
||||
release.headers.get('x-content-type-options') === 'nosniff',
|
||||
);
|
||||
const releaseAsset = await fetch(
|
||||
`${API}/api/game-distribution/releases/${gameId}/assets/app.js`,
|
||||
`${API}/api/game-distribution/releases/${gameId}/${built.assetPath}`,
|
||||
);
|
||||
const assetBody = await releaseAsset.arrayBuffer();
|
||||
check(
|
||||
'发行网关返回包内资源',
|
||||
releaseAsset.status === 200,
|
||||
`status=${releaseAsset.status}`,
|
||||
releaseAsset.status === 200 && assetBody.byteLength > 0,
|
||||
`status=${releaseAsset.status} path=${built.assetPath} bytes=${assetBody.byteLength}`,
|
||||
);
|
||||
|
||||
console.log(`\n结果:${failures === 0 ? '全部通过' : `${failures} 项失败`}`);
|
||||
|
||||
Reference in New Issue
Block a user