package main import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/multica-ai/multica/server/internal/cli" ) func TestTruncateID(t *testing.T) { tests := []struct { name string id string want string }{ {"short", "abc", "abc"}, {"exact 8", "abcdefgh", "abcdefgh"}, {"longer than 8", "abcdefgh-1234-5678", "abcdefgh"}, {"empty", "", ""}, {"unicode", "日本語テスト文字列追加", "日本語テスト文字"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := truncateID(tt.id) if got != tt.want { t.Errorf("truncateID(%q) = %q, want %q", tt.id, got, tt.want) } }) } } func TestFormatAssignee(t *testing.T) { tests := []struct { name string issue map[string]any want string }{ {"empty", map[string]any{}, ""}, {"no type", map[string]any{"assignee_id": "abc"}, ""}, {"no id", map[string]any{"assignee_type": "member"}, ""}, {"member", map[string]any{"assignee_type": "member", "assignee_id": "abcdefgh-1234"}, "member:abcdefgh"}, {"agent", map[string]any{"assignee_type": "agent", "assignee_id": "xyz"}, "agent:xyz"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := formatAssignee(tt.issue) if got != tt.want { t.Errorf("formatAssignee() = %q, want %q", got, tt.want) } }) } } func TestResolveAssignee(t *testing.T) { membersResp := []map[string]any{ {"user_id": "user-1111", "name": "Alice Smith"}, {"user_id": "user-2222", "name": "Bob Jones"}, } agentsResp := []map[string]any{ {"id": "agent-3333", "name": "CodeBot"}, } srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/workspaces/ws-1/members": json.NewEncoder(w).Encode(membersResp) case "/api/agents": json.NewEncoder(w).Encode(agentsResp) default: http.NotFound(w, r) } })) defer srv.Close() client := cli.NewAPIClient(srv.URL, "ws-1", "test-token") ctx := context.Background() t.Run("exact match member", func(t *testing.T) { aType, aID, err := resolveAssignee(ctx, client, "Alice Smith") if err != nil { t.Fatalf("unexpected error: %v", err) } if aType != "member" || aID != "user-1111" { t.Errorf("got (%q, %q), want (member, user-1111)", aType, aID) } }) t.Run("case-insensitive substring", func(t *testing.T) { aType, aID, err := resolveAssignee(ctx, client, "bob") if err != nil { t.Fatalf("unexpected error: %v", err) } if aType != "member" || aID != "user-2222" { t.Errorf("got (%q, %q), want (member, user-2222)", aType, aID) } }) t.Run("match agent", func(t *testing.T) { aType, aID, err := resolveAssignee(ctx, client, "codebot") if err != nil { t.Fatalf("unexpected error: %v", err) } if aType != "agent" || aID != "agent-3333" { t.Errorf("got (%q, %q), want (agent, agent-3333)", aType, aID) } }) t.Run("no match", func(t *testing.T) { _, _, err := resolveAssignee(ctx, client, "nobody") if err == nil { t.Fatal("expected error for no match") } }) t.Run("ambiguous", func(t *testing.T) { // Both "Alice Smith" and "Bob Jones" contain a space — but let's use a broader query // "e" matches "Alice Smith" and "Bob Jones" and "CodeBot" _, _, err := resolveAssignee(ctx, client, "o") if err == nil { t.Fatal("expected error for ambiguous match") } if got := err.Error(); !strings.Contains(got, "ambiguous") { t.Errorf("expected ambiguous error, got: %s", got) } }) t.Run("missing workspace ID", func(t *testing.T) { noWSClient := cli.NewAPIClient(srv.URL, "", "test-token") _, _, err := resolveAssignee(ctx, noWSClient, "alice") if err == nil { t.Fatal("expected error for missing workspace ID") } }) } func TestValidIssueStatuses(t *testing.T) { expected := map[string]bool{ "backlog": true, "todo": true, "in_progress": true, "in_review": true, "done": true, "blocked": true, "cancelled": true, } for _, s := range validIssueStatuses { if !expected[s] { t.Errorf("unexpected status in validIssueStatuses: %q", s) } } if len(validIssueStatuses) != len(expected) { t.Errorf("validIssueStatuses has %d entries, expected %d", len(validIssueStatuses), len(expected)) } }