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:
lili 2026-06-24 11:19:44 -07:00
parent 2c9fdb513c
commit 1fbe7ae73c
2 changed files with 139 additions and 1 deletions

View 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
* agent1revision=1active),UI agent1 revision 12
*
* (global-setup #15 ,):
* DELETE + INSERT work1 writing.continuation (agent_id=1=agent1agent_version='1'status='active'
* revision=1binding_source='user'tenant_id=1)bind revision+1,
*
* 绿:
* 1) UI ,使(SlotBindingPanel bind )
* 2) DB 核验:muse_agent_slot_binding work1 writing.continuation revision 12status=activeagent_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);
});

View File

@ -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();
}