test(admin-e2e): jobs 子域转真连(重试/取消/来源重试真打+真 PG 反查)、governance config 断层如实标注 skip

jobs 真连(反假绿:真打写命令+真 PG 反查 muse_ai_job 派生行/状态):①重试(:262)→后端校验 retryable 新插 operation_id=adminRetryJob 同 retry_group queued 子 job(MuseJobServiceImpl.retryAdminJob 纯 DB 状态机、活体 code:0),反查派生子 job 行数;②取消+来源重试(:291)→cancel 改 muse_ai_job.status=cancelled、source retry 新插 job_type=source_event_retry job(muse_ai_source_event 90003 needs_recheck 锚点),反查状态/派生行。关键坑:切片库后台 AI runtime worker ~3s 抢 queued→failed,取消锚点须用 running(worker 不接管、前端 canCancelJob 放行);queued 锚点活体 cancel 真返 1040100014 不可取消。

global-setup 加 resetJobsFixture:幂等 seed 90001(failed/retryable muse_ai_job)/90002(running muse_ai_job)/90003(needs_recheck muse_ai_source_event)+清派生子 job/source_event_retry/命令幂等行,每轮自愈。真契约:retry 请求体只接受 {commandId}(JobRetryReqVO 仅此字段、reason 仅 UI 留痕不外发);cancel/source-retry 接受 {commandId,reason}。旧 skip 靠 mock 拦截 payload 字段,改真打+DB 派生行反查(等价更强),删 museAdminPayloadsByPage 收集器。

核心治理配置页(:456)保留 skip:非端点 500,而是切片数据/表断层——muse_function_chain/muse_protection_node 表在 muse_slice_live 根本不存在(psql to_regclass 均 None)、AI Prompt/全局知识库/story.draft 切片未灌种子,faithfully 转需跨 6 子系统伪造数据且两表连结构都没有,不硬转。用例上方写明断层+待补后端项。

独立核实:连跑全量 account+market+jobs e2e 6 passed/1 skipped(背靠背幂等无第二轮假红);psql 复核 90002=cancelled、retry 派生子 job=1、source_event_retry job=1、:456 断层表均 None(证标注诚实)。待补后端:①切片库缺 muse_function_chain/muse_protection_node 表 ②AI Prompt/全局 KB/story.draft 种子 ③jobs/source 三写命令后端无 P1r 真库 IT(现 Mockito 单测、本次 e2e 真连可视首个端到端真验)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-25 12:32:21 -07:00
parent ce637ac2b6
commit 2d06ec5f31
2 changed files with 237 additions and 47 deletions

View File

