diff --git a/muse-studio/e2e/agent-slot-bind.spec.ts b/muse-studio/e2e/agent-slot-bind.spec.ts new file mode 100644 index 00000000..7015b2cf --- /dev/null +++ b/muse-studio/e2e/agent-slot-bind.spec.ts @@ -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 { + 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)。 + // 页面唯一