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) => {
|
||||
setSelectedMessage(message);
|
||||
// Mark as read if unread
|
||||
if (!message.read) {
|
||||
// Only auto-mark as read when viewing the dashboard user's own inbox.
|
||||
// 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 {
|
||||
const updated = await markMessageRead(message.id, projectId);
|
||||
// Update inbox state
|
||||
@@ -233,7 +235,7 @@ export function MailboxModal({
|
||||
} catch {
|
||||
setConversationMessages([message]);
|
||||
}
|
||||
}, [projectId]);
|
||||
}, [projectId, activeTab]);
|
||||
|
||||
const handleCloseMessage = useCallback(() => {
|
||||
setSelectedMessage(null);
|
||||
|
||||
@@ -291,8 +291,10 @@ export function MailboxView({
|
||||
|
||||
const handleOpenMessage = useCallback(async (message: Message) => {
|
||||
setSelectedMessage(message);
|
||||
// Mark as read if unread
|
||||
if (!message.read) {
|
||||
// Only auto-mark as read when viewing the dashboard user's own inbox.
|
||||
// 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 {
|
||||
const updated = await markMessageRead(message.id, projectId);
|
||||
// Update inbox state
|
||||
@@ -321,7 +323,7 @@ export function MailboxView({
|
||||
} catch {
|
||||
setConversationMessages([message]);
|
||||
}
|
||||
}, [projectId, unreadCount, onUnreadCountChange]);
|
||||
}, [projectId, unreadCount, onUnreadCountChange, activeTab]);
|
||||
|
||||
const handleCloseMessage = useCallback(() => {
|
||||
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 () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
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 () => {
|
||||
mockFetchInbox.mockResolvedValue({
|
||||
messages: [],
|
||||
|
||||
Reference in New Issue
Block a user