diff --git a/docs/development/ai-platform-demo-acceptance.md b/docs/development/ai-platform-demo-acceptance.md index 4b265a4..1b6ab97 100644 --- a/docs/development/ai-platform-demo-acceptance.md +++ b/docs/development/ai-platform-demo-acceptance.md @@ -8,7 +8,7 @@ |---|---|---| | 后端自动化 | 已通过 | 71 项 pytest;Ruff、迁移漂移与 Django system check 通过 | | 全新数据库迁移 | 已通过 | 测试配置下从空库应用全部迁移 | -| 前端自动化 | 已通过 | 11 项 Vitest;ESLint、类型检查与生产构建通过 | +| 前端自动化 | 已通过 | 15 项 Vitest;覆盖异步轮询、失败态、发布、运行详情和取消;ESLint、类型检查与生产构建通过 | | 真实模型三步运行 | 待执行 | 凭证仅允许管理员在本地录入,不进入仓库 | 真实模型三步运行通过前,本演示版只能描述为“实现完成、真实渠道验收待完成”。 diff --git a/docs/product/capability-status.md b/docs/product/capability-status.md index 2a83f7e..61a4e8f 100644 --- a/docs/product/capability-status.md +++ b/docs/product/capability-status.md @@ -34,7 +34,7 @@ ## 当前质量基线 - 后端测试:71 项通过;Ruff、迁移检查、Django system check 和全新数据库迁移通过。 -- 前端测试:11 项通过;ESLint、TypeScript 类型检查和生产构建通过。 +- 前端测试:15 项通过;ESLint、TypeScript 类型检查和生产构建通过。 - 前端生产入口包约 626 KB,存在 Vite 体积告警;后续按路由懒加载拆包。 - 真实模型渠道的三步端到端脚本尚未执行,因为凭证必须由管理员在本地提供;在该步骤通过前,不声明演示里程碑验收完成。 diff --git a/frontend/src/features/ai-platform/ai-platform-pages.test.tsx b/frontend/src/features/ai-platform/ai-platform-pages.test.tsx new file mode 100644 index 0000000..01fe907 --- /dev/null +++ b/frontend/src/features/ai-platform/ai-platform-pages.test.tsx @@ -0,0 +1,146 @@ +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { AgentRunDetailPage } from '@/features/agents/AgentRunDetailPage' +import { ModelProviderPage } from '@/features/modelhub/ModelProviderPage' +import { SkillDetailPage } from '@/features/skills/SkillDetailPage' + +const mocks = vi.hoisted(() => ({ + listProviders: vi.fn(), + listDeployments: vi.fn(), + testDeployment: vi.fn(), + getModelTest: vi.fn(), + getSkill: vi.fn(), + publishSkill: vi.fn(), + getAgentRun: vi.fn(), + cancelAgentRun: vi.fn() +})) + +vi.mock('@/features/modelhub/api', () => ({ + listProviders: mocks.listProviders, + listDeployments: mocks.listDeployments, + testDeployment: mocks.testDeployment, + getModelTest: mocks.getModelTest, + createProvider: vi.fn(), + updateProvider: vi.fn(), + createDeployment: vi.fn(), + updateDeployment: vi.fn() +})) + +vi.mock('@/features/skills/api', () => ({ + getSkill: mocks.getSkill, + publishSkill: mocks.publishSkill, + ensureSkillDraft: vi.fn(), + updateSkill: vi.fn(), + updateSkillDraft: vi.fn(), + rollbackSkill: vi.fn() +})) + +vi.mock('@/features/agents/api', () => ({ + getAgentRun: mocks.getAgentRun, + cancelAgentRun: mocks.cancelAgentRun +})) + +const provider = { + id: 1, + name: '企业渠道', + base_url: 'https://models.example.com/v1', + enabled: true, + default_timeout_seconds: 60, + credential_set: true, + credential_masked: '****1234', + created_at: '2026-07-14T00:00:00Z', + updated_at: '2026-07-14T00:00:00Z' +} + +const deployment = { + id: 2, + provider_id: 1, + provider_name: '企业渠道', + display_name: '支持模型', + model_name: 'support-model', + capability: 'text', + default_temperature: 0.2, + max_output_tokens: 2048, + enabled: true +} + +const queuedTest = { + public_id: 'test-uuid', deployment_id: 2, status: 'queued', model_snapshot: {}, output_text: '', input_tokens: null, output_tokens: null, duration_ms: 0, retry_count: 0, error_code: '', error_message: '', created_at: '2026-07-14T00:00:00Z', started_at: null, finished_at: null +} + +const run = { + public_id: '11111111-1111-1111-1111-111111111111', agent_id: 1, agent_name: '技术支持分析 Agent', agent_version: 1, status: 'running', sandbox: true, current_step: 'structure', total_steps: 1, input_tokens: 3, output_tokens: 2, duration_ms: 120, error_code: '', error_message: '', created_at: '2026-07-14T00:00:00Z', started_at: '2026-07-14T00:00:00Z', finished_at: null, input_payload: { problem: '无法登录' }, final_output: null, config_snapshot: { agent: { version: 1 } }, cancellation_requested_at: null, + steps: [{ id: 10, code: 'structure', name: '问题结构化', order: 1, status: 'running', input_payload: { problem: '无法登录' }, output_payload: null, input_tokens: 3, output_tokens: 2, duration_ms: 120, error_code: '', error_message: '', attempts: [] }] +} + +afterEach(() => { + cleanup() + vi.clearAllMocks() +}) + +beforeEach(() => { + window.matchMedia = vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + dispatchEvent: vi.fn() + })) +}) + +describe('AI 平台真实页面', () => { + it('展示脱敏渠道并轮询异步模型测试', async () => { + mocks.listProviders.mockResolvedValue([provider]) + mocks.listDeployments.mockResolvedValue([deployment]) + mocks.testDeployment.mockResolvedValue(queuedTest) + mocks.getModelTest.mockResolvedValue({ ...queuedTest, status: 'succeeded', output_text: '模型连接正常', input_tokens: 8, output_tokens: 4, duration_ms: 320, finished_at: '2026-07-14T00:00:01Z' }) + + render() + expect(await screen.findByText('****1234')).toBeInTheDocument() + fireEvent.click(screen.getByRole('button', { name: '连通测试' })) + fireEvent.click(screen.getByRole('button', { name: '开始测试' })) + + await waitFor(() => expect(mocks.getModelTest).toHaveBeenCalledWith('test-uuid'), { timeout: 2500 }) + expect(await screen.findByText('模型连接正常')).toBeInTheDocument() + }) + + it('展示列表失败态', async () => { + mocks.listProviders.mockRejectedValue(new Error('后端不可用')) + mocks.listDeployments.mockResolvedValue([]) + + render() + + expect(await screen.findByText('无法加载模型配置')).toBeInTheDocument() + expect(screen.getByText('后端不可用')).toBeInTheDocument() + }) + + it('发布 Skill 草稿版本', async () => { + mocks.getSkill.mockResolvedValue({ + id: 1, code: 'structure', name: '问题结构化', description: '', active_version: null, draft_version: 1, updated_at: '2026-07-14T00:00:00Z', + versions: [{ id: 11, version: 1, status: 'draft', prompt_template: '{{ input.problem }}', input_schema: { type: 'object' }, output_schema: { type: 'object' }, example_input: {}, created_at: '2026-07-14T00:00:00Z', updated_at: '2026-07-14T00:00:00Z', published_at: null }] + }) + mocks.publishSkill.mockResolvedValue({ id: 11, version: 1, status: 'published' }) + + render() + fireEvent.click(await screen.findByRole('button', { name: '发布 v1' })) + + await waitFor(() => expect(mocks.publishSkill).toHaveBeenCalledWith(1)) + }) + + it('展示运行执行脊柱并提交尽力取消', async () => { + mocks.getAgentRun.mockResolvedValue(run) + mocks.cancelAgentRun.mockResolvedValue({ ...run, status: 'cancelled', cancellation_requested_at: '2026-07-14T00:00:01Z' }) + + render() + expect(await screen.findByText('执行脊柱')).toBeInTheDocument() + expect(screen.getByText('问题结构化')).toBeInTheDocument() + fireEvent.click(screen.getByRole('button', { name: '尽力取消' })) + + await waitFor(() => expect(mocks.cancelAgentRun).toHaveBeenCalledWith(run.public_id)) + expect(await screen.findByText('已取消')).toBeInTheDocument() + }) +})