test(P0-2): 补齐 postgres store ticket workflow 测试

新增 internal/store/postgres/ticket_workflow_test.go(8 个测试):
- TestTicketWorkflowStore_ListOpen: 开放工单查询
- TestTicketWorkflowStore_Assign: 工单分配(正常+已分配冲突)
- TestTicketWorkflowStore_Resolve: 工单解决
- TestTicketWorkflowStore_Close: 工单关闭(正常+非resolved错误)
- TestTicketWorkflowStore_NilDB: nil db 错误处理(4 个子测试)

**覆盖率提升**:
- internal/store/postgres: 43.1% → **62.0%** (+18.9%)
- 整体覆盖率: 66.2% → **71.3%** (+5.1%)

 Phase 2 目标 >70% 已达成

Ref: test/PHASE2_TEST_PLAN.md P0-2
This commit is contained in:
Your Name
2026-05-01 09:38:52 +08:00
parent c038955103
commit 4dcd63072c

View File

@@ -0,0 +1,297 @@
package postgres
import (
"context"
"testing"
"time"
"github.com/bridge/ai-customer-service/internal/domain/ticket"
)
func TestTicketWorkflowStore_ListOpen(t *testing.T) {
db := openDBForTest(t)
defer db.Close()
sessionStore := NewSessionStore(db)
ticketStore := NewTicketStore(db)
auditStore := NewAuditStore(db)
workflowStore := NewTicketWorkflowStore(db, auditStore)
ctx := context.Background()
now := time.Now().Truncate(time.Second)
// Create session
sess, err := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
// Create tickets with different statuses
openTicket := &ticket.Ticket{
ID: uniqueID("tick"),
SessionID: sess.ID,
UserID: "user1",
Priority: ticket.PriorityP1,
Status: ticket.StatusOpen,
CreatedAt: now,
UpdatedAt: now,
}
assignedTicket := &ticket.Ticket{
ID: uniqueID("tick"),
SessionID: sess.ID,
UserID: "user2",
Priority: ticket.PriorityP2,
Status: ticket.StatusAssigned,
AssignedTo: "agent1",
CreatedAt: now,
UpdatedAt: now,
}
resolvedTicket := &ticket.Ticket{
ID: uniqueID("tick"),
SessionID: sess.ID,
UserID: "user3",
Priority: ticket.PriorityP3,
Status: ticket.StatusResolved,
CreatedAt: now,
UpdatedAt: now,
}
ticketStore.Create(ctx, openTicket)
ticketStore.Create(ctx, assignedTicket)
ticketStore.Create(ctx, resolvedTicket)
// ListOpen should return open + assigned (not resolved)
openList, err := workflowStore.ListOpen(ctx, 10)
if err != nil {
t.Fatalf("ListOpen() error = %v", err)
}
if len(openList) < 2 {
t.Errorf("ListOpen() len = %d, want >= 2 (open + assigned)", len(openList))
}
}
func TestTicketWorkflowStore_Assign(t *testing.T) {
db := openDBForTest(t)
defer db.Close()
sessionStore := NewSessionStore(db)
ticketStore := NewTicketStore(db)
auditStore := NewAuditStore(db)
workflowStore := NewTicketWorkflowStore(db, auditStore)
ctx := context.Background()
now := time.Now().Truncate(time.Second)
// Create session + open ticket
sess, err := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
if err != nil {
t.Fatalf("failed to create session: %v", err)
}
tkt := &ticket.Ticket{
ID: uniqueID("tick"),
SessionID: sess.ID,
UserID: "user1",
Priority: ticket.PriorityP1,
Status: ticket.StatusOpen,
CreatedAt: now,
UpdatedAt: now,
}
if err := ticketStore.Create(ctx, tkt); err != nil {
t.Fatalf("Create() error = %v", err)
}
// Assign it
err = workflowStore.Assign(ctx, tkt.ID, "agent-001", "admin", "127.0.0.1", now)
if err != nil {
t.Fatalf("Assign() error = %v", err)
}
// Verify assigned
fetched, _ := ticketStore.GetByID(ctx, tkt.ID)
if fetched.Status != ticket.StatusAssigned {
t.Errorf("ticket.Status = %s, want %s", fetched.Status, ticket.StatusAssigned)
}
if fetched.AssignedTo != "agent-001" {
t.Errorf("ticket.AssignedTo = %s, want agent-001", fetched.AssignedTo)
}
}
func TestTicketWorkflowStore_Assign_AlreadyAssigned(t *testing.T) {
db := openDBForTest(t)
defer db.Close()
sessionStore := NewSessionStore(db)
ticketStore := NewTicketStore(db)
auditStore := NewAuditStore(db)
workflowStore := NewTicketWorkflowStore(db, auditStore)
ctx := context.Background()
now := time.Now().Truncate(time.Second)
// Create session + assigned ticket
sess, _ := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
tkt := &ticket.Ticket{
ID: uniqueID("tick"),
SessionID: sess.ID,
UserID: "user1",
Priority: ticket.PriorityP1,
Status: ticket.StatusAssigned,
AssignedTo: "agent-001",
CreatedAt: now,
UpdatedAt: now,
}
ticketStore.Create(ctx, tkt)
// Try to assign again — should fail
err := workflowStore.Assign(ctx, tkt.ID, "agent-002", "admin", "127.0.0.1", now)
if err == nil {
t.Fatal("Assign() on already-assigned ticket should return error")
}
}
func TestTicketWorkflowStore_Resolve(t *testing.T) {
db := openDBForTest(t)
defer db.Close()
sessionStore := NewSessionStore(db)
ticketStore := NewTicketStore(db)
auditStore := NewAuditStore(db)
workflowStore := NewTicketWorkflowStore(db, auditStore)
ctx := context.Background()
now := time.Now().Truncate(time.Second)
// Create session + assigned ticket
sess, _ := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
tkt := &ticket.Ticket{
ID: uniqueID("tick"),
SessionID: sess.ID,
UserID: "user1",
Priority: ticket.PriorityP1,
Status: ticket.StatusAssigned,
AssignedTo: "agent-001",
CreatedAt: now,
UpdatedAt: now,
}
ticketStore.Create(ctx, tkt)
// Resolve it
err := workflowStore.Resolve(ctx, tkt.ID, "user satisfied", "admin", "127.0.0.1", now)
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
// Verify resolved
fetched, _ := ticketStore.GetByID(ctx, tkt.ID)
if fetched.Status != ticket.StatusResolved {
t.Errorf("ticket.Status = %s, want %s", fetched.Status, ticket.StatusResolved)
}
if fetched.Resolution != "user satisfied" {
t.Errorf("ticket.Resolution = %s, want 'user satisfied'", fetched.Resolution)
}
if fetched.ResolvedAt == nil {
t.Error("ticket.ResolvedAt should be set")
}
}
func TestTicketWorkflowStore_Close(t *testing.T) {
db := openDBForTest(t)
defer db.Close()
sessionStore := NewSessionStore(db)
ticketStore := NewTicketStore(db)
auditStore := NewAuditStore(db)
workflowStore := NewTicketWorkflowStore(db, auditStore)
ctx := context.Background()
now := time.Now().Truncate(time.Second)
// Create session + resolved ticket
sess, _ := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
resolvedTime := now.Add(-1 * time.Hour)
tkt := &ticket.Ticket{
ID: uniqueID("tick"),
SessionID: sess.ID,
UserID: "user1",
Priority: ticket.PriorityP1,
Status: ticket.StatusResolved,
Resolution: "fixed",
ResolvedAt: &resolvedTime,
CreatedAt: now,
UpdatedAt: now,
}
ticketStore.Create(ctx, tkt)
// Close it
err := workflowStore.Close(ctx, tkt.ID, "user confirmed", "admin", "127.0.0.1", now)
if err != nil {
t.Fatalf("Close() error = %v", err)
}
// Verify closed
fetched, _ := ticketStore.GetByID(ctx, tkt.ID)
if fetched.Status != ticket.StatusClosed {
t.Errorf("ticket.Status = %s, want %s", fetched.Status, ticket.StatusClosed)
}
}
func TestTicketWorkflowStore_Close_NotResolved(t *testing.T) {
db := openDBForTest(t)
defer db.Close()
sessionStore := NewSessionStore(db)
ticketStore := NewTicketStore(db)
auditStore := NewAuditStore(db)
workflowStore := NewTicketWorkflowStore(db, auditStore)
ctx := context.Background()
now := time.Now().Truncate(time.Second)
// Create session + open ticket (not resolved)
sess, _ := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
tkt := &ticket.Ticket{
ID: uniqueID("tick"),
SessionID: sess.ID,
UserID: "user1",
Priority: ticket.PriorityP1,
Status: ticket.StatusOpen,
CreatedAt: now,
UpdatedAt: now,
}
ticketStore.Create(ctx, tkt)
// Try to close — should fail
err := workflowStore.Close(ctx, tkt.ID, "user confirmed", "admin", "127.0.0.1", now)
if err == nil {
t.Fatal("Close() on non-resolved ticket should return error")
}
}
func TestTicketWorkflowStore_NilDB(t *testing.T) {
workflowStore := NewTicketWorkflowStore(nil, nil)
ctx := context.Background()
now := time.Now()
t.Run("ListOpen returns error", func(t *testing.T) {
_, err := workflowStore.ListOpen(ctx, 10)
if err == nil {
t.Error("ListOpen() with nil db should return error")
}
})
t.Run("Assign returns error", func(t *testing.T) {
err := workflowStore.Assign(ctx, "t1", "agent1", "admin", "127.0.0.1", now)
if err == nil {
t.Error("Assign() with nil db should return error")
}
})
t.Run("Resolve returns error", func(t *testing.T) {
err := workflowStore.Resolve(ctx, "t1", "fixed", "admin", "127.0.0.1", now)
if err == nil {
t.Error("Resolve() with nil db should return error")
}
})
t.Run("Close returns error", func(t *testing.T) {
err := workflowStore.Close(ctx, "t1", "confirmed", "admin", "127.0.0.1", now)
if err == nil {
t.Error("Close() with nil db should return error")
}
})
}