oh-my-muse/muse-studio/e2e/global-setup.ts
lili 37e7a8dc27 fix(market): 修 install 用户可见假绿——isAcquired/isInstalled/canInstall 按真实授权安装态 enrich
三路盘点发现 market install 头号用户可见假绿:install 后端真实已验(P1rMarketLicenseInstallCompletedApprovalIT 批18 绿),但 MarketAssetQueryServiceImpl 把 isAcquired/isInstalled 裸硬编 false(toCard:181-182 列表 + userActions:386-387 详情 + canInstall:383),致 studio MarketBrowse(assetType!=work && isAcquired && !isInstalled)安装按钮在真后端永不渲染,mock 伪造状态位才现身;批17 IT 把 isAcquired=false 断言为正确→假绿固化。

后端 enrich(读模型、未碰 install 写逻辑/授权语义):仿 isFavorite 范式加 isAcquired(active 授权)/isInstalled(installation)/isInstallable(类型集),与 install 写端口 MarketInstallServiceImpl 严格同源(selectActiveByOwnerAndAsset + INSTALLABLE_ASSET_TYPES={agent,knowledge_base}),避免"显示可装但装不了"新假绿;canInstall=listed && isInstallable && acquired && !installed(对齐 studio assetType!=work 门);批量加载(整页 2 次 IN vs 3N)避免放大 isFavorite 既有 N+1;mapper 加 selectActiveByOwnerAndAssetIds/existsByUserAndAsset/selectByUserAndAssetIds;批17 IT 改三态断言+订正裸写注释。

前端 MarketBrowse 补 onError(install/acquire 失败内联可见提示,对齐 AgentCreateForm/SlotBindingPanel,不再静默吞 ApiError);e2e 新增 market-install.spec(MSW-off:purchase 前置→断安装按钮真渲染→install→psql 验,连跑 2 次)+ global-setup #16 market 表复位。

活体真验(muse_slice_live,重启新 jar 10:08):purchase→isAcquired=T/canInstall=T(修前恒 false)→install→isInstalled=T/canInstall=F;独立 psql+GET 复核 asset1/4(owner1)isAcquired/isInstalled=true。批17 IT 5/5 三态绿(首跑遇 .m2 stale jar 假红、mvn install 刷新转真绿)、market 单测 9/9+11/11+7/7、UI e2e 连跑 2 次 passed。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:28:33 -07:00

379 lines
23 KiB
TypeScript

