承用户选 B(补 admin config 断层)。前提纠正:原 skip 注释"切片库根本没有 muse_function_chain/muse_protection_node 表"误判——psql 实证真表名带 meta_ 前缀=muse_meta_function_chain/muse_meta_protection_node(V3 schema、都在 muse_slice_live、行数 0),真断层是缺系统级公共治理对象种子、非缺表/缺功能。
每锚点真表+真后端过滤契约(curl 实证):story.draft 元结构=muse_meta_schema(列表无 status 过滤);故事链路=muse_meta_function_chain(listQuery 只放行 status=active、displayName 取 chain_snapshot.displayName);事实校验保护节点=muse_meta_protection_node(status=active+policy_snapshot->>chainKey 绑定链路、irreplaceable);候选生成 Prompt=muse_prompt(非 muse_ai_prompt);系统故事智能体=muse_agent(非 muse_ai_agent、agent_type=system、scope=all 不过滤);系统世界观库=muse_knowledge_base(kb_type=global、owner_user_id=0)。
global-setup 加 resetMetaGovernanceFixture(7 幂等 upsert ON CONFLICT、tenant_id=1、照 resetMarketFixture/resetJobsFixture 范式)。spec 删 :456 skip→拆 6 条细粒度真连用例(元结构/功能编排/AI/全局知识/市场/New-API),修 2 处 strict-mode 多命中。New-API 锚点契约纠正(mock 期 user-e2e/usage-e2e 伪造值):真后端绑定行是用户数字 id 字符串、用量 recordId 是聚合派生 {userId}:{period},改对真锚点(活体用户+1:month)、无后端缺口。
独立核实:连跑全量 e2e x2 各 12 passed/0 skipped(account/market/jobs 既有 6 真连无回归、幂等可复现);psql 复核 6 锚点种子真灌(function_chain/protection_node active、muse_prompt/muse_agent system/global KB 各 1 行真实契约)。至此 admin governance e2e 从全 page.route mock 全转真后端:12 真连用例 0 skip。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
452 lines
26 KiB
TypeScript
452 lines
26 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);
|
|
|
|
// 复位 jobs/source-event 写命令子域 fixture(详见函数注释)。
|
|
await resetJobsFixture(client);
|
|
|
|
// 灌核心治理配置只读巡航子域 fixture(元结构/功能编排/AI/全局知识/New-API 用量,详见函数注释)。
|
|
await resetMetaGovernanceFixture(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=已是初态)`,
|
|
);
|
|
}
|
|
|
|
/** jobs/source-event e2e 锚点 id(高位固定 id,避开切片库基础种子的 1~29)。 */
|
|
const JOB_RETRY_FIXTURE_ID = 90_001;
|
|
const JOB_CANCEL_FIXTURE_ID = 90_002;
|
|
const SOURCE_EVENT_FIXTURE_ID = 90_003;
|
|
|
|
/**
|
|
* 核心治理配置(元结构/功能编排/AI/全局知识/New-API)只读巡航子域 e2e 锚点。
|
|
*
|
|
* 这些锚点都是**系统级公共治理对象**(非用户私有),切片库基础种子未灌(各列表端点真返 total=0,但表都在),
|
|
* 故本前置照 market/jobs 范式幂等灌入真实行,让"核心治理配置全页巡航"用例的只读断言能对真数据成立。
|
|
* 全部用高位固定 id(901xx),tenant_id=1,ON CONFLICT 幂等;锚点业务键带 `.e2e` 后缀,避免与真实治理对象冲突。
|
|
*
|
|
* 锚点与真实后端契约的对应关系(均 curl 实证 surface,见下方函数注释):
|
|
* - 元结构 story.draft(muse_meta_schema):列表无 status 过滤即可见;详情页 activeVersion=null 也能正常渲染。
|
|
* - 功能链路「故事链路」(muse_meta_function_chain):列表 listQuery 只放行 status='active';displayName 取 chain_snapshot.displayName。
|
|
* - 保护节点「事实校验」(muse_meta_protection_node):列表按 status='active' + policy_snapshot->>'chainKey' 过滤,故 policy_snapshot 必须带 chainKey 绑定到故事链路。
|
|
* - AI Prompt「候选生成 Prompt」(muse_prompt):列表无 status 过滤,name 即展示名。
|
|
* - 系统智能体「系统故事智能体」(muse_agent):scope=all 不过滤,前端「系统智能体」tab 按 agent_type=system 展示;name 即展示名。
|
|
* - 全局知识库「系统世界观库」(muse_knowledge_base):列表按 kb_type='global' 过滤(owner_user_id=0);name 即展示名。
|
|
* - New-API 锚点不需独立种子:网关绑定行由 account 用户投影(userId='1'/昵称'活体用户'已存在),
|
|
* 调用归属用量记录(muse_member_usage_record)灌 user1 一行 → recordId 真后端聚合为 '1:month'。
|
|
*/
|
|
const META_SCHEMA_FIXTURE_ID = 90_101;
|
|
const FUNCTION_CHAIN_FIXTURE_ID = 90_102;
|
|
const PROTECTION_NODE_FIXTURE_ID = 90_103;
|
|
const PROMPT_FIXTURE_ID = 90_104;
|
|
const SYSTEM_AGENT_FIXTURE_ID = 90_105;
|
|
const GLOBAL_KB_FIXTURE_ID = 90_106;
|
|
const USAGE_RECORD_FIXTURE_ID = 90_107;
|
|
/** 功能链路与保护节点的关联键:保护节点 policy_snapshot.chainKey 指向它,前端按 chainKey 拉保护节点列表。 */
|
|
const FUNCTION_CHAIN_FIXTURE_KEY = 'story.pipeline.e2e';
|
|
/** 重试 fixture 的 retry group;重试命令派生子 job 会复用同一 group,reset 据此清理上一轮派生行。 */
|
|
const JOB_RETRY_FIXTURE_GROUP = 'retry-group-e2e-fixture-retry';
|
|
/** 来源事件 fixture 的业务事件 id;来源重试派生的传播 job 在 input_summary.sourceEventId 里带它,reset 据此清理。 */
|
|
const SOURCE_EVENT_FIXTURE_EVENT_ID = 'evt-e2e-fixture-src';
|
|
|
|
/**
|
|
* 复位 jobs/source-event 子域 e2e fixture(幂等、tenant_id=1 限定、直连真实 muse_ai_* 表)。
|
|
*
|
|
* 这三条命令(任务重试 / 任务取消 / 来源传播重试)后端都是纯 DB 状态机、无外部副作用(已活体核验:
|
|
* 合法输入 code:0、非法输入返回业务错误码而非 500),但切片库基础种子里没有可命令的行
|
|
* (jobs 现有行全部 retryable=false,source-event 表为空),故本前置造稳定锚点。
|
|
*
|
|
* 锚点设计:
|
|
* - 任务重试锚点 id=90001:status=failed + retryable=true,input_summary 不含 userInstructionLength/Hash
|
|
* (否则后端要求 runtime payload 行,缺则重试被拒)。e2e 真打 retry → 后端**新插**一行
|
|
* operation_id=adminRetryJob、retry_group_id 同组的 queued job;反查据此断言"本轮真的派生了重试 job"。
|
|
* - 任务取消锚点 id=90002:status=**running**(刻意不用 queued)。切片库有后台 AI runtime worker 会在 ~3s 内
|
|
* 把 queued job 流转成 failed(实测),而 e2e 导航+渲染+填表+提交远超 3s,queued 锚点会在点取消前被 worker
|
|
* 抢成 failed → 取消按钮 disabled 假红。running 状态 worker 不接管(视作已有 worker 持有),实测长期稳定,
|
|
* 且前端 canCancelJob 同样放行 running。e2e 真打 cancel → 后端把该行改成 cancelled;反查据此断言。
|
|
* - 来源事件锚点 id=90003:action_policy=needs_recheck(前端 canRetrySourceEvent 只放行此策略)。e2e 真打
|
|
* 来源重试 → 后端**新插**一行 job_type=source_event_retry 的 queued job(不改 source_event 行);反查据此断言。
|
|
*
|
|
* 为什么要先清派生行:重试 / 来源重试每轮都会 insert 一条新 job(retry group 唯一索引在子 job 流转成
|
|
* 终态后即释放,故第二轮能再插);命令幂等表 muse_ai_command 也按 commandId 累积。前端每次用
|
|
* `job-retry-${Date.now()}-…` 这类带时间戳的唯一 commandId,不会命中幂等回放,但派生 job 与命令行会越积越多,
|
|
* 让"本轮新增一行"反查计数失真。故 reset 先按锚点专属标记删净上一轮派生 job + 命令行,再把三条锚点写回初态。
|
|
*
|
|
* id 列是 GENERATED ALWAYS IDENTITY,显式写锚点 id 必须带 OVERRIDING SYSTEM VALUE。
|
|
*/
|
|
async function resetJobsFixture(client: Client): Promise<void> {
|
|
// 1) 清上一轮派生行:重试派生子 job(operation=adminRetryJob 且同 retry group;不动同组的源锚点
|
|
// operation=createAiTask)、来源重试派生的传播 job(按 input_summary.sourceEventId 锚定)。
|
|
const clearRetryChildren = await client.query(
|
|
`DELETE FROM muse_ai_job
|
|
WHERE tenant_id = 1 AND operation_id = 'adminRetryJob' AND retry_group_id = $1`,
|
|
[JOB_RETRY_FIXTURE_GROUP],
|
|
);
|
|
const clearSourceRetryJobs = await client.query(
|
|
`DELETE FROM muse_ai_job
|
|
WHERE tenant_id = 1 AND job_type = 'source_event_retry'
|
|
AND input_summary->>'sourceEventId' = $1`,
|
|
[SOURCE_EVENT_FIXTURE_EVENT_ID],
|
|
);
|
|
// 2) 清这三条锚点相关的命令幂等行(按 target 精确限定,避免误删其他命令审计)。
|
|
const clearCommands = await client.query(
|
|
`DELETE FROM muse_ai_command
|
|
WHERE tenant_id = 1
|
|
AND ((operation_id IN ('adminRetryJob', 'adminCancelJob') AND target_id IN ($1, $2))
|
|
OR (operation_id = 'adminRetrySourceEvent' AND target_id = $3))`,
|
|
[JOB_RETRY_FIXTURE_ID, JOB_CANCEL_FIXTURE_ID, SOURCE_EVENT_FIXTURE_ID],
|
|
);
|
|
|
|
// 3) 重置三条锚点为初态(先删后插,保证 input_summary / status / retryable 等列回到期望值)。
|
|
await client.query(`DELETE FROM muse_ai_job WHERE id IN ($1, $2)`, [
|
|
JOB_RETRY_FIXTURE_ID,
|
|
JOB_CANCEL_FIXTURE_ID,
|
|
]);
|
|
await client.query(`DELETE FROM muse_ai_source_event WHERE id = $1`, [
|
|
SOURCE_EVENT_FIXTURE_ID,
|
|
]);
|
|
// 任务重试锚点:failed + retryable,input_summary 必须"干净"(不含用户指令长度/哈希)。
|
|
await client.query(
|
|
`INSERT INTO muse_ai_job
|
|
(id, job_id, job_type, operation_id, actor_user_id, owner_user_id, status, retry_count,
|
|
retryable, retry_group_id, correlation_id, input_summary, tenant_id, creator, updater,
|
|
deleted, create_time, update_time)
|
|
OVERRIDING SYSTEM VALUE
|
|
VALUES ($1, 'job-e2e-fixture-retry', 'ai_task_runtime', 'createAiTask', 1, 1, 'failed', 0,
|
|
true, $2, 'corr-e2e-fixture-retry', '{"e2eFixture": true, "taskType": "continuation"}'::jsonb,
|
|
1, '', '', false, now(), now())`,
|
|
[JOB_RETRY_FIXTURE_ID, JOB_RETRY_FIXTURE_GROUP],
|
|
);
|
|
// 任务取消锚点:running(避开 worker 把 queued 抢成 failed)。
|
|
await client.query(
|
|
`INSERT INTO muse_ai_job
|
|
(id, job_id, job_type, operation_id, actor_user_id, owner_user_id, status, retry_count,
|
|
retryable, correlation_id, input_summary, tenant_id, creator, updater,
|
|
deleted, create_time, update_time, started_at)
|
|
OVERRIDING SYSTEM VALUE
|
|
VALUES ($1, 'job-e2e-fixture-cancel', 'ai_task_runtime', 'createAiTask', 1, 1, 'running', 0,
|
|
false, 'corr-e2e-fixture-cancel', '{"e2eFixture": true}'::jsonb,
|
|
1, '', '', false, now(), now(), now())`,
|
|
[JOB_CANCEL_FIXTURE_ID],
|
|
);
|
|
// 来源事件锚点:needs_recheck 策略,stale 状态。
|
|
await client.query(
|
|
`INSERT INTO muse_ai_source_event
|
|
(id, source_event_id, source_owner, source_type, source_id, source_revision, owner_user_id,
|
|
status, action_policy, event_summary, tenant_id, creator, updater, deleted,
|
|
create_time, update_time)
|
|
OVERRIDING SYSTEM VALUE
|
|
VALUES ($1, $2, 'knowledge', 'knowledge_base', '777', '3', 1, 'stale', 'needs_recheck',
|
|
'{"affectedScope": "e2e fixture scope", "affectedBindings": 2, "propagationStatus": "pending"}'::jsonb,
|
|
1, '', '', false, now(), now())`,
|
|
[SOURCE_EVENT_FIXTURE_ID, SOURCE_EVENT_FIXTURE_EVENT_ID],
|
|
);
|
|
|
|
console.log(
|
|
`[admin e2e globalSetup] reset jobs fixture: clearRetryChildren=${clearRetryChildren.rowCount} ` +
|
|
`clearSourceRetryJobs=${clearSourceRetryJobs.rowCount} clearCommands=${clearCommands.rowCount} ` +
|
|
`(已写回锚点 job ${JOB_RETRY_FIXTURE_ID}/${JOB_CANCEL_FIXTURE_ID}、source-event ${SOURCE_EVENT_FIXTURE_ID})`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 灌核心治理配置只读巡航子域 e2e fixture(幂等、tenant_id=1 限定、直连真实治理表)。
|
|
*
|
|
* 这些都是只读端点的展示锚点:本前置只**幂等 upsert**系统级公共治理对象,不做"清上一轮"(只读不产生增量),
|
|
* ON CONFLICT 复位 deleted=false / status / 展示名,保证锚点稳定可见。每行都按上面常量块注释里实证过的真实后端
|
|
* 过滤契约灌真实字段(故事链路 status=active、保护节点 policy_snapshot.chainKey 绑定、全局 KB kb_type=global 等),
|
|
* 不是"伪造糊弄断言"——curl 复核过六个治理端点 + 两个 New-API 端点(account 投影)都真返这些锚点。
|
|
*
|
|
* 锚点存在性是 best-effort 硬前置失败语义的反面:这些表本就空(切片库没灌),所以这里是"灌入",不是"复位已有行";
|
|
* 若 upsert 任一行 rowCount=0(理论上不会,ON CONFLICT DO UPDATE 总会写),不致命,下方断言会真实暴露缺数据。
|
|
*
|
|
* id 列是 GENERATED ALWAYS IDENTITY,显式写锚点 id 必须带 OVERRIDING SYSTEM VALUE。
|
|
*/
|
|
async function resetMetaGovernanceFixture(client: Client): Promise<void> {
|
|
// 1) 元结构 story.draft:列表端点不按 status 过滤,draft 也可见;详情页 activeVersion=null 能正常渲染「草稿 JSON 编辑」。
|
|
// uk 是 (tenant_id, domain, scope, target_type, schema_key),冲突即复位展示名/状态。
|
|
const metaSchema = await client.query(
|
|
`INSERT INTO muse_meta_schema
|
|
(id, schema_key, domain, scope, target_type, display_name, status, projection_version,
|
|
creator, updater, deleted, tenant_id)
|
|
OVERRIDING SYSTEM VALUE
|
|
VALUES ($1, 'story.draft', 'content', 'work', 'novel_work', '故事草稿元结构', 'draft', 1,
|
|
'e2e-fixture', 'e2e-fixture', false, 1)
|
|
ON CONFLICT (tenant_id, domain, scope, target_type, schema_key)
|
|
DO UPDATE SET display_name = EXCLUDED.display_name, status = EXCLUDED.status, deleted = false`,
|
|
[META_SCHEMA_FIXTURE_ID],
|
|
);
|
|
|
|
// 2) 功能链路「故事链路」:列表 listQuery 只放行 status='active';displayName 取 chain_snapshot.displayName,
|
|
// activeVersion 取 active_version 字符串(无版本行时也能解析成 1)。uk 是 (tenant_id, chain_key)。
|
|
const functionChain = await client.query(
|
|
`INSERT INTO muse_meta_function_chain
|
|
(id, chain_key, active_version, chain_snapshot, status, creator, updater, deleted, tenant_id)
|
|
OVERRIDING SYSTEM VALUE
|
|
VALUES ($1, $2, '1',
|
|
'{"displayName": "故事链路", "description": "e2e fixture 故事生成功能链路"}'::jsonb,
|
|
'active', 'e2e-fixture', 'e2e-fixture', false, 1)
|
|
ON CONFLICT (tenant_id, chain_key)
|
|
DO UPDATE SET chain_snapshot = EXCLUDED.chain_snapshot, active_version = '1',
|
|
status = 'active', deleted = false`,
|
|
[FUNCTION_CHAIN_FIXTURE_ID, FUNCTION_CHAIN_FIXTURE_KEY],
|
|
);
|
|
|
|
// 3) 保护节点「事实校验」:列表按 status='active' + policy_snapshot->>'chainKey' 过滤,故 policy_snapshot 必须带
|
|
// chainKey 绑定到故事链路;replaceable=false → 前端展示「不可替换」。uk 是 (tenant_id, node_key)。
|
|
const protectionNode = await client.query(
|
|
`INSERT INTO muse_meta_protection_node
|
|
(id, node_key, display_name, owner_module, replaceable, policy_snapshot, status, node_type,
|
|
irreplaceable_reason, creator, updater, deleted, tenant_id)
|
|
OVERRIDING SYSTEM VALUE
|
|
VALUES ($1, 'protection.fact_check.e2e', '事实校验', 'meta', false,
|
|
('{"chainKey": "' || $2 || '", "chainDisplayName": "故事链路", "auditSummary": "e2e 事实校验保护节点"}')::jsonb,
|
|
'active', 'static_check', '事实校验为不可替换保护节点', 'e2e-fixture', 'e2e-fixture', false, 1)
|
|
ON CONFLICT (tenant_id, node_key)
|
|
DO UPDATE SET display_name = EXCLUDED.display_name, policy_snapshot = EXCLUDED.policy_snapshot,
|
|
status = 'active', deleted = false`,
|
|
[PROTECTION_NODE_FIXTURE_ID, FUNCTION_CHAIN_FIXTURE_KEY],
|
|
);
|
|
|
|
// 4) AI Prompt「候选生成 Prompt」:列表无 status 过滤;name 即前端展示名(默认「Prompt 模板」tab 可见)。
|
|
// uk 是 (tenant_id, prompt_key)。
|
|
const prompt = await client.query(
|
|
`INSERT INTO muse_prompt
|
|
(id, prompt_key, name, description, content, active_version, status, category,
|
|
creator, updater, deleted, tenant_id)
|
|
OVERRIDING SYSTEM VALUE
|
|
VALUES ($1, 'system.candidate.gen.e2e', '候选生成 Prompt', 'e2e fixture 候选生成 prompt',
|
|
'生成候选内容', '1', 'active', 'generation', 'e2e-fixture', 'e2e-fixture', false, 1)
|
|
ON CONFLICT (tenant_id, prompt_key)
|
|
DO UPDATE SET name = EXCLUDED.name, status = 'active', deleted = false`,
|
|
[PROMPT_FIXTURE_ID],
|
|
);
|
|
|
|
// 5) 系统智能体「系统故事智能体」:scope=all 不过滤,前端「系统智能体」tab 按 agent_type=system 展示;
|
|
// name 即展示名。uk 是 (tenant_id, agent_key)。
|
|
const systemAgent = await client.query(
|
|
`INSERT INTO muse_agent
|
|
(id, agent_key, name, description, agent_type, status, creator, updater, deleted, tenant_id)
|
|
OVERRIDING SYSTEM VALUE
|
|
VALUES ($1, 'system:story.e2e', '系统故事智能体', 'e2e fixture 系统故事智能体', 'system', 'active',
|
|
'e2e-fixture', 'e2e-fixture', false, 1)
|
|
ON CONFLICT (tenant_id, agent_key)
|
|
DO UPDATE SET name = EXCLUDED.name, agent_type = 'system', status = 'active', deleted = false`,
|
|
[SYSTEM_AGENT_FIXTURE_ID],
|
|
);
|
|
|
|
// 6) 全局知识库「系统世界观库」:列表按 kb_type='global' 过滤(系统级 owner_user_id=0);name 即展示名。
|
|
// 无业务 uk(uk 仅在 command_id 非空时生效),按主键 id 幂等。
|
|
const globalKb = await client.query(
|
|
`INSERT INTO muse_knowledge_base
|
|
(id, name, description, kb_type, owner_user_id, status, active_version, revision,
|
|
creator, updater, deleted, tenant_id)
|
|
OVERRIDING SYSTEM VALUE
|
|
VALUES ($1, '系统世界观库', 'e2e fixture 系统级世界观知识库', 'global', 0, 'active', 1, 1,
|
|
'e2e-fixture', 'e2e-fixture', false, 1)
|
|
ON CONFLICT (id)
|
|
DO UPDATE SET name = EXCLUDED.name, kb_type = 'global', status = 'active', deleted = false`,
|
|
[GLOBAL_KB_FIXTURE_ID],
|
|
);
|
|
|
|
// 7) New-API 调用归属用量记录:account usage 端点把同用户同周期的用量聚合成一行,recordId='{userId}:{period}',
|
|
// 前端默认 period=month。灌 user1 一行 completed 用量(create_time=now 落在本月,month periodStart 过滤可见),
|
|
// 真后端聚合出 recordId='1:month' → New-API「调用归属」tab 可断言。create_time 复位为 now 保证永远落在当月。
|
|
const usageRecord = await client.query(
|
|
`INSERT INTO muse_member_usage_record
|
|
(id, account_user_id, usage_type, token_input, token_output, cost_reference, correlation_id,
|
|
attribution_snapshot, status, creator, updater, deleted, tenant_id, create_time, update_time)
|
|
OVERRIDING SYSTEM VALUE
|
|
VALUES ($1, 1, 'ai_generation', 1200, 800, 'cost-e2e', 'corr-usage-e2e',
|
|
'{"attributedUserId": "1", "source": "e2e fixture"}'::jsonb, 'completed',
|
|
'e2e-fixture', 'e2e-fixture', false, 1, now(), now())
|
|
ON CONFLICT (id)
|
|
DO UPDATE SET token_input = EXCLUDED.token_input, token_output = EXCLUDED.token_output,
|
|
status = 'completed', deleted = false, create_time = now(), update_time = now()`,
|
|
[USAGE_RECORD_FIXTURE_ID],
|
|
);
|
|
|
|
console.log(
|
|
`[admin e2e globalSetup] reset meta-governance fixture: metaSchema=${metaSchema.rowCount} ` +
|
|
`functionChain=${functionChain.rowCount} protectionNode=${protectionNode.rowCount} ` +
|
|
`prompt=${prompt.rowCount} systemAgent=${systemAgent.rowCount} globalKb=${globalKb.rowCount} ` +
|
|
`usageRecord=${usageRecord.rowCount}(均为 upsert 写入条数)`,
|
|
);
|
|
}
|
|
|
|
export default globalSetup;
|