feat(FN-4109): add aggregate all-agents view to mailbox modal
Adds aggregate mailbox functionality to the MailboxModal, including the plumbing for aggregating participants across all agents, an "all agents" view mode, and corresponding tests. The feature is documented with participant aggregation logic. Fusion-Task-Id: FN-4109 Fusion-Task-Lineage: 122b19cd-2b14-4f50-836b-e524d3b96a9c
This commit is contained in:
@@ -5180,7 +5180,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
// Populate cache with current state. The watcher only needs metadata to
|
// Populate cache with current state. The watcher only needs metadata to
|
||||||
// detect created/updated/moved/deleted events; full task logs stay on the
|
// detect created/updated/moved/deleted events; full task logs stay on the
|
||||||
// detail path.
|
// detail path.
|
||||||
const tasks = await this.listTasks({ slim: true, startupMemo: true });
|
const tasks = await this.listTasks({ slim: true, startupMemo: false });
|
||||||
this.taskCache.clear();
|
this.taskCache.clear();
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
this.taskCache.set(task.id, { ...task });
|
this.taskCache.set(task.id, { ...task });
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
fetchOutbox,
|
fetchOutbox,
|
||||||
fetchUnreadCount,
|
fetchUnreadCount,
|
||||||
fetchAgentMailbox,
|
fetchAgentMailbox,
|
||||||
|
fetchAllAgentMailbox,
|
||||||
markMessageRead,
|
markMessageRead,
|
||||||
markAllMessagesRead,
|
markAllMessagesRead,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
type InboxResponse,
|
type InboxResponse,
|
||||||
type OutboxResponse,
|
type OutboxResponse,
|
||||||
type AgentMailboxResponse,
|
type AgentMailboxResponse,
|
||||||
|
type AllAgentsMailboxResponse,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
import { MessageComposer } from "./MessageComposer";
|
import { MessageComposer } from "./MessageComposer";
|
||||||
import { MailboxMessageContent } from "./MailboxMessageContent";
|
import { MailboxMessageContent } from "./MailboxMessageContent";
|
||||||
@@ -42,6 +44,8 @@ import { subscribeSse } from "../sse-bus";
|
|||||||
|
|
||||||
type MailboxTab = "inbox" | "outbox" | "agents";
|
type MailboxTab = "inbox" | "outbox" | "agents";
|
||||||
|
|
||||||
|
const ALL_AGENTS_MAILBOX_ID = "__all_agents__";
|
||||||
|
|
||||||
interface MailboxModalProps {
|
interface MailboxModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -161,9 +165,10 @@ export function MailboxModal({
|
|||||||
const [showComposer, setShowComposer] = useState(false);
|
const [showComposer, setShowComposer] = useState(false);
|
||||||
const [composeRecipient, setComposeRecipient] = useState<{ id: string; type: ParticipantType } | null>(null);
|
const [composeRecipient, setComposeRecipient] = useState<{ id: string; type: ParticipantType } | null>(null);
|
||||||
const [composeReplyContext, setComposeReplyContext] = useState<{ messageId: string; preview: string } | null>(null);
|
const [composeReplyContext, setComposeReplyContext] = useState<{ messageId: string; preview: string } | null>(null);
|
||||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
const [selectedAgentId, setSelectedAgentId] = useState<string>(ALL_AGENTS_MAILBOX_ID);
|
||||||
const [agentSubTab, setAgentSubTab] = useState<"inbox" | "outbox">("inbox");
|
const [agentSubTab, setAgentSubTab] = useState<"inbox" | "outbox">("inbox");
|
||||||
const [agentMailbox, setAgentMailbox] = useState<AgentMailboxResponse | null>(null);
|
const [agentMailbox, setAgentMailbox] = useState<AgentMailboxResponse | null>(null);
|
||||||
|
const [allAgentsMailbox, setAllAgentsMailbox] = useState<AllAgentsMailboxResponse | null>(null);
|
||||||
const [replyContextExpanded, setReplyContextExpanded] = useState<Record<string, boolean>>({});
|
const [replyContextExpanded, setReplyContextExpanded] = useState<Record<string, boolean>>({});
|
||||||
const [replyContextLoading, setReplyContextLoading] = useState<Record<string, boolean>>({});
|
const [replyContextLoading, setReplyContextLoading] = useState<Record<string, boolean>>({});
|
||||||
const [replyContextErrors, setReplyContextErrors] = useState<Record<string, string>>({});
|
const [replyContextErrors, setReplyContextErrors] = useState<Record<string, string>>({});
|
||||||
@@ -234,6 +239,18 @@ export function MailboxModal({
|
|||||||
}
|
}
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
|
const loadAllAgentsMailbox = useCallback(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await fetchAllAgentMailbox(projectId);
|
||||||
|
setAllAgentsMailbox(data);
|
||||||
|
} catch {
|
||||||
|
// Silently fail
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
const refreshUnreadCount = useCallback(async () => {
|
const refreshUnreadCount = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await fetchUnreadCount(projectId);
|
const data = await fetchUnreadCount(projectId);
|
||||||
@@ -252,9 +269,13 @@ export function MailboxModal({
|
|||||||
|
|
||||||
// Load agent mailbox when selected
|
// Load agent mailbox when selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen || !selectedAgentId) return;
|
if (!isOpen) return;
|
||||||
loadAgentMailbox(selectedAgentId);
|
if (selectedAgentId === ALL_AGENTS_MAILBOX_ID) {
|
||||||
}, [isOpen, selectedAgentId, loadAgentMailbox]);
|
void loadAllAgentsMailbox();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void loadAgentMailbox(selectedAgentId);
|
||||||
|
}, [isOpen, selectedAgentId, loadAgentMailbox, loadAllAgentsMailbox]);
|
||||||
|
|
||||||
// Refresh unread count on open
|
// Refresh unread count on open
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -277,7 +298,9 @@ export function MailboxModal({
|
|||||||
void loadOutbox();
|
void loadOutbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedAgentId) {
|
if (selectedAgentId === ALL_AGENTS_MAILBOX_ID) {
|
||||||
|
void loadAllAgentsMailbox();
|
||||||
|
} else {
|
||||||
void loadAgentMailbox(selectedAgentId);
|
void loadAgentMailbox(selectedAgentId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -290,7 +313,7 @@ export function MailboxModal({
|
|||||||
"message:deleted": onMailboxUpdate,
|
"message:deleted": onMailboxUpdate,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [isOpen, projectId, activeTab, selectedAgentId, refreshUnreadCount, loadInbox, loadOutbox, loadAgentMailbox]);
|
}, [isOpen, projectId, activeTab, selectedAgentId, refreshUnreadCount, loadInbox, loadOutbox, loadAgentMailbox, loadAllAgentsMailbox]);
|
||||||
|
|
||||||
// ── Actions ───────────────────────────────────────────────────────────
|
// ── Actions ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -345,6 +368,7 @@ export function MailboxModal({
|
|||||||
...(outbox?.messages ?? []),
|
...(outbox?.messages ?? []),
|
||||||
...(agentMailbox?.inbox ?? []),
|
...(agentMailbox?.inbox ?? []),
|
||||||
...(agentMailbox?.outbox ?? []),
|
...(agentMailbox?.outbox ?? []),
|
||||||
|
...(allAgentsMailbox?.messages ?? []),
|
||||||
...conversationMessages,
|
...conversationMessages,
|
||||||
].find((candidate) => candidate.id === deepLinkedMessageId);
|
].find((candidate) => candidate.id === deepLinkedMessageId);
|
||||||
|
|
||||||
@@ -353,7 +377,7 @@ export function MailboxModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleOpenMessage(message);
|
void handleOpenMessage(message);
|
||||||
}, [isOpen, inbox, outbox, agentMailbox, conversationMessages, handleOpenMessage]);
|
}, [isOpen, inbox, outbox, agentMailbox, allAgentsMailbox, conversationMessages, handleOpenMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@@ -416,12 +440,13 @@ export function MailboxModal({
|
|||||||
// Refresh current tab
|
// Refresh current tab
|
||||||
if (activeTab === "inbox") loadInbox();
|
if (activeTab === "inbox") loadInbox();
|
||||||
else if (activeTab === "outbox") loadOutbox();
|
else if (activeTab === "outbox") loadOutbox();
|
||||||
|
else if (selectedAgentId === ALL_AGENTS_MAILBOX_ID) loadAllAgentsMailbox();
|
||||||
else if (selectedAgentId) loadAgentMailbox(selectedAgentId);
|
else if (selectedAgentId) loadAgentMailbox(selectedAgentId);
|
||||||
addToast?.("Message deleted", "success");
|
addToast?.("Message deleted", "success");
|
||||||
} catch {
|
} catch {
|
||||||
addToast?.("Failed to delete message", "error");
|
addToast?.("Failed to delete message", "error");
|
||||||
}
|
}
|
||||||
}, [projectId, activeTab, selectedAgentId, loadInbox, loadOutbox, loadAgentMailbox, addToast]);
|
}, [projectId, activeTab, selectedAgentId, loadInbox, loadOutbox, loadAgentMailbox, loadAllAgentsMailbox, addToast]);
|
||||||
|
|
||||||
const handleReply = useCallback((message: Message) => {
|
const handleReply = useCallback((message: Message) => {
|
||||||
setComposeRecipient({ id: message.fromId, type: message.fromType });
|
setComposeRecipient({ id: message.fromId, type: message.fromType });
|
||||||
@@ -439,12 +464,13 @@ export function MailboxModal({
|
|||||||
addToast?.("Message sent", "success");
|
addToast?.("Message sent", "success");
|
||||||
// Refresh current tab
|
// Refresh current tab
|
||||||
if (activeTab === "outbox") loadOutbox();
|
if (activeTab === "outbox") loadOutbox();
|
||||||
|
else if (activeTab === "agents" && selectedAgentId === ALL_AGENTS_MAILBOX_ID) loadAllAgentsMailbox();
|
||||||
else if (activeTab === "agents" && selectedAgentId) loadAgentMailbox(selectedAgentId);
|
else if (activeTab === "agents" && selectedAgentId) loadAgentMailbox(selectedAgentId);
|
||||||
}, [activeTab, loadOutbox, selectedAgentId, loadAgentMailbox, addToast]);
|
}, [activeTab, loadOutbox, selectedAgentId, loadAgentMailbox, loadAllAgentsMailbox, addToast]);
|
||||||
|
|
||||||
const handleOpenCompose = useCallback(() => {
|
const handleOpenCompose = useCallback(() => {
|
||||||
// Pre-fill recipient from selected agent if available
|
// Pre-fill recipient from selected agent if available
|
||||||
if (activeTab === "agents" && selectedAgentId) {
|
if (activeTab === "agents" && selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID) {
|
||||||
setComposeRecipient({ id: selectedAgentId, type: "agent" });
|
setComposeRecipient({ id: selectedAgentId, type: "agent" });
|
||||||
} else {
|
} else {
|
||||||
setComposeRecipient(null);
|
setComposeRecipient(null);
|
||||||
@@ -621,6 +647,7 @@ export function MailboxModal({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (activeTab === "inbox") loadInbox();
|
if (activeTab === "inbox") loadInbox();
|
||||||
else if (activeTab === "outbox") loadOutbox();
|
else if (activeTab === "outbox") loadOutbox();
|
||||||
|
else if (selectedAgentId === ALL_AGENTS_MAILBOX_ID) loadAllAgentsMailbox();
|
||||||
else if (selectedAgentId) loadAgentMailbox(selectedAgentId);
|
else if (selectedAgentId) loadAgentMailbox(selectedAgentId);
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -885,11 +912,11 @@ export function MailboxModal({
|
|||||||
<div className="mailbox-agents-dropdown">
|
<div className="mailbox-agents-dropdown">
|
||||||
<select
|
<select
|
||||||
className="message-composer-select mailbox-agent-select"
|
className="message-composer-select mailbox-agent-select"
|
||||||
value={selectedAgentId ?? ""}
|
value={selectedAgentId}
|
||||||
onChange={(e) => { setSelectedAgentId(e.target.value || null); setAgentSubTab("inbox"); }}
|
onChange={(e) => { setSelectedAgentId(e.target.value); setAgentSubTab("inbox"); }}
|
||||||
data-testid="mailbox-agent-select"
|
data-testid="mailbox-agent-select"
|
||||||
>
|
>
|
||||||
<option value="">Select an agent…</option>
|
<option value={ALL_AGENTS_MAILBOX_ID}>All agents</option>
|
||||||
{agents.map((agent) => (
|
{agents.map((agent) => (
|
||||||
<option key={agent.id} value={agent.id}>
|
<option key={agent.id} value={agent.id}>
|
||||||
{agent.name || agent.id}
|
{agent.name || agent.id}
|
||||||
@@ -908,7 +935,7 @@ export function MailboxModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Agent Sub-Tabs (Inbox/Outbox) */}
|
{/* Agent Sub-Tabs (Inbox/Outbox) */}
|
||||||
{selectedAgentId && (
|
{selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && (
|
||||||
<div className="mailbox-agent-subtabs" data-testid="mailbox-agent-subtabs">
|
<div className="mailbox-agent-subtabs" data-testid="mailbox-agent-subtabs">
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "inbox" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "inbox" ? "active" : ""}`}
|
||||||
@@ -932,26 +959,54 @@ export function MailboxModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mailbox-agents-content">
|
<div className="mailbox-agents-content">
|
||||||
{!selectedAgentId && (
|
{selectedAgentId === ALL_AGENTS_MAILBOX_ID && isLoading && !allAgentsMailbox && <MailboxSkeleton />}
|
||||||
|
{selectedAgentId === ALL_AGENTS_MAILBOX_ID && allAgentsMailbox && allAgentsMailbox.messages.length === 0 && (
|
||||||
<div className="mailbox-empty">
|
<div className="mailbox-empty">
|
||||||
<Bot size={32} />
|
<InboxIcon size={32} />
|
||||||
<p>Select an agent to view their mailbox</p>
|
<p>No agent-to-agent messages</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedAgentId && isLoading && !agentMailbox && <MailboxSkeleton />}
|
{selectedAgentId === ALL_AGENTS_MAILBOX_ID && allAgentsMailbox && allAgentsMailbox.messages.map((msg) => (
|
||||||
{selectedAgentId && agentMailbox && agentSubTab === "inbox" && agentMailbox.inbox.length === 0 && (
|
<div
|
||||||
|
key={msg.id}
|
||||||
|
id={`message-${msg.id}`}
|
||||||
|
className={`mailbox-item ${!msg.read ? "unread" : ""}`}
|
||||||
|
onClick={() => handleOpenMessage(msg)}
|
||||||
|
data-testid={`mailbox-item-${msg.id}`}
|
||||||
|
>
|
||||||
|
<div className="mailbox-item-avatar">
|
||||||
|
{msg.fromType === "agent" ? <Bot size={16} /> : <User size={16} />}
|
||||||
|
</div>
|
||||||
|
<div className="mailbox-item-content">
|
||||||
|
<div className="mailbox-item-header">
|
||||||
|
<span className="mailbox-item-from">
|
||||||
|
{participantLabel(msg.fromId, msg.fromType, agentNamesById)}
|
||||||
|
</span>
|
||||||
|
<span className="mailbox-item-time">{formatTimestamp(msg.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mailbox-item-participants" data-testid={`mailbox-item-participants-${msg.id}`}>
|
||||||
|
<span>From: {participantLabel(msg.fromId, msg.fromType, agentNamesById)}</span>
|
||||||
|
<span>To: {participantLabel(msg.toId, msg.toType, agentNamesById)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mailbox-item-preview">{msg.content.slice(0, 80)}{msg.content.length > 80 ? "…" : ""}</div>
|
||||||
|
</div>
|
||||||
|
{!msg.read && <div className="mailbox-item-unread-dot" data-testid={`mailbox-unread-dot-${msg.id}`} />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && isLoading && !agentMailbox && <MailboxSkeleton />}
|
||||||
|
{selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && agentMailbox && agentSubTab === "inbox" && agentMailbox.inbox.length === 0 && (
|
||||||
<div className="mailbox-empty">
|
<div className="mailbox-empty">
|
||||||
<InboxIcon size={32} />
|
<InboxIcon size={32} />
|
||||||
<p>No received messages for this agent</p>
|
<p>No received messages for this agent</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedAgentId && agentMailbox && agentSubTab === "outbox" && agentMailbox.outbox.length === 0 && (
|
{selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && agentMailbox && agentSubTab === "outbox" && agentMailbox.outbox.length === 0 && (
|
||||||
<div className="mailbox-empty">
|
<div className="mailbox-empty">
|
||||||
<Send size={32} />
|
<Send size={32} />
|
||||||
<p>No sent messages for this agent</p>
|
<p>No sent messages for this agent</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedAgentId && agentMailbox && agentSubTab === "inbox" && agentMailbox.inbox.map((msg) => (
|
{selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && agentMailbox && agentSubTab === "inbox" && agentMailbox.inbox.map((msg) => (
|
||||||
<div
|
<div
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
id={`message-${msg.id}`}
|
id={`message-${msg.id}`}
|
||||||
@@ -973,7 +1028,7 @@ export function MailboxModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{selectedAgentId && agentMailbox && agentSubTab === "outbox" && agentMailbox.outbox.map((msg) => (
|
{selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && agentMailbox && agentSubTab === "outbox" && agentMailbox.outbox.map((msg) => (
|
||||||
<div
|
<div
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
id={`message-${msg.id}`}
|
id={`message-${msg.id}`}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ vi.mock("../../api", () => ({
|
|||||||
fetchOutbox: vi.fn(),
|
fetchOutbox: vi.fn(),
|
||||||
fetchUnreadCount: vi.fn(),
|
fetchUnreadCount: vi.fn(),
|
||||||
fetchAgentMailbox: vi.fn(),
|
fetchAgentMailbox: vi.fn(),
|
||||||
|
fetchAllAgentMailbox: vi.fn(),
|
||||||
markMessageRead: vi.fn(),
|
markMessageRead: vi.fn(),
|
||||||
markAllMessagesRead: vi.fn(),
|
markAllMessagesRead: vi.fn(),
|
||||||
deleteMessage: vi.fn(),
|
deleteMessage: vi.fn(),
|
||||||
@@ -54,6 +55,7 @@ const mockFetchInbox = vi.mocked(apiModule.fetchInbox);
|
|||||||
const mockFetchOutbox = vi.mocked(apiModule.fetchOutbox);
|
const mockFetchOutbox = vi.mocked(apiModule.fetchOutbox);
|
||||||
const mockFetchUnreadCount = vi.mocked(apiModule.fetchUnreadCount);
|
const mockFetchUnreadCount = vi.mocked(apiModule.fetchUnreadCount);
|
||||||
const mockFetchAgentMailbox = vi.mocked(apiModule.fetchAgentMailbox);
|
const mockFetchAgentMailbox = vi.mocked(apiModule.fetchAgentMailbox);
|
||||||
|
const mockFetchAllAgentMailbox = vi.mocked(apiModule.fetchAllAgentMailbox);
|
||||||
const mockMarkMessageRead = vi.mocked(apiModule.markMessageRead);
|
const mockMarkMessageRead = vi.mocked(apiModule.markMessageRead);
|
||||||
const mockMarkAllMessagesRead = vi.mocked(apiModule.markAllMessagesRead);
|
const mockMarkAllMessagesRead = vi.mocked(apiModule.markAllMessagesRead);
|
||||||
const mockDeleteMessage = vi.mocked(apiModule.deleteMessage);
|
const mockDeleteMessage = vi.mocked(apiModule.deleteMessage);
|
||||||
@@ -136,6 +138,7 @@ describe("MailboxModal", () => {
|
|||||||
mockFetchInbox.mockResolvedValue({ messages: [mockMessage, mockReadMessage], total: 2, unreadCount: 1 });
|
mockFetchInbox.mockResolvedValue({ messages: [mockMessage, mockReadMessage], total: 2, unreadCount: 1 });
|
||||||
mockFetchOutbox.mockResolvedValue({ messages: [], total: 0 });
|
mockFetchOutbox.mockResolvedValue({ messages: [], total: 0 });
|
||||||
mockFetchUnreadCount.mockResolvedValue({ unreadCount: 1 });
|
mockFetchUnreadCount.mockResolvedValue({ unreadCount: 1 });
|
||||||
|
mockFetchAllAgentMailbox.mockResolvedValue({ messages: [], total: 0, unreadCount: 0 });
|
||||||
mockFetchConversation.mockResolvedValue([mockMessage]);
|
mockFetchConversation.mockResolvedValue([mockMessage]);
|
||||||
mockFetchMessage.mockResolvedValue(mockMessage);
|
mockFetchMessage.mockResolvedValue(mockMessage);
|
||||||
mockMarkMessageRead.mockResolvedValue({ ...mockMessage, read: true });
|
mockMarkMessageRead.mockResolvedValue({ ...mockMessage, read: true });
|
||||||
@@ -289,24 +292,64 @@ describe("MailboxModal", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("mailbox-agent-select")).toBeDefined();
|
expect(screen.getByTestId("mailbox-agent-select")).toBeDefined();
|
||||||
});
|
});
|
||||||
// Should have placeholder plus two agent options
|
|
||||||
const select = screen.getByTestId("mailbox-agent-select") as HTMLSelectElement;
|
const select = screen.getByTestId("mailbox-agent-select") as HTMLSelectElement;
|
||||||
expect(select.options.length).toBe(3); // placeholder + 2 agents
|
expect(select.options.length).toBe(3);
|
||||||
expect(select.options[0].textContent).toBe("Select an agent…");
|
expect(select.options[0].value).toBe("__all_agents__");
|
||||||
|
expect(select.options[0].textContent).toBe("All agents");
|
||||||
expect(select.options[1].textContent).toBe("Test Agent 1");
|
expect(select.options[1].textContent).toBe("Test Agent 1");
|
||||||
expect(select.options[2].textContent).toBe("Test Agent 2");
|
expect(select.options[2].textContent).toBe("Test Agent 2");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows Select an agent… placeholder in dropdown", async () => {
|
it("defaults the agent dropdown to All agents with no empty placeholder", async () => {
|
||||||
render(<MailboxModal {...defaultProps} />);
|
render(<MailboxModal {...defaultProps} />);
|
||||||
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
|
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const select = screen.getByTestId("mailbox-agent-select") as HTMLSelectElement;
|
const select = screen.getByTestId("mailbox-agent-select") as HTMLSelectElement;
|
||||||
expect(select.value).toBe("");
|
expect(select.value).toBe("__all_agents__");
|
||||||
expect(select.options[0].textContent).toBe("Select an agent…");
|
expect(select.options[0].value).toBe("__all_agents__");
|
||||||
|
expect(select.options[0].textContent).toBe("All agents");
|
||||||
|
expect(Array.from(select.options).some((option) => option.value === "")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("FN-4109 defaults the Agents tab to All agents and loads the aggregate mailbox", async () => {
|
||||||
|
mockFetchAllAgentMailbox.mockResolvedValue({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: "msg-agent-thread",
|
||||||
|
fromId: "agent-001",
|
||||||
|
fromType: "agent",
|
||||||
|
toId: "agent-002",
|
||||||
|
toType: "agent",
|
||||||
|
content: "Coordinator handoff update for the mailbox modal.",
|
||||||
|
type: "agent-to-agent",
|
||||||
|
read: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
unreadCount: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<MailboxModal {...defaultProps} />);
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const select = screen.getByTestId("mailbox-agent-select") as HTMLSelectElement;
|
||||||
|
expect(select.value).toBe("__all_agents__");
|
||||||
|
expect(select.options[0].value).toBe("__all_agents__");
|
||||||
|
expect(select.options[0].textContent).toBe("All agents");
|
||||||
|
expect(mockFetchAllAgentMailbox).toHaveBeenCalledWith(undefined);
|
||||||
|
expect(screen.getByTestId("mailbox-item-msg-agent-thread")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId("mailbox-item-participants-msg-agent-thread").textContent).toContain("From: Agent: Test Agent 1");
|
||||||
|
expect(screen.getByTestId("mailbox-item-participants-msg-agent-thread").textContent).toContain("To: Agent: Test Agent 2");
|
||||||
|
expect(screen.queryByText("No agent-to-agent messages")).toBeNull();
|
||||||
|
expect(screen.queryByTestId("mailbox-agent-subtabs")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("loads agent mailbox when selecting an agent from dropdown", async () => {
|
it("loads agent mailbox when selecting an agent from dropdown", async () => {
|
||||||
mockFetchAgentMailbox.mockResolvedValue({
|
mockFetchAgentMailbox.mockResolvedValue({
|
||||||
ownerId: "agent-001",
|
ownerId: "agent-001",
|
||||||
@@ -742,7 +785,7 @@ describe("MailboxModal", () => {
|
|||||||
expect(agentsComposeButton).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-compose-btn");
|
expect(agentsComposeButton).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-compose-btn");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("compose opened from Agents tab without selected agent shows recipient select", async () => {
|
it("compose opened from Agents tab with All agents selected shows recipient select", async () => {
|
||||||
render(<MailboxModal {...defaultProps} />);
|
render(<MailboxModal {...defaultProps} />);
|
||||||
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
|
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -1217,8 +1260,8 @@ describe("MailboxModal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("light theme overrides new tokens", () => {
|
it("light theme overrides new tokens", () => {
|
||||||
// Find the base [data-theme="light"] block (not combined with other selectors)
|
// Match the root-scoped light theme token block used by the app stylesheet.
|
||||||
const lightBlockMatch = css.match(/^\[data-theme="light"\]\s*\{[\s\S]*?^\}\s*$/m);
|
const lightBlockMatch = css.match(/^:root\[data-theme="light"\]\s*\{[\s\S]*?^\}\s*$/m);
|
||||||
expect(lightBlockMatch).toBeTruthy();
|
expect(lightBlockMatch).toBeTruthy();
|
||||||
const lightContent = lightBlockMatch![0];
|
const lightContent = lightBlockMatch![0];
|
||||||
expect(lightContent).toContain("--terminal-bg");
|
expect(lightContent).toContain("--terminal-bg");
|
||||||
|
|||||||
Reference in New Issue
Block a user