This commit is contained in:
@@ -3,17 +3,17 @@ package audit
|
||||
import "time"
|
||||
|
||||
type Event struct {
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
TicketID string `json:"ticket_id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
OpenID string `json:"open_id,omitempty"`
|
||||
ActorID string `json:"actor_id,omitempty"`
|
||||
SourceIP string `json:"source_ip,omitempty"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
BeforeState map[string]any `json:"before_state,omitempty"`
|
||||
AfterState map[string]any `json:"after_state,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
TicketID string `json:"ticket_id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
OpenID string `json:"open_id,omitempty"`
|
||||
ActorID string `json:"actor_id,omitempty"`
|
||||
SourceIP string `json:"source_ip,omitempty"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
BeforeState map[string]any `json:"before_state,omitempty"`
|
||||
AfterState map[string]any `json:"after_state,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
@@ -164,10 +164,10 @@ func TestEvent_TicketAndSessionFields(t *testing.T) {
|
||||
|
||||
// Session-scoped event
|
||||
sessionEvent := Event{
|
||||
ID: "e2",
|
||||
ID: "e2",
|
||||
SessionID: "s-1",
|
||||
Type: "session",
|
||||
Action: "message",
|
||||
Type: "session",
|
||||
Action: "message",
|
||||
}
|
||||
|
||||
if sessionEvent.SessionID != "s-1" {
|
||||
|
||||
@@ -79,8 +79,8 @@ func TestErrorMsg_UnknownCode(t *testing.T) {
|
||||
|
||||
func TestErrorMsg_SpecificCodes(t *testing.T) {
|
||||
tests := []struct {
|
||||
code string
|
||||
expectedMsg string
|
||||
code string
|
||||
expectedMsg string
|
||||
}{
|
||||
{CS_SES_4001, "session not found"},
|
||||
{CS_SES_4002, "message rate limit exceeded"},
|
||||
@@ -102,38 +102,38 @@ func TestErrorMsg_SpecificCodes(t *testing.T) {
|
||||
func TestErrorMsg_AllKnownCodesReturnNonEmpty(t *testing.T) {
|
||||
// Verify all codes defined in the switch have non-empty messages
|
||||
knownCodes := map[string]string{
|
||||
CS_SES_4001: "session not found",
|
||||
CS_SES_4002: "message rate limit exceeded",
|
||||
CS_SES_4003: "identity verification locked",
|
||||
CS_IDT_4001: "identity information mismatch",
|
||||
CS_IDT_4002: "verification code incorrect",
|
||||
CS_SES_4001: "session not found",
|
||||
CS_SES_4002: "message rate limit exceeded",
|
||||
CS_SES_4003: "identity verification locked",
|
||||
CS_IDT_4001: "identity information mismatch",
|
||||
CS_IDT_4002: "verification code incorrect",
|
||||
CS_TICKET_4001: "ticket not found",
|
||||
CS_TICKET_4002: "ticket already assigned",
|
||||
CS_TICKET_4092: "ticket resolve conflict",
|
||||
CS_TICKET_4093: "ticket close conflict",
|
||||
CS_KB_4001: "knowledge-base entry not found",
|
||||
CS_KB_4002: "entry name already exists",
|
||||
CS_LLM_5001: "LLM service unavailable",
|
||||
CS_LLM_5002: "LLM request timeout",
|
||||
CS_AUTH_4001: "access denied",
|
||||
CS_AUTH_4031: "missing webhook signature",
|
||||
CS_AUTH_4032: "invalid webhook timestamp",
|
||||
CS_AUTH_4033: "stale webhook request",
|
||||
CS_AUTH_4034: "invalid webhook signature",
|
||||
CS_HTTP_405: "method not allowed",
|
||||
CS_REQ_4001: "invalid JSON",
|
||||
CS_REQ_4131: "request body too large",
|
||||
CS_REQ_4002: "channel, open_id and content are required",
|
||||
CS_REQ_4003: "content exceeds maximum length",
|
||||
CS_REQ_4004: "unable to read request body",
|
||||
CS_REQ_4008: "channel is required",
|
||||
CS_REQ_4005: "ticket_id and agent_id are required",
|
||||
CS_REQ_4006: "ticket_id and resolution are required",
|
||||
CS_REQ_4007: "ticket_id and resolution are required",
|
||||
CS_REQ_4009: "feedback score must be between 1 and 5",
|
||||
CS_REQ_4010: "handoff reason is required",
|
||||
CS_SYS_5001: "internal server error",
|
||||
CS_SYS_5002: "list tickets failed",
|
||||
CS_KB_4001: "knowledge-base entry not found",
|
||||
CS_KB_4002: "entry name already exists",
|
||||
CS_LLM_5001: "LLM service unavailable",
|
||||
CS_LLM_5002: "LLM request timeout",
|
||||
CS_AUTH_4001: "access denied",
|
||||
CS_AUTH_4031: "missing webhook signature",
|
||||
CS_AUTH_4032: "invalid webhook timestamp",
|
||||
CS_AUTH_4033: "stale webhook request",
|
||||
CS_AUTH_4034: "invalid webhook signature",
|
||||
CS_HTTP_405: "method not allowed",
|
||||
CS_REQ_4001: "invalid JSON",
|
||||
CS_REQ_4131: "request body too large",
|
||||
CS_REQ_4002: "channel, open_id and content are required",
|
||||
CS_REQ_4003: "content exceeds maximum length",
|
||||
CS_REQ_4004: "unable to read request body",
|
||||
CS_REQ_4008: "channel is required",
|
||||
CS_REQ_4005: "ticket_id and agent_id are required",
|
||||
CS_REQ_4006: "ticket_id and resolution are required",
|
||||
CS_REQ_4007: "ticket_id and resolution are required",
|
||||
CS_REQ_4009: "feedback score must be between 1 and 5",
|
||||
CS_REQ_4010: "handoff reason is required",
|
||||
CS_SYS_5001: "internal server error",
|
||||
CS_SYS_5002: "list tickets failed",
|
||||
}
|
||||
|
||||
for code, expectedMsg := range knownCodes {
|
||||
@@ -142,4 +142,4 @@ func TestErrorMsg_AllKnownCodesReturnNonEmpty(t *testing.T) {
|
||||
t.Errorf("ErrorMsg(%q): expected %q, got %q", code, expectedMsg, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ type Result struct {
|
||||
}
|
||||
|
||||
const (
|
||||
IntentQuota = "quota"
|
||||
IntentToken = "token"
|
||||
IntentError = "error"
|
||||
IntentHandoff = "handoff"
|
||||
IntentGeneral = "general"
|
||||
IntentRefund = "refund"
|
||||
IntentQuota = "quota"
|
||||
IntentToken = "token"
|
||||
IntentError = "error"
|
||||
IntentHandoff = "handoff"
|
||||
IntentGeneral = "general"
|
||||
IntentRefund = "refund"
|
||||
IntentSecurity = "security"
|
||||
)
|
||||
|
||||
@@ -5,10 +5,10 @@ import "time"
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusIdle Status = "idle"
|
||||
StatusProcessing Status = "processing"
|
||||
StatusHandoff Status = "handoff"
|
||||
StatusClosed Status = "closed"
|
||||
StatusIdle Status = "idle"
|
||||
StatusProcessing Status = "processing"
|
||||
StatusHandoff Status = "handoff"
|
||||
StatusClosed Status = "closed"
|
||||
)
|
||||
|
||||
type MessageContext struct {
|
||||
|
||||
@@ -187,4 +187,4 @@ func TestSession_FullLifecycle(t *testing.T) {
|
||||
if sess.Status != StatusClosed {
|
||||
t.Error("failed to transition to Closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,16 +22,16 @@ const (
|
||||
)
|
||||
|
||||
type Ticket struct {
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Priority Priority `json:"priority"`
|
||||
Status Status `json:"status"`
|
||||
HandoffReason string `json:"handoff_reason"`
|
||||
AssignedTo string `json:"assigned_to,omitempty"`
|
||||
ContextSnapshot map[string]any `json:"context_snapshot"`
|
||||
Resolution string `json:"resolution,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Priority Priority `json:"priority"`
|
||||
Status Status `json:"status"`
|
||||
HandoffReason string `json:"handoff_reason"`
|
||||
AssignedTo string `json:"assigned_to,omitempty"`
|
||||
ContextSnapshot map[string]any `json:"context_snapshot"`
|
||||
Resolution string `json:"resolution,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -132,8 +132,8 @@ func TestTicket_Fields(t *testing.T) {
|
||||
func TestTicket_ResolvedAtOptional(t *testing.T) {
|
||||
// Test that ResolvedAt can be nil (open ticket)
|
||||
tk := Ticket{
|
||||
ID: "open-ticket",
|
||||
Status: StatusOpen,
|
||||
ID: "open-ticket",
|
||||
Status: StatusOpen,
|
||||
ResolvedAt: nil,
|
||||
}
|
||||
if tk.ResolvedAt != nil {
|
||||
@@ -170,4 +170,4 @@ func TestTicket_StatusTransitions(t *testing.T) {
|
||||
if tk.Status != StatusClosed {
|
||||
t.Error("failed to transition to Closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ package ticketstats
|
||||
|
||||
// Stats represents aggregated ticket statistics for monitoring dashboards.
|
||||
type Stats struct {
|
||||
Total int `json:"total_tickets"`
|
||||
Open int `json:"open"`
|
||||
Resolved int `json:"resolved"`
|
||||
Closed int `json:"closed"`
|
||||
ByChannel map[string]int `json:"by_channel"`
|
||||
ByPriority map[string]int `json:"by_priority"`
|
||||
HandoffCount int `json:"handoff_count"`
|
||||
AvgResolutionTimeMinutes float64 `json:"avg_resolution_time_minutes"`
|
||||
Total int `json:"total_tickets"`
|
||||
Open int `json:"open"`
|
||||
Resolved int `json:"resolved"`
|
||||
Closed int `json:"closed"`
|
||||
ByChannel map[string]int `json:"by_channel"`
|
||||
ByPriority map[string]int `json:"by_priority"`
|
||||
HandoffCount int `json:"handoff_count"`
|
||||
AvgResolutionTimeMinutes float64 `json:"avg_resolution_time_minutes"`
|
||||
}
|
||||
|
||||
@@ -106,13 +106,13 @@ func TestHealthHandler_Health_ReturnsOK(t *testing.T) {
|
||||
func TestTicketStatsHandler_Get_Success(t *testing.T) {
|
||||
mock := &mockTicketStatsServiceForStats{
|
||||
stats: ticketstats.Stats{
|
||||
Total: 100,
|
||||
Open: 30,
|
||||
Resolved: 50,
|
||||
Closed: 20,
|
||||
ByChannel: map[string]int{"api": 40, "web": 60},
|
||||
ByPriority: map[string]int{"P1": 10, "P2": 60, "P3": 30},
|
||||
HandoffCount: 15,
|
||||
Total: 100,
|
||||
Open: 30,
|
||||
Resolved: 50,
|
||||
Closed: 20,
|
||||
ByChannel: map[string]int{"api": 40, "web": 60},
|
||||
ByPriority: map[string]int{"P1": 10, "P2": 60, "P3": 30},
|
||||
HandoffCount: 15,
|
||||
AvgResolutionTimeMinutes: 45.5,
|
||||
},
|
||||
err: nil,
|
||||
|
||||
@@ -173,4 +173,4 @@ func TestClientIP_NoPort(t *testing.T) {
|
||||
if ip != "192.168.1.100" {
|
||||
t.Errorf("clientIP() = %s, want 192.168.1.100", ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
)
|
||||
|
||||
// TestWebhookSecurity_InvalidTimestampFormat covers CS_AUTH_4032:
|
||||
|
||||
@@ -143,4 +143,4 @@ func TestRateLimiter_WithRateLimit_XForwardedFor(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("different IP: expected 200, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bridge/ai-customer-service/internal/domain/audit"
|
||||
intentdomain "github.com/bridge/ai-customer-service/internal/domain/intent"
|
||||
"github.com/bridge/ai-customer-service/internal/domain/message"
|
||||
"github.com/bridge/ai-customer-service/internal/domain/session"
|
||||
"github.com/bridge/ai-customer-service/internal/domain/ticket"
|
||||
intentdomain "github.com/bridge/ai-customer-service/internal/domain/intent"
|
||||
"github.com/bridge/ai-customer-service/internal/service/handoff"
|
||||
intentservice "github.com/bridge/ai-customer-service/internal/service/intent"
|
||||
"github.com/bridge/ai-customer-service/internal/service/reply"
|
||||
|
||||
@@ -160,4 +160,4 @@ func TestGenerate_ContextCancellation(t *testing.T) {
|
||||
if result == "" {
|
||||
t.Error("Generate with cancelled context should still return answer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ func TestDialogService_AC02_IntentMatrix(t *testing.T) {
|
||||
svc := dialog.NewService(sessions, audits, tickets, dedup, intentservice.NewService(), reply.NewService(knowledge), handoff.NewService())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
wantIntent string
|
||||
wantHandoff bool
|
||||
wantPriority string // empty if no handoff expected
|
||||
wantReply bool // whether to check reply is non-empty
|
||||
name string
|
||||
content string
|
||||
wantIntent string
|
||||
wantHandoff bool
|
||||
wantPriority string // empty if no handoff expected
|
||||
wantReply bool // whether to check reply is non-empty
|
||||
}{
|
||||
{
|
||||
name: "AC-02: 退款意图 → P1 handoff",
|
||||
@@ -58,11 +58,11 @@ func TestDialogService_AC02_IntentMatrix(t *testing.T) {
|
||||
wantReply: true,
|
||||
},
|
||||
{
|
||||
name: "AC-02: 正常查询 → bot 回复无 handoff",
|
||||
content: "查询额度",
|
||||
wantIntent: "quota",
|
||||
wantHandoff: false,
|
||||
wantReply: true,
|
||||
name: "AC-02: 正常查询 → bot 回复无 handoff",
|
||||
content: "查询额度",
|
||||
wantIntent: "quota",
|
||||
wantHandoff: false,
|
||||
wantReply: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -70,9 +70,9 @@ func TestDialogService_AC02_IntentMatrix(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := svc.Process(context.Background(), &message.UnifiedMessage{
|
||||
MessageID: "m_" + tc.name,
|
||||
Channel: "widget",
|
||||
OpenID: "u_" + tc.name,
|
||||
Content: tc.content,
|
||||
Channel: "widget",
|
||||
OpenID: "u_" + tc.name,
|
||||
Content: tc.content,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Process() error = %v", err)
|
||||
|
||||
@@ -41,27 +41,27 @@ func setupTicketStatsHandler(stats ticketstats.Stats) (*httptest.ResponseRecorde
|
||||
|
||||
// ticketStatsResponse mirrors the JSON shape of ticketstats.Stats.
|
||||
type ticketStatsResponse struct {
|
||||
Total int `json:"total_tickets"`
|
||||
Open int `json:"open"`
|
||||
Resolved int `json:"resolved"`
|
||||
Closed int `json:"closed"`
|
||||
ByChannel map[string]int `json:"by_channel"`
|
||||
ByPriority map[string]int `json:"by_priority"`
|
||||
HandoffCount int `json:"handoff_count"`
|
||||
AvgResolutionTimeMinutes float64 `json:"avg_resolution_time_minutes"`
|
||||
Total int `json:"total_tickets"`
|
||||
Open int `json:"open"`
|
||||
Resolved int `json:"resolved"`
|
||||
Closed int `json:"closed"`
|
||||
ByChannel map[string]int `json:"by_channel"`
|
||||
ByPriority map[string]int `json:"by_priority"`
|
||||
HandoffCount int `json:"handoff_count"`
|
||||
AvgResolutionTimeMinutes float64 `json:"avg_resolution_time_minutes"`
|
||||
}
|
||||
|
||||
// TestTicketStats_Success verifies the stats endpoint returns correct
|
||||
// counts when the store has tickets.
|
||||
func TestTicketStats_Success(t *testing.T) {
|
||||
stats := ticketstats.Stats{
|
||||
Total: 100,
|
||||
Open: 30,
|
||||
Resolved: 50,
|
||||
Closed: 20,
|
||||
ByChannel: map[string]int{"api": 40, "web": 60},
|
||||
ByPriority: map[string]int{"P1": 10, "P2": 60, "P3": 30},
|
||||
HandoffCount: 15,
|
||||
Total: 100,
|
||||
Open: 30,
|
||||
Resolved: 50,
|
||||
Closed: 20,
|
||||
ByChannel: map[string]int{"api": 40, "web": 60},
|
||||
ByPriority: map[string]int{"P1": 10, "P2": 60, "P3": 30},
|
||||
HandoffCount: 15,
|
||||
AvgResolutionTimeMinutes: 45.5,
|
||||
}
|
||||
|
||||
@@ -114,13 +114,13 @@ func TestTicketStats_Success(t *testing.T) {
|
||||
// TestTicketStats_Empty verifies that an empty store returns all-zero stats.
|
||||
func TestTicketStats_Empty(t *testing.T) {
|
||||
stats := ticketstats.Stats{
|
||||
Total: 0,
|
||||
Open: 0,
|
||||
Resolved: 0,
|
||||
Closed: 0,
|
||||
ByChannel: map[string]int{},
|
||||
ByPriority: map[string]int{},
|
||||
HandoffCount: 0,
|
||||
Total: 0,
|
||||
Open: 0,
|
||||
Resolved: 0,
|
||||
Closed: 0,
|
||||
ByChannel: map[string]int{},
|
||||
ByPriority: map[string]int{},
|
||||
HandoffCount: 0,
|
||||
AvgResolutionTimeMinutes: 0,
|
||||
}
|
||||
|
||||
@@ -163,14 +163,14 @@ func TestTicketStats_GroupedCounts(t *testing.T) {
|
||||
Resolved: 10,
|
||||
Closed: 5,
|
||||
ByChannel: map[string]int{
|
||||
"api": 8,
|
||||
"web": 12,
|
||||
"api": 8,
|
||||
"web": 12,
|
||||
"wechat": 5,
|
||||
},
|
||||
ByPriority: map[string]int{
|
||||
"P1": 3,
|
||||
"P1": 3,
|
||||
"P2": 15,
|
||||
"P3": 7,
|
||||
"P3": 7,
|
||||
},
|
||||
HandoffCount: 6,
|
||||
AvgResolutionTimeMinutes: 120.0,
|
||||
|
||||
Reference in New Issue
Block a user