S-01: generate_alert_fingerprint() 移至 alert_analyzer_service (Router→Service) S-02: 移除廢棄 USE_NEW_ENGINE config (Phase R 已完成歷史使命) S-03: github_webhook.py linter 清理 (Field unused + delivery_id noqa) S-04: Pydantic v2 遷移 - approval/incident models (class Config → ConfigDict) S-05: Skill 09 v1.1 更新 (USE_NEW_ENGINE 廢棄說明) 測試: 393 passed, 零失敗 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
246 lines
8.3 KiB
TypeScript
246 lines
8.3 KiB
TypeScript
/**
|
||
* Terminal E2E Tests
|
||
* ==================
|
||
* Phase 19.6: Omni-Terminal Playwright E2E 測試
|
||
*
|
||
* 測試範圍 (補充 phase19-production-verification.spec.ts 已涵蓋的 UI 測試):
|
||
* 1. Terminal API 端點驗證 (POST /intent, GET /status, POST /abort, GET /stream)
|
||
* 2. Pydantic 驗證錯誤 (422)
|
||
* 3. POST intent → GET status 完整 Session 流程
|
||
* 4. Abort Session 流程
|
||
* 5. 404 錯誤案例 (非存在的 session)
|
||
*
|
||
* 注意: UI 互動測試 (鍵盤快捷鍵、Z-index、i18n) 已在
|
||
* phase19-production-verification.spec.ts tests 13-18 中涵蓋。
|
||
*
|
||
* @see ADR-031 Omni-Terminal SSE Architecture
|
||
* @see ADR-032 GenUI Dynamic Rendering
|
||
* @author Claude Code (首席架構師)
|
||
* @version 1.0.0
|
||
* @date 2026-03-31 (台北時間)
|
||
*/
|
||
|
||
import { test, expect } from '@playwright/test'
|
||
|
||
const BASE_URL = 'https://awoooi.wooo.work'
|
||
const API_URL = `${BASE_URL}/api/v1/terminal`
|
||
|
||
test.setTimeout(30000)
|
||
|
||
// =============================================================================
|
||
// POST /terminal/intent — 提交意圖
|
||
// =============================================================================
|
||
|
||
test.describe('POST /terminal/intent', () => {
|
||
test('合法請求返回 200 + session_id + stream_url', async ({ request }) => {
|
||
const resp = await request.post(`${API_URL}/intent`, {
|
||
data: {
|
||
intent: 'check system status',
|
||
context: { current_page: '/zh-TW' },
|
||
},
|
||
})
|
||
expect(resp.status()).toBe(200)
|
||
const data = await resp.json()
|
||
expect(data).toHaveProperty('session_id')
|
||
expect(data).toHaveProperty('stream_url')
|
||
expect(data).toHaveProperty('created_at')
|
||
expect(data.stream_url).toContain(data.session_id)
|
||
})
|
||
|
||
test('stream_url 格式為 /api/v1/terminal/stream/{session_id}', async ({ request }) => {
|
||
const resp = await request.post(`${API_URL}/intent`, {
|
||
data: {
|
||
intent: 'show pending approvals',
|
||
context: { current_page: '/zh-TW/authorizations' },
|
||
},
|
||
})
|
||
const data = await resp.json()
|
||
const expected = `/api/v1/terminal/stream/${data.session_id}`
|
||
expect(data.stream_url).toBe(expected)
|
||
})
|
||
|
||
test('缺少 intent 欄位返回 422', async ({ request }) => {
|
||
const resp = await request.post(`${API_URL}/intent`, {
|
||
data: { context: { current_page: '/' } },
|
||
})
|
||
expect(resp.status()).toBe(422)
|
||
})
|
||
|
||
test('缺少 context 欄位返回 422', async ({ request }) => {
|
||
const resp = await request.post(`${API_URL}/intent`, {
|
||
data: { intent: 'check status' },
|
||
})
|
||
expect(resp.status()).toBe(422)
|
||
})
|
||
|
||
test('空 intent 字串返回 422 (min_length=1)', async ({ request }) => {
|
||
const resp = await request.post(`${API_URL}/intent`, {
|
||
data: { intent: '', context: { current_page: '/' } },
|
||
})
|
||
expect(resp.status()).toBe(422)
|
||
})
|
||
|
||
test('帶 focused_entity_id 的 SpatialContext 請求成功', async ({ request }) => {
|
||
const resp = await request.post(`${API_URL}/intent`, {
|
||
data: {
|
||
intent: 'analyze this incident',
|
||
context: {
|
||
current_page: '/zh-TW/incidents',
|
||
focused_entity_id: 'INC-2026-0001',
|
||
},
|
||
},
|
||
})
|
||
expect(resp.status()).toBe(200)
|
||
const data = await resp.json()
|
||
expect(data.session_id).toBeTruthy()
|
||
})
|
||
})
|
||
|
||
// =============================================================================
|
||
// GET /terminal/status/{session_id} — 查詢狀態
|
||
// =============================================================================
|
||
|
||
test.describe('GET /terminal/status/{session_id}', () => {
|
||
test('非存在的 session_id 返回 404', async ({ request }) => {
|
||
const resp = await request.get(`${API_URL}/status/nonexistent-session-id-xyz`)
|
||
expect(resp.status()).toBe(404)
|
||
})
|
||
|
||
test('POST intent → GET status 完整 Session 流程', async ({ request }) => {
|
||
// 1. 提交意圖
|
||
const submit = await request.post(`${API_URL}/intent`, {
|
||
data: {
|
||
intent: 'list k8s pods',
|
||
context: { current_page: '/zh-TW' },
|
||
},
|
||
})
|
||
expect(submit.status()).toBe(200)
|
||
const { session_id } = await submit.json()
|
||
|
||
// 2. 查詢狀態
|
||
const status = await request.get(`${API_URL}/status/${session_id}`)
|
||
expect(status.status()).toBe(200)
|
||
|
||
const data = await status.json()
|
||
expect(data.session_id).toBe(session_id)
|
||
expect(['pending', 'processing', 'completed', 'aborted', 'error']).toContain(data.status)
|
||
expect(typeof data.last_event_id).toBe('number')
|
||
expect(typeof data.message_count).toBe('number')
|
||
})
|
||
|
||
test('Status 回應包含所有必要欄位', async ({ request }) => {
|
||
const submit = await request.post(`${API_URL}/intent`, {
|
||
data: {
|
||
intent: 'check metrics',
|
||
context: { current_page: '/zh-TW' },
|
||
},
|
||
})
|
||
const { session_id } = await submit.json()
|
||
|
||
const status = await request.get(`${API_URL}/status/${session_id}`)
|
||
const data = await status.json()
|
||
|
||
const requiredFields = ['session_id', 'status', 'created_at', 'last_event_id', 'message_count']
|
||
requiredFields.forEach(field => {
|
||
expect(data).toHaveProperty(field)
|
||
})
|
||
})
|
||
})
|
||
|
||
// =============================================================================
|
||
// POST /terminal/abort/{session_id} — 中斷執行
|
||
// =============================================================================
|
||
|
||
test.describe('POST /terminal/abort/{session_id}', () => {
|
||
test('非存在的 session_id 返回 404', async ({ request }) => {
|
||
const resp = await request.post(`${API_URL}/abort/nonexistent-session-xyz`)
|
||
expect(resp.status()).toBe(404)
|
||
})
|
||
|
||
test('POST intent → POST abort 中斷流程', async ({ request }) => {
|
||
// 1. 提交意圖
|
||
const submit = await request.post(`${API_URL}/intent`, {
|
||
data: {
|
||
intent: 'rca analysis',
|
||
context: { current_page: '/zh-TW' },
|
||
},
|
||
})
|
||
expect(submit.status()).toBe(200)
|
||
const { session_id } = await submit.json()
|
||
|
||
// 2. 中斷
|
||
const abort = await request.post(`${API_URL}/abort/${session_id}`)
|
||
expect(abort.status()).toBe(200)
|
||
|
||
const data = await abort.json()
|
||
expect(data.session_id).toBe(session_id)
|
||
expect(data.aborted).toBe(true)
|
||
expect(data.message).toBeTruthy()
|
||
})
|
||
|
||
test('帶理由的中斷請求成功,message 含理由', async ({ request }) => {
|
||
const submit = await request.post(`${API_URL}/intent`, {
|
||
data: {
|
||
intent: 'restart pod',
|
||
context: { current_page: '/zh-TW' },
|
||
},
|
||
})
|
||
const { session_id } = await submit.json()
|
||
|
||
const abort = await request.post(`${API_URL}/abort/${session_id}`, {
|
||
data: { reason: 'user cancelled' },
|
||
})
|
||
expect(abort.status()).toBe(200)
|
||
const data = await abort.json()
|
||
expect(data.message).toContain('user cancelled')
|
||
})
|
||
|
||
test('中斷後 GET /status 返回 aborted 狀態', async ({ request }) => {
|
||
const submit = await request.post(`${API_URL}/intent`, {
|
||
data: {
|
||
intent: 'scale deployment',
|
||
context: { current_page: '/zh-TW' },
|
||
},
|
||
})
|
||
const { session_id } = await submit.json()
|
||
|
||
await request.post(`${API_URL}/abort/${session_id}`)
|
||
|
||
const status = await request.get(`${API_URL}/status/${session_id}`)
|
||
expect(status.status()).toBe(200)
|
||
expect((await status.json()).status).toBe('aborted')
|
||
})
|
||
})
|
||
|
||
// =============================================================================
|
||
// GET /terminal/stream/{session_id} — SSE 串流
|
||
// =============================================================================
|
||
|
||
test.describe('GET /terminal/stream/{session_id}', () => {
|
||
test('非存在的 session_id 返回 404', async ({ request }) => {
|
||
const resp = await request.get(`${API_URL}/stream/nonexistent-stream-session`)
|
||
expect(resp.status()).toBe(404)
|
||
})
|
||
})
|
||
|
||
// =============================================================================
|
||
// Session 續傳 (Resume)
|
||
// =============================================================================
|
||
|
||
test.describe('Session 續傳', () => {
|
||
test('指定 session_id 的請求復用相同 session', async ({ request }) => {
|
||
const specificId = `resume-test-${Date.now()}`
|
||
|
||
const resp = await request.post(`${API_URL}/intent`, {
|
||
data: {
|
||
intent: 'continue analysis',
|
||
context: { current_page: '/zh-TW' },
|
||
session_id: specificId,
|
||
},
|
||
})
|
||
expect(resp.status()).toBe(200)
|
||
const data = await resp.json()
|
||
expect(data.session_id).toBe(specificId)
|
||
})
|
||
})
|