import { Client } from 'pg';
/**
* Playwright globalSetup —— 活体 e2e 每轮 fixture 自动重置(可入库,替代一次性 /tmp 种子脚本)。
*
* WHY:整套 e2e 关 MSW 直连真实 muse-server + 真 PostgreSQL(muse_slice_live);多条旅程会"消费"种子状态
* (采纳推进 block revision、确认消费草稿、确认安全事件、申诉状态流转),故每轮须把这些 fixture 复位为可重跑态,
* 否则二次跑必红(历史用一次性 `/tmp/*.java`,每会话丢失需反推——本文件即其可入库版)。
*
* 连接凭据从环境变量读取(`MUSE_POSTGRES_*`,见 ~/.config/muse-repo/infra.env;**绝不入库**):
* 跑前先 `set -a; . ~/.config/muse-repo/infra.env; set +a`。缺凭据时直接报错(本套件仅活体跑,无 DB 不可能真绿)。
* 如确知库已就绪、想跳过重置:置 `E2E_SKIP_SEED=1`。
*
* 说明:仅"复位/补充"每轮消费的 fixture,假定基础数据(work3/block3/suggestion3、安全事件、市场资产/申诉、账户/智能体)
* 已存在于目标库(由基础种子建立);其中 block3/suggestion3/安全事件按 spec 既有 id/内容定位复位,不重建
* (muse_ai_suggestion.id 为 GENERATED ALWAYS,无法强制还原原 id)。
*/
async function globalSetup(): Promise<void> {
if (process.env.E2E_SKIP_SEED === '1') {
console.log('[e2e globalSetup] E2E_SKIP_SEED=1 → 跳过 fixture 重置');
return;
}
const host = process.env.MUSE_POSTGRES_HOST;
const password = process.env.MUSE_POSTGRES_PASSWORD;
if (!host || !password) {
throw new Error(
'[e2e globalSetup] 缺少 MUSE_POSTGRES_* 环境变量。活体 e2e 需直连真实 PG 复位每轮 fixture,' +
'请先 `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 {
// 1) knowledge-graph:作品 1 下 2 个 Canonical 实体 + 1 条关系(幂等:先删 marker 再插)。
await client.query("DELETE FROM muse_knowledge_relation WHERE tenant_id=1 AND relation_type='huoti_related'");
await client.query("DELETE FROM muse_knowledge_draft WHERE tenant_id=1 AND draft_payload->>'name'='huotituopusuqing'");
await client.query(
"DELETE FROM muse_knowledge_entity WHERE tenant_id=1 AND normalized_name IN ('huotituopusuqing','huotituopuqingzhou')"
);
const ent = async (name: string, desc: string): Promise<number> => {
const r = await client.query(
`INSERT INTO muse_knowledge_entity
(work_id, entity_type, normalized_name, scope, description, status, source_status, source_action_policy, revision, tenant_id)
VALUES (1,'character',$1,'global',$2,'active','active','allowed',1,1) RETURNING id`,
[name, desc]
);
return r.rows[0].id as number;
};
const id1 = await ent('huotituopusuqing', '活体图谱实体·苏青(e2e graph 种子)');
const id2 = await ent('huotituopuqingzhou', '活体图谱实体·青舟(e2e graph 种子)');
await client.query(
`INSERT INTO muse_knowledge_relation
(work_id, source_entity_id, target_entity_id, relation_type, description, source_status, source_action_policy, revision, tenant_id)
VALUES (1,$1,$2,'huoti_related','活体图谱关系·师徒(e2e graph 种子)','active','allowed',1,1)`,
[id1, id2]
);
// 2) knowledge-confirm:一个"可确认"的 pending 草稿(唯一名,避免与既有 Canonical 实体撞唯一身份被新 dup 预检拦成 conflict)。
await client.query(
"DELETE FROM muse_knowledge_draft WHERE tenant_id=1 AND status='pending' AND draft_payload->>'name' LIKE 'queren%'"
);
const draftName = 'queren' + Date.now();
await client.query(
`INSERT INTO muse_knowledge_draft
(work_id, draft_type, draft_payload, status, source_status, source_action_policy, source_snapshot_id, authorization_snapshot_id, revision, tenant_id)
VALUES (1,'entity',$1::jsonb,'pending','active','allowed',8001,8002,1,1)`,
[JSON.stringify({ entityType: 'character', name: draftName, scope: 'global' })]
);
// 2b) knowledge-draft-actions:作品 2 下两个固定 name 的 pending 草稿,供 recheck/ignore e2e。
// 用 work2 与 confirm 的 work1 隔离(避免同作品多草稿时按钮定位歧义);recheck 与 ignore 各用独立草稿
// (recheck 可能改草稿态,与 ignore 互不影响)。固定 name 幂等:先按 name 删(不论状态:重跑时可能已 ignored)再插。
for (const draftActionName of ['recheck-e2e', 'ignore-e2e']) {
await client.query(
"DELETE FROM muse_knowledge_draft WHERE tenant_id=1 AND work_id=2 AND draft_payload->>'name'=$1",
[draftActionName]
);
await client.query(
`INSERT INTO muse_knowledge_draft
(work_id, draft_type, draft_payload, status, source_status, source_action_policy, source_snapshot_id, authorization_snapshot_id, revision, tenant_id)
VALUES (2,'entity',$1::jsonb,'pending','active','allowed',8001,8002,1,1)`,
[JSON.stringify({ entityType: 'character', name: draftActionName, scope: 'global' })]
);
}
// 2c) 实体/关系 CRUD:作品 2 下三个 Canonical 实体——jia 供编辑实体名、yi/bing 供新建关系(各自独立避免互相影响)。
// 用 work2 与 knowledge-graph 的 work1 fixture 隔离;复位删 work2 全部关系 + 按 description marker 删实体
// (编辑会改 normalized_name 故不能按名删,description marker 不被编辑触碰)。
await client.query('DELETE FROM muse_knowledge_relation WHERE tenant_id=1 AND work_id=2');
await client.query(
"DELETE FROM muse_knowledge_entity WHERE tenant_id=1 AND work_id=2 AND description LIKE 'e2e-crud-marker:%'"
);
const crudEnt = async (entName: string): Promise<void> => {
await client.query(
`INSERT INTO muse_knowledge_entity
(work_id, entity_type, normalized_name, scope, description, status, source_status, source_action_policy, revision, tenant_id)
VALUES (2,'character',$1,'global',$2,'active','active','allowed',1,1)`,
[entName, 'e2e-crud-marker:' + entName]
);
};
await crudEnt('crudshiti-jia');
await crudEnt('crudshiti-yi');
await crudEnt('crudshiti-bing');
// 3) accept-suggestion:复位 block3→rev1 + 清来源归因;suggestion3 复 pending/active(spec 硬编码 work3/block3/suggestion3)。
await client.query('DELETE FROM muse_content_block_source_attribution WHERE tenant_id=1 AND block_id=3');
const blk = await client.query(
"UPDATE muse_content_block SET revision=1, content_text='活体原始正文待采纳', word_count=9 WHERE tenant_id=1 AND id=3"
);
await client.query("UPDATE muse_ai_suggestion SET status='pending', source_status='active' WHERE tenant_id=1 AND id=3");
if (blk.rowCount === 0) {
console.warn('[e2e globalSetup] ⚠️ 未找到 block id=3(accept-suggestion 基础种子缺失,需先建基础数据)');
}
// 4) account-security-ack:把"待确认演练"事件复位为未确认 + 清其确认记录(已确认态由 ack 记录派生)。
const evs = await client.query(
"SELECT id FROM muse_member_security_event WHERE tenant_id=1 AND account_user_id=1 AND device_info->>'description'='活体安全事件·待确认演练'"
);
for (const row of evs.rows) {
await client.query('DELETE FROM muse_account_security_event_ack WHERE tenant_id=1 AND event_id=$1', [row.id]);
await client.query(
'UPDATE muse_member_security_event SET acknowledged=false, acknowledged_at=NULL WHERE tenant_id=1 AND id=$1',
[row.id]
);
}
if (evs.rowCount === 0) {
console.warn('[e2e globalSetup] ⚠️ 未找到"待确认演练"安全事件(account-security-ack 基础种子缺失)');
}
// 5a) market 申诉提交:刷新被驳回 publish_request 的 reviewed_at 到近期,使申诉 deadline(reviewedAt + 后端
// APPEAL_DEADLINE_DAYS=7 天)未过。否则种子 request 随日期推进必然过期,「发起申诉」用例提交被后端拒
// MARKET_APPEAL_DEADLINE_EXPIRED(1044000027)——此 fixture 老化与代码无关、纯时间推进所致
// (见 MarketAppealServiceImpl.requireWithinDeadline / reviewRejectionRelation 取 request.reviewedAt)。
await client.query(
"UPDATE muse_market_publish_request SET reviewed_at = NOW() WHERE tenant_id=1 AND status IN ('rejected','compliance_blocked')"
);
// 5) market 申诉补充材料:把当前用户最早的一条申诉置 supplementing(canSupplement→展示「补充材料」)。
const ap = await client.query(
"UPDATE muse_market_appeal SET status='supplementing' WHERE tenant_id=1 AND id=(SELECT min(id) FROM muse_market_appeal WHERE tenant_id=1 AND user_id=1)"
);
if (ap.rowCount === 0) {
console.warn('[e2e globalSetup] ⚠️ 未找到当前用户的申诉(market 补充材料用例需基础申诉种子)');
}
// 6) knowledge-bindings:作品 1 一条<生效>的来源绑定读模型投影(此前 global-setup 未覆盖、基础种子缺失,
// 本轮固化为可入库;幂等:先删同 projection_id 再插,id 为 identity 自增故重跑不冲突)。
await client.query(
"DELETE FROM muse_knowledge_source_binding_projection WHERE tenant_id=1 AND projection_id='e2e-seed-w1-kb4001'"
);
await client.query(
`INSERT INTO muse_knowledge_source_binding_projection
(projection_id, source_owner, source_type, source_id, owner_user_id, work_id, kb_id, binding_id,
status, action_policy, projection_summary, tenant_id)
VALUES ('e2e-seed-w1-kb4001','market','market_kb','4001',1,1,4001,5001,'active','allowed','{}'::jsonb,1)`
);
// 7) meta-projection:种 2 个 active MetaSchema(setting/worldview,target_type=novel_work)+字段+版本级可见性,
// 并把 work1 绑到 setting——供 B5 planning 编辑器 e2e 真跑(按 schema 字段填值→usedFieldKeys→快照)。
// 幂等:先按 schema_key 删子表(field/visibility/version)再删 schema,重跑不冲突(id 为 IDENTITY)。
const metaSchemaKeys = ['setting', 'worldview'];
await client.query(
`DELETE FROM muse_meta_field WHERE tenant_id=1 AND schema_version_id IN
(SELECT id FROM muse_meta_schema_version WHERE tenant_id=1 AND schema_key = ANY($1))`,
[metaSchemaKeys]
);
await client.query(
`DELETE FROM muse_meta_visibility_policy WHERE tenant_id=1 AND schema_version_id IN
(SELECT id FROM muse_meta_schema_version WHERE tenant_id=1 AND schema_key = ANY($1))`,
[metaSchemaKeys]
);
await client.query('DELETE FROM muse_meta_schema_version WHERE tenant_id=1 AND schema_key = ANY($1)', [metaSchemaKeys]);
await client.query('DELETE FROM muse_meta_schema WHERE tenant_id=1 AND schema_key = ANY($1)', [metaSchemaKeys]);
// 种一个 active schema(含字段 + 版本级可见性),返回 schemaId(id 为 IDENTITY 故 RETURNING)。
const seedSchema = async (
domain: string,
schemaKey: string,
displayName: string,
fields: Array<{ key: string; name: string; type: string; required: boolean; enumValues?: string[] }>
): Promise<number> => {
const s = await client.query(
`INSERT INTO muse_meta_schema (schema_key, domain, scope, target_type, display_name, projection_version, status, tenant_id)
VALUES ($1,$2,'work','novel_work',$3,1,'active',1) RETURNING id`,
[schemaKey, domain, displayName]
);
const schemaId = s.rows[0].id as number;
const v = await client.query(
`INSERT INTO muse_meta_schema_version (schema_id, schema_key, version_no, status, active_flag, tenant_id)
VALUES ($1,$2,1,'active',TRUE,1) RETURNING id`,
[schemaId, schemaKey]
);
const versionId = v.rows[0].id as number;
await client.query('UPDATE muse_meta_schema SET active_version_id=$1 WHERE id=$2', [versionId, schemaId]);
let sort = 0;
for (const f of fields) {
await client.query(
`INSERT INTO muse_meta_field (schema_version_id, field_key, display_name, field_type, is_required, enum_values, sort_order, tenant_id)
VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,1)`,
[versionId, f.key, f.name, f.type, f.required, f.enumValues ? JSON.stringify(f.enumValues) : null, sort++]
);
}
// 版本级可见性:可见 + 可编辑(供用户填),其余保守 false。
await client.query(
`INSERT INTO muse_meta_visibility_policy
(schema_version_id, ui_visible, ai_context, user_editable, user_searchable, exportable, tenant_id)
VALUES ($1,TRUE,FALSE,TRUE,FALSE,FALSE,1)`,
[versionId]
);
return schemaId;
};
const settingSchemaId = await seedSchema('content', 'setting', '故事设定', [
{ key: 'time_period', name: '时代背景', type: 'enum', required: true, enumValues: ['古代', '现代', '未来'] },
{ key: 'world_overview', name: '世界观概述', type: 'text', required: false },
]);
await seedSchema('world', 'worldview', '世界观', [
{ key: 'magic_system', name: '魔法体系', type: 'text', required: false },
]);
// work1 绑 setting(其 target_type=novel_work → 投影列出同 targetType 的 setting+worldview)。
await client.query('UPDATE muse_content_work SET work_schema_id=$1 WHERE tenant_id=1 AND id=1', [settingSchemaId]);
// 8) block split/merge:复位 work1 chapter1 为单 Block——删 split 残留的多余 Block(保留 order_no 最小的首 Block),
// 供 D block 结构编辑 e2e 幂等重跑(split→2→merge→1,中途失败的残留也清掉)。
await client.query(
`DELETE FROM muse_content_block WHERE tenant_id=1 AND work_id=1 AND chapter_id=1
AND id <> (SELECT id FROM muse_content_block WHERE tenant_id=1 AND work_id=1 AND chapter_id=1 ORDER BY order_no, id LIMIT 1)`
);
// 9) knowledge 停用/恢复:复位自建 KB(id=1)status='draft'——供 F disable/restore e2e 幂等重跑
// (e2e 会 disable→'disabled'、restore→'searchable',含中途残留态一并归位到初始 draft)。
const kbReset = await client.query(
"UPDATE muse_knowledge_base SET status='draft' WHERE tenant_id=1 AND id=1"
);
if (kbReset.rowCount === 0) {
console.warn('[e2e globalSetup] ⚠️ 未找到 KB id=1(knowledge 停用/恢复用例需基础 KB 种子)');
}
// 10) 已安装知识库停用/恢复:复位 installed binding(id=1)binding_status='active'——供 G installed disable/restore e2e
// 幂等重跑(开局 active→先显示停用;e2e disable→'disabled'、restore→'active' 均归位)。
const installedReset = await client.query(
"UPDATE muse_knowledge_binding SET binding_status='active' WHERE tenant_id=1 AND id=1"
);
if (installedReset.rowCount === 0) {
console.warn('[e2e globalSetup] ⚠️ 未找到 installed binding id=1(已安装 KB 停用/恢复用例需基础 binding 种子)');
}
// 11) AgentList:清理 agent-create 历次累积的自建智能体(uniqueName=`活体智能体-e2e-${Date.now()}` 不幂等、
// 每跑新建一个)。否则列表被 e2e agent 淹没,后端默认分页按 updatedAt 倒序把种子「活体测试智能体」
// 挤出第一页,致 live-read「智能体列表渲染真实 Agent」断言 timeout。只删 e2e 前缀、保留种子。
const agentCleanup = await client.query(
"DELETE FROM muse_agent WHERE tenant_id=1 AND name LIKE '活体智能体-e2e-%'"
);
// 12) handoff-knowledge:清理 e2e 正路兑现产生的 market_kb 绑定(work4+kb1),供兑现闭环幂等重跑。
// 用 work4(creator=test1、未占 uk);work1+kb1 已被 installed binding(id=1,user_kb)占,故 e2e 用 work4 避 uk 冲突。
// 按 source_snapshot_id 格式(source-market_kb-{sourceId}-v*)精准删 binding;物理 DELETE 释放 uk_muse_knowledge_binding_work_kb。
await client.query(
"DELETE FROM muse_knowledge_binding WHERE tenant_id=1 AND work_id=4 AND source_snapshot_id LIKE 'source-market_kb-1-%'"
);
await client.query(
"DELETE FROM muse_knowledge_source_binding_projection WHERE tenant_id=1 AND work_id=4 AND kb_id=1 AND source_type='market_kb'"
);
// 13) handoff-agent(P2):seed market 类型 agent + active version + work4 槽位绑定,供 market→agent 跨空间 handoff e2e。
// market agent 用固定 agent_key 便于 spec 查其 id 作 sourceAgentId;requireVisibleActiveSourceAgent 校验 agent/version
// active(放宽只跳 agentType 可见性、不放宽 active 态)。幂等:先删子表(slot binding/version)再删 agent 再插。
// 注:market→agent 资产物化(install 自动建)未接,此处为 e2e 预置 fixture(见进度账"资产物化维度"订正)。
const HANDOFF_AGENT_KEY = 'market:handoff:e2e';
const HANDOFF_SLOT_KEY = 'handoff-writer';
await client.query('DELETE FROM muse_agent_slot_binding WHERE tenant_id=1 AND work_id=4 AND slot_key=$1', [
HANDOFF_SLOT_KEY,
]);
await client.query(
'DELETE FROM muse_agent_version WHERE tenant_id=1 AND agent_id IN (SELECT id FROM muse_agent WHERE tenant_id=1 AND agent_key=$1)',
[HANDOFF_AGENT_KEY]
);
await client.query('DELETE FROM muse_agent WHERE tenant_id=1 AND agent_key=$1', [HANDOFF_AGENT_KEY]);
const mktAgent = await client.query(
`INSERT INTO muse_agent (agent_key, name, agent_type, status, tenant_id)
VALUES ($1,'活体市场智能体-handoff','market','active',1) RETURNING id`,
[HANDOFF_AGENT_KEY]
);
const mktAgentId = mktAgent.rows[0].id as number;
const mktVersion = await client.query(
`INSERT INTO muse_agent_version (agent_id, version, status, tenant_id)
VALUES ($1,'1','active',1) RETURNING id`,
[mktAgentId]
);
await client.query('UPDATE muse_agent SET current_version_id=$1 WHERE id=$2', [mktVersion.rows[0].id, mktAgentId]);
// work4 槽位初始绑现有 agent1(被兑现替换成 market agent);precheck requireSlotBinding 需现有 binding。
await client.query(
`INSERT INTO muse_agent_slot_binding (work_id, slot_key, agent_id, agent_version, status, revision, tenant_id)
VALUES (4,$1,1,'1','active',1,1)`,
[HANDOFF_SLOT_KEY]
);
// 14) handoff-content(P3):清理 e2e 正路兑现产生的 asset_use precheck(work4),供兑现闭环幂等重跑。
// asset_use 不绑目标实体(单表 precheck 即使用凭证),物理删释放 (tenant_id,command_id) uk;asset 1 + work4 无需额外 seed。
await client.query('DELETE FROM muse_content_work_asset_use_precheck WHERE tenant_id=1 AND work_id=4');
// 15) agent-slot-bind(UI e2e):复位 work1 writing.continuation 槽位到固定起点(agent1、revision=1、active),
// 供 AgentPage→SlotBindingPanel「替换已有槽位」UI e2e 每轮稳定可重复。WHY:bind 成功会 revision+1
// (实测 2→3),不复位则每跑漂移,spec「revision 1→2」断言二次跑必红。物理删后重插(非 update)以释放
// 软删行可能占用的唯一约束名额,并把 id/审计列归回干净态。绝不能用 work4(§13 handoff-agent 占用其
// handoff-writer 槽位,且 work4 也有 writing.continuation 风险——本节只碰 work1)。agent1 DB id=1(user 类型、
// owner=1、active version='1';跑前 `SELECT id FROM muse_agent WHERE tenant_id=1` 复核)。
await client.query(
"DELETE FROM muse_agent_slot_binding WHERE tenant_id=1 AND work_id=1 AND slot_key='writing.continuation'"
);
await client.query(
`INSERT INTO muse_agent_slot_binding (work_id, slot_key, agent_id, agent_version, status, revision, binding_source, tenant_id)
VALUES (1,'writing.continuation',1,'1','active',1,'user',1)`
);
// 16) market-install(UI e2e):seed 一个固定 marker 的 listed agent 资产,并把当前用户(user1)对它的
// 授权/安装/命令复位为「未获取、未安装」干净起点。WHY:market-install.spec 走 purchase→install 真链,
// UI 每次用随机 commandId(crypto.randomUUID)→install 每跑落一条新 installation;不复位则二次跑资产已安装,
// 安装按钮的渲染条件(isAcquired && !isInstalled)不再成立 → 断言必红。故每轮删净 user1 在该资产上的
// authz/installation/command,保证 UI 起点恒为「可获取、未安装」。资产本身幂等保留(只删用户态)。
const MARKET_INSTALL_ASSET = '活体市场资产·安装e2e';
let installE2eAssetId: number;
const existed = await client.query(
"SELECT id, current_version_id FROM muse_market_asset WHERE tenant_id=1 AND name=$1 AND deleted=false",
[MARKET_INSTALL_ASSET]
);
if (existed.rowCount && existed.rows[0]) {
installE2eAssetId = existed.rows[0].id as number;
} else {
// 资产首建:listed agent + 一条 published 版本,并回填 current_version_id(详情/安装需可见且有版本)。
const a = await client.query(
`INSERT INTO muse_market_asset
(name, description, asset_type, category, publisher_id, listing_status, license_type, status, creator, updater, tenant_id)
VALUES ($1,'安装 e2e 专用资产','agent','general',2,'listed','standard','active','2','2',1) RETURNING id`,
[MARKET_INSTALL_ASSET]
);
installE2eAssetId = a.rows[0].id as number;
const v = await client.query(
`INSERT INTO muse_market_asset_version (asset_id, version, change_note, status, creator, updater, tenant_id)
VALUES ($1,'1.0.0','首发','published','2','2',1) RETURNING id`,
[installE2eAssetId]
);
await client.query('UPDATE muse_market_asset SET current_version_id=$1 WHERE id=$2', [
v.rows[0].id,
installE2eAssetId,
]);
}
// 复位 user1 在该资产上的获取/安装/命令到干净起点(未获取、未安装),令安装按钮渲染条件每轮可复现。
await client.query('DELETE FROM muse_market_installation WHERE tenant_id=1 AND user_id=1 AND asset_id=$1', [
installE2eAssetId,
]);
await client.query(
'DELETE FROM muse_market_authorization_snapshot WHERE tenant_id=1 AND owner_user_id=1 AND asset_id=$1',
[installE2eAssetId]
);
// UI 随机 commandId 的 purchase/install 命令记录按 target_id 清(target_id 存资产 id 字符串),避免幂等回放。
await client.query("DELETE FROM muse_market_command WHERE tenant_id=1 AND target_id=$1", [
String(installE2eAssetId),
]);
console.log(`[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal/binding/meta-schema/block/kb-status/installed-kb/handoff-bind/handoff-agent/handoff-content/agent-slot-bind/market-install assetId=${installE2eAssetId});清理 e2e 累积 agent ${agentCleanup.rowCount}`);
} finally {
await client.end();
}
}
export default globalSetup;