test(studio): 补 agent 槽位绑定 UI e2e(AgentPage→真后端替换槽位,反假绿)
承 02D 死锁修复(9115d4f)+ AgentPage 作品选择器(4c069d4),补端到端 UI e2e(进度总账 line176② 缺口)。agent-slot-bind.spec:真浏览器 goto /agents→selectOption work1→点 agent1 卡片→点"绑定选中智能体"→真后端 precheck→bind。三重反假绿:DB 起点 revision=1 + bind 响应 code=0/slotRevision=2 + UI"绑定已更新"提示 + DB 终态 revision 1→2 active agent1。global-setup §15 每轮复位 work1 writing.continuation 槽位(agent1 revision1 active)保证可重复(bind 成功 revision+1 漂移、不复位二次跑红)。MSW off 真连 muse_slice_live;自验 + 子代理各跑均 passed 可重复、tsc 零报错。诚实范围:只覆盖"替换已有槽位",首次创建 UI 仍缺槽位骨架(Override Slot Contract=方案 B,人类架构决策域未做)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2c9fdb513c
commit
1fbe7ae73c
124
muse-studio/e2e/agent-slot-bind.spec.ts
Normal file
124
muse-studio/e2e/agent-slot-bind.spec.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { Client } from 'pg';
|
||||
|
||||
/**
|
||||
* 智能体槽位绑定 UI e2e(真实后端,反假绿)。
|
||||
*
|
||||
* 验前端 AgentPage → SlotBindingPanel 经真实浏览器 UI → 真实后端完成槽位「替换」,落库真生效非 mock 假绿。
|
||||
* 对应进度总账 line176② 的端到端 UI 验证缺口。
|
||||
*
|
||||
* 为何只测「替换」不测「首次创建」:
|
||||
* SlotBindingPanel 列出的是 work 的现有 active 槽位(GET /works/{id}/agent-slots = listWorkAgentSlots)。
|
||||
* 全新 work 无槽位 → UI 列空 → 无 slot 可点。首次创建的 UI 入口需 Override Slot Contract 槽位骨架(方案 B),
|
||||
* 该项属人类架构决策域、未做。故本 e2e 走「替换已有槽位」:work1 + writing.continuation(global-setup #15
|
||||
* 每轮复位为 agent1、revision=1、active),UI 上把它「再绑定一次 agent1」→ 后端 revision 1→2。
|
||||
*
|
||||
* 前置(global-setup #15 每轮复位,保证可重复):
|
||||
* DELETE + INSERT work1 writing.continuation 槽位(agent_id=1=agent1、agent_version='1'、status='active'、
|
||||
* revision=1、binding_source='user'、tenant_id=1)。bind 成功 revision+1,不复位则漂移致二次跑红。
|
||||
*
|
||||
* 反假绿双断言:
|
||||
* 1) UI 成功提示「绑定已更新,后续作品生成会使用新的槽位智能体。」(SlotBindingPanel 仅 bind 成功才渲染)。
|
||||
* 2) DB 核验:muse_agent_slot_binding work1 writing.continuation revision 从 1→2、status=active、agent_id=1,
|
||||
* 证 UI 操作真落库(非 mock 截获的假绿)。
|
||||
*
|
||||
* 真实后端 muse_slice_live(48080,MSW off:playwright webServer 已配 VITE_API_MOCK=false)。
|
||||
*/
|
||||
const TOKEN = 'test1';
|
||||
|
||||
/** work1(id=1,title「活体验证样书」)+ agent1(agentId=1,name「活体测试智能体」,user 类型 active);均 owner=1。 */
|
||||
const WORK_ID = '1';
|
||||
const SLOT_KEY = 'writing.continuation';
|
||||
const AGENT_DB_ID = 1; // muse_agent.id;global-setup #15 INSERT 用,DB 核验比对
|
||||
const AGENT_NAME = '活体测试智能体';
|
||||
|
||||
async function seedToken(page: Page): Promise<void> {
|
||||
await page.addInitScript((t) => {
|
||||
window.localStorage.setItem('accessToken', t);
|
||||
window.localStorage.setItem('tenantId', '1');
|
||||
}, TOKEN);
|
||||
}
|
||||
|
||||
/** 直连 muse_slice_live 读 work1 writing.continuation 当前 active 绑定(DB 核验用,凭据从 infra.env 环境变量)。 */
|
||||
async function readSlotBinding(): Promise<{ revision: number; status: string; agentId: number } | null> {
|
||||
const c = new Client({
|
||||
host: process.env.MUSE_POSTGRES_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: process.env.MUSE_POSTGRES_PASSWORD,
|
||||
ssl: false,
|
||||
});
|
||||
await c.connect();
|
||||
try {
|
||||
const r = await c.query(
|
||||
`SELECT revision, status, agent_id FROM muse_agent_slot_binding
|
||||
WHERE tenant_id=1 AND work_id=$1 AND slot_key=$2 AND deleted=false`,
|
||||
[Number(WORK_ID), SLOT_KEY]
|
||||
);
|
||||
if (r.rowCount === 0) return null;
|
||||
const row = r.rows[0];
|
||||
return { revision: Number(row.revision), status: String(row.status), agentId: Number(row.agent_id) };
|
||||
} finally {
|
||||
await c.end();
|
||||
}
|
||||
}
|
||||
|
||||
test('用户可经真实浏览器 UI 在真实后端替换作品开放槽位的智能体', async ({ page }) => {
|
||||
// 0. DB 起点核验:global-setup #15 已把 work1 writing.continuation 复位为 agent1、revision=1、active。
|
||||
// 先确认起点,否则 1→2 断言失去基准(若起点已漂移,后面递增断言会误判)。
|
||||
const before = await readSlotBinding();
|
||||
expect(before, '前置:global-setup #15 应已 seed work1 writing.continuation 槽位').not.toBeNull();
|
||||
expect(before!.revision, 'bind 前 revision 起点应为 1(global-setup 复位)').toBe(1);
|
||||
expect(before!.status).toBe('active');
|
||||
expect(before!.agentId).toBe(AGENT_DB_ID);
|
||||
|
||||
await seedToken(page);
|
||||
await page.goto('/agents');
|
||||
await expect(page.getByRole('heading', { name: '智能体工作台' })).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// 1. 显式选 work1(AgentPage 默认选 works[0],分页顺序未必稳定 → selectOption 锁定 value=work.id)。
|
||||
// 页面唯一 <select>(combobox)即「选择作品」下拉;option value=work.id、文本=title。
|
||||
const workSelect = page.getByRole('combobox');
|
||||
await expect(workSelect).toBeVisible({ timeout: 15_000 });
|
||||
await workSelect.selectOption(WORK_ID);
|
||||
|
||||
// 2. 选中 agent1:AgentList 卡片是 <button>,accessible name 含名称 + 版本(「活体测试智能体 ... v1」)。
|
||||
// 用 name 子串(含描述/状态等多行文本)定位,收窄到含 agent 名的那张卡片;exact:false。
|
||||
const agentCard = page.getByRole('button', { name: new RegExp(AGENT_NAME) });
|
||||
await expect(agentCard).toBeVisible({ timeout: 15_000 });
|
||||
await agentCard.click();
|
||||
|
||||
// 3. SlotBindingPanel 应列出 writing.continuation 槽位(revision 1),其绑定按钮文案「绑定选中智能体」。
|
||||
// 槽位区先等其渲染(读路径直连真后端,关 MSW)。本 work 仅一个开放槽位 → 按钮唯一,无 strict 命中歧义。
|
||||
await expect(page.getByText(`当前:${AGENT_NAME} v1`)).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByText('revision 1')).toBeVisible();
|
||||
|
||||
const bindButton = page.getByRole('button', { name: '绑定选中智能体' });
|
||||
await expect(bindButton).toBeEnabled({ timeout: 15_000 });
|
||||
|
||||
// 4. 等真实 bind 响应(precheck → bind 两跳,只断 bind 成功)。bind 返回 slotRevision=2。
|
||||
const bindResponsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/works/${WORK_ID}/agent-slots/${SLOT_KEY}/bind`) &&
|
||||
response.request().method() === 'POST'
|
||||
);
|
||||
await bindButton.click();
|
||||
|
||||
const bindResponse = await bindResponsePromise;
|
||||
const bindBody = await bindResponse.json();
|
||||
expect(bindBody.code, `bind 响应: ${JSON.stringify(bindBody)}`).toBe(0);
|
||||
expect(bindBody.data?.slotRevision, 'bind 应把 revision 推进到 2').toBe(2);
|
||||
|
||||
// 5. UI 成功提示出现(SlotBindingPanel 仅 bindSlot.isSuccess 才渲染此文案)。
|
||||
await expect(
|
||||
page.getByText('绑定已更新,后续作品生成会使用新的槽位智能体。')
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// 6. DB 核验(反假绿核心):revision 1→2、仍 active、仍 agent1,证 UI 操作真落库非 mock 假绿。
|
||||
const after = await readSlotBinding();
|
||||
expect(after, 'bind 后槽位绑定行应存在').not.toBeNull();
|
||||
expect(after!.revision, 'UI 绑定后 DB revision 应递增到 2').toBe(2);
|
||||
expect(after!.status).toBe('active');
|
||||
expect(after!.agentId).toBe(AGENT_DB_ID);
|
||||
});
|
||||
@ -310,7 +310,21 @@ async function globalSetup(): Promise<void> {
|
||||
// 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');
|
||||
|
||||
console.log(`[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal/binding/meta-schema/block/kb-status/installed-kb/handoff-bind/handoff-agent/handoff-content);清理 e2e 累积 agent ${agentCleanup.rowCount} 条`);
|
||||
// 15) agent-slot-bind(UI e2e):复位 work1 writing.continuation 槽位到固定起点(agent1、revision=1、active),
|
||||
// 供 AgentPage→SlotBindingPanel「替换已有槽位」UI e2e 每轮稳定可重复。WHY:bind 成功会 revision+1
|
||||
// (实测 2→3),不复位则每跑漂移,spec「revision 1→2」断言二次跑必红。物理删后重插(非 update)以释放
|
||||
// 软删行可能占用的唯一约束名额,并把 id/审计列归回干净态。绝不能用 work4(§13 handoff-agent 占用其
|
||||
// handoff-writer 槽位,且 work4 也有 writing.continuation 风险——本节只碰 work1)。agent1 DB id=1(user 类型、
|
||||
// owner=1、active version='1';跑前 `SELECT id FROM muse_agent WHERE tenant_id=1` 复核)。
|
||||
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)`
|
||||
);
|
||||
|
||||
console.log(`[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal/binding/meta-schema/block/kb-status/installed-kb/handoff-bind/handoff-agent/handoff-content/agent-slot-bind);清理 e2e 累积 agent ${agentCleanup.rowCount} 条`);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user