fix(dashboard): don't mark agent inbox messages read when user views them
When the dashboard user browses another agent's mailbox (e.g. the CEO's inbox), opening a message no longer triggers POST /messages/:id/read. The previous behavior silently consumed the agent's unread state, so the agent's heartbeat never surfaced the message and fn_read_messages (which defaults to unread_only=true) returned nothing. Auto-mark-read now only fires on the dashboard user's own inbox tab. Adds regression tests in MailboxView.test.tsx and MailboxModal.test.tsx. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
.changeset/agent-mailbox-no-mark-read.md
Normal file
12
.changeset/agent-mailbox-no-mark-read.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Stop the dashboard from auto-marking another agent's messages as read when
|
||||||
|
the user opens them while browsing that agent's mailbox. Previously, viewing
|
||||||
|
a message in an agent's inbox (e.g. the CEO's mailbox) would call
|
||||||
|
`POST /messages/:id/read`, which silently consumed the agent's unread state.
|
||||||
|
The agent's heartbeat would then never see the message as pending, and the
|
||||||
|
agent's `fn_read_messages` tool (which defaults to `unread_only: true`)
|
||||||
|
returned nothing. The mark-as-read call now only fires for the dashboard
|
||||||
|
user's own inbox tab.
|
||||||
@@ -207,8 +207,10 @@ export function MailboxModal({
|
|||||||
|
|
||||||
const handleOpenMessage = useCallback(async (message: Message) => {
|
const handleOpenMessage = useCallback(async (message: Message) => {
|
||||||
setSelectedMessage(message);
|
setSelectedMessage(message);
|
||||||
// Mark as read if unread
|
// Only auto-mark as read when viewing the dashboard user's own inbox.
|
||||||
if (!message.read) {
|
// Browsing another agent's mailbox must not consume their unread messages
|
||||||
|
// out from under them — the agent's heartbeat is the one that reads + acks.
|
||||||
|
if (!message.read && activeTab === "inbox") {
|
||||||
try {
|
try {
|
||||||
const updated = await markMessageRead(message.id, projectId);
|
const updated = await markMessageRead(message.id, projectId);
|
||||||
// Update inbox state
|
// Update inbox state
|
||||||
@@ -233,7 +235,7 @@ export function MailboxModal({
|
|||||||
} catch {
|
} catch {
|
||||||
setConversationMessages([message]);
|
setConversationMessages([message]);
|
||||||
}
|
}
|
||||||
}, [projectId]);
|
}, [projectId, activeTab]);
|
||||||
|
|
||||||
const handleCloseMessage = useCallback(() => {
|
const handleCloseMessage = useCallback(() => {
|
||||||
setSelectedMessage(null);
|
setSelectedMessage(null);
|
||||||
|
|||||||
@@ -291,8 +291,10 @@ export function MailboxView({
|
|||||||
|
|
||||||
const handleOpenMessage = useCallback(async (message: Message) => {
|
const handleOpenMessage = useCallback(async (message: Message) => {
|
||||||
setSelectedMessage(message);
|
setSelectedMessage(message);
|
||||||
// Mark as read if unread
|
// Only auto-mark as read when viewing the dashboard user's own inbox.
|
||||||
if (!message.read) {
|
// Browsing another agent's mailbox must not consume their unread messages
|
||||||
|
// out from under them — the agent's heartbeat is the one that reads + acks.
|
||||||
|
if (!message.read && activeTab === "inbox") {
|
||||||
try {
|
try {
|
||||||
const updated = await markMessageRead(message.id, projectId);
|
const updated = await markMessageRead(message.id, projectId);
|
||||||
// Update inbox state
|
// Update inbox state
|
||||||
@@ -321,7 +323,7 @@ export function MailboxView({
|
|||||||
} catch {
|
} catch {
|
||||||
setConversationMessages([message]);
|
setConversationMessages([message]);
|
||||||
}
|
}
|
||||||
}, [projectId, unreadCount, onUnreadCountChange]);
|
}, [projectId, unreadCount, onUnreadCountChange, activeTab]);
|
||||||
|
|
||||||
const handleCloseMessage = useCallback(() => {
|
const handleCloseMessage = useCallback(() => {
|
||||||
setSelectedMessage(null);
|
setSelectedMessage(null);
|
||||||
|
|||||||
@@ -285,6 +285,53 @@ describe("MailboxModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not mark agent inbox messages as read when the dashboard user opens them", async () => {
|
||||||
|
const agentInboxMessage: Message = {
|
||||||
|
id: "msg-agent-unread",
|
||||||
|
fromId: "user-001",
|
||||||
|
fromType: "user",
|
||||||
|
toId: "agent-001",
|
||||||
|
toType: "agent",
|
||||||
|
content: "Important — please reply",
|
||||||
|
type: "user-to-agent",
|
||||||
|
read: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
mockFetchAgentMailbox.mockResolvedValue({
|
||||||
|
ownerId: "agent-001",
|
||||||
|
ownerType: "agent",
|
||||||
|
unreadCount: 1,
|
||||||
|
messages: [agentInboxMessage],
|
||||||
|
inbox: [agentInboxMessage],
|
||||||
|
outbox: [],
|
||||||
|
});
|
||||||
|
mockFetchConversation.mockResolvedValue([agentInboxMessage]);
|
||||||
|
|
||||||
|
render(<MailboxModal {...defaultProps} />);
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-agent-select")).toBeDefined();
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByTestId("mailbox-agent-select"), { target: { value: "agent-001" } });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-item-msg-agent-unread")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-item-msg-agent-unread"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-message-detail")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Critical: the dashboard user browsing an agent's mailbox MUST NOT
|
||||||
|
// consume the agent's unread state — the agent's heartbeat is the
|
||||||
|
// authoritative reader.
|
||||||
|
expect(mockMarkMessageRead).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("shows back button in message detail", async () => {
|
it("shows back button in message detail", async () => {
|
||||||
render(<MailboxModal {...defaultProps} />);
|
render(<MailboxModal {...defaultProps} />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -974,6 +974,58 @@ describe("MailboxView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not mark agent inbox messages as read when the dashboard user opens them", async () => {
|
||||||
|
const agentInboxMessage: Message = {
|
||||||
|
id: "msg-agent-unread",
|
||||||
|
fromId: "user-001",
|
||||||
|
fromType: "user",
|
||||||
|
toId: "agent-001",
|
||||||
|
toType: "agent",
|
||||||
|
content: "Important — please reply",
|
||||||
|
type: "user-to-agent",
|
||||||
|
read: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFetchInbox.mockResolvedValue({ messages: [], unreadCount: 0, total: 0 });
|
||||||
|
mockFetchAgentMailbox.mockResolvedValue({
|
||||||
|
ownerId: "agent-001",
|
||||||
|
ownerType: "agent",
|
||||||
|
unreadCount: 1,
|
||||||
|
messages: [agentInboxMessage],
|
||||||
|
inbox: [agentInboxMessage],
|
||||||
|
outbox: [],
|
||||||
|
});
|
||||||
|
mockFetchConversation.mockResolvedValue([agentInboxMessage]);
|
||||||
|
|
||||||
|
render(<MailboxView {...defaultProps} />);
|
||||||
|
|
||||||
|
const agentsTab = screen.getByTestId("mailbox-tab-agents");
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(agentsTab);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByTestId("mailbox-agent-select"), { target: { value: "agent-001" } });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-item-msg-agent-unread")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-item-msg-agent-unread"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-message-detail")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Critical: the dashboard user browsing an agent's mailbox MUST NOT
|
||||||
|
// consume the agent's unread state — the agent's heartbeat is the
|
||||||
|
// authoritative reader.
|
||||||
|
expect(mockMarkMessageRead).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("switches to outbox view when clicking outbox sub-tab", async () => {
|
it("switches to outbox view when clicking outbox sub-tab", async () => {
|
||||||
mockFetchInbox.mockResolvedValue({
|
mockFetchInbox.mockResolvedValue({
|
||||||
messages: [],
|
messages: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user