b58fab7b52
Project CI / AI game creator shell Rust crates (push) Successful in 1m33s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m5s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 5m53s
Project CI / Backend tests (push) Successful in 4m47s
Project CI / Frontend tests (push) Successful in 2m6s
Project CI / Native shell tests (push) Successful in 6m46s
Project CI / Repository checks (push) Successful in 2m49s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m24s
Project CI / AI game creator shell web tests (push) Successful in 1m50s
- 新增 scripts/check-game-distribution-package-limits-e2e.mjs:196 MiB 合法包走通上传+校验+送审并记录耗时与工作集,声明超限 413,文件数/单文件/展开量/压缩比四类边界各自 422 失败关闭并落 upload_failed - 校验实现无进程拉起,包内脚本未被执行 - 游戏分发里程碑阶段 D 第 2 条改为已勾选,并写明本轮可复核数据
533 lines
18 KiB
JavaScript
533 lines
18 KiB
JavaScript
// 游戏分发发行包边界复跑:现行上限 200 MiB 档的成功链路 + 文件数 / 单文件 / 展开量 /
|
||
// 压缩比四个边界失败关闭,并记录可复核的耗时与 api-server 工作集变化。
|
||
//
|
||
// 需要:本地 dev 栈(`npm run dev` 或 `dev:spacetime` + `dev:api-server`)+ 管理员账号。
|
||
// E2E_ADMIN_USER=... E2E_ADMIN_PASSWORD=... npm run check:game-distribution-package-limits-e2e
|
||
// E2E_API_BASE 默认 http://127.0.0.1:8082;E2E_SKIP_BIG_PACKAGE=1 可跳过 200 MiB 成功档。
|
||
import { execSync } from 'node:child_process';
|
||
import { createHash, randomBytes } from 'node:crypto';
|
||
|
||
import JSZip from 'jszip';
|
||
|
||
const COVER_PNG = Buffer.from(
|
||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||
'base64',
|
||
);
|
||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:8082';
|
||
const API_PORT = Number(new URL(API).port || '80');
|
||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||
const ADMIN_USER = (process.env.E2E_ADMIN_USER ?? '').trim();
|
||
const ADMIN_PASSWORD = process.env.E2E_ADMIN_PASSWORD ?? '';
|
||
const DEV_PASSWORD = 'GenE2e123!';
|
||
const MIB = 1024 * 1024;
|
||
|
||
if (!ADMIN_USER || !ADMIN_PASSWORD) {
|
||
console.error('缺少 E2E_ADMIN_USER / E2E_ADMIN_PASSWORD。');
|
||
process.exit(2);
|
||
}
|
||
|
||
let failures = 0;
|
||
function check(name, ok, detail = '') {
|
||
if (!ok) failures += 1;
|
||
console.log(
|
||
`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` :: ${detail}` : ''}`,
|
||
);
|
||
}
|
||
|
||
async function api(pathname, options = {}) {
|
||
const { method = 'GET', token, body, headers = {}, binary } = options;
|
||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||
let finalBody;
|
||
if (binary) {
|
||
finalBody = binary;
|
||
} else if (body !== undefined) {
|
||
finalHeaders['Content-Type'] = 'application/json';
|
||
finalBody = JSON.stringify(body);
|
||
}
|
||
const response = await fetch(`${API}${pathname}`, {
|
||
method,
|
||
headers: finalHeaders,
|
||
body: finalBody,
|
||
});
|
||
const text = await response.text();
|
||
let json = null;
|
||
try {
|
||
json = JSON.parse(text);
|
||
} catch {
|
||
json = null;
|
||
}
|
||
return {
|
||
status: response.status,
|
||
text,
|
||
json,
|
||
data: json?.data,
|
||
error: json?.error,
|
||
};
|
||
}
|
||
|
||
/** api-server 进程当前工作集(MB);Windows 本地栈用 PowerShell 读监听进程。 */
|
||
function apiServerWorkingSetMb() {
|
||
try {
|
||
const output = execSync(
|
||
`powershell -NoProfile -Command "$c = Get-NetTCPConnection -LocalPort ${API_PORT} -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1; if ($c) { (Get-Process -Id $c.OwningProcess).WorkingSet64 }"`,
|
||
{ encoding: 'utf8', timeout: 30_000 },
|
||
);
|
||
const value = Number(output.trim());
|
||
return Number.isFinite(value) && value > 0 ? Math.round(value / MIB) : null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function uploadCover(token, id) {
|
||
const fileName = `limits-${id}.png`;
|
||
const ticket = await api('/api/assets/direct-upload-tickets', {
|
||
method: 'POST',
|
||
token,
|
||
body: {
|
||
legacyPrefix: 'generated-character-drafts',
|
||
pathSegments: ['game-distribution', 'limits', String(id)],
|
||
fileName,
|
||
contentType: 'image/png',
|
||
access: 'private',
|
||
maxSizeBytes: COVER_PNG.length,
|
||
metadata: { asset_kind: 'game_distribution_cover' },
|
||
},
|
||
});
|
||
if (ticket.status !== 200)
|
||
throw new Error(`创建直传凭证失败 ${ticket.status}`);
|
||
const upload = ticket.data.upload;
|
||
const form = new FormData();
|
||
for (const [key, value] of Object.entries(upload.formFields ?? {})) {
|
||
if (value !== null && value !== undefined) form.append(key, String(value));
|
||
}
|
||
form.append('file', new Blob([COVER_PNG], { type: 'image/png' }), fileName);
|
||
const put = await fetch(upload.host, { method: 'POST', body: form });
|
||
if (!put.ok) throw new Error(`直传对象存储失败 ${put.status}`);
|
||
const confirm = await api('/api/assets/objects/confirm', {
|
||
method: 'POST',
|
||
token,
|
||
body: {
|
||
bucket: upload.bucket,
|
||
objectKey: upload.objectKey,
|
||
contentType: 'image/png',
|
||
contentLength: COVER_PNG.length,
|
||
assetKind: 'game_distribution_cover',
|
||
accessPolicy: 'private',
|
||
entityId: 'game-distribution-limits',
|
||
},
|
||
});
|
||
if (confirm.status !== 200) throw new Error(`确认素材失败 ${confirm.status}`);
|
||
return confirm.data.assetObject.assetObjectId;
|
||
}
|
||
|
||
async function createGameWithVersion(
|
||
author,
|
||
coverAssetId,
|
||
title,
|
||
suffix,
|
||
declaration,
|
||
) {
|
||
const metadata = {
|
||
title,
|
||
summary: '边界复跑临时游戏',
|
||
description: '',
|
||
category: '休闲',
|
||
tags: ['limits'],
|
||
coverAssetId,
|
||
deviceSupport: { desktop: true, mobile: false, touch: false },
|
||
inputModes: ['keyboard', 'mouse'],
|
||
orientation: 'landscape',
|
||
};
|
||
const created = await api('/api/game-distribution/games', {
|
||
method: 'POST',
|
||
token: author,
|
||
headers: { 'Idempotency-Key': `limits-game-${suffix}` },
|
||
body: metadata,
|
||
});
|
||
const gameId = created.data?.id;
|
||
if (created.status !== 200 || !gameId) {
|
||
throw new Error(
|
||
`创建游戏失败 ${created.status} ${created.text.slice(0, 160)}`,
|
||
);
|
||
}
|
||
const version = await api(`/api/game-distribution/games/${gameId}/versions`, {
|
||
method: 'POST',
|
||
token: author,
|
||
headers: { 'Idempotency-Key': `limits-version-${suffix}` },
|
||
body: {
|
||
packageSha256: declaration.packageSha256 ?? 'a'.repeat(64),
|
||
packageBytes: declaration.packageBytes,
|
||
packageFileCount: declaration.packageFileCount,
|
||
packageEntryPath: 'index.html',
|
||
gameMetadata: metadata,
|
||
},
|
||
});
|
||
return { gameId, metadata, version };
|
||
}
|
||
|
||
async function uploadAndConfirm(author, versionId, bytes, suffix) {
|
||
const uploaded = await api(
|
||
`/api/game-distribution/versions/${versionId}/package`,
|
||
{
|
||
method: 'PUT',
|
||
token: author,
|
||
headers: {
|
||
'Idempotency-Key': `limits-upload-${suffix}`,
|
||
'Content-Type': 'application/zip',
|
||
},
|
||
binary: bytes,
|
||
},
|
||
);
|
||
const confirmed = await api(
|
||
`/api/game-distribution/versions/${versionId}/package/complete`,
|
||
{
|
||
method: 'POST',
|
||
token: author,
|
||
headers: { 'Idempotency-Key': `limits-confirm-${suffix}` },
|
||
},
|
||
);
|
||
return { uploaded, confirmed };
|
||
}
|
||
|
||
async function main() {
|
||
const adminLogin = await api('/admin/api/login', {
|
||
method: 'POST',
|
||
body: { username: ADMIN_USER, password: ADMIN_PASSWORD },
|
||
});
|
||
const admin = adminLogin.data?.token ?? adminLogin.data?.accessToken;
|
||
check(
|
||
'管理员登录成功',
|
||
adminLogin.status === 200 && Boolean(admin),
|
||
`status=${adminLogin.status}`,
|
||
);
|
||
if (!admin) process.exit(1);
|
||
await api('/admin/api/feature-gates', {
|
||
method: 'PUT',
|
||
token: admin,
|
||
body: {
|
||
gateKey: 'game-distribution:publish',
|
||
enabled: true,
|
||
rolloutPercent: 100,
|
||
allowUserIds: [],
|
||
allowUserTags: [],
|
||
denyUserIds: [],
|
||
description: 'E2E 发包边界',
|
||
},
|
||
});
|
||
|
||
const stamp = Date.now();
|
||
const register = await api('/api/auth/entry', {
|
||
method: 'POST',
|
||
body: {
|
||
purePhoneNumber: `138${String(stamp).slice(-8)}`,
|
||
password: DEV_PASSWORD,
|
||
},
|
||
});
|
||
const author = register.data?.token;
|
||
check(
|
||
'作者注册拿到 token',
|
||
register.status === 200 && Boolean(author),
|
||
`status=${register.status}`,
|
||
);
|
||
if (!author) process.exit(1);
|
||
const coverAssetId = await uploadCover(author, stamp);
|
||
|
||
// A. 声明超过 200 MiB:创建版本阶段就该失败关闭,不必真的传包。
|
||
const declaredOver = await createGameWithVersion(
|
||
author,
|
||
coverAssetId,
|
||
`边界 声明超限 ${String(stamp).slice(-5)}`,
|
||
`${stamp}-declared`,
|
||
{ packageBytes: 200 * MIB + 1, packageFileCount: 1 },
|
||
);
|
||
check(
|
||
'声明超过 200 MiB 的版本在创建阶段被拒(413)',
|
||
declaredOver.version.status === 413,
|
||
`status=${declaredOver.version.status} msg=${declaredOver.version.error?.message ?? ''}`,
|
||
);
|
||
|
||
// B. 文件数边界:10,001 个条目必须失败关闭。
|
||
const manyFiles = new JSZip();
|
||
manyFiles.file('index.html', '<!doctype html><html><body>many</body></html>');
|
||
for (let index = 0; index < 10_000; index += 1) {
|
||
manyFiles.file(`f/${index}.txt`, 'x');
|
||
}
|
||
const manyBytes = Buffer.from(
|
||
await manyFiles.generateAsync({
|
||
type: 'uint8array',
|
||
compression: 'DEFLATE',
|
||
}),
|
||
);
|
||
const manyCase = await createGameWithVersion(
|
||
author,
|
||
coverAssetId,
|
||
`边界 文件数 ${String(stamp).slice(-5)}`,
|
||
`${stamp}-many`,
|
||
{
|
||
packageBytes: manyBytes.length,
|
||
packageFileCount: 10_001,
|
||
packageSha256: createHash('sha256').update(manyBytes).digest('hex'),
|
||
},
|
||
);
|
||
const manyResult = await uploadAndConfirm(
|
||
author,
|
||
manyCase.version.data?.versionId,
|
||
manyBytes,
|
||
`${stamp}-many`,
|
||
);
|
||
const manyVersion = await api(
|
||
`/api/game-distribution/versions/${manyCase.version.data?.versionId}`,
|
||
{ token: author },
|
||
);
|
||
check(
|
||
'10,001 个条目的包在上传校验时按 TooManyFiles 失败关闭',
|
||
manyResult.uploaded.status === 422 &&
|
||
manyResult.uploaded.error?.details?.reason === 'TooManyFiles' &&
|
||
manyVersion.data?.version?.status === 'upload_failed',
|
||
`upload=${manyResult.uploaded.status} reason=${manyResult.uploaded.error?.details?.reason ?? ''} versionStatus=${manyVersion.data?.version?.status ?? ''}`,
|
||
);
|
||
|
||
// C. 单文件边界:声明 65 MiB 的文件超过 64 MiB 上限。
|
||
const bigFile = new JSZip();
|
||
bigFile.file(
|
||
'index.html',
|
||
'<!doctype html><html><body>big file</body></html>',
|
||
);
|
||
bigFile.file('assets/huge.bin', Buffer.alloc(65 * MIB, 0));
|
||
const bigFileBytes = Buffer.from(
|
||
await bigFile.generateAsync({ type: 'uint8array', compression: 'DEFLATE' }),
|
||
);
|
||
const bigFileCase = await createGameWithVersion(
|
||
author,
|
||
coverAssetId,
|
||
`边界 单文件 ${String(stamp).slice(-5)}`,
|
||
`${stamp}-bigfile`,
|
||
{
|
||
packageBytes: bigFileBytes.length,
|
||
packageFileCount: 2,
|
||
packageSha256: createHash('sha256').update(bigFileBytes).digest('hex'),
|
||
},
|
||
);
|
||
const bigFileResult = await uploadAndConfirm(
|
||
author,
|
||
bigFileCase.version.data?.versionId,
|
||
bigFileBytes,
|
||
`${stamp}-bigfile`,
|
||
);
|
||
const bigFileVersion = await api(
|
||
`/api/game-distribution/versions/${bigFileCase.version.data?.versionId}`,
|
||
{ token: author },
|
||
);
|
||
check(
|
||
'单个文件超过 64 MiB 按 FileTooLarge 失败关闭',
|
||
bigFileResult.uploaded.status === 422 &&
|
||
bigFileResult.uploaded.error?.details?.reason === 'FileTooLarge' &&
|
||
bigFileVersion.data?.version?.status === 'upload_failed',
|
||
`upload=${bigFileResult.uploaded.status} reason=${bigFileResult.uploaded.error?.details?.reason ?? ''} versionStatus=${bigFileVersion.data?.version?.status ?? ''}`,
|
||
);
|
||
|
||
// D. 展开量边界:9 × 60 MiB = 540 MiB,超过 500 MiB 上限。
|
||
const expanded = new JSZip();
|
||
expanded.file(
|
||
'index.html',
|
||
'<!doctype html><html><body>expanded</body></html>',
|
||
);
|
||
for (let index = 0; index < 9; index += 1) {
|
||
// 半可压缩内容:每 6 字节放 1 个随机字节,整包约 145 MiB(在 200 MiB 上限内),
|
||
// 但 9 × 60 MiB = 540 MiB 的展开总量会越过 500 MiB 上限。
|
||
const chunk = Buffer.alloc(60 * MIB, 0);
|
||
const noise = randomBytes((60 * MIB) / 6);
|
||
for (let offset = 0; offset < noise.length; offset += 1) {
|
||
chunk[offset * 6] = noise[offset];
|
||
}
|
||
expanded.file(`assets/blob-${index}.bin`, chunk);
|
||
}
|
||
const expandedBytes = Buffer.from(
|
||
await expanded.generateAsync({
|
||
type: 'uint8array',
|
||
compression: 'DEFLATE',
|
||
}),
|
||
);
|
||
const expandedCase = await createGameWithVersion(
|
||
author,
|
||
coverAssetId,
|
||
`边界 展开量 ${String(stamp).slice(-5)}`,
|
||
`${stamp}-expanded`,
|
||
{
|
||
packageBytes: expandedBytes.length,
|
||
packageFileCount: 10,
|
||
packageSha256: createHash('sha256').update(expandedBytes).digest('hex'),
|
||
},
|
||
);
|
||
const expandedResult = await uploadAndConfirm(
|
||
author,
|
||
expandedCase.version.data?.versionId,
|
||
expandedBytes,
|
||
`${stamp}-expanded`,
|
||
);
|
||
const expandedVersion = await api(
|
||
`/api/game-distribution/versions/${expandedCase.version.data?.versionId}`,
|
||
{ token: author },
|
||
);
|
||
check(
|
||
'展开总量超过 500 MiB 按 ExpandedPackageTooLarge 失败关闭',
|
||
expandedResult.uploaded.status === 422 &&
|
||
expandedResult.uploaded.error?.details?.reason ===
|
||
'ExpandedPackageTooLarge' &&
|
||
expandedVersion.data?.version?.status === 'upload_failed',
|
||
`upload=${expandedResult.uploaded.status} reason=${expandedResult.uploaded.error?.details?.reason ?? ''} versionStatus=${expandedVersion.data?.version?.status ?? ''}`,
|
||
);
|
||
|
||
// E. 压缩比边界:10 MiB 全零内容压成十几 KB,比例超过 100:1。
|
||
const ratio = new JSZip();
|
||
ratio.file('index.html', '<!doctype html><html><body>ratio</body></html>');
|
||
ratio.file('assets/zeros.bin', Buffer.alloc(10 * MIB, 0));
|
||
const ratioBytes = Buffer.from(
|
||
await ratio.generateAsync({ type: 'uint8array', compression: 'DEFLATE' }),
|
||
);
|
||
const ratioCase = await createGameWithVersion(
|
||
author,
|
||
coverAssetId,
|
||
`边界 压缩比 ${String(stamp).slice(-5)}`,
|
||
`${stamp}-ratio`,
|
||
{
|
||
packageBytes: ratioBytes.length,
|
||
packageFileCount: 2,
|
||
packageSha256: createHash('sha256').update(ratioBytes).digest('hex'),
|
||
},
|
||
);
|
||
const ratioResult = await uploadAndConfirm(
|
||
author,
|
||
ratioCase.version.data?.versionId,
|
||
ratioBytes,
|
||
`${stamp}-ratio`,
|
||
);
|
||
const ratioVersion = await api(
|
||
`/api/game-distribution/versions/${ratioCase.version.data?.versionId}`,
|
||
{ token: author },
|
||
);
|
||
check(
|
||
'压缩比超过 100:1 按 CompressionRatioTooHigh 失败关闭',
|
||
ratioResult.uploaded.status === 422 &&
|
||
ratioResult.uploaded.error?.details?.reason ===
|
||
'CompressionRatioTooHigh' &&
|
||
ratioVersion.data?.version?.status === 'upload_failed',
|
||
`upload=${ratioResult.uploaded.status} reason=${ratioResult.uploaded.error?.details?.reason ?? ''} versionStatus=${ratioVersion.data?.version?.status ?? ''} zip=${ratioBytes.length}`,
|
||
);
|
||
|
||
// F. 200 MiB 档成功链路(默认执行;E2E_SKIP_BIG_PACKAGE=1 可跳过)。
|
||
if ((process.env.E2E_SKIP_BIG_PACKAGE ?? '').trim() === '1') {
|
||
console.log('SKIP 200 MiB 档成功链路(E2E_SKIP_BIG_PACKAGE=1)');
|
||
} else {
|
||
const big = new JSZip();
|
||
big.file(
|
||
'index.html',
|
||
'<!doctype html><html><head><script>' +
|
||
"fetch('/api/game-distribution/games');window.__packageExecuted=true;" +
|
||
'</script></head><body><h1>limits-big</h1></body></html>',
|
||
);
|
||
for (let index = 0; index < 4; index += 1) {
|
||
big.file(`assets/blob-${index}.bin`, randomBytes(49 * MIB));
|
||
}
|
||
const bigBytes = Buffer.from(
|
||
await big.generateAsync({ type: 'uint8array', compression: 'STORE' }),
|
||
);
|
||
const bigSha = createHash('sha256').update(bigBytes).digest('hex');
|
||
check(
|
||
'构造的合法包在 200 MiB 上限以内',
|
||
bigBytes.length < 200 * MIB,
|
||
`packageBytes=${bigBytes.length} (${(bigBytes.length / MIB).toFixed(1)} MiB)`,
|
||
);
|
||
const bigCase = await createGameWithVersion(
|
||
author,
|
||
coverAssetId,
|
||
`边界 200MiB 档 ${String(stamp).slice(-5)}`,
|
||
`${stamp}-big`,
|
||
{
|
||
packageBytes: bigBytes.length,
|
||
packageFileCount: 5,
|
||
packageSha256: bigSha,
|
||
},
|
||
);
|
||
const rssBefore = apiServerWorkingSetMb();
|
||
const uploadStarted = Date.now();
|
||
const bigUploaded = await api(
|
||
`/api/game-distribution/versions/${bigCase.version.data?.versionId}/package`,
|
||
{
|
||
method: 'PUT',
|
||
token: author,
|
||
headers: {
|
||
'Idempotency-Key': `limits-big-upload-${stamp}`,
|
||
'Content-Type': 'application/zip',
|
||
},
|
||
binary: bigBytes,
|
||
},
|
||
);
|
||
const uploadMs = Date.now() - uploadStarted;
|
||
const confirmStarted = Date.now();
|
||
const bigConfirmed = await api(
|
||
`/api/game-distribution/versions/${bigCase.version.data?.versionId}/package/complete`,
|
||
{
|
||
method: 'POST',
|
||
token: author,
|
||
headers: { 'Idempotency-Key': `limits-big-confirm-${stamp}` },
|
||
},
|
||
);
|
||
const confirmMs = Date.now() - confirmStarted;
|
||
const rssAfter = apiServerWorkingSetMb();
|
||
check(
|
||
'200 MiB 档包上传并被校验通过',
|
||
bigUploaded.status === 200 &&
|
||
bigConfirmed.status === 200 &&
|
||
bigConfirmed.data?.status === 'uploaded',
|
||
`upload=${bigUploaded.status}/${uploadMs}ms confirm=${bigConfirmed.status}/${confirmMs}ms versionStatus=${bigConfirmed.data?.status ?? ''}`,
|
||
);
|
||
console.log(
|
||
`INFO 200 MiB 档耗时:上传 ${uploadMs}ms,确认(含回读与校验) ${confirmMs}ms;api-server 工作集 ${rssBefore ?? 'n/a'}MB → ${rssAfter ?? 'n/a'}MB`,
|
||
);
|
||
const submitted = await api(
|
||
`/api/game-distribution/versions/${bigCase.version.data?.versionId}/submit`,
|
||
{
|
||
method: 'POST',
|
||
token: author,
|
||
headers: { 'Idempotency-Key': `limits-big-submit-${stamp}` },
|
||
body: { expectedPublicationRevision: 0 },
|
||
},
|
||
);
|
||
check(
|
||
'200 MiB 档包能正常送审',
|
||
submitted.status === 202,
|
||
`status=${submitted.status}`,
|
||
);
|
||
}
|
||
|
||
// G. 校验不执行上传代码:校验路径是纯 Rust 解析,没有拉起任何进程。
|
||
const moduleSource = await import('node:fs').then((fs) =>
|
||
['package.rs', 'release.rs']
|
||
.map((file) =>
|
||
fs.readFileSync(
|
||
`server-rs/crates/module-game-distribution/src/${file}`,
|
||
'utf8',
|
||
),
|
||
)
|
||
.join('\n'),
|
||
);
|
||
check(
|
||
'校验实现里没有进程拉起 / 代码执行入口',
|
||
!/Command::new|std::process::Command|spawn\(/u.test(moduleSource),
|
||
'module-game-distribution 的 package.rs / release.rs',
|
||
);
|
||
|
||
console.log(`\n${failures === 0 ? '全部通过' : `${failures} 项失败`}`);
|
||
process.exit(failures === 0 ? 0 : 1);
|
||
}
|
||
|
||
main().catch((error) => {
|
||
console.error(
|
||
`[check-game-distribution-package-limits-e2e] 运行失败:${error}`,
|
||
);
|
||
process.exit(1);
|
||
});
|