Some checks failed
Backend Maven CI / backend-local (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
183 lines
8.1 KiB
TypeScript
183 lines
8.1 KiB
TypeScript
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');
|
||
});
|