Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion agent/channels/linq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,15 @@ export default linqChannel({
"react_to_message requires an active Linq conversation thread."
);
}
const messageId = context.thread.toJSON().currentMessage?.id;
// The thread is a persisted snapshot; auth identifies this incoming message.
const target = resolveLinqReplyTarget(
{ kind: "current" },
session.session.auth
);
const messageId =
target?.conversationId === context.thread.id
? target.messageId
: undefined;
if (!messageId) {
throw new Error("react_to_message requires a current Linq message.");
}
Expand Down
71 changes: 71 additions & 0 deletions tests/agent/channels/linq-message-delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,77 @@ describe("Linq message delivery", () => {
);
expect(post).not.toHaveBeenCalled();
});

it.each(["add", "remove"] as const)(
"%s targets the incoming message when the thread snapshot is stale",
async (operation) => {
const { addReaction, context, removeReaction } =
handlerContext("first-message");

await handleActionResult(
reactToMessageResult({ operation, type: "thumbs_up" }),
context,
sessionContext("linq-message", undefined, "latest-message")
);

const deliver = operation === "add" ? addReaction : removeReaction;
const unused = operation === "add" ? removeReaction : addReaction;
expect(deliver).toHaveBeenCalledExactlyOnceWith(
"linq:dm:chat-1",
"latest-message",
"thumbs_up"
);
expect(unused).not.toHaveBeenCalled();
}
);

it("reacts to the incoming message without a cached current message", async () => {
const { addReaction, context } = handlerContext(null);

await handleActionResult(
reactToMessageResult({ operation: "add", type: "thumbs_up" }),
context,
sessionContext()
);

expect(addReaction).toHaveBeenCalledExactlyOnceWith(
"linq:dm:chat-1",
"message-1",
"thumbs_up"
);
});

it("does not react to a cached message without an incoming message ID", async () => {
const { addReaction, context, removeReaction } = handlerContext();

await expect(
handleActionResult(
reactToMessageResult({ operation: "add", type: "thumbs_up" }),
context,
sessionContext("linq-message", undefined, null)
)
).rejects.toThrow("react_to_message requires a current Linq message.");

expect(addReaction).not.toHaveBeenCalled();
expect(removeReaction).not.toHaveBeenCalled();
});

it("rejects an incoming reaction target from a different conversation", async () => {
const { addReaction, context, removeReaction } = handlerContext();
const session = sessionContext();
session.session.auth.current.attributes.conversationId = "linq:other-chat";

await expect(
handleActionResult(
reactToMessageResult({ operation: "add", type: "thumbs_up" }),
context,
session
)
).rejects.toThrow("react_to_message requires a current Linq message.");

expect(addReaction).not.toHaveBeenCalled();
expect(removeReaction).not.toHaveBeenCalled();
});
});

function sendMessageResult(
Expand Down
Loading