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()
+ })
+})