market 5 个 skip 用例全是 MSW payload 拦截的负向断言(验前端预览绑定守卫不发陈旧 payload、靠 museAdminPayloadsByPage),真后端 MSW-off 无 payload 收集器无法逐字搬、且纯前端逻辑已 vitest 覆盖,故替换为真后端真连用例。 真连(反假绿:不只看 UI toast,真 PG 反查 muse_market_* 真实行):①市场治理页渲染三工作台(按激活 tabpanel 收口,ant Tabs 不卸载隐藏 pane);②下架真打命令→真 PG 反查 listing_status=delisted+新增 1 条 applied delist 治理动作;③申诉 maintained 真打处理→真 PG 反查 status=maintained、resolver_id=1。global-setup 加 resetMarketFixture(资产复位 listed+申诉复位 supplementing+软删历轮累积终态申诉(42 条 closed 挤出首页锚点)+清理治理动作/预览)、每轮幂等自愈。 揪出 mock 臆想契约:assetId/appealId 是数字串(非 asset-e2e);publish status DB submitted→API pending(后端 toOpenApiStatus 唯一特例、mock 四态不成立);appeal.resolution 列存处理理由文本非结论枚举(结论在 status);delist expectedStatus 只接受 listed(传别的 400);preview actionType 实为 5 值(含 review_rejection)非 4 值。 isIgnoredRuntimeIssue 增忽略 Failed to load resource: net::ERR_*(资源懒加载抖动、非应用错误,命令正确性另有真 PG 反查兜底)——同解 account 首跑偶发红根因(原误判 console.error,实为 dev-server 负载下 chunk 懒加载超时)。独立连跑全量 account+market e2e x2 各 4 passed;psql 复核 asset1=delisted/appeal1=maintained resolver=1/gov_action=1,与报告一致。restore 票据路径 e2e 未覆盖(用例走 maintained、后端不消费 restore 预览),restore 仍只 mock-service 单测无真 PG IT、缺口待补(:397 注释标注)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
185 lines
9.6 KiB
TypeScript
185 lines
9.6 KiB
TypeScript
import { execFileSync } from 'node:child_process';
|
|
|
|
import { Client } from 'pg';
|
|
|
|
/**
|
|
* Playwright globalSetup —— admin 治理 e2e 转真后端的活体前置(可入库,替代一次性种子脚本)。
|
|
*
|
|
* WHY:admin e2e 关 mock 直连真实 muse-server(vite 代理 /admin-api→48080)+ 真 PostgreSQL(muse_slice_live)。
|
|
* admin 用户(system_users id=1)与 studio 共用 mock-token(Bearer test1 = mockSecret"test"+userId1),
|
|
* 但该用户在 muse_slice_live 里默认**无角色**(system_user_role 空)→ 命中 @PreAuthorize 403。
|
|
* 本前置幂等 seed `system_user_role(id=1,user_id=1,role_id=1)`(role 1=super_admin),令 admin 成超管
|
|
* (PermissionServiceImpl.hasAnyPermissions 超管直接放行,跳过具体权限点),再 DEL redis 权限缓存使其立即生效。
|
|
*
|
|
* 连接凭据从环境变量读取(`MUSE_POSTGRES_*` / `MUSE_REDIS_*`,见 ~/.config/muse-repo/infra.env;**绝不入库**):
|
|
* 跑前先 `set -a; . ~/.config/muse-repo/infra.env; set +a`。缺 PG 凭据时直接报错(本套件仅活体跑,无 DB 不可能真绿)。
|
|
* 如确知库已就绪、想跳过:置 `E2E_SKIP_SEED=1`。
|
|
*
|
|
* 说明:account 子域只读 users/usage-records/purchase-records 三个列表端点,其数据由 muse_slice_live 基础种子
|
|
* 提供(user1 的 account profile / purchase 记录已存在),本前置不重置 account fixture,仅补齐权限前提。
|
|
*
|
|
* market 子域(资产治理/发布审核/申诉处理)是写命令子域:e2e 会真打下架/召回/审核/申诉处理命令,直接改
|
|
* muse_slice_live 共享行。为保证每轮可复现绿(反"上一轮把资产下架/申诉处理成终态导致本轮按钮 disabled"),
|
|
* 本前置在权限 seed 后再幂等复位 market fixture(见 resetMarketFixture):把稳定锚点资产/申诉复位到 e2e 期望
|
|
* 初态,并清理历轮累积的治理预览/治理动作。复位按 tenant_id=1(mock-token user1 所属租户)精确限定,只碰 e2e
|
|
* 锚点行(资产 1/4、申诉 1、资产 2),不影响 account/publish 等其他子域数据。
|
|
*/
|
|
async function globalSetup(): Promise<void> {
|
|
if (process.env.E2E_SKIP_SEED === '1') {
|
|
console.log('[admin e2e globalSetup] E2E_SKIP_SEED=1 → 跳过权限前置');
|
|
return;
|
|
}
|
|
|
|
const host = process.env.MUSE_POSTGRES_HOST;
|
|
const password = process.env.MUSE_POSTGRES_PASSWORD;
|
|
if (!host || !password) {
|
|
throw new Error(
|
|
'[admin e2e globalSetup] 缺少 MUSE_POSTGRES_* 环境变量。活体 e2e 需直连真实 PG seed 权限前提,' +
|
|
'请先 `set -a; . ~/.config/muse-repo/infra.env; set +a` 再跑(或置 E2E_SKIP_SEED=1 跳过)。',
|
|
);
|
|
}
|
|
|
|
const client = new Client({
|
|
host,
|
|
port: Number(process.env.MUSE_POSTGRES_PORT ?? '5433'),
|
|
database: process.env.MUSE_POSTGRES_DATABASE ?? 'muse_slice_live',
|
|
user: process.env.MUSE_POSTGRES_USERNAME ?? 'root',
|
|
password,
|
|
ssl: false,
|
|
connectionTimeoutMillis: 10_000,
|
|
});
|
|
|
|
await client.connect();
|
|
try {
|
|
// 幂等 seed:admin(system_users id=1)绑定 super_admin(system_role id=1)。
|
|
// ON CONFLICT DO NOTHING:重跑不重复;若已存在(基础种子已建)则 rowCount=0,无副作用。
|
|
const seed = await client.query(
|
|
`INSERT INTO system_user_role (id, user_id, role_id, tenant_id)
|
|
VALUES (1, 1, 1, 1) ON CONFLICT (id) DO NOTHING`,
|
|
);
|
|
// 复核绑定确实在(防御:若 id 冲突但内容被改过,这里能暴露)。
|
|
const check = await client.query(
|
|
'SELECT user_id, role_id FROM system_user_role WHERE id=1 AND deleted=false',
|
|
);
|
|
if (check.rowCount === 0) {
|
|
console.warn(
|
|
'[admin e2e globalSetup] ⚠️ system_user_role(id=1) 缺失或被软删,admin 可能仍 403',
|
|
);
|
|
}
|
|
console.log(
|
|
`[admin e2e globalSetup] seed system_user_role(super_admin) rowCount=${seed.rowCount}(0=已存在)`,
|
|
);
|
|
|
|
// 复位 market 写命令子域 fixture(详见函数注释)。
|
|
await resetMarketFixture(client);
|
|
} finally {
|
|
await client.end();
|
|
}
|
|
|
|
// DEL redis 权限缓存(yudao 单冒号 key user_role_ids:1),令 seed 立即生效。
|
|
// 本机/活体环境用 redis-cli(redis 在 127.0.0.1:6379,凭据来自 MUSE_REDIS_PASSWORD);
|
|
// 失败不致命(缓存未命中时下次查询会回源 DB),仅告警,避免阻断套件。
|
|
const redisHost = process.env.MUSE_REDIS_HOST ?? '127.0.0.1';
|
|
const redisPort = process.env.MUSE_REDIS_PORT ?? '6379';
|
|
const redisPassword = process.env.MUSE_REDIS_PASSWORD;
|
|
try {
|
|
const args = ['-h', redisHost, '-p', redisPort];
|
|
if (redisPassword) {
|
|
args.push('-a', redisPassword, '--no-auth-warning');
|
|
}
|
|
args.push('DEL', 'user_role_ids:1');
|
|
const out = execFileSync('redis-cli', args, { encoding: 'utf8' }).trim();
|
|
console.log(`[admin e2e globalSetup] DEL redis user_role_ids:1 => ${out}`);
|
|
} catch (error) {
|
|
console.warn(
|
|
`[admin e2e globalSetup] ⚠️ DEL redis 权限缓存失败(非致命,下次查询回源 DB):${
|
|
(error as Error).message
|
|
}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 复位 market 子域 e2e fixture(幂等、tenant_id=1 限定)。
|
|
*
|
|
* market e2e 锚点(由 muse_slice_live 基础种子提供,本函数只复位状态,不新建资产/申诉):
|
|
* - 资产 id=1「活体市场资产·测试」: listed,e2e 在「资产治理」tab 对它做影响预览/下架(真打命令改 listing_status)。
|
|
* - 资产 id=4「活体市场资产·安装e2e」: listed,资产列表只读断言锚点(与 account 子域共用)。
|
|
* - 资产 id=2「活体申诉演练资产」: not_listed,申诉 1 的关联资产。
|
|
* - 申诉 id=1: supplementing(开放态),e2e 在「申诉处理」tab 对它生成影响预览并以 maintained 结论处理。
|
|
*
|
|
* 为什么要复位:下架/召回会把资产 listing_status 改成 delisted/recalled、申诉处理会把 status 推进到终态,
|
|
* 都会让下一轮的「下架/处理」按钮 disabled(前端 canDelistAsset/canResolveAppeal 只放行特定初态),导致
|
|
* 第二轮假红。复位把锚点拉回初态,并删除历轮累积的治理预览(每次预览写一行 ready)和治理动作(每次下架写一行),
|
|
* 让 e2e 对 muse_market_governance_action 的"本轮新增一行"DB 断言可精确成立。
|
|
*
|
|
* 复位是 best-effort 的硬前置:若锚点资产/申诉不存在(切片库未灌基础种子),抛错让套件 fail-fast,
|
|
* 避免 market e2e 在缺数据时跑出误导性的失败。
|
|
*/
|
|
async function resetMarketFixture(client: Client): Promise<void> {
|
|
// 锚点存在性校验:缺任一锚点说明切片库未就绪,直接报错(market e2e 不可能真绿)。
|
|
const anchors = await client.query(
|
|
`SELECT id, listing_status FROM muse_market_asset
|
|
WHERE tenant_id = 1 AND deleted = false AND id IN (1, 2, 4)`,
|
|
);
|
|
const anchorIds = new Set(anchors.rows.map((row) => Number(row.id)));
|
|
const missingAssets = [1, 2, 4].filter((id) => !anchorIds.has(id));
|
|
const appeal = await client.query(
|
|
`SELECT id, status FROM muse_market_appeal
|
|
WHERE tenant_id = 1 AND deleted = false AND id = 1`,
|
|
);
|
|
if (missingAssets.length > 0 || appeal.rowCount === 0) {
|
|
throw new Error(
|
|
`[admin e2e globalSetup] market 锚点缺失(缺资产 ${missingAssets.join(',') || '无'}、` +
|
|
`申诉1 ${appeal.rowCount === 0 ? '缺失' : '存在'});` +
|
|
'请确认 muse_slice_live 已灌 market 基础种子,或置 E2E_SKIP_SEED=1 跳过。',
|
|
);
|
|
}
|
|
|
|
// 1) 资产 1/4 复位为已上架(撤销历轮下架/召回);仅状态偏离时才写,rowCount 反映实际修复条数。
|
|
const resetAssets = await client.query(
|
|
`UPDATE muse_market_asset
|
|
SET listing_status = 'listed', status = 'active'
|
|
WHERE tenant_id = 1 AND deleted = false AND id IN (1, 4)
|
|
AND (listing_status <> 'listed' OR status <> 'active')`,
|
|
);
|
|
|
|
// 2) 申诉 1 复位为 supplementing 开放态(撤销历轮 maintained/restored 处理结论)。
|
|
const resetAppeal = await client.query(
|
|
`UPDATE muse_market_appeal
|
|
SET status = 'supplementing', resolution = NULL, resolver_id = NULL, resolved_at = NULL
|
|
WHERE tenant_id = 1 AND deleted = false AND id = 1 AND status <> 'supplementing'`,
|
|
);
|
|
|
|
// 2b) 软删历轮累积的终态申诉(关联资产 2、非锚点 id≠1):前端申诉 tab 只拉 pageNo=1/pageSize=20 且按 id DESC,
|
|
// 历轮 e2e 处理出的 closed/maintained 申诉(都挂在资产 2 上)会把唯一开放态锚点申诉 1(最老 id)挤出首页,
|
|
// 导致申诉 tab 看不到可处理行。软删(deleted=true)让列表(查询过滤 deleted=false)只剩锚点申诉 1,
|
|
// 使行定位唯一、按钮可点;不动用户真实申诉(仅清 e2e 在资产 2 上造的终态噪声),也不碰锚点申诉 1。
|
|
const softDeleteClosedAppeals = await client.query(
|
|
`UPDATE muse_market_appeal
|
|
SET deleted = true
|
|
WHERE tenant_id = 1 AND deleted = false AND id <> 1 AND asset_id = 2
|
|
AND status IN ('closed', 'maintained', 'restored')`,
|
|
);
|
|
|
|
// 3) 清理历轮累积的治理动作与治理预览(只清 e2e 锚点资产 1/2/4 的行),让本轮 DB 断言可精确计数。
|
|
// 申诉处理(maintained)不写治理动作,故这里清理足以让"下架本轮新增一行 delist 治理动作"成立。
|
|
const clearActions = await client.query(
|
|
`DELETE FROM muse_market_governance_action
|
|
WHERE tenant_id = 1 AND asset_id IN (1, 2, 4)`,
|
|
);
|
|
const clearPreviews = await client.query(
|
|
`DELETE FROM muse_market_governance_preview
|
|
WHERE tenant_id = 1 AND asset_id IN (1, 2, 4)`,
|
|
);
|
|
|
|
console.log(
|
|
`[admin e2e globalSetup] reset market fixture: assets=${resetAssets.rowCount} ` +
|
|
`appeal=${resetAppeal.rowCount} softDeleteClosedAppeals=${softDeleteClosedAppeals.rowCount} ` +
|
|
`clearActions=${clearActions.rowCount} clearPreviews=${clearPreviews.rowCount}` +
|
|
`(均为修复/清理条数,0=已是初态)`,
|
|
);
|
|
}
|
|
|
|
export default globalSetup;
|