test(游戏共创): 主题 e2e 跟进 rootsTruncated 与后台成员名单,修正过期断言
Project CI / AI game creator shell Rust crates (pull_request) Successful in 6m0s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m54s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m55s
Project CI / Backend tests (pull_request) Successful in 9m31s
Project CI / Frontend tests (pull_request) Successful in 3m6s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m21s
Project CI / Native shell tests (pull_request) Successful in 6m50s
Project CI / Repository checks (pull_request) Successful in 4m58s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 6m0s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m54s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m55s
Project CI / Backend tests (pull_request) Successful in 9m31s
Project CI / Frontend tests (pull_request) Successful in 3m6s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m21s
Project CI / Native shell tests (pull_request) Successful in 6m50s
Project CI / Repository checks (pull_request) Successful in 4m58s
- scripts/check-game-distribution-theme-e2e.mjs:
· 头部契约引用**全部改成符号检索**(函数名/常量名),不再写会随提交漂移的行号
· 新增「公开详情顶层键集合恰为契约七个(含 rootsTruncated)」与「小主题下 rootsTruncated 是布尔且
为 false」两条;同时把「memberCount === roots.length」明确标注为只在小主题成立的观察,不再当不变式
· 新增第 10 组:后台成员名单(未套公开可见性过滤、draft 也能读)——51 个未公开根作品 + 1 个软删除成员
共 52 行,逐条断言:limit=999 截断到 50 且 totalMembers 仍为 52(limit=1 时 members 只有 1 行、
totalMembers 不变)、默认 20 游标翻页 3 页 52 行不重不漏且末页 nextCursor=null、成员行键集合恰为
{rootGameId,title,sortOrder,createdAt,visible,visibility}、按 sortOrder 升序、未公开成员
visible=false+visibility=unpublished、软删除成员 visible=false+visibility=deleted(落在末页)、
非法游标 400 THEME_INVALID_CURSOR、主题不存在 404、draft 主题也能读到成员、未登录 401 / 普通作者 403
· 上一轮标红的「?cursor=abc → 稳定错误码 THEME_INVALID_CURSOR」断言**本轮转绿**(模块侧已把
GAME_DISTRIBUTION_THEME_INVALID_CURSOR_CODE 前缀拼进游标错误文案,映射分支可达)
· 实测:71 项 71 PASS / 0 FAIL / 0 SKIP
- scripts/check-game-distribution-collection-e2e.mjs:修正随主题功能上线的**过期断言**——
匿名作品详情现在比公开目录条目多一个 `themes` 键(详情侧增量,目录侧保持精简)。断言改为
「目录条目是详情子集:共有键取值全一致 + 详情只多 themes 一处」,不再要求两边键数相等。
实测:46 项 46 PASS / 0 FAIL / 0 SKIP
This commit is contained in:
@@ -734,11 +734,14 @@ async function main() {
|
||||
JSON.stringify(anonymousDetail.data?.[key]),
|
||||
);
|
||||
check(
|
||||
'8) 公开目录里的同作品条目与匿名详情键集合与取值都一致',
|
||||
'8) 公开目录条目是匿名详情的子集(共有键取值全一致),详情仅多出 themes 这一处增量',
|
||||
Boolean(catalogEntry) &&
|
||||
catalogKeys.length === anonymousKeys.length &&
|
||||
mismatchedCatalogKeys.length === 0,
|
||||
`catalogKeys=${catalogKeys.length} detailKeys=${anonymousKeys.length} 不一致=${mismatchedCatalogKeys.join(',') || '无'}`,
|
||||
mismatchedCatalogKeys.length === 0 &&
|
||||
anonymousKeys.filter((key) => !catalogKeys.includes(key)).length === 1 &&
|
||||
anonymousKeys.filter((key) => !catalogKeys.includes(key))[0] ===
|
||||
'themes' &&
|
||||
Array.isArray(anonymousDetail.data?.themes),
|
||||
`catalogKeys=${catalogKeys.length} detailKeys=${anonymousKeys.length} 不一致=${mismatchedCatalogKeys.join(',') || '无'} 仅详情有=${anonymousKeys.filter((key) => !catalogKeys.includes(key)).join(',') || '无'}`,
|
||||
);
|
||||
check(
|
||||
'8) 我的收藏列表条数与「已收藏」状态一致(nextCursor=null)',
|
||||
|
||||
@@ -7,27 +7,31 @@
|
||||
// node scripts/check-game-distribution-theme-e2e.mjs
|
||||
// E2E_API_BASE 可覆盖 api-server 地址(默认从 CWD 的 .app/dev-stack.json 读取,不写死端口)
|
||||
//
|
||||
// 契约来源(逐条读实现确认):
|
||||
// [1] 公开路由(匿名 + no-store):api-server/src/modules/game_distribution.rs:2921-2930
|
||||
// [2] 公开列表分页::1635-1690(limit 归一化 :1757-1764、cursor 由模块解析并 400 透传)
|
||||
// [3] 公开详情::1670-1697(未发布/不存在都回「主题不存在」→ 404;已发布空成员是 200 + 空 roots)
|
||||
// + module-game-distribution/src/theme.rs:105-108(公开可见 = status == published)
|
||||
// [4] 详情成员与列表 memberCount 同一份可见性判定(未删 + 已公开 + 有当前公开版本):
|
||||
// spacetime-module/src/game_distribution.rs:5455-5477 + theme.rs:131-147
|
||||
// [5] 作品详情 `themes` 增量「先取根再反查」:spacetime-module/...:5620-5670 + 根解析 :5450-5456
|
||||
// [6] 后台写路由(admin 鉴权):api-server/...:2887-2900;创建/更新幂等 :1780-1880;
|
||||
// 成员 PUT/DELETE 不要求幂等键且确定性主键去重:spacetime-module/...:5890-5965
|
||||
// [7] 错误码映射:api-server/...:5329-5365(THEME_NOT_FOUND→404、THEME_BAD_REQUEST→400、
|
||||
// THEME_INVALID_CURSOR→400、THEME_IDEMPOTENCY_CONFLICT→409、THEME_MEMBER_NOT_ROOT→409、
|
||||
// THEME_MEMBER_GAME_NOT_FOUND→404)
|
||||
// [8] 分页极限:theme.rs:156-172(默认 20、上限 50、超界截断、0 取默认)+ 游标格式 `{micros}:{themeId}` :205-218
|
||||
// 契约来源(逐条读实现确认;**一律用符号检索定位,不写行号**——行号会随提交漂移):
|
||||
// [1] 公开主题路由(匿名 + no-store):`api-server/src/modules/game_distribution.rs` 里
|
||||
// `public_games` 路由族的 `themes` / `themes/{theme_id}` 两条(`themes` 前缀可定位)
|
||||
// [2] 公开列表分页与响应形状:`list_public_themes` + `public_themes_payload` + `themes_page_limit`
|
||||
// [3] 公开详情:`get_public_theme` + `public_theme_detail_payload`
|
||||
// (未发布/不存在都回「主题不存在」→ 404;已发布空成员是 200 + 空 roots)
|
||||
// + `module-game-distribution/src/theme.rs` 的 `game_distribution_theme_public_visible`
|
||||
// [4] 可见性与 memberCount 判定:`game_distribution_theme_visible_members`
|
||||
// (未删 + 已公开 + 有当前公开版本)+ `game_distribution_theme_roots_truncated`
|
||||
// [5] 作品详情 `themes` 增量「先取根再反查」:`game_distribution_themes_for_game`
|
||||
// [6] 后台写路由(admin 鉴权):`admin_create_theme` / `admin_update_theme` /
|
||||
// `admin_set_theme_member` / `admin_delete_theme_member` / `admin_list_themes` /
|
||||
// `admin_list_theme_members`(成员名单不套公开可见性过滤)
|
||||
// [7] 错误码映射:`map_spacetime_error` 里的 `GAME_DISTRIBUTION_THEME_*` 分支前缀判定
|
||||
// [8] 分页极限与游标:`game_distribution_theme_page_limit`(默认 20 / 上限 50 / 0 取默认 / 超界截断)、
|
||||
// `parse_game_distribution_theme_cursor`、`page_admin_theme_members`
|
||||
// (成员名单缺省 20 / 上限 50,按 `sort_order` 升序 + `member_id` 兜底,游标 `{sort_order}:{memberId}`)
|
||||
//
|
||||
// 复用/照抄的 helper(注释标了出处;抽公共模块会改动其它已提交脚本,故照抄):
|
||||
// - 从 .app/dev-stack.json 读地址:scripts/check-game-distribution-lineage-e2e.mjs:115-133
|
||||
// - 从 .app/dev-stack.json 读地址:scripts/check-game-distribution-lineage-e2e.mjs 的
|
||||
// `const devStack = JSON.parse(...)` 那一段
|
||||
// - check/note/api/brief/register/gameMetadata/uploadCover/createGame/ownerGame/publicDetail:
|
||||
// lineage 脚本 :139-318
|
||||
// - 完整发布链路(建版本 → 传发行包 → 送审 → 管理员通过):lineage 脚本 :320-407
|
||||
// - 改编声明(fork 字段)+ 公开版本 id 取法:lineage 脚本 :758-812
|
||||
// lineage 脚本同名 helper
|
||||
// - 完整发布链路(建版本 → 传发行包 → 送审 → 管理员通过):lineage 脚本的 `publishToPublic`
|
||||
// - 改编声明(fork 字段)+ 公开版本 id 取法:lineage 脚本里取 `versions.find(status==='published')` 那一段
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -403,6 +407,24 @@ function publicThemeDetail(themeId) {
|
||||
return api(`/api/game-distribution/themes/${themeId}`);
|
||||
}
|
||||
|
||||
function adminThemeMembers({ admin, themeId, limit, cursor } = {}) {
|
||||
const query = new URLSearchParams();
|
||||
if (limit !== undefined) query.set('limit', String(limit));
|
||||
if (cursor !== undefined) query.set('cursor', cursor);
|
||||
const suffix = query.toString() ? `?${query.toString()}` : '';
|
||||
return api(
|
||||
`/admin/api/game-distribution/themes/${themeId}/members${suffix}`,
|
||||
{ token: admin },
|
||||
);
|
||||
}
|
||||
|
||||
function softDeleteGame({ token, gameId, revision, key }) {
|
||||
return api(
|
||||
`/api/game-distribution/my-games/${gameId}?expectedPublicationRevision=${revision}`,
|
||||
{ method: 'DELETE', token, headers: { 'Idempotency-Key': key } },
|
||||
);
|
||||
}
|
||||
|
||||
function themeIdsOf(body) {
|
||||
return (body?.data?.themes ?? []).map((theme) => theme.themeId);
|
||||
}
|
||||
@@ -637,6 +659,29 @@ async function main() {
|
||||
JSON.stringify(Object.keys(catalogEntry ?? {}).sort()),
|
||||
`roots=${detailT1.data?.roots?.length} memberCount=${detailT1.data?.memberCount} rootKeys=${Object.keys(detailT1.data?.roots?.[0] ?? {}).length} catalogKeys=${Object.keys(catalogEntry ?? {}).length}`,
|
||||
);
|
||||
check(
|
||||
'1) 公开详情顶层键集合恰为契约的七个(含 rootsTruncated)',
|
||||
JSON.stringify(Object.keys(detailT1.data ?? {}).sort()) ===
|
||||
JSON.stringify(
|
||||
[
|
||||
'badge',
|
||||
'memberCount',
|
||||
'name',
|
||||
'roots',
|
||||
'rootsTruncated',
|
||||
'summary',
|
||||
'themeId',
|
||||
].sort(),
|
||||
),
|
||||
`keys=${Object.keys(detailT1.data ?? {}).join(',')}`,
|
||||
);
|
||||
check(
|
||||
'1) 小主题下 rootsTruncated 是布尔且为 false(memberCount === roots.length 只在这种小主题下成立)',
|
||||
typeof detailT1.data?.rootsTruncated === 'boolean' &&
|
||||
detailT1.data?.rootsTruncated === false &&
|
||||
detailT1.data?.memberCount === detailT1.data?.roots?.length,
|
||||
`rootsTruncated=${JSON.stringify(detailT1.data?.rootsTruncated)} memberCount=${detailT1.data?.memberCount} roots=${detailT1.data?.roots?.length}`,
|
||||
);
|
||||
|
||||
// ---------- 2:只允许根 ----------
|
||||
const notRoot = await adminPutMember({
|
||||
@@ -967,15 +1012,14 @@ async function main() {
|
||||
brief(badCursor),
|
||||
);
|
||||
check(
|
||||
'6) ?cursor=abc → 稳定错误码 THEME_INVALID_CURSOR',
|
||||
'6) ?cursor=abc → 稳定错误码 THEME_INVALID_CURSOR(此前红断言,本轮应转绿)',
|
||||
errorCode(badCursor) === 'THEME_INVALID_CURSOR',
|
||||
brief(badCursor),
|
||||
);
|
||||
note(
|
||||
'产品缺口(未修,仅报告):实现为非法游标注册了稳定码 THEME_INVALID_CURSOR' +
|
||||
'(api-server/...:5343-5346 + shared-contracts/src/game_distribution.rs:46),' +
|
||||
'但模块侧只回纯文案「主题列表游标格式无效」(module-game-distribution/src/theme.rs:205-215),不含 THEME_ 前缀,' +
|
||||
'而映射分支的前置条件是 message.contains("THEME_") → 该分支不可达,客户端实际拿到 code=BAD_REQUEST(状态码仍是 400 ✓)。',
|
||||
'该断言是上一轮标红的产品缺口回归网:模块侧现在把常量前缀拼进游标错误文案' +
|
||||
'(module-game-distribution/src/theme.rs 的 `GAME_DISTRIBUTION_THEME_INVALID_CURSOR_CODE` + 「: 主题列表游标格式无效」),' +
|
||||
'api-server 的 `map_spacetime_error` 前缀分支因此可达,客户端拿到的 error.code 从 BAD_REQUEST 变成 THEME_INVALID_CURSOR(状态码仍是 400)。',
|
||||
);
|
||||
|
||||
// ---------- 7:后台权限 ----------
|
||||
@@ -1158,6 +1202,297 @@ async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- 10:后台成员名单(含不可见成员 / 草稿主题 / 分页 / 错误码) ----------
|
||||
// 契约:`admin_list_theme_members`(不套公开可见性过滤、draft/archived 也能读)、
|
||||
// `totalMembers` 是成员行总数(不受分页影响)、`visibility ∈ missing|deleted|published|unpublished|suspended`、
|
||||
// `visible` 复用公开侧同一份判定、分页沿用主题列表的 20/50 与游标解析。
|
||||
const bulkTheme = await adminCreateTheme({
|
||||
admin,
|
||||
key: `theme-bulk-${stamp}`,
|
||||
body: {
|
||||
name: `主题批量成员 ${suffix}`,
|
||||
summary: '成员行数 > 50、可见成员为 0',
|
||||
badge: 'B',
|
||||
sortOrder: 8,
|
||||
status: 'published',
|
||||
},
|
||||
});
|
||||
const bulkThemeId = bulkTheme.data?.theme?.themeId;
|
||||
check('10) 批量成员主题创建成功', Boolean(bulkThemeId), brief(bulkTheme));
|
||||
|
||||
// 用「未公开草稿作品」当成员:可见成员恒为 0,但成员行数 > 50,用来验证
|
||||
// memberCount 只数可见成员、后台名单 totalMembers 数行且分页可翻。
|
||||
let reusableCoverAssetId = await uploadCover(author.token, `${stamp}-bulk`);
|
||||
const bulkRootIds = [];
|
||||
let reuseNoted = false;
|
||||
for (let index = 0; index < 51; index += 1) {
|
||||
const title = `批量成员根作品 ${suffix}-${index}`;
|
||||
let created = await createGame({
|
||||
token: author.token,
|
||||
metadata: gameMetadata({ title, coverAssetId: reusableCoverAssetId }),
|
||||
idemKey: `theme-bulk-root-${index}-${stamp}`,
|
||||
});
|
||||
if (created.status !== 200) {
|
||||
if (!reuseNoted) {
|
||||
note(
|
||||
`封面 assetId 不能跨作品复用时改为逐件上传(首次失败:${brief(created)})`,
|
||||
);
|
||||
reuseNoted = true;
|
||||
}
|
||||
reusableCoverAssetId = await uploadCover(
|
||||
author.token,
|
||||
`${stamp}-bulk-${index}`,
|
||||
);
|
||||
created = await createGame({
|
||||
token: author.token,
|
||||
metadata: gameMetadata({ title, coverAssetId: reusableCoverAssetId }),
|
||||
idemKey: `theme-bulk-root-retry-${index}-${stamp}`,
|
||||
});
|
||||
}
|
||||
const id = created.data?.id;
|
||||
if (!id) {
|
||||
check(
|
||||
'10) 批量根作品创建成功',
|
||||
false,
|
||||
`index=${index} ${brief(created)}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
bulkRootIds.push(id);
|
||||
const memberResponse = await adminPutMember({
|
||||
admin,
|
||||
themeId: bulkThemeId,
|
||||
rootGameId: id,
|
||||
sortOrder: index,
|
||||
});
|
||||
if (memberResponse.status !== 200) {
|
||||
check(
|
||||
'10) 批量成员写入成功',
|
||||
false,
|
||||
`index=${index} ${brief(memberResponse)}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
check(
|
||||
'10) 51 个未公开根作品加入同一主题(成员行数 > 50)',
|
||||
bulkRootIds.length === 51,
|
||||
`rows=${bulkRootIds.length}`,
|
||||
);
|
||||
|
||||
const bulkPublicDetail = await publicThemeDetail(bulkThemeId);
|
||||
check(
|
||||
'10) memberCount 只数可见成员:51 行不可见成员 → memberCount=0、roots=[]、rootsTruncated=false',
|
||||
bulkPublicDetail.status === 200 &&
|
||||
bulkPublicDetail.data?.memberCount === 0 &&
|
||||
bulkPublicDetail.data?.roots?.length === 0 &&
|
||||
bulkPublicDetail.data?.rootsTruncated === false,
|
||||
`memberCount=${bulkPublicDetail.data?.memberCount} roots=${bulkPublicDetail.data?.roots?.length} rootsTruncated=${JSON.stringify(bulkPublicDetail.data?.rootsTruncated)}`,
|
||||
);
|
||||
note(
|
||||
'未覆盖(成本):>50 个**可见**成员导致 roots 真被截断、rootsTruncated=true 的真实数据用例——' +
|
||||
'需要 51 个走完整发布链路的公开作品。该分支由 module-game-distribution/src/theme.rs 里 ' +
|
||||
'`game_distribution_theme_roots_truncated` 的单测(51/50 → true)覆盖;本脚本只覆盖了' +
|
||||
'「行数 >50、可见 0」的对照面(rootsTruncated=false)。',
|
||||
);
|
||||
|
||||
// 软删除成员:行仍在 → visible=false 且 visibility=deleted
|
||||
const softDeletedRoot = await createGame({
|
||||
token: author.token,
|
||||
metadata: gameMetadata({
|
||||
title: `软删除成员 ${suffix}`,
|
||||
coverAssetId: await uploadCover(author.token, `${stamp}-soft`),
|
||||
}),
|
||||
idemKey: `theme-soft-root-${stamp}`,
|
||||
});
|
||||
const softDeletedRootId = softDeletedRoot.data?.id;
|
||||
if (!softDeletedRootId) {
|
||||
check('10) 软删除 fixture 作品创建成功', false, brief(softDeletedRoot));
|
||||
}
|
||||
const softDeleted = softDeletedRootId
|
||||
? await softDeleteGame({
|
||||
token: author.token,
|
||||
gameId: softDeletedRootId,
|
||||
revision: softDeletedRoot.data?.publicationRevision ?? 0,
|
||||
key: `theme-soft-delete-${stamp}`,
|
||||
})
|
||||
: { status: 0, text: 'fixture 未创建' };
|
||||
if (softDeletedRootId) {
|
||||
await adminPutMember({
|
||||
admin,
|
||||
themeId: bulkThemeId,
|
||||
rootGameId: softDeletedRootId,
|
||||
sortOrder: 100,
|
||||
});
|
||||
}
|
||||
check(
|
||||
'10) 软删除 fixture 作品就绪',
|
||||
softDeleted.status === 200,
|
||||
brief(softDeleted),
|
||||
);
|
||||
|
||||
const bulkMembers = await adminThemeMembers({
|
||||
admin,
|
||||
themeId: bulkThemeId,
|
||||
limit: 999,
|
||||
});
|
||||
const bulkRows = bulkMembers.data?.members ?? [];
|
||||
check(
|
||||
'10) 行数 >50 时 limit=999 截断到 50(不是错误),totalMembers 仍是行总数 52,且仍有下一页',
|
||||
bulkMembers.status === 200 &&
|
||||
bulkRows.length === 50 &&
|
||||
bulkMembers.data?.totalMembers === 52 &&
|
||||
typeof bulkMembers.data?.nextCursor === 'string',
|
||||
`rows=${bulkRows.length} totalMembers=${bulkMembers.data?.totalMembers} nextCursor=${JSON.stringify(bulkMembers.data?.nextCursor ?? null).slice(0, 30)}`,
|
||||
);
|
||||
check(
|
||||
'10) totalMembers 不受分页影响:limit=1 时 members 只有 1 行但 totalMembers 仍为 52',
|
||||
(await adminThemeMembers({ admin, themeId: bulkThemeId, limit: 1 })).data
|
||||
?.totalMembers === 52 &&
|
||||
(await adminThemeMembers({ admin, themeId: bulkThemeId, limit: 1 })).data
|
||||
?.members?.length === 1,
|
||||
`totalMembers=${(await adminThemeMembers({ admin, themeId: bulkThemeId, limit: 1 })).data?.totalMembers}`,
|
||||
);
|
||||
const unpublishedRow = bulkRows.find(
|
||||
(member) => member.rootGameId === bulkRootIds[0],
|
||||
);
|
||||
check(
|
||||
'10) 未公开成员照实返回:visible=false 且 visibility=unpublished',
|
||||
unpublishedRow?.visible === false &&
|
||||
unpublishedRow?.visibility === 'unpublished',
|
||||
`visible=${JSON.stringify(unpublishedRow?.visible)} visibility=${unpublishedRow?.visibility}`,
|
||||
);
|
||||
check(
|
||||
'10) 成员行键集合恰为契约的六个(visible 与 visibility 都在)',
|
||||
bulkRows.length > 0 &&
|
||||
JSON.stringify(Object.keys(bulkRows[0]).sort()) ===
|
||||
JSON.stringify(
|
||||
[
|
||||
'createdAt',
|
||||
'rootGameId',
|
||||
'sortOrder',
|
||||
'title',
|
||||
'visibility',
|
||||
'visible',
|
||||
].sort(),
|
||||
),
|
||||
`keys=${Object.keys(bulkRows[0] ?? {}).join(',')}`,
|
||||
);
|
||||
check(
|
||||
'10) 成员行按 sortOrder 升序返回',
|
||||
bulkRows.every(
|
||||
(row, index) =>
|
||||
index === 0 || bulkRows[index - 1].sortOrder <= row.sortOrder,
|
||||
),
|
||||
`sortOrders=${bulkRows
|
||||
.slice(0, 5)
|
||||
.map((row) => row.sortOrder)
|
||||
.join(',')}…`,
|
||||
);
|
||||
|
||||
const memberPageIds = [];
|
||||
const memberRows = [];
|
||||
let memberCursor;
|
||||
let memberPages = 0;
|
||||
let lastMemberCursor = 'unset';
|
||||
for (;;) {
|
||||
const page = await adminThemeMembers({
|
||||
admin,
|
||||
themeId: bulkThemeId,
|
||||
limit: 20,
|
||||
...(memberCursor ? { cursor: memberCursor } : {}),
|
||||
});
|
||||
if (page.status !== 200) {
|
||||
check('10) 成员名单游标翻页每页 200', false, brief(page));
|
||||
break;
|
||||
}
|
||||
memberPages += 1;
|
||||
const pageRows = page.data?.members ?? [];
|
||||
memberRows.push(...pageRows);
|
||||
memberPageIds.push(...pageRows.map((row) => row.rootGameId));
|
||||
lastMemberCursor = page.data?.nextCursor ?? null;
|
||||
if (!lastMemberCursor) break;
|
||||
memberCursor = lastMemberCursor;
|
||||
if (memberPages > 10) {
|
||||
check(
|
||||
'10) 成员名单游标翻页在合理页数内结束',
|
||||
false,
|
||||
`pages=${memberPages}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
check(
|
||||
'10) 成员名单游标翻页不重不漏(52 行分 3 页,末页 nextCursor=null)',
|
||||
memberPages === 3 &&
|
||||
memberPageIds.length === 52 &&
|
||||
new Set(memberPageIds).size === 52 &&
|
||||
lastMemberCursor === null &&
|
||||
bulkRootIds.every((id) => memberPageIds.includes(id)),
|
||||
`pages=${memberPages} total=${memberPageIds.length} unique=${new Set(memberPageIds).size} lastCursor=${JSON.stringify(lastMemberCursor)}`,
|
||||
);
|
||||
const softDeletedRow = memberRows.find(
|
||||
(member) => member.rootGameId === softDeletedRootId,
|
||||
);
|
||||
check(
|
||||
'10) 已软删除成员照实返回:visible=false 且 visibility=deleted(行未删除,末页仍能翻到)',
|
||||
softDeletedRow?.visible === false &&
|
||||
softDeletedRow?.visibility === 'deleted',
|
||||
`visible=${JSON.stringify(softDeletedRow?.visible)} visibility=${softDeletedRow?.visibility ?? '∅'}(该行 sortOrder=100,落最后一页)`,
|
||||
);
|
||||
|
||||
const badMemberCursor = await adminThemeMembers({
|
||||
admin,
|
||||
themeId: bulkThemeId,
|
||||
cursor: 'abc',
|
||||
});
|
||||
check(
|
||||
'10) 成员名单非法游标 → 400 THEME_INVALID_CURSOR(与主题列表同一套解析)',
|
||||
badMemberCursor.status === 400 &&
|
||||
errorCode(badMemberCursor) === 'THEME_INVALID_CURSOR',
|
||||
brief(badMemberCursor),
|
||||
);
|
||||
const missingThemeMembers = await adminThemeMembers({
|
||||
admin,
|
||||
themeId: `theme_${'0'.repeat(32)}`,
|
||||
});
|
||||
check(
|
||||
'10) 主题不存在 → 404',
|
||||
missingThemeMembers.status === 404,
|
||||
brief(missingThemeMembers),
|
||||
);
|
||||
const draftThemeMembers = await adminThemeMembers({
|
||||
admin,
|
||||
themeId: draftId,
|
||||
limit: 20,
|
||||
});
|
||||
check(
|
||||
'10) draft 主题也能读到成员名单(该接口存在的理由:公开侧看不见、运营要能看)',
|
||||
draftThemeMembers.status === 200 &&
|
||||
(draftThemeMembers.data?.members ?? []).some(
|
||||
(member) => member.rootGameId === rootId,
|
||||
) &&
|
||||
draftThemeMembers.data?.totalMembers === 1,
|
||||
`status=${draftThemeMembers.status} totalMembers=${draftThemeMembers.data?.totalMembers}`,
|
||||
);
|
||||
const anonymousMembers = await api(
|
||||
`/admin/api/game-distribution/themes/${bulkThemeId}/members`,
|
||||
);
|
||||
const strangerMembers = await api(
|
||||
`/admin/api/game-distribution/themes/${bulkThemeId}/members`,
|
||||
{ token: stranger.token },
|
||||
);
|
||||
check(
|
||||
'10) 未登录读成员名单 → 401;普通作者 token → 403',
|
||||
anonymousMembers.status === 401 && strangerMembers.status === 403,
|
||||
`anonymous=${anonymousMembers.status} stranger=${strangerMembers.status}`,
|
||||
);
|
||||
note(
|
||||
'未覆盖:`visibility=missing`(游戏行本身不存在)与「visibility=published 但无当前公开版本 → visible=false」' +
|
||||
'两个防御性分支——前者需要删游戏行、后者需要在 kept-published 状态下清空当前公开版本,现有 HTTP 面都构造不出来;' +
|
||||
'两者在 `game_distribution_theme_member_visibility` / `game_distribution_theme_member_visible` 的单测里覆盖。',
|
||||
);
|
||||
|
||||
note(
|
||||
'未覆盖:本脚本不做主题成员的可见性上限(详情 roots 受 50 条响应上限约束)与「同一个根重复加入两个主题后删除其一的读取一致性」之外的并发场景;' +
|
||||
'分页排序键(created_at 倒序 + themeId 兜底)只通过「不重不漏」间接验证,未构造同微秒创建的主题对。',
|
||||
|
||||
Reference in New Issue
Block a user