Files
awoooi/apps/web/tests/e2e/terminal.spec.ts
OG T 22de22c989 refactor(phase-s): Phase S 技術債清理 - 五項架構改善
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>
2026-04-01 13:12:02 +08:00

246 lines
8.3 KiB
TypeScript
Raw 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.
/**
* 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)
})
})