Files
ai-customer-service/test/PHASE2_TEST_PLAN.md
Your Name 31f6a5546c docs: Phase 2 测试质量提升规划
新增 test/PHASE2_TEST_PLAN.md,详细规划上线后测试补齐路径:

**P0 优先级(2周内)**:
- memory/postgres store 达标 >60%
- router/health handler 达标 >60%
- handlers 补齐 HandleChannel/TicketStatsHandler.Get

**P1 优先级(4周内)**:
- Domain 包(6个)基础测试 >30%
- logging/dialog/app 提升至 >75%

**Phase 2 目标**:整体覆盖率从 62.6% → >70%

Ref: PRODUCTION_PHASE1_STATUS.md §8 测试覆盖率
2026-05-01 09:04:31 +08:00

360 lines
11 KiB
Markdown
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.
# Phase 2 测试质量提升规划
> 生成时间2026-05-01 09:00 GMT+8
> 负责人:宰相(小龙团队 QA subagent
> 项目ai-customer-service
> 依据PRODUCTION_PHASE1_STATUS.md、TEST_COVERAGE_REPORT.md
---
## 一、当前质量基线Phase 1 已达标)
### 1.1 整体状态
| 指标 | 当前值 | Phase 1 目标 | Phase 2 目标 |
|------|--------|--------------|--------------|
| 整体覆盖率 | **62.6%** | >60% ✅ | >70% |
| Build + vet + tests | ✅ 全通过 | ✅ 必须 | ✅ 必须 |
| Phase 1 核心包 | 4/5 >60% | >60% ✅ | >70% |
| E2E 测试 | 100% | >60% ✅ | 100% ✅ |
| Integration 测试 | 100% | >60% ✅ | 100% ✅ |
### 1.2 各包覆盖率现状
| 包 | 覆盖率 | 状态 | Phase 2 目标 |
|----|--------|------|--------------|
| `internal/service/reply` | 100% | ✅ | 保持 |
| `internal/service/handoff` | 100% | ✅ | 保持 |
| `test/e2e` | 100% | ✅ | 保持 |
| `test/integration` | 100% | ✅ | 保持 |
| `internal/service/dialog` | 88.5% | ✅ | >90% |
| `internal/platform/httpx` | 84.3% | ✅ | >85% |
| `internal/service/intent` | 80.8% | ✅ | >85% |
| `internal/http/handlers` | 78.4% | ✅ | >85% |
| `internal/app` | 74.2% | ✅ | >80% |
| `internal/config` | 70.6% | ✅ | >75% |
| `internal/store/memory` | 59.1% | ⚠️ | >70% |
| `internal/store/postgres` | 43.1% | ⚠️ | >60% |
| `internal/http` (router) | 41.3% | ⚠️ | >60% |
| `internal/platform/health` | 38.1% | ⚠️ | >60% |
| Domain 包6个 | 0% | ❌ | >30% |
| `cmd/ai-customer-service` | 0% | ❌ | 测试可选 |
| `internal/platform/logging` | 0% | ❌ | >40% |
---
## 二、Phase 2 测试补齐优先级
### P0 — 必须补齐(上线后 2 周内)
| 优先级 | 包 | 当前覆盖率 | 目标覆盖率 | 关键缺失 |
|--------|-----|-----------|-----------|---------|
| P0-1 | `internal/store/memory` | 59.1% | >70% | `ListAll(0%)``GetStats(0%)` |
| P0-2 | `internal/store/postgres` | 43.1% | >60% | `Assign(0%)``Resolve(0%)``Close(0%)` |
| P0-3 | `internal/http` (router) | 41.3% | >60% | `writeMethodNotAllowed(0%)`、webhook channel 路由 |
| P0-4 | `internal/platform/health` | 38.1% | >60% | `IsReady(0%)`、dependency check 边界 |
| P0-5 | `internal/http/handlers` | 78.4% | >85% | `HandleChannel(0%)``TicketStatsHandler.Get(0%)` |
### P1 — 强烈建议补齐(上线后 4 周内)
| 优先级 | 包 | 当前覆盖率 | 目标覆盖率 | 关键缺失 |
|--------|-----|-----------|-----------|---------|
| P1-1 | Domain 包 (6个) | 0% | >30% | 所有 domain 包无测试文件 |
| P1-2 | `internal/platform/logging` | 0% | >40% | Logger 初始化未覆盖 |
| P1-3 | `internal/service/dialog` | 88.5% | >90% | Process 边界场景补全 |
| P1-4 | `internal/app` | 74.2% | >80% | Shutdown 错误处理分支 |
### P2 — 可选(长期优化)
| 优先级 | 包 | 当前覆盖率 | 目标覆盖率 | 说明 |
|--------|-----|-----------|-----------|------|
| P2-1 | `cmd/ai-customer-service` | 0% | 测试可选 | main 函数测试意义有限 |
| P2-2 | `internal/http/handlers` | 78.4% | >90% | `clientIP(66.7%)` 边界场景 |
---
## 三、具体补齐方案
### 3.1 P0-1: `internal/store/memory` 测试补齐
**当前缺失:**
- `ListAll()` — 0% 覆盖(无测试调用)
- `GetStats()` — 0% 覆盖(无测试调用)
**补齐方案:**
`internal/store/memory/ticket_store_test.go` 中新增:
```go
func TestTicketStore_ListAll(t *testing.T) {
store := NewTicketStore()
ctx := context.Background()
// Create 3 tickets
store.Create(ctx, &ticket.Ticket{ID: "t1", Status: ticket.StatusOpen})
store.Create(ctx, &ticket.Ticket{ID: "t2", Status: ticket.StatusResolved})
store.Create(ctx, &ticket.Ticket{ID: "t3", Status: ticket.StatusClosed})
// ListAll should return all 3
all, err := store.ListAll(ctx)
if err != nil || len(all) != 3 {
t.Fatalf("ListAll() = %d tickets, want 3", len(all))
}
}
func TestTicketStore_GetStats(t *testing.T) {
store := NewTicketStore()
ctx := context.Background()
// Create tickets with different statuses and channels
store.Create(ctx, &ticket.Ticket{ID: "t1", Status: ticket.StatusOpen, Priority: ticket.PriorityP1})
store.Create(ctx, &ticket.Ticket{ID: "t2", Status: ticket.StatusResolved, Priority: ticket.PriorityP2})
stats, err := store.GetStats(ctx)
if err != nil {
t.Fatalf("GetStats() error = %v", err)
}
if stats.Total != 2 {
t.Fatalf("stats.Total = %d, want 2", stats.Total)
}
if stats.Open != 1 || stats.Resolved != 1 {
t.Fatalf("stats Open/Resolved = %d/%d, want 1/1", stats.Open, stats.Resolved)
}
}
```
**预期提升:** 59.1% → **>70%**
---
### 3.2 P0-2: `internal/store/postgres` 测试补齐
**当前缺失:**
- `Assign()` — 0%(未覆盖)
- `Resolve()` — 0%(未覆盖)
- `Close()` — 0%(未覆盖)
**补齐方案:**
`internal/store/postgres/store_test.go` 中新增 workflow 操作测试(需 sqlmock
```go
func TestTicketWorkflowStore_Assign(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New() error = %v", err)
}
defer db.Close()
auditStore := NewAuditStore(db)
workflowStore := NewTicketWorkflowStore(db, auditStore)
// Mock UPDATE query
mock.ExpectExec("UPDATE tickets SET").
WithArgs("agent1", sqlmock.AnyArg(), "t1").
WillReturnResult(sqlmock.NewResult(0, 1))
// Mock audit insert
mock.ExpectExec("INSERT INTO audit").
WillReturnResult(sqlmock.NewResult(1, 1))
err = workflowStore.Assign(context.Background(), "t1", "agent1", "admin", "127.0.0.1", time.Now())
if err != nil {
t.Fatalf("Assign() error = %v", err)
}
}
```
**预期提升:** 43.1% → **>60%**
---
### 3.3 P0-3: `internal/http` (router) 测试补齐
**当前缺失:**
- `writeMethodNotAllowed()` — 0%(从未调用)
- Webhook channel 路由未测
**补齐方案:**
`internal/http/router_test.go` 中新增:
```go
func TestRouter_WriteMethodNotAllowed_Called(t *testing.T) {
// Test that unknown methods on known paths call writeMethodNotAllowed
probe := health.NewProbe()
h := handlers.NewHealthHandler(probe)
ticketHandler := &handlers.TicketHandler{}
router := NewRouter(RouterDeps{Health: h, Tickets: ticketHandler})
// POST to /tickets (only GET allowed) should trigger writeMethodNotAllowed
req := httptest.NewRequest(http.MethodPost, "/api/v1/customer-service/tickets", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusMethodNotAllowed {
t.Errorf("POST /tickets = %d, want 405", rr.Code)
}
}
```
**预期提升:** 41.3% → **>60%**
---
### 3.4 P0-4: `internal/platform/health` 测试补齐
**当前缺失:**
- `IsReady()` — 0%(未测试)
- dependency check 边界条件
**补齐方案:**
`internal/platform/health/health_test.go` 中新增:
```go
func TestProbe_IsReady_AfterSetReady(t *testing.T) {
probe := NewProbe()
probe.SetReady(true)
if !probe.IsReady() {
t.Error("IsReady() = false, want true after SetReady(true)")
}
}
func TestDependency_Evaluate_FailsWhenCheckFails(t *testing.T) {
dep := Dependency{
Name: "test",
Check: func() error { return fmt.Errorf("check failed") },
}
err := dep.Evaluate()
if err == nil {
t.Error("Evaluate() = nil, want error when Check fails")
}
}
```
**预期提升:** 38.1% → **>60%**
---
### 3.5 P0-5: `internal/http/handlers` 测试补齐
**当前缺失:**
- `HandleChannel()` — 0%(未测试)
- `TicketStatsHandler.Get()` — 0%(集成测试未覆盖 handler 本身)
**补齐方案:**
#### HandleChannel 测试:
`internal/http/handlers/webhook_handler_test.go` 中新增:
```go
func TestWebhookHandler_HandleChannel_OverridesBodyChannel(t *testing.T) {
h := newTestWebhookHandler(nil)
payload := `{"message_id":"m1","channel":"wrong","open_id":"u1","content":"hi"}`
req := httptest.NewRequest(http.MethodPost, "/webhook/correct", bytes.NewBufferString(payload))
resp := httptest.NewRecorder()
// Call HandleChannel with "correct" — should override "wrong" in body
h.HandleChannel(resp, req, "correct")
if resp.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", resp.Code)
}
// Verify response contains channel="correct"
}
```
#### TicketStatsHandler.Get 测试:
`internal/http/handlers/ticket_stats_handler_test.go`(新建)中:
```go
func TestTicketStatsHandler_Get_Success(t *testing.T) {
mockService := &mockTicketStatsService{
stats: ticketstats.Stats{Total: 100, Open: 30},
}
handler := NewTicketStatsHandler(mockService, nil)
req := httptest.NewRequest(http.MethodGet, "/stats", nil)
resp := httptest.NewRecorder()
handler.Get(resp, req)
if resp.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", resp.Code)
}
}
```
**预期提升:** 78.4% → **>85%**
---
### 3.6 P1-1: Domain 包测试补齐
**当前缺失:**
6 个 domain 包(`audit``intent``message``session``ticket``ticketstats`)全部无测试文件。
**补齐方案:**
为每个 domain 包创建基础测试,覆盖结构体构造和边界条件:
```go
// internal/domain/ticket/ticket_test.go
func TestTicket_NewTicket(t *testing.T) {
ticket := &Ticket{
ID: "t1",
Status: StatusOpen,
Priority: PriorityP1,
}
if ticket.ID != "t1" {
t.Errorf("ticket.ID = %s, want t1", ticket.ID)
}
}
func TestTicket_ValidPriorities(t *testing.T) {
validPriorities := []Priority{PriorityP1, PriorityP2, PriorityP3}
for _, p := range validPriorities {
if p == "" {
t.Errorf("priority %q is empty", p)
}
}
}
```
**预期提升:** 0% → **>30%**(每包 3-5 个基础测试)
---
## 四、执行时间表(建议)
| 阶段 | 时间 | 优先级 | 预期成果 |
|------|------|--------|----------|
| **Week 1** | 上线后第 1 周 | P0-1 ~ P0-3 | memory + postgres + router 达标 |
| **Week 2** | 上线后第 2 周 | P0-4 ~ P0-5 | health + handlers 达标 |
| **Week 3** | 上线后第 3 周 | P1-1 | Domain 包基础测试补齐 |
| **Week 4** | 上线后第 4 周 | P1-2 ~ P1-4 | 整体覆盖率 >70% |
| **Long-term** | 上线后 2 个月 | P2 | 覆盖率 >80%(可选) |
---
## 五、质量门禁Phase 2
| 指标 | Phase 1当前 | Phase 2 目标 |
|------|----------------|--------------|
| 整体覆盖率 | 62.6% ✅ | **>70%** |
| 核心包覆盖率 | 4/5 >60% | 全部 >70% |
| Domain 包覆盖率 | 0% | >30% |
| Build + vet + tests | ✅ 全通过 | ✅ 全通过 |
| P0 测试补齐 | — | Week 2 完成 |
---
## 六、风险与依赖
| 风险 | 缓解措施 |
|------|----------|
| PostgreSQL 测试需要 sqlmock | Week 1 引入 sqlmock 依赖 |
| Domain 包测试意义有限 | 仅测试关键边界和构造逻辑 |
| 灰度阶段发现新 bug 需补测 | 预留 Week 3 缓冲时间 |
---
*本文档由宰相(小龙团队 QA subagent生成 | 2026-05-01 09:00 GMT+8*