PG 宿主 100.64.0.8 恢复在线→查出旧 jar(Jun-19 00:59)缺 GET bindings 端点(commit 3a7a72f Jun-19 13:30)致 405→rebuild Jun-20 新 jar(反假绿:不用过时 jar)→起栈 48080(local,infra,清 socks)→playwright MSW-off 真打:22 passed/0 failed/0 skipped/0 flaky(9.1s)。 覆盖:accept-suggestion 正负×2 + account-security-ack + agent-create + knowledge-bindings(06-19 PG 离线阻塞项 unblock) + knowledge-confirm + knowledge-graph + live-read×6 + market-publish×8 + workspace。 种子:核验 4/5 基础数据在,knowledge-bindings 投影固化进 e2e/global-setup.ts #6(可入库、下次自动补,补此前未覆盖项);0 红故无就地修。回写进度总账 §五C + 总览待办(受阻→完成) + spec 注释。附本会话 ce-brainstorm/ce-plan 产出文档。 诚实边界:本地全栈活体(rebuild jar + 真 PG muse_slice_live),非 CI(maven.yml 不跑 e2e/需活体后端+浏览器)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
133 lines
7.2 KiB
TypeScript
133 lines
7.2 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' })]
|
|
);
|
|
|
|
// 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 基础种子缺失)');
|
|
}
|
|
|
|
// 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)`
|
|
);
|
|
|
|
console.log('[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal/binding)');
|
|
} finally {
|
|
await client.end();
|
|
}
|
|
}
|
|
|
|
export default globalSetup;
|