test(studio): e2e globalSetup 直连真实 PG 自动复位每轮 fixture(可入库,替代一次性 /tmp 种子)

新增 e2e/global-setup.ts:用 pg 直连真实 PG(凭据从 infra.env 环境变量读,不入库)幂等复位 graph/confirm-draft/accept(block3→rev1)/security-ack(事件→未确认+清 ack)/appeal(→supplementing)等每轮被消费的 fixture;playwright.config 接 globalSetup + workers:1(共享活体 DB 串行避竞态)。

证:消费态下 source infra.env 后 pnpm 直跑整套→globalSetup 复位→20/20 单次全绿,零手工种子(反假绿可复现)。E2E_SKIP_SEED=1 可跳过;缺 MUSE_POSTGRES_* 报清晰错。tsc -b 干净。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-15 18:35:51 -07:00
parent fa64280819
commit 6fd483ea1d
4 changed files with 249 additions and 1 deletions

View File

@ -0,0 +1,120 @@
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 补充材料用例需基础申诉种子)');
}
console.log('[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal)');
} finally {
await client.end();
}
}
export default globalSetup;

View File

@ -33,6 +33,7 @@
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@types/node": "^24.12.3", "@types/node": "^24.12.3",
"@types/pg": "^8.20.0",
"@types/react": "^18.3.29", "@types/react": "^18.3.29",
"@types/react-dom": "^18.3.7", "@types/react-dom": "^18.3.7",
"@typescript-eslint/parser": "^8.59.4", "@typescript-eslint/parser": "^8.59.4",
@ -45,6 +46,7 @@
"globals": "^17.6.0", "globals": "^17.6.0",
"jsdom": "^29.1.1", "jsdom": "^29.1.1",
"msw": "^2.14.6", "msw": "^2.14.6",
"pg": "^8.21.0",
"playwright": "^1.60.0", "playwright": "^1.60.0",
"postcss": "^8.5.15", "postcss": "^8.5.15",
"prettier": "^3.8.3", "prettier": "^3.8.3",

View File

@ -3,6 +3,10 @@ import { defineConfig, devices } from '@playwright/test';
export default defineConfig({ export default defineConfig({
testDir: './e2e', testDir: './e2e',
fullyParallel: true, fullyParallel: true,
// 活体 e2e 每轮经 globalSetup 直连真实 PG 复位 fixture(替代一次性 /tmp 种子脚本);需先 source infra.env(凭据不入库)。
globalSetup: './e2e/global-setup.ts',
// 共享同一真实后端 DB(muse_slice_live)、旅程间有状态消费/流转 → 串行跑避免跨用例竞态,保证可复现绿。
workers: 1,
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
reporter: 'list', reporter: 'list',
use: { use: {

View File

@ -60,6 +60,9 @@ importers:
'@types/node': '@types/node':
specifier: ^24.12.3 specifier: ^24.12.3
version: 24.12.4 version: 24.12.4
'@types/pg':
specifier: ^8.20.0
version: 8.20.0
'@types/react': '@types/react':
specifier: ^18.3.29 specifier: ^18.3.29
version: 18.3.29 version: 18.3.29
@ -96,6 +99,9 @@ importers:
msw: msw:
specifier: ^2.14.6 specifier: ^2.14.6
version: 2.14.6(@types/node@24.12.4)(typescript@6.0.3) version: 2.14.6(@types/node@24.12.4)(typescript@6.0.3)
pg:
specifier: ^8.21.0
version: 8.21.0
playwright: playwright:
specifier: ^1.60.0 specifier: ^1.60.0
version: 1.60.0 version: 1.60.0
@ -1108,6 +1114,9 @@ packages:
'@types/node@24.12.4': '@types/node@24.12.4':
resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==}
'@types/pg@8.20.0':
resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==}
'@types/prop-types@15.7.15': '@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
@ -2231,6 +2240,40 @@ packages:
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
engines: {node: '>= 14.16'} engines: {node: '>= 14.16'}
pg-cloudflare@1.4.0:
resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==}
pg-connection-string@2.13.0:
resolution: {integrity: sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
pg-pool@3.14.0:
resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==}
peerDependencies:
pg: '>=8.0'
pg-protocol@1.14.0:
resolution: {integrity: sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==}
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
pg@8.21.0:
resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==}
engines: {node: '>= 16.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
pgpass@1.0.5:
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@ -2307,6 +2350,22 @@ packages:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
postgres-bytea@1.0.1:
resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
engines: {node: '>=0.10.0'}
postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
prelude-ls@1.2.1: prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -2505,6 +2564,10 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
stackback@0.0.2: stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@ -2858,6 +2921,10 @@ packages:
xmlchars@2.2.0: xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
y18n@5.0.8: y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3274,7 +3341,7 @@ snapshots:
'@isaacs/cliui@8.0.2': '@isaacs/cliui@8.0.2':
dependencies: dependencies:
string-width: 5.1.2 string-width: 5.1.2
string-width-cjs: string-width@4.2.0 string-width-cjs: string-width@4.2.3
strip-ansi: 7.2.0 strip-ansi: 7.2.0
strip-ansi-cjs: strip-ansi@6.0.1 strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0 wrap-ansi: 8.1.0
@ -3746,6 +3813,12 @@ snapshots:
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.16.0
'@types/pg@8.20.0':
dependencies:
'@types/node': 24.12.4
pg-protocol: 1.14.0
pg-types: 2.2.0
'@types/prop-types@15.7.15': {} '@types/prop-types@15.7.15': {}
'@types/react-dom@18.3.7(@types/react@18.3.29)': '@types/react-dom@18.3.7(@types/react@18.3.29)':
@ -4876,6 +4949,41 @@ snapshots:
pathval@2.0.1: {} pathval@2.0.1: {}
pg-cloudflare@1.4.0:
optional: true
pg-connection-string@2.13.0: {}
pg-int8@1.0.1: {}
pg-pool@3.14.0(pg@8.21.0):
dependencies:
pg: 8.21.0
pg-protocol@1.14.0: {}
pg-types@2.2.0:
dependencies:
pg-int8: 1.0.1
postgres-array: 2.0.0
postgres-bytea: 1.0.1
postgres-date: 1.0.7
postgres-interval: 1.2.0
pg@8.21.0:
dependencies:
pg-connection-string: 2.13.0
pg-pool: 3.14.0(pg@8.21.0)
pg-protocol: 1.14.0
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.4.0
pgpass@1.0.5:
dependencies:
split2: 4.2.0
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@2.3.2: {} picomatch@2.3.2: {}
@ -4931,6 +5039,16 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
postgres-array@2.0.0: {}
postgres-bytea@1.0.1: {}
postgres-date@1.0.7: {}
postgres-interval@1.2.0:
dependencies:
xtend: 4.0.2
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier@3.8.3: {} prettier@3.8.3: {}
@ -5177,6 +5295,8 @@ snapshots:
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
split2@4.2.0: {}
stackback@0.0.2: {} stackback@0.0.2: {}
start-server-and-test@3.0.5: start-server-and-test@3.0.5:
@ -5515,6 +5635,8 @@ snapshots:
xmlchars@2.2.0: {} xmlchars@2.2.0: {}
xtend@4.0.2: {}
y18n@5.0.8: {} y18n@5.0.8: {}
yallist@3.1.1: {} yallist@3.1.1: {}