补齐错误 envelope 的请求上下文并扩展游戏分发审核取证
Project CI / AI game creator shell Rust crates (push) Successful in 1m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m59s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 5m26s
Project CI / Backend tests (push) Successful in 4m13s
Project CI / Frontend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Successful in 1m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m59s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 5m26s
Project CI / Backend tests (push) Successful in 4m13s
Project CI / Frontend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
- api-server 的错误响应改用 attach_request_context 建立的 CURRENT_REQUEST_CONTEXT(tokio task_local),错误 envelope 与成功 envelope 一样带 meta.requestId / meta.operation,脱离请求任务时退回原形状 - 越权隔离脚本新增成功/失败 envelope 断言:修复前错误响应 requestId 为空让断言变红,修复后 23 项 PASS - 媒体链路脚本扩展审核治理段:普通作者不能提交审核动作、更新待审与被拒都不改变在线旧版 URL 与可玩内容、审核结论可在后台追溯,44 项 PASS - 游戏分发里程碑阶段 A 第 6 条、阶段 B 第 1/2 条改为已勾选并写入取证,阶段 B 其余条目列出具体缺口 - decision-log 补记错误 envelope 同一份 meta 与「按可消费取证」两条口径
This commit is contained in:
@@ -192,6 +192,26 @@ async function buildZip() {
|
||||
};
|
||||
}
|
||||
|
||||
/** 更新版本用的小包:入口里带标记,用来证明在线旧版没有被待审/被拒的新版本替换。 */
|
||||
async function buildZipWithMarker(marker) {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
'index.html',
|
||||
`<!doctype html><html><head><meta charset="utf-8"><title>E2E ${marker}</title><script src="assets/app.js"></script></head><body><h1>${marker}</h1></body></html>`,
|
||||
);
|
||||
zip.file(
|
||||
'assets/app.js',
|
||||
`document.documentElement.dataset.e2e="${marker}";`,
|
||||
);
|
||||
const bytes = await zip.generateAsync({ type: 'uint8array' });
|
||||
return {
|
||||
bytes: Buffer.from(bytes),
|
||||
fileCount: 2,
|
||||
assetPath: 'assets/app.js',
|
||||
entryMarker: marker,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 1. 作者注册
|
||||
const phone = `137${String(Date.now()).slice(-8)}`;
|
||||
@@ -610,6 +630,163 @@ async function main() {
|
||||
`status=${releaseAsset.status} path=${built.assetPath} bytes=${assetBody.byteLength}`,
|
||||
);
|
||||
|
||||
// 11. 审核治理:普通作者不能提交审核动作;更新待审 / 更新被拒都不改变在线旧版
|
||||
const authorReview = await api(
|
||||
`/admin/api/game-distribution/versions/${versionId}/review`,
|
||||
{
|
||||
method: 'POST',
|
||||
token: author,
|
||||
headers: { 'Idempotency-Key': `e2e-author-review-${id}` },
|
||||
body: { decision: 'approve', expectedPublicationRevision: 0 },
|
||||
},
|
||||
);
|
||||
check(
|
||||
'普通作者不能提交审核动作',
|
||||
authorReview.status === 401 || authorReview.status === 403,
|
||||
`status=${authorReview.status} code=${authorReview.error?.code ?? ''}`,
|
||||
);
|
||||
|
||||
const builtV2 = await buildZipWithMarker('E2E-V2-OK');
|
||||
const sha256V2 = crypto
|
||||
.createHash('sha256')
|
||||
.update(builtV2.bytes)
|
||||
.digest('hex');
|
||||
const versionV2 = await api(
|
||||
`/api/game-distribution/games/${gameId}/versions`,
|
||||
{
|
||||
method: 'POST',
|
||||
token: author,
|
||||
headers: { 'Idempotency-Key': `e2e-v2-version-${id}` },
|
||||
body: {
|
||||
packageSha256: sha256V2,
|
||||
packageBytes: builtV2.bytes.length,
|
||||
packageFileCount: builtV2.fileCount,
|
||||
packageEntryPath: 'index.html',
|
||||
gameMetadata: metadata,
|
||||
},
|
||||
},
|
||||
);
|
||||
const versionIdV2 = versionV2.data?.versionId;
|
||||
check(
|
||||
'已公开游戏可以创建更新版本',
|
||||
versionV2.status === 200 && Boolean(versionIdV2),
|
||||
`status=${versionV2.status}`,
|
||||
);
|
||||
const uploadV2 = await api(
|
||||
`/api/game-distribution/versions/${versionIdV2}/package`,
|
||||
{
|
||||
method: 'PUT',
|
||||
token: author,
|
||||
headers: {
|
||||
'Idempotency-Key': `e2e-v2-upload-${id}`,
|
||||
'Content-Type': 'application/zip',
|
||||
},
|
||||
binary: builtV2.bytes,
|
||||
},
|
||||
);
|
||||
const submitV2 = await api(
|
||||
`/api/game-distribution/versions/${versionIdV2}/submit`,
|
||||
{
|
||||
method: 'POST',
|
||||
token: author,
|
||||
headers: { 'Idempotency-Key': `e2e-v2-submit-${id}` },
|
||||
body: {
|
||||
expectedPublicationRevision: publishedGame?.publicationRevision ?? 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
check(
|
||||
'更新版本上传并送审后进入待审',
|
||||
uploadV2.status === 200 && submitV2.status === 202,
|
||||
`upload=${uploadV2.status} submit=${submitV2.status}`,
|
||||
);
|
||||
|
||||
const reviewQueue = await api('/admin/api/game-distribution/reviews', {
|
||||
token: admin,
|
||||
});
|
||||
const pendingEntry = (reviewQueue.data?.entries ?? []).find(
|
||||
(entry) => entry.versionId === versionIdV2,
|
||||
);
|
||||
check(
|
||||
'管理员能看到真实待审条目',
|
||||
pendingEntry?.status === 'pending_review',
|
||||
`status=${pendingEntry?.status ?? 'missing'}`,
|
||||
);
|
||||
|
||||
const detailWhilePending = await api(
|
||||
`/api/game-distribution/games/${gameId}`,
|
||||
);
|
||||
check(
|
||||
'更新待审期间公开详情仍指向在线旧版',
|
||||
detailWhilePending.data?.currentVersion?.sha256 === sha256,
|
||||
`sha=${String(detailWhilePending.data?.currentVersion?.sha256).slice(0, 12)} v1=${sha256.slice(0, 12)}`,
|
||||
);
|
||||
const releaseWhilePending = await fetch(
|
||||
`${API}/api/game-distribution/releases/${gameId}/index.html`,
|
||||
);
|
||||
const releaseWhilePendingBody = await releaseWhilePending.text();
|
||||
check(
|
||||
'更新待审期间发行网关仍服务旧版内容',
|
||||
releaseWhilePending.status === 200 &&
|
||||
!releaseWhilePendingBody.includes('E2E-V2-OK') &&
|
||||
(built.entryMarker === null ||
|
||||
releaseWhilePendingBody.includes(built.entryMarker)),
|
||||
`status=${releaseWhilePending.status}`,
|
||||
);
|
||||
|
||||
const rejectedV2 = await api(
|
||||
`/admin/api/game-distribution/versions/${versionIdV2}/review`,
|
||||
{
|
||||
method: 'POST',
|
||||
token: admin,
|
||||
headers: { 'Idempotency-Key': `e2e-v2-reject-${id}` },
|
||||
body: {
|
||||
decision: 'reject',
|
||||
expectedPublicationRevision:
|
||||
detailWhilePending.data?.publicationRevision ?? 0,
|
||||
reviewReason: 'E2E 拒绝原因',
|
||||
},
|
||||
},
|
||||
);
|
||||
check(
|
||||
'管理员可以拒绝更新版本并留下原因',
|
||||
rejectedV2.status === 200 &&
|
||||
rejectedV2.data?.version?.status === 'rejected' &&
|
||||
rejectedV2.data?.version?.reviewReason === 'E2E 拒绝原因',
|
||||
`status=${rejectedV2.status} versionStatus=${rejectedV2.data?.version?.status ?? ''} reason=${rejectedV2.data?.version?.reviewReason ?? ''}`,
|
||||
);
|
||||
|
||||
const detailAfterReject = await api(`/api/game-distribution/games/${gameId}`);
|
||||
const releaseAfterReject = await fetch(
|
||||
`${API}/api/game-distribution/releases/${gameId}/index.html`,
|
||||
);
|
||||
const releaseAfterRejectBody = await releaseAfterReject.text();
|
||||
check(
|
||||
'更新被拒后公开 URL 与可玩内容仍是旧版',
|
||||
detailAfterReject.data?.currentVersion?.sha256 === sha256 &&
|
||||
releaseAfterReject.status === 200 &&
|
||||
!releaseAfterRejectBody.includes('E2E-V2-OK'),
|
||||
`status=${releaseAfterReject.status} sha=${String(detailAfterReject.data?.currentVersion?.sha256).slice(0, 12)}`,
|
||||
);
|
||||
|
||||
const adminGamesAfterReject = await api(
|
||||
'/admin/api/game-distribution/games',
|
||||
{ token: admin },
|
||||
);
|
||||
const adminGameAfterReject = (adminGamesAfterReject.data?.games ?? []).find(
|
||||
(game) => game.gameId === gameId,
|
||||
);
|
||||
const adminVersionAfterReject = (adminGameAfterReject?.versions ?? []).find(
|
||||
(version) => version.versionId === versionIdV2,
|
||||
);
|
||||
check(
|
||||
'审核结论可在后台追溯(status / reviewReason / reviewedAt 都在版本行上)',
|
||||
adminVersionAfterReject?.status === 'rejected' &&
|
||||
adminVersionAfterReject?.reviewReason === 'E2E 拒绝原因' &&
|
||||
Boolean(adminVersionAfterReject?.reviewedAt),
|
||||
`status=${adminVersionAfterReject?.status ?? 'missing'} reviewedAt=${adminVersionAfterReject?.reviewedAt ?? ''}`,
|
||||
);
|
||||
|
||||
console.log(`\n结果:${failures === 0 ? '全部通过' : `${failures} 项失败`}`);
|
||||
process.exitCode = failures === 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ async function api(path, options = {}) {
|
||||
text,
|
||||
data: json?.data,
|
||||
error: json?.error,
|
||||
meta: json?.meta,
|
||||
ok: json?.ok,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -265,6 +267,32 @@ async function main() {
|
||||
`status=${foreignRead.status} code=${foreignRead.error?.code ?? ''}`,
|
||||
);
|
||||
|
||||
// 成功 / 失败 envelope 的形状:TS 侧 `packages/shared/src/http.ts` 与 `src/services/apiClient.ts`
|
||||
// 的运行时守卫读的就是这几个字段(data / error.code / meta.apiVersion / meta.requestId)。
|
||||
check(
|
||||
'成功响应 envelope 带 ok/data 与 meta.apiVersion / meta.requestId',
|
||||
created.status === 200 &&
|
||||
created.data !== undefined &&
|
||||
created.ok === true &&
|
||||
created.error === null &&
|
||||
typeof created.meta?.apiVersion === 'string' &&
|
||||
created.meta.apiVersion.length > 0 &&
|
||||
typeof created.meta?.requestId === 'string' &&
|
||||
created.meta.requestId.length > 0,
|
||||
`apiVersion=${created.meta?.apiVersion ?? ''} requestId=${created.meta?.requestId ?? ''}`,
|
||||
);
|
||||
check(
|
||||
'失败响应 envelope 带 ok=false/error.code 与 meta.requestId',
|
||||
foreignRead.status === 404 &&
|
||||
foreignRead.ok === false &&
|
||||
typeof foreignRead.error?.code === 'string' &&
|
||||
foreignRead.error.code.length > 0 &&
|
||||
foreignRead.data === null &&
|
||||
typeof foreignRead.meta?.requestId === 'string' &&
|
||||
foreignRead.meta.requestId.length > 0,
|
||||
`code=${foreignRead.error?.code ?? ''} requestId=${foreignRead.meta?.requestId ?? ''}`,
|
||||
);
|
||||
|
||||
const foreignState = await api(
|
||||
`/api/game-distribution/versions/${versionId}/package/upload-state`,
|
||||
{ token: intruder.token },
|
||||
|
||||
Reference in New Issue
Block a user