@ -72,6 +72,9 @@ async function globalSetup(): Promise<void> {
// 复位 market 写命令子域 fixture(详见函数注释)。
await resetMarketFixture(client);
// 复位 jobs/source-event 写命令子域 fixture(详见函数注释)。
await resetJobsFixture(client);
} finally {
await client.end();
}
@ -181,4 +184,113 @@ async function resetMarketFixture(client: Client): Promise<void> {
);
}
/** 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;
/** 重试 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=adminRetryJobretry_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 runninge2e 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})`,
);
}
export default globalSetup;

View File

@ -75,6 +75,55 @@ async function queryAppealFacts(
});
}
// jobs/source-event 子域 e2e 锚点 id(与 global-setup resetJobsFixture 保持一致)。
const JOB_RETRY_FIXTURE_ID = 90_001;
const JOB_CANCEL_FIXTURE_ID = 90_002;
const SOURCE_EVENT_FIXTURE_ID = 90_003;
const JOB_RETRY_FIXTURE_GROUP = 'retry-group-e2e-fixture-retry';
const SOURCE_EVENT_FIXTURE_EVENT_ID = 'evt-e2e-fixture-src';
/** 读取某个 muse_ai_job 的真实状态(tenant_id=1),用于取消命令后的 DB 反查。 */
async function queryJobStatus(jobId: number): Promise<string> {
return withMarketDb(async (client) => {
const result = await client.query(
`SELECT status FROM muse_ai_job WHERE tenant_id = 1 AND deleted = false AND id = $1`,
[jobId],
);
return result.rows[0]?.status ?? '';
});
}
/**
* retry group "重试派生子 job"(operation_id=adminRetryJob,tenant_id=1)
* globalSetup , 1,"重试真的派生了一条新 job"DB
*/
async function countAdminRetryChildJobs(retryGroupId: string): Promise<number> {
return withMarketDb(async (client) => {
const result = await client.query(
`SELECT count(*)::int AS c FROM muse_ai_job
WHERE tenant_id = 1 AND operation_id = 'adminRetryJob' AND retry_group_id = $1`,
[retryGroupId],
);
return result.rows[0]?.c ?? 0;
});
}
/**
* "来源传播重试 job"(job_type=source_event_retry,tenant_id=1)
* input_summary.sourceEventId ;globalSetup , 1
*/
async function countSourceRetryJobs(sourceEventId: string): Promise<number> {
return withMarketDb(async (client) => {
const result = await client.query(
`SELECT count(*)::int AS c FROM muse_ai_job
WHERE tenant_id = 1 AND job_type = 'source_event_retry'
AND input_summary->>'sourceEventId' = $1`,
[sourceEventId],
);
return result.rows[0]?.c ?? 0;
});
}
// 运行时确认的真实 Pinia 持久化 key:namespace=`${VITE_APP_NAMESPACE}-${VITE_APP_VERSION}-${env}`,
// 而 VITE_APP_NAMESPACE 未在任何 .env 定义 → 运行时取值为字符串 'undefined';VITE_APP_VERSION=package.json 版本 5.7.0;
// dev 环境 env='dev';persist 后缀 store id='core-access'(见 packages/stores setup.ts `${namespace}-${storeKey}`)。
@ -83,10 +132,6 @@ const ACCESS_STORE_KEY = 'undefined-5.7.0-dev-core-access';
const runtimeIssuesByPage = new WeakMap<Page, string[]>();
// 暂 skip 的其他子域(jobs/market/governance/ai/knowledge)用例仍引用此写命令 payload 收集器以编译;
// 这些子域转真后端时再恢复填充逻辑(届时改为真 POST 后从 DB/响应核验,而非 mock 拦截 payload)。
const museAdminPayloadsByPage = new WeakMap<Page, Record<string, unknown>[]>();
const isIgnoredRuntimeIssue = (message: string) =>
/ResizeObserver loop (completed with undelivered notifications|limit exceeded)/i.test(
message,
@ -259,71 +304,91 @@ test('账号治理页对真后端 account 子域渲染脱敏摘要', async ({ pa
expectNoUnhandledMuseAdminApis(page);
});
test.skip('任务重试只发送当前 OpenAPI 支持的 commandId', async ({ page }) => {
// 任务重试真命令(转真后端 + 真 PG 反查)。
// 锚点失败任务 90001(globalSetup 复位:status=failed、retryable=true、input_summary 不含用户指令哈希)。
// 真打重试 → 后端校验 retryable 后新插一条 operation_id=adminRetryJob、同 retry group 的 queued job。
// 反假绿核心:真 PG 反查"本轮真的派生了一条重试子 job"(不只看 UI toast)。
//
// 契约差异(mock vs 真):retry 端点请求体只接受 { commandId }(JobRetryReqVO 仅 commandId 字段),
// reason 仅作 UI 人工复核留痕、不外发——与前端 submitJobAction 一致;旧 skip 用例靠 mock 拦截 payload
// 断言这一点,现改为真打 + DB 派生行反查,等价且更强(真后端真的接受了该请求并落库)。
test('任务重试对锚点失败任务真打命令并派生重试 job', async ({ page }) => {
// 前置真值核验:锚点 90001 必须是 failed(globalSetup 已复位),否则前端"重试"按钮逻辑前提不成立。
expect(await queryJobStatus(JOB_RETRY_FIXTURE_ID)).toBe('failed');
await page.goto('/muse/jobs');
await page
.getByRole('row', { name: /1001/ })
.getByRole('button', { name: '重试' })
.click();
// 锚点失败任务行(按 jobId 定位)。点"重试"前前端会先拉详情确认 retryable=true 才开弹窗。
const jobRow = page.getByRole('row', { name: new RegExp(`${JOB_RETRY_FIXTURE_ID}`) });
await jobRow.getByRole('button', { name: '重试' }).click();
// 重试前必须完成四项重验勾选 + 填可审计原因(前端硬门禁)。
await page.getByLabel('owner 和当前操作者权限已重验').check();
await page.getByLabel('来源状态、市场状态和版本已重验').check();
await page.getByLabel('授权快照和运行权限包已重验').check();
await page
.getByLabel('幂等键、retry group 和旧上下文复用风险已重验')
.check();
await page
.locator('.ant-modal textarea')
.fill('人工复核留痕,不发送到 retry 接口');
await page.locator('.ant-modal textarea').fill('e2e 真后端重试,已完成四项重验');
await page.locator('.ant-modal-footer .ant-btn-primary').click();
const retryPayload = museAdminPayloadsByPage
.get(page)
?.find(
(item) => item.method === 'POST' && item.path === '/jobs/1001/retry',
)?.payload;
// UI 反馈:成功 toast(命令 code:0)。
await expect(page.getByText('任务重试命令已提交')).toBeVisible();
// 反假绿核心:真 PG 反查本轮真的派生了一条 adminRetryJob 子 job(后台 worker 随后可能把它流转成其他终态,
// 但 operation_id/retry_group_id 标记不变,故按"存在一条派生行"断言,不对其后续 status 较真)。
await expect
.poll(() => countAdminRetryChildJobs(JOB_RETRY_FIXTURE_GROUP), {
timeout: 10_000,
})
.toBe(1);
expect(retryPayload).toEqual({
commandId: expect.stringMatching(/^job-retry-/),
});
expectNoUnhandledMuseAdminApis(page);
});
test.skip('任务取消和来源传播重试提交 commandId 与 reason', async ({ page }) => {
// 任务取消 + 来源传播重试真命令(转真后端 + 真 PG 反查)。
// 取消锚点 90002(globalSetup 复位为 running——刻意非 queued:切片库后台 worker 会在 ~3s 内把 queued 抢成
// failed,e2e 操作耗时远超 3s,running 状态 worker 不接管、长期稳定,且前端 canCancelJob 同样放行 running)。
// 来源事件锚点 90003(action_policy=needs_recheck,前端 canRetrySourceEvent 只放行此策略)。
//
// 契约差异(mock vs 真):cancel 端点接受 { commandId, reason }(JobCancelReqVO 两字段);source-event retry
// 接受 { commandId, reason }(SourceEventRetryReqVO,commandId @NotBlank)。旧 skip 用例靠 mock 拦截 payload
// 断言字段,现改为真打 + DB 反查(取消落 status=cancelled;来源重试派生 source_event_retry job),等价且更强。
test('任务取消与来源传播重试真打命令并落库', async ({ page }) => {
// 前置真值核验:取消锚点 90002 必须是 running(globalSetup 已复位),前端"取消"按钮才放行。
expect(await queryJobStatus(JOB_CANCEL_FIXTURE_ID)).toBe('running');
await page.goto('/muse/jobs');
await page
.getByRole('row', { name: /1002/ })
.getByRole('button', { name: '取消' })
.click();
// 取消锚点运行中任务(按 jobId 定位)。cancel 弹窗无需重验勾选,只需可审计原因。
const jobRow = page.getByRole('row', { name: new RegExp(`${JOB_CANCEL_FIXTURE_ID}`) });
await jobRow.getByRole('button', { name: '取消' }).click();
const jobActionDialog = page.getByRole('dialog', { name: '任务治理操作' });
await jobActionDialog.locator('textarea').fill('上游召回,停止排队任务');
await jobActionDialog.locator('textarea').fill('e2e 真后端取消运行中任务');
await jobActionDialog.locator('.ant-modal-footer .ant-btn-primary').click();
// UI 反馈 + 真 PG 反查:任务真的落为 cancelled 终态。
await expect(page.getByText('任务取消命令已提交')).toBeVisible();
await expect
.poll(() => queryJobStatus(JOB_CANCEL_FIXTURE_ID), { timeout: 10_000 })
.toBe('cancelled');
// 切到来源事件 tab,对 needs_recheck 锚点事件真打传播重试。
await page.getByRole('tab', { name: '来源事件' }).click();
await page
.getByRole('row', { name: /2001/ })
.getByRole('button', { name: '重试传播' })
.click();
const sourceRow = page.getByRole('row', { name: new RegExp(`${SOURCE_EVENT_FIXTURE_ID}`) });
await sourceRow.getByRole('button', { name: '重试传播' }).click();
const sourceRetryDialog = page.getByRole('dialog', { name: '重试来源传播' });
await sourceRetryDialog.locator('textarea').fill('来源状态已复核,重新传播');
await sourceRetryDialog.locator('textarea').fill('e2e 来源状态已复核,重新传播');
await sourceRetryDialog.locator('.ant-modal-footer .ant-btn-primary').click();
const payloads = museAdminPayloadsByPage.get(page) ?? [];
const cancelPayload = payloads.find(
(item) => item.method === 'POST' && item.path === '/jobs/1002/cancel',
)?.payload;
const sourceRetryPayload = payloads.find(
(item) =>
item.method === 'POST' && item.path === '/source-events/2001/retry',
)?.payload;
// UI 反馈 + 真 PG 反查:本轮真的派生了一条 source_event_retry job(后端不改 source-event 行,只投影传播 job)。
await expect(page.getByText('来源传播重试已提交')).toBeVisible();
await expect
.poll(() => countSourceRetryJobs(SOURCE_EVENT_FIXTURE_EVENT_ID), {
timeout: 10_000,
})
.toBe(1);
expect(cancelPayload).toEqual({
commandId: expect.stringMatching(/^job-cancel-/),
reason: '上游召回,停止排队任务',
});
expect(sourceRetryPayload).toEqual({
commandId: expect.stringMatching(/^source-retry-/),
reason: '来源状态已复核,重新传播',
});
expectNoUnhandledMuseAdminApis(page);
});
@ -453,6 +518,19 @@ test('市场申诉以 maintained 结论真打处理命令并落库', async ({ pa
expectNoUnhandledMuseAdminApis(page);
});
// 断层标注(诚实记录,保留 skip):这条"核心治理配置全页巡航"用例当前**不能**转真后端。
// 它的断言混了两类:① 系统边界静态文案(各页 chrome,与数据无关,真后端能渲染);② 一批 mock 期数据
// 锚点——story.draft 元结构、"故事链路"功能链路、"事实校验"保护节点、"候选生成 Prompt"、"系统故事智能体"、
// "系统世界观库"、new-api 的 user-e2e/usage-e2e。这些数据锚点在 muse_slice_live **不存在**,且其中:
// - 功能链路 / 保护节点:切片库**根本没有 muse_function_chain / muse_protection_node 表**
// (端点真返 total=0 仅因无后端支撑表),"故事链路""事实校验"断言无表可灌,无法真连;
// - AI Prompt(/ai/prompts total=0)、全局知识库(/knowledge/global-kbs total=0):切片库未灌对应种子;
// - 元结构现有 worldview/setting 而非 story.draft;系统智能体现有"活体测试智能体"而非"系统故事智能体"。
// 用例还做数据依赖的交互(点"查看详情"跳 story.draft、点"保护节点"看"事实校验"、切 tab 看"系统故事智能体"),
// 这些行在缺数据(且保护节点/功能链路缺表)时无法定位 → 无法硬转。faithfully 转需跨 6 个子系统伪造数据,
// 远超本批"3 个 skip 转真连"范围,且功能链路/保护节点连表都没有、本质是后端切片支撑断层。
// 故保留 skip。account/market/jobs/source-event 子域的系统边界已分别由各自真连用例覆盖;此页"全页巡航"
// 待后端补齐功能链路/保护节点表 + 切片灌全 AI Prompt/全局知识库种子后再转真连。下方 body 留作"完整覆盖应断言什么"的记录。
test.skip('管理员可进入核心治理配置页面并看到系统边界', async ({ page }) => {
await page.goto('/muse/governance/meta-schemas');
await expect(page.getByText('元结构定义').first()).toBeVisible();