补齐错误 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

- 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:
kdletters
2026-09-28 18:58:38 +08:00
parent 23de78b476
commit 253f99d921
6 changed files with 256 additions and 14 deletions
@@ -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 },