// message_test.go 测试消息格式,内存收件箱和路由器. // // 测试即文档:每个测试对应一个明确的行为约束. // 覆盖范围:NewMessage,Payload 序列化,MemoryInbox,Router. package inbox import ( "context" "encoding/json" "testing" "time" ) // --- NewMessage --- func TestNewMessage_UniqueIDs(t *testing.T) { ids := make(map[string]bool) for i := 0; i < 100; i++ { msg, err := NewMessage("from", "to", MsgTaskAssignment, map[string]string{"k": "v"}) if err != nil { t.Fatalf("NewMessage error: %v", err) } if ids[msg.ID] { t.Errorf("duplicate ID: %s", msg.ID) } ids[msg.ID] = true } } func TestNewMessage_FieldsPopulated(t *testing.T) { payload := TaskAssignmentPayload{ TaskID: "task-1", Subject: "Test", AssignedBy: "leader", } msg, err := NewMessage("leader", "worker-1", MsgTaskAssignment, payload) if err != nil { t.Fatalf("NewMessage error: %v", err) } if msg.From != "leader" { t.Errorf("From: got %q", msg.From) } if msg.To != "worker-1" { t.Errorf("To: got %q", msg.To) } if msg.Type != MsgTaskAssignment { t.Errorf("Type: got %q", msg.Type) } if msg.ID == "" { t.Error("ID should not be empty") } if msg.Timestamp.IsZero() { t.Error("Timestamp should be set") } } func TestNewMessage_TimestampIsRecent(t *testing.T) { before := time.Now() msg, _ := NewMessage("a", "b", MsgShutdownRequest, nil) after := time.Now() if msg.Timestamp.Before(before) || msg.Timestamp.After(after) { t.Errorf("Timestamp %v not in [%v, %v]", msg.Timestamp, before, after) } } // --- Payload 序列化/反序列化 --- func TestPermissionRequestPayload_RoundTrip(t *testing.T) { orig := PermissionRequestPayload{ RequestID: "req-1", ToolName: "Bash", ToolUseID: "tu-1", Description: "Run ls", Input: map[string]any{"command": "ls -la"}, } msg, err := NewMessage("worker", "leader", MsgPermissionRequest, orig) if err != nil { t.Fatalf("NewMessage: %v", err) } var decoded PermissionRequestPayload if err := json.Unmarshal(msg.Payload, &decoded); err != nil { t.Fatalf("Unmarshal: %v", err) } if decoded.RequestID != orig.RequestID { t.Errorf("RequestID: %q != %q", decoded.RequestID, orig.RequestID) } if decoded.ToolName != orig.ToolName { t.Errorf("ToolName: %q != %q", decoded.ToolName, orig.ToolName) } } func TestPermissionResponsePayload_RoundTrip(t *testing.T) { orig := PermissionResponsePayload{ RequestID: "req-1", Approved: true, Reason: "auto-approved", } msg, err := NewMessage("leader", "worker", MsgPermissionResponse, orig) if err != nil { t.Fatalf("NewMessage: %v", err) } var decoded PermissionResponsePayload if err := json.Unmarshal(msg.Payload, &decoded); err != nil { t.Fatalf("Unmarshal: %v", err) } if !decoded.Approved { t.Error("Approved should be true") } if decoded.Reason != orig.Reason { t.Errorf("Reason: %q != %q", decoded.Reason, orig.Reason) } } func TestIdleNotificationPayload_RoundTrip(t *testing.T) { orig := IdleNotificationPayload{ IdleReason: "available", Summary: "Task completed", CompletedTaskID: "task-1", CompletedStatus: "resolved", } msg, _ := NewMessage("worker", "leader", MsgIdleNotification, orig) var decoded IdleNotificationPayload _ = json.Unmarshal(msg.Payload, &decoded) if decoded.IdleReason != "available" { t.Errorf("IdleReason: %q", decoded.IdleReason) } if decoded.CompletedStatus != "resolved" { t.Errorf("CompletedStatus: %q", decoded.CompletedStatus) } } func TestTaskAssignmentPayload_RoundTrip(t *testing.T) { orig := TaskAssignmentPayload{ TaskID: "task-42", Subject: "Refactor module", Description: "Refactor the payment module", AssignedBy: "leader", } msg, _ := NewMessage("leader", "worker-2", MsgTaskAssignment, orig) var decoded TaskAssignmentPayload _ = json.Unmarshal(msg.Payload, &decoded) if decoded.TaskID != "task-42" { t.Errorf("TaskID: %q", decoded.TaskID) } if decoded.AssignedBy != "leader" { t.Errorf("AssignedBy: %q", decoded.AssignedBy) } } // --- MemoryInbox --- func TestMemoryInbox_SendRecv(t *testing.T) { box := NewMemoryInbox() defer box.Close() msg, _ := NewMessage("a", "b", MsgShutdownRequest, nil) if err := box.Send(msg); err != nil { t.Fatalf("Send: %v", err) } ctx := context.Background() got, err := box.Recv(ctx) if err != nil { t.Fatalf("Recv: %v", err) } if got.ID != msg.ID { t.Errorf("ID mismatch: %q != %q", got.ID, msg.ID) } } func TestMemoryInbox_Poll_NoMessage(t *testing.T) { box := NewMemoryInbox() defer box.Close() msg, err := box.Poll() if err != nil { t.Fatalf("Poll error: %v", err) } if msg != nil { t.Error("expected nil message on empty inbox") } } func TestMemoryInbox_Poll_WithMessage(t *testing.T) { box := NewMemoryInbox() defer box.Close() sent, _ := NewMessage("a", "b", MsgModeSetRequest, nil) _ = box.Send(sent) got, err := box.Poll() if err != nil { t.Fatalf("Poll: %v", err) } if got == nil { t.Fatal("expected message, got nil") } if got.ID != sent.ID { t.Errorf("ID mismatch") } } func TestMemoryInbox_CloseReturnsError(t *testing.T) { box := NewMemoryInbox() _ = box.Close() ctx := context.Background() _, err := box.Recv(ctx) if err == nil { t.Error("expected error from Recv after Close") } if err != ErrInboxClosed { t.Errorf("expected ErrInboxClosed, got %v", err) } } func TestMemoryInbox_DoubleCloseNoPanic(t *testing.T) { box := NewMemoryInbox() if err := box.Close(); err != nil { t.Fatalf("first Close: %v", err) } // 不应 panic if err := box.Close(); err != nil { t.Fatalf("second Close: %v", err) } } func TestMemoryInbox_ContextCancelReturnsError(t *testing.T) { box := NewMemoryInbox() defer box.Close() ctx, cancel := context.WithCancel(context.Background()) cancel() // 立即取消 _, err := box.Recv(ctx) if err == nil { t.Error("expected error from Recv with cancelled context") } if err != context.Canceled { t.Errorf("expected context.Canceled, got %v", err) } } func TestMemoryInbox_SendAfterCloseReturnsError(t *testing.T) { box := NewMemoryInbox() _ = box.Close() msg, _ := NewMessage("a", "b", MsgShutdownRequest, nil) err := box.Send(msg) if err == nil { t.Error("expected error sending to closed inbox") } } // --- Router --- func TestRouter_SendToNewAgentAutoCreatesInbox(t *testing.T) { r := NewRouter() defer r.Close() msg, _ := NewMessage("leader", "worker-99", MsgTaskAssignment, nil) if err := r.Send("worker-99", msg); err != nil { t.Fatalf("Send: %v", err) } // worker-99 的 inbox 应该已经被创建 box := r.Inbox("worker-99") ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() got, err := box.Recv(ctx) if err != nil { t.Fatalf("Recv: %v", err) } if got.ID != msg.ID { t.Errorf("ID mismatch") } } func TestRouter_CloseClosesAllInboxes(t *testing.T) { r := NewRouter() // 创建两个 inbox box1 := r.Inbox("agent-1") box2 := r.Inbox("agent-2") if err := r.Close(); err != nil { t.Fatalf("Router.Close: %v", err) } // 关闭后 Send 应返回错误 msg, _ := NewMessage("a", "b", MsgShutdownRequest, nil) if err := box1.Send(msg); err == nil { t.Error("box1: expected error after router close") } if err := box2.Send(msg); err == nil { t.Error("box2: expected error after router close") } } func TestRouter_InboxSameAgentReturnsSameInstance(t *testing.T) { r := NewRouter() defer r.Close() box1 := r.Inbox("worker-1") box2 := r.Inbox("worker-1") if box1 != box2 { t.Error("same agent name should return same Inbox instance") } } func TestRouter_RemoveDeletesEntry(t *testing.T) { r := NewRouter() defer r.Close() _ = r.Inbox("temp-agent") r.Remove("temp-agent") names := r.AgentNames() for _, n := range names { if n == "temp-agent" { t.Error("Remove should delete the agent entry") } } } func TestRouter_SendMultipleMessages(t *testing.T) { r := NewRouter() defer r.Close() for i := 0; i < 5; i++ { msg, _ := NewMessage("leader", "worker", MsgTaskAssignment, TaskAssignmentPayload{TaskID: "task"}) if err := r.Send("worker", msg); err != nil { t.Fatalf("Send %d: %v", i, err) } } box := r.Inbox("worker") ctx := context.Background() for i := 0; i < 5; i++ { msg, err := box.Recv(ctx) if err != nil { t.Fatalf("Recv %d: %v", i, err) } if msg == nil { t.Fatalf("Recv %d: nil message", i) } } }