完成 2.0.0 S4 的外部 AI、admin、account 与 solo SQL 收口,并推进 S5 studio 市场/handoff/旧编辑器入口裁剪。S5 的前端单测、构建与 route-only e2e 已留证;MSW-off 创作主线 e2e 仍因本地后端活体启动口径待修正,已在总账标为未完成验收。
745 lines
43 KiB
TypeScript
745 lines
43 KiB
TypeScript
import type { Client as PgClient } 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 } = await import('pg');
|
||
const client: PgClient = 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 {
|
||
const enableMarketFixture = process.env.MUSE_STUDIO_E2E_MARKET !== 'false';
|
||
let marketFixtureDetails = 'market/handoff fixtures skipped';
|
||
|
||
// 0) AI 真生成额度:每轮把 test1(user1) 的 ai_calls 配额复位为可消费态。
|
||
// WHY:AI 生成 e2e 会真调用 Account 额度预扣,额度属于会被测试消费的状态;若不复位,
|
||
// 历史运行耗尽或基础种子缺失都会让 POST /ai/tasks 在业务入口返回「账户额度不足」,形成夹具假红。
|
||
await client.query(
|
||
`INSERT INTO muse_member_quota
|
||
(account_user_id, quota_type, total_amount, used_amount, reset_cycle, last_reset_at, next_reset_at,
|
||
revision, creator, updater, deleted, tenant_id)
|
||
VALUES (1,'ai_calls',1000,0,'manual',CURRENT_TIMESTAMP,NULL,1,'e2e','e2e',FALSE,1)
|
||
ON CONFLICT (tenant_id, account_user_id, quota_type)
|
||
DO UPDATE SET total_amount=EXCLUDED.total_amount,
|
||
used_amount=0,
|
||
reset_cycle=EXCLUDED.reset_cycle,
|
||
last_reset_at=CURRENT_TIMESTAMP,
|
||
next_reset_at=NULL,
|
||
revision=1,
|
||
updater='e2e',
|
||
update_time=CURRENT_TIMESTAMP,
|
||
deleted=FALSE`
|
||
);
|
||
|
||
// 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 + source-attribution 只读 fixture:复位 block3(work3)→rev1 + 重建来源归因为 user_original。
|
||
// WHY(source-attribution 反污染,根治偶发红):source-attribution.spec 读「某 Block 当前 revision 的来源」并断言
|
||
// 「用户原创」。它原指向 /workspace/1(=block1),但 block1 被 accept-suggestion(写 ai_suggestion 来源)、
|
||
// block-autosave、block-structure 三个 spec 在前序消费(三者按字母序均先于 source-attribution 跑),其当前 revision
|
||
// 的 source_type 由「最后一个写 block1 的 spec」决定 → 非确定(实测 block1 顶层 revision 在 user_original/ai_suggestion
|
||
// 间漂移,顶层落 ai_suggestion 时 source-attribution 渲染「AI 生成」而非「用户原创」→ 偶发红)。globalSetup 每轮只跑
|
||
// 一次(在所有 spec 之前),无法撤销 spec 跑中途的污染,故根治法是让 source-attribution 改读「无任何 spec 会写」的
|
||
// pristine Block:work3/block3(grep 全 spec 确认无人导航 /workspace/3、无人写 block3;此处每轮把 block3 复位 rev1 +
|
||
// 重建唯一一条 user_original 来源,使该读断言恒确定真)。block3 的 user_original 是真实初态(用户原创正文),断言未放宽。
|
||
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"
|
||
);
|
||
// 重建 block3 rev1 的 user_original 来源(source-attribution.spec 读此并断言「用户原创」);幂等:上面已先删 block3 全部归因。
|
||
await client.query(
|
||
`INSERT INTO muse_content_block_source_attribution
|
||
(work_id, block_id, revision, source_type, source_object_id, source_status, tenant_id)
|
||
VALUES (3,3,1,'user_original','3','active',1)`
|
||
);
|
||
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(source-attribution 只读 fixture 需基础种子,需先建基础数据)');
|
||
}
|
||
|
||
// 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 基础种子缺失)');
|
||
}
|
||
|
||
if (enableMarketFixture) {
|
||
// 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 种子)');
|
||
}
|
||
|
||
// 9b) knowledge 停用 ≥2 绑定回归锚点:确保 KB id=1 至少有 2 条未删 binding(binding id=1=work1 + 本条 work2),
|
||
// 使 F disable/restore e2e 真正走「KB 绑 ≥2 作品→传播停用」路径——这正是历史 500 bug 的触发条件
|
||
// (旧 propagateKbBlocked 给同 KB 全部 binding 写同一 commandId,撞 uk_muse_knowledge_binding_command 唯一约束)。
|
||
// 后端修复后传播只翻 binding_status、不再覆盖 command_id,本锚点用于守住该回归不复发。
|
||
// 选 work2(owner=1、未占 (work,kb) uk):work1+kb1 已被 installed binding(§10,id=1)占,work4+kb1 由 handoff-knowledge
|
||
// spec 运行时占用,故必须避开二者,选 work2。幂等:先按 (work2,kb1) 删再插(释放 uk_muse_knowledge_binding_work_kb),
|
||
// command_id 用本条自身创建命令(与 binding id=1 不同值),模拟真实「各 binding 各自创建命令」语义。
|
||
await client.query('DELETE FROM muse_knowledge_binding WHERE tenant_id=1 AND work_id=2 AND kb_id=1');
|
||
await client.query(
|
||
`INSERT INTO muse_knowledge_binding
|
||
(work_id, kb_id, binding_type, binding_scope, binding_status, source_snapshot_id, command_id, revision, tenant_id)
|
||
VALUES (2,1,'user_kb','search','active','source-user_kb-1-v1','e2e-seed-kb1-binding-work2',1,1)`
|
||
);
|
||
|
||
// 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 前缀、保留种子。
|
||
// E2 智能体生命周期 e2e 会为每个临时 agent 写版本行;清理时必须先删子事实,避免残留版本污染分页与 DB 核验。
|
||
await client.query(
|
||
`DELETE FROM muse_agent_slot_binding
|
||
WHERE tenant_id=1
|
||
AND agent_id IN (SELECT id FROM muse_agent WHERE tenant_id=1 AND name LIKE '活体智能体-e2e-%')`
|
||
);
|
||
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 name LIKE '活体智能体-e2e-%')`
|
||
);
|
||
const agentCleanup = await client.query(
|
||
"DELETE FROM muse_agent WHERE tenant_id=1 AND name LIKE '活体智能体-e2e-%'"
|
||
);
|
||
|
||
if (enableMarketFixture) {
|
||
// 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(E4):seed 发布者侧 user agent + asset1.source_id,供 market→agent handoff 真实物化 e2e。
|
||
// 旧形态是预置 agent_type=market 的裸 agent 并把 sourceAgentId 透传给目标侧;E4 后目标侧只信 market
|
||
// assetId/token,后端按 asset.source_id 找发布者 agent,再物化成安装者本地 user agent。
|
||
const HANDOFF_AGENT_KEY = 'publisher:handoff:e2e';
|
||
const HANDOFF_SLOT_KEY = 'handoff-writer';
|
||
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 owner_user_id=1 AND source_market_asset_id=1 AND deleted=false
|
||
)`
|
||
);
|
||
await client.query(
|
||
"DELETE FROM muse_agent WHERE tenant_id=1 AND owner_user_id=1 AND source_market_asset_id=1 AND deleted=false"
|
||
);
|
||
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 publisherAgent = await client.query(
|
||
`INSERT INTO muse_agent (agent_key, name, description, agent_type, owner_user_id, status, tenant_id)
|
||
VALUES ($1,'活体市场智能体-handoff','market agent handoff e2e 发布者来源','user',2,'active',1) RETURNING id`,
|
||
[HANDOFF_AGENT_KEY]
|
||
);
|
||
const publisherAgentId = publisherAgent.rows[0].id as number;
|
||
const publisherVersion = await client.query(
|
||
`INSERT INTO muse_agent_version (agent_id, version, config, status, tenant_id)
|
||
VALUES ($1,'1',$2::jsonb,'active',1) RETURNING id`,
|
||
[
|
||
publisherAgentId,
|
||
JSON.stringify({
|
||
modelKey: 'MiniMax-M2.5',
|
||
promptTemplateVersion: 'prompt-v1',
|
||
}),
|
||
]
|
||
);
|
||
await client.query('UPDATE muse_agent SET current_version_id=$1 WHERE id=$2', [
|
||
publisherVersion.rows[0].id,
|
||
publisherAgentId,
|
||
]);
|
||
await client.query(
|
||
`UPDATE muse_market_asset
|
||
SET asset_type='agent',
|
||
source_id=$1,
|
||
publisher_id=2,
|
||
category='general',
|
||
tags=coalesce(tags, '{}'::jsonb) || $2::jsonb,
|
||
update_time=CURRENT_TIMESTAMP
|
||
WHERE tenant_id=1 AND id=1 AND deleted=false`,
|
||
[
|
||
publisherAgentId,
|
||
JSON.stringify({
|
||
sourceOwner: 'agent',
|
||
sourceType: 'agent',
|
||
sourceRevision: '1',
|
||
}),
|
||
]
|
||
);
|
||
// work4 槽位初始绑现有 agent1(被兑现替换成安装者本地 user agent);precheck revision 从 1 起。
|
||
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');
|
||
}
|
||
|
||
// 15a) AI 真生成主链:复位 work1 writing.continuation 槽位到固定起点(agent1、revision=1、active)。
|
||
// accept-suggestion / ai-generation 均依赖 work1 解析到 active Agent;该槽位不能被其它 e2e 写坏。
|
||
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)`
|
||
);
|
||
|
||
// 15b) agent-slot-bind(UI e2e):使用 work2 的同名槽位做「替换 + 解绑」写侧验证。
|
||
// WHY:该用例最终会把槽位解绑为 archived;若复用 work1,会在全量 e2e 中污染后续 ai-generation 主链。
|
||
// work2 与知识草稿/实体用例共享作品但不共享 agent 槽位,写入边界互不影响。
|
||
// AgentPage 的作品下拉只读作品列表第一页;历史 work-schema e2e 会累积大量新作品,故每轮刷新 work2 的
|
||
// 标题与 update_time,保证它稳定出现在第一页供 UI 选择。
|
||
await client.query(
|
||
"UPDATE muse_content_work SET title='槽位绑定验证作品', update_time=CURRENT_TIMESTAMP WHERE tenant_id=1 AND id=2"
|
||
);
|
||
await client.query(
|
||
"DELETE FROM muse_agent_slot_binding WHERE tenant_id=1 AND work_id=2 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 (2,'writing.continuation',1,'1','active',1,'user',1)`
|
||
);
|
||
|
||
if (enableMarketFixture) {
|
||
// 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),
|
||
]);
|
||
|
||
// 17) 复位种子资产 asset1「活体市场资产·测试」为上架态(listing_status='listed'、status='active')。
|
||
const asset1Reset = await client.query(
|
||
"UPDATE muse_market_asset SET listing_status='listed', status='active' WHERE tenant_id=1 AND id=1 AND deleted=false"
|
||
);
|
||
if (asset1Reset.rowCount === 0) {
|
||
console.warn('[e2e globalSetup] ⚠️ 未找到 asset id=1「活体市场资产·测试」(handoff/market 系列正路用例需该种子资产)');
|
||
}
|
||
|
||
// 18) handoff-knowledge / market-handoff-precheck:seed 一个专用 listed knowledge_base 资产。
|
||
const MARKET_KB_HANDOFF_ASSET = '活体市场知识库·handoff e2e';
|
||
const handoffDataset = await client.query(
|
||
`SELECT kb.id AS kb_id, rb.ragflow_dataset_id
|
||
FROM muse_knowledge_ragflow_binding rb
|
||
JOIN muse_knowledge_base kb
|
||
ON kb.id=rb.kb_id AND kb.tenant_id=rb.tenant_id AND kb.deleted=false
|
||
WHERE rb.tenant_id=1
|
||
AND rb.deleted=false
|
||
AND rb.document_id IS NULL
|
||
AND rb.status='active'
|
||
AND rb.ragflow_dataset_id IS NOT NULL
|
||
AND kb.name='端到端检索验证KB'
|
||
ORDER BY rb.id DESC
|
||
LIMIT 1`
|
||
);
|
||
if (handoffDataset.rowCount === 0) {
|
||
throw new Error('[e2e globalSetup] 缺少稳定 active RAGFlow dataset: 端到端检索验证KB');
|
||
}
|
||
const handoffPublicDatasetId = String(handoffDataset.rows[0].ragflow_dataset_id);
|
||
let handoffPublisherKbId: number;
|
||
const handoffPublisherKb = await client.query(
|
||
"SELECT id FROM muse_knowledge_base WHERE tenant_id=1 AND name='E3 market handoff publisher KB' AND deleted=false ORDER BY id DESC LIMIT 1"
|
||
);
|
||
if (handoffPublisherKb.rowCount && handoffPublisherKb.rows[0]) {
|
||
handoffPublisherKbId = Number(handoffPublisherKb.rows[0].id);
|
||
} else {
|
||
const kb = await client.query(
|
||
`INSERT INTO muse_knowledge_base
|
||
(name, description, kb_type, owner_user_id, status, active_version, command_id, revision, creator, updater, tenant_id)
|
||
VALUES ('E3 market handoff publisher KB','市场 KB handoff e2e 发布者侧来源占位','user',2,'active',1,
|
||
'e2e-market-kb-handoff-publisher',1,'2','2',1)
|
||
RETURNING id`
|
||
);
|
||
handoffPublisherKbId = Number(kb.rows[0].id);
|
||
}
|
||
const handoffTags = JSON.stringify({
|
||
publicForkDatasetId: handoffPublicDatasetId,
|
||
forkStatus: 'ready',
|
||
forkAssetVersion: '1.0.0',
|
||
sourceOwner: 'knowledge',
|
||
sourceType: 'knowledge_base',
|
||
actionPolicy: 'allowed',
|
||
});
|
||
let handoffKbAssetId: number;
|
||
let handoffKbVersionId: number | null = null;
|
||
const handoffAsset = await client.query(
|
||
"SELECT id, current_version_id FROM muse_market_asset WHERE tenant_id=1 AND name=$1 AND deleted=false ORDER BY id DESC LIMIT 1",
|
||
[MARKET_KB_HANDOFF_ASSET]
|
||
);
|
||
if (handoffAsset.rowCount && handoffAsset.rows[0]) {
|
||
handoffKbAssetId = Number(handoffAsset.rows[0].id);
|
||
handoffKbVersionId = handoffAsset.rows[0].current_version_id == null
|
||
? null
|
||
: Number(handoffAsset.rows[0].current_version_id);
|
||
await client.query(
|
||
`UPDATE muse_market_asset
|
||
SET description='知识库 handoff e2e 专用资产',
|
||
asset_type='knowledge_base',
|
||
category='knowledge',
|
||
source_id=$2,
|
||
publisher_id=2,
|
||
listing_status='listed',
|
||
license_type='standard',
|
||
status='active',
|
||
tags=$3::jsonb,
|
||
updater='2',
|
||
update_time=CURRENT_TIMESTAMP
|
||
WHERE tenant_id=1 AND id=$1`,
|
||
[handoffKbAssetId, handoffPublisherKbId, handoffTags]
|
||
);
|
||
} else {
|
||
const asset = await client.query(
|
||
`INSERT INTO muse_market_asset
|
||
(name, description, asset_type, category, source_id, publisher_id, listing_status, license_type, status,
|
||
tags, creator, updater, tenant_id)
|
||
VALUES ($1,'知识库 handoff e2e 专用资产','knowledge_base','knowledge',$2,2,'listed','standard','active',
|
||
$3::jsonb,'2','2',1)
|
||
RETURNING id`,
|
||
[MARKET_KB_HANDOFF_ASSET, handoffPublisherKbId, handoffTags]
|
||
);
|
||
handoffKbAssetId = Number(asset.rows[0].id);
|
||
}
|
||
if (handoffKbVersionId == null) {
|
||
const version = await client.query(
|
||
`INSERT INTO muse_market_asset_version (asset_id, version, change_note, status, creator, updater, tenant_id)
|
||
VALUES ($1,'1.0.0','知识库 handoff e2e 首发','published','2','2',1)
|
||
RETURNING id`,
|
||
[handoffKbAssetId]
|
||
);
|
||
handoffKbVersionId = Number(version.rows[0].id);
|
||
await client.query('UPDATE muse_market_asset SET current_version_id=$1 WHERE tenant_id=1 AND id=$2', [
|
||
handoffKbVersionId,
|
||
handoffKbAssetId,
|
||
]);
|
||
} else {
|
||
await client.query(
|
||
`UPDATE muse_market_asset_version
|
||
SET version='1.0.0',
|
||
change_note='知识库 handoff e2e 首发',
|
||
status='published',
|
||
updater='2',
|
||
update_time=CURRENT_TIMESTAMP
|
||
WHERE tenant_id=1 AND id=$1`,
|
||
[handoffKbVersionId]
|
||
);
|
||
}
|
||
// 复位 user1 对该知识资产的消费态,确保 purchase→bind-precheck→handoff→knowledge bind 每轮从干净起点执行。
|
||
const handoffSourceSnapshotId = `source-market_kb-${handoffKbAssetId}-v1`;
|
||
await client.query(
|
||
'DELETE FROM muse_knowledge_source_binding_projection WHERE tenant_id=1 AND work_id=4 AND source_snapshot_id=$1',
|
||
[handoffSourceSnapshotId]
|
||
);
|
||
await client.query(
|
||
'DELETE FROM muse_knowledge_binding WHERE tenant_id=1 AND work_id=4 AND source_snapshot_id=$1',
|
||
[handoffSourceSnapshotId]
|
||
);
|
||
const handoffInstalledKbs = await client.query(
|
||
`SELECT id FROM muse_knowledge_base
|
||
WHERE tenant_id=1 AND owner_user_id=1 AND source_market_asset_id=$1 AND kb_type='installed_ref' AND deleted=false`,
|
||
[handoffKbAssetId]
|
||
);
|
||
for (const row of handoffInstalledKbs.rows) {
|
||
const kbId = Number(row.id);
|
||
await client.query('DELETE FROM muse_knowledge_source_binding_projection WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query('DELETE FROM muse_knowledge_binding WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query('DELETE FROM muse_knowledge_ragflow_call WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query('DELETE FROM muse_knowledge_processing_task WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query('DELETE FROM muse_knowledge_ragflow_binding WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query(
|
||
`DELETE FROM muse_knowledge_document_version
|
||
WHERE tenant_id=1
|
||
AND document_id IN (SELECT id FROM muse_knowledge_document WHERE tenant_id=1 AND kb_id=$1)`,
|
||
[kbId]
|
||
);
|
||
await client.query('DELETE FROM muse_knowledge_document WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query('DELETE FROM muse_knowledge_base WHERE tenant_id=1 AND id=$1', [kbId]);
|
||
}
|
||
await client.query(
|
||
'DELETE FROM muse_knowledge_bind_precheck WHERE tenant_id=1 AND owner_user_id=1 AND work_id=4 AND source_snapshot_id=$1',
|
||
[handoffSourceSnapshotId]
|
||
);
|
||
await client.query('DELETE FROM muse_market_handoff_event WHERE tenant_id=1 AND owner_user_id=1 AND asset_id=$1', [
|
||
handoffKbAssetId,
|
||
]);
|
||
await client.query('DELETE FROM muse_market_authorization_summary WHERE tenant_id=1 AND owner_user_id=1 AND asset_id=$1', [
|
||
handoffKbAssetId,
|
||
]);
|
||
await client.query('DELETE FROM muse_market_installation WHERE tenant_id=1 AND user_id=1 AND asset_id=$1', [
|
||
handoffKbAssetId,
|
||
]);
|
||
await client.query('DELETE FROM muse_market_authorization_snapshot WHERE tenant_id=1 AND owner_user_id=1 AND asset_id=$1', [
|
||
handoffKbAssetId,
|
||
]);
|
||
await client.query('DELETE FROM muse_market_command WHERE tenant_id=1 AND target_id=$1', [handoffKbAssetId]);
|
||
marketFixtureDetails = `market-install assetId=${installE2eAssetId}/asset1-listed/market-kb-handoff assetId=${handoffKbAssetId}/handoff installed_ref KB ${handoffInstalledKbs.rowCount ?? 0} 条`;
|
||
}
|
||
|
||
// 19) 清理 work1/work2 除 writing.continuation 外的多余 active 槽位(去污)。
|
||
// WHY:agent-slot-bind.spec 假设 work2 仅一个开放槽位(writing.continuation,§15b 每轮复位),据此用
|
||
// getByText('revision 1') 唯一定位该槽位卡片。若 work1 残留了别的 active 槽位(实测有 writing.expansion、
|
||
// 同为 revision 1),则页面出现 2 个「revision 1」节点 → strict 模式命中歧义、该断言红。§15a/§15b 只管
|
||
// continuation 槽位,未清其它,故此处补一刀删净 work1/work2 非 continuation 槽位(物理 DELETE 释放唯一约束名额)。
|
||
const workSlotCleanup = await client.query(
|
||
"DELETE FROM muse_agent_slot_binding WHERE tenant_id=1 AND work_id IN (1,2) AND slot_key<>'writing.continuation'"
|
||
);
|
||
|
||
// 20) E3 knowledge-retrieval:创建一个 work3 专用的「已绑定但无 active dataset」KB。
|
||
// WHY:不能复用业务种子 KB 做 no_dataset 断言——一旦有人给该 KB 上传资料并生成 dataset,用例就从稳定真证变偶发红。
|
||
// 本 fixture 只清理固定 e2e marker 的 KB/binding/projection/ragflow 行,再重建绑定与投影,刻意不建 dataset 级
|
||
// muse_knowledge_ragflow_binding,从而稳定触发 RetrievalApiImpl 的 no_dataset 门。
|
||
const E3_NO_DATASET_KB = 'E3 no_dataset e2e KB';
|
||
const E3_UPLOAD_KB = 'E3 upload e2e KB';
|
||
const cleanupE3Kb = async (name: string): Promise<number> => {
|
||
const existingKb = await client.query(
|
||
'SELECT id FROM muse_knowledge_base WHERE tenant_id=1 AND name=$1',
|
||
[name]
|
||
);
|
||
for (const row of existingKb.rows) {
|
||
const kbId = Number(row.id);
|
||
await client.query('DELETE FROM muse_knowledge_source_binding_projection WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query('DELETE FROM muse_knowledge_binding WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query('DELETE FROM muse_knowledge_ragflow_call WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query('DELETE FROM muse_knowledge_processing_task WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query('DELETE FROM muse_knowledge_ragflow_binding WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query(
|
||
`DELETE FROM muse_knowledge_document_version
|
||
WHERE tenant_id=1
|
||
AND document_id IN (SELECT id FROM muse_knowledge_document WHERE tenant_id=1 AND kb_id=$1)`,
|
||
[kbId]
|
||
);
|
||
await client.query('DELETE FROM muse_knowledge_document WHERE tenant_id=1 AND kb_id=$1', [kbId]);
|
||
await client.query('DELETE FROM muse_knowledge_base WHERE tenant_id=1 AND id=$1', [kbId]);
|
||
}
|
||
return existingKb.rowCount ?? 0;
|
||
};
|
||
|
||
const cleanedNoDatasetKb = await cleanupE3Kb(E3_NO_DATASET_KB);
|
||
const noDatasetKb = await client.query(
|
||
`INSERT INTO muse_knowledge_base
|
||
(name, description, kb_type, owner_user_id, status, active_version, command_id, revision, creator, updater, tenant_id)
|
||
VALUES ($1,'E3 主动检索 no_dataset e2e 专用夹具','user',1,'active',1,'e2e-e3-no-dataset-kb',1,'1','1',1)
|
||
RETURNING id`,
|
||
[E3_NO_DATASET_KB]
|
||
);
|
||
const noDatasetKbId = Number(noDatasetKb.rows[0].id);
|
||
const noDatasetBinding = await client.query(
|
||
`INSERT INTO muse_knowledge_binding
|
||
(work_id, kb_id, binding_type, binding_scope, binding_status, source_snapshot_id, authorization_snapshot_id,
|
||
source_version, target_version, command_id, revision, creator, updater, tenant_id)
|
||
VALUES (3,$1,'user_kb','search','active','source-user_kb-e3-no-dataset-v1','auth-e3-no-dataset',
|
||
1,1,'e2e-e3-no-dataset-binding',1,'1','1',1)
|
||
RETURNING id`,
|
||
[noDatasetKbId]
|
||
);
|
||
await client.query(
|
||
`INSERT INTO muse_knowledge_source_binding_projection
|
||
(projection_id, source_owner, source_type, source_id, source_revision, owner_user_id, work_id, kb_id, binding_id,
|
||
status, action_policy, source_snapshot_id, authorization_snapshot_id, projection_summary, creator, updater, tenant_id)
|
||
VALUES ('e2e-e3-no-dataset-w3','knowledge','user_kb',$1,'1',1,3,$2,$3,'active','allowed',
|
||
'source-user_kb-e3-no-dataset-v1','auth-e3-no-dataset',$4::jsonb,'1','1',1)`,
|
||
[
|
||
String(noDatasetKbId),
|
||
noDatasetKbId,
|
||
Number(noDatasetBinding.rows[0].id),
|
||
JSON.stringify({ targetOwner: 'knowledge', sourceName: E3_NO_DATASET_KB, purposes: ['search'] }),
|
||
]
|
||
);
|
||
|
||
// 21) E3 knowledge-upload:创建一个干净自建 KB,供 UI 上传资料主流程 e2e 使用。
|
||
// 上传用例会真打后端/RAGFlow,本节每轮物理清理该 marker KB 的文档、任务和 dataset binding,保证可重跑。
|
||
const cleanedUploadKb = await cleanupE3Kb(E3_UPLOAD_KB);
|
||
const uploadKb = await client.query(
|
||
`INSERT INTO muse_knowledge_base
|
||
(name, description, kb_type, owner_user_id, status, active_version, command_id, revision, creator, updater, tenant_id)
|
||
VALUES ($1,'E3 Studio 上传资料 e2e 专用夹具','user',1,'active',1,'e2e-e3-upload-kb',1,'1','1',1)
|
||
RETURNING id`,
|
||
[E3_UPLOAD_KB]
|
||
);
|
||
const uploadKbId = Number(uploadKb.rows[0].id);
|
||
|
||
console.log(`[e2e globalSetup] 已复位每轮 fixture(ai-quota/graph/confirm-draft/accept/security-ack/binding/meta-schema/block/kb-status/installed-kb/agent-slot-bind/work-slot-dedup/e3 noDatasetKbId=${noDatasetKbId} uploadKbId=${uploadKbId}/${marketFixtureDetails});清理 e2e 累积 agent ${agentCleanup.rowCount} 条、work1/work2 多余槽位 ${workSlotCleanup.rowCount} 条、E3 no_dataset KB ${cleanedNoDatasetKb} 条、E3 upload KB ${cleanedUploadKb} 条`);
|
||
} finally {
|
||
await client.end();
|
||
}
|
||
}
|
||
|
||
export default globalSetup;
|