oh-my-muse/muse-studio/e2e/agent-create.spec.ts
lili 3585219637
Some checks failed
Backend Maven CI / backend-local (push) Has been cancelled
feat(mvp): 收束1.0.0线A交付闭环
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:52:10 -07:00

183 lines
8.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test, type Page } from '@playwright/test';
import { Client } from 'pg';
/**
* 智能体生命周期 e2e真实后端MSW off
*
* 覆盖“新建 → 初始版本落库 → 编辑生成新版本 → 切回旧版本 → 归档非当前版本 → 归档智能体”。
* 刻意不点击“运行试用”,避免把 New-API 外部依赖带入本切片AI 生成链由 accept-suggestion.spec 覆盖。
*/
const TOKEN = 'test1';
interface AgentVersionDbState {
status: string;
promptTemplate: string | null;
changeNote: string | null;
}
interface AgentDbState {
status: string;
currentVersion: string | null;
versions: Record<string, AgentVersionDbState>;
}
async function seedToken(page: Page): Promise<void> {
await page.addInitScript((t) => {
window.localStorage.setItem('accessToken', t);
window.localStorage.setItem('tenantId', '1');
}, TOKEN);
}
/** 直连真实 PG 读 Agent 与版本状态,证明 UI 命令不是被 MSW 或本地状态伪装成成功。 */
async function readAgentDbState(agentId: string): Promise<AgentDbState> {
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,
connectionTimeoutMillis: 10_000,
});
await c.connect();
try {
const agent = await c.query(
`SELECT a.status, cv.version AS current_version
FROM muse_agent a
LEFT JOIN muse_agent_version cv ON cv.tenant_id=a.tenant_id AND cv.id=a.current_version_id
WHERE a.tenant_id=1 AND a.id=$1 AND a.deleted=false`,
[Number(agentId)]
);
if (agent.rowCount === 0) {
throw new Error(`agent id=${agentId} 不存在`);
}
const versions = await c.query(
`SELECT version, status, change_note, config->>'promptTemplate' AS prompt_template
FROM muse_agent_version
WHERE tenant_id=1 AND agent_id=$1 AND deleted=false
ORDER BY id`,
[Number(agentId)]
);
const versionMap: Record<string, AgentVersionDbState> = {};
for (const row of versions.rows) {
versionMap[String(row.version)] = {
status: String(row.status),
promptTemplate: row.prompt_template == null ? null : String(row.prompt_template),
changeNote: row.change_note == null ? null : String(row.change_note),
};
}
return {
status: String(agent.rows[0].status),
currentVersion: agent.rows[0].current_version == null ? null : String(agent.rows[0].current_version),
versions: versionMap,
};
} finally {
await c.end();
}
}
test('用户可经真实后端管理自建智能体完整生命周期', async ({ page }) => {
await seedToken(page);
const uniqueName = `活体智能体-e2e-${Date.now()}`;
const updatedName = `${uniqueName}-已编辑`;
const updatedPrompt = '只做结构化润色建议,输出必须包含问题、建议、风险三段。';
await page.goto('/agents');
await expect(page.getByRole('heading', { name: '智能体工作台' })).toBeVisible({ timeout: 15_000 });
await page.getByRole('button', { name: '新建智能体' }).click();
await page.getByPlaceholder('例如:角色口吻校对员').fill(uniqueName);
await page.getByPlaceholder('说明智能体适合的写作场景').fill('Playwright 活体验收创建的自建智能体');
await page.getByPlaceholder('写下能力目标、输入要求、输出格式和禁止越权范围')
.fill('只做结构化润色建议,不写入作品事实。');
const createResponsePromise = page.waitForResponse((response) =>
response.url().includes('/app-api/muse/agents') &&
response.request().method() === 'POST' &&
response.status() === 201
);
await page.getByRole('button', { name: '创建智能体' }).click();
const createResponse = await createResponsePromise;
const createBody = await createResponse.json();
expect(createBody.code).toBe(0);
expect(createBody.data?.agentId).toBeTruthy();
const agentId = String(createBody.data.agentId);
await expect(page.getByRole('button', { name: '关闭创建' })).toBeHidden({ timeout: 15_000 });
await expect(page.getByRole('button', { name: new RegExp(`${uniqueName} v1`) })).toBeVisible({ timeout: 15_000 });
await expect(page.getByText('我创建的').first()).toBeVisible();
const createdDb = await readAgentDbState(agentId);
expect(createdDb.status).toBe('active');
expect(createdDb.currentVersion).toBe('1');
expect(createdDb.versions['1']?.status).toBe('active');
const createdCard = page.locator('div.rounded-2xl').filter({ hasText: uniqueName }).first();
await createdCard.getByRole('button', { name: '编辑' }).click();
await page.getByLabel('名称').fill(updatedName);
await page.getByLabel('Prompt 模板').fill(updatedPrompt);
const updateResponsePromise = page.waitForResponse((response) =>
response.url().includes(`/app-api/muse/agents/${agentId}`) &&
response.request().method() === 'PUT'
);
await page.getByRole('button', { name: '保存并创建新版本' }).click();
const updateResponse = await updateResponsePromise;
const updateBody = await updateResponse.json();
expect(updateBody.code, `更新智能体响应: ${JSON.stringify(updateBody)}`).toBe(0);
expect(updateBody.data?.activeVersion).toBe(2);
await expect(page.getByRole('button', { name: new RegExp(`${updatedName} v2`) })).toBeVisible({ timeout: 15_000 });
const updatedDb = await readAgentDbState(agentId);
expect(updatedDb.currentVersion).toBe('2');
expect(updatedDb.versions['2']?.status).toBe('active');
expect(updatedDb.versions['2']?.promptTemplate).toBe(updatedPrompt);
const versionPanel = page.locator('div.rounded-2xl').filter({ hasText: '版本' }).filter({ hasText: updatedName }).first();
const v1Row = versionPanel.locator('div.rounded-xl').filter({ hasText: 'v1' }).first();
const activateV1ResponsePromise = page.waitForResponse((response) =>
response.url().includes(`/app-api/muse/agents/${agentId}/versions/1/activate`) &&
response.request().method() === 'POST'
);
await v1Row.getByRole('button', { name: '激活' }).click();
const activateV1Response = await activateV1ResponsePromise;
const activateV1Body = await activateV1Response.json();
expect(activateV1Body.code, `激活 v1 响应: ${JSON.stringify(activateV1Body)}`).toBe(0);
expect(activateV1Body.data?.version).toBe(1);
await expect(page.getByRole('button', { name: new RegExp(`${updatedName} v1`) })).toBeVisible({ timeout: 15_000 });
const activatedDb = await readAgentDbState(agentId);
expect(activatedDb.currentVersion).toBe('1');
const v2Row = versionPanel.locator('div.rounded-xl').filter({ hasText: 'v2' }).first();
const archiveV2ResponsePromise = page.waitForResponse((response) =>
response.url().includes(`/app-api/muse/agents/${agentId}/versions/2/archive`) &&
response.request().method() === 'POST'
);
await v2Row.getByRole('button', { name: '归档' }).click();
const archiveV2Response = await archiveV2ResponsePromise;
const archiveV2Body = await archiveV2Response.json();
expect(archiveV2Body.code, `归档 v2 响应: ${JSON.stringify(archiveV2Body)}`).toBe(0);
expect(archiveV2Body.data?.version).toBe(2);
const versionArchivedDb = await readAgentDbState(agentId);
expect(versionArchivedDb.currentVersion).toBe('1');
expect(versionArchivedDb.versions['2']?.status).toBe('archived');
const activeCard = page.locator('div.rounded-2xl').filter({ hasText: updatedName }).first();
const archiveAgentResponsePromise = page.waitForResponse((response) =>
response.url().includes(`/app-api/muse/agents/${agentId}/archive`) &&
response.request().method() === 'POST'
);
await activeCard.getByRole('button', { name: '归档' }).click();
const archiveAgentResponse = await archiveAgentResponsePromise;
const archiveAgentBody = await archiveAgentResponse.json();
expect(archiveAgentBody.code, `归档 Agent 响应: ${JSON.stringify(archiveAgentBody)}`).toBe(0);
const archivedAgentDb = await readAgentDbState(agentId);
expect(archivedAgentDb.status).toBe('archived');
});