mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-31 08:56:48 +00:00
🧵 fix: Reject Preliminary Parent Follow-Ups (#13619)
* fix: Reject preliminary parent follow-ups * chore: Sort frontend imports * fix: Narrow preliminary parent detection * fix: Preserve refused submit state * fix: Propagate refused submit result
This commit is contained in:
parent
753e53eddd
commit
fd4728232c
12 changed files with 426 additions and 16 deletions
|
|
@ -4,6 +4,8 @@ import {
|
|||
sanitizeFileForTransmit,
|
||||
buildMessageFiles,
|
||||
getThreadData,
|
||||
isPreliminaryMessageId,
|
||||
isUnpersistedPreliminaryParent,
|
||||
} from './message';
|
||||
|
||||
/** Cast to string for type compatibility with ThreadMessage */
|
||||
|
|
@ -130,6 +132,55 @@ describe('sanitizeMessageForTransmit', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('isUnpersistedPreliminaryParent', () => {
|
||||
it('returns false without querying for non-preliminary parent ids', async () => {
|
||||
const getMessages = jest.fn();
|
||||
|
||||
await expect(
|
||||
isUnpersistedPreliminaryParent({
|
||||
userId: 'user-123',
|
||||
conversationId: 'conversation-123',
|
||||
parentMessageId: 'persisted-response',
|
||||
getMessages,
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
|
||||
expect(isPreliminaryMessageId('persisted-response')).toBe(false);
|
||||
expect(getMessages).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns true when the underscore-suffixed parent is not persisted', async () => {
|
||||
const getMessages = jest.fn().mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
isUnpersistedPreliminaryParent({
|
||||
userId: 'user-123',
|
||||
conversationId: 'conversation-123',
|
||||
parentMessageId: 'pending-response_',
|
||||
getMessages,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
|
||||
expect(getMessages).toHaveBeenCalledWith(
|
||||
{ user: 'user-123', messageId: 'pending-response_', conversationId: 'conversation-123' },
|
||||
'_id',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false when the underscore-suffixed parent is already persisted', async () => {
|
||||
const getMessages = jest.fn().mockResolvedValue([{ _id: 'persisted-parent' }]);
|
||||
|
||||
await expect(
|
||||
isUnpersistedPreliminaryParent({
|
||||
userId: 'user-123',
|
||||
conversationId: 'conversation-123',
|
||||
parentMessageId: 'persisted-response_',
|
||||
getMessages,
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildMessageFiles', () => {
|
||||
const baseAttachment = {
|
||||
file_id: 'file-1',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ import type { TFile, TMessage } from 'librechat-data-provider';
|
|||
/** Minimal shape for request file entries (from `req.body.files`) */
|
||||
type RequestFile = { file_id?: string };
|
||||
|
||||
type GetMessagesByParentId = (
|
||||
filter: { user: string; messageId: string; conversationId?: string },
|
||||
select: '_id',
|
||||
) => Promise<unknown[]>;
|
||||
|
||||
/** Fields to strip from files before client transmission */
|
||||
const FILE_STRIP_FIELDS = ['text', '_id', '__v'] as const;
|
||||
|
||||
|
|
@ -92,6 +97,37 @@ export function sanitizeMessageForTransmit<T extends Partial<TMessage>>(
|
|||
return sanitized;
|
||||
}
|
||||
|
||||
export function isPreliminaryMessageId(messageId: unknown): messageId is string {
|
||||
return typeof messageId === 'string' && messageId.endsWith('_');
|
||||
}
|
||||
|
||||
export async function isUnpersistedPreliminaryParent({
|
||||
userId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
getMessages,
|
||||
}: {
|
||||
userId: string;
|
||||
conversationId?: string | null;
|
||||
parentMessageId?: string | null;
|
||||
getMessages: GetMessagesByParentId;
|
||||
}): Promise<boolean> {
|
||||
if (!isPreliminaryMessageId(parentMessageId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const filter: { user: string; messageId: string; conversationId?: string } = {
|
||||
user: userId,
|
||||
messageId: parentMessageId,
|
||||
};
|
||||
if (conversationId && conversationId !== Constants.NEW_CONVO) {
|
||||
filter.conversationId = conversationId;
|
||||
}
|
||||
|
||||
const messages = await getMessages(filter, '_id');
|
||||
return messages.length === 0;
|
||||
}
|
||||
|
||||
/** Minimal message shape for thread traversal.
|
||||
*
|
||||
* Both `files` and `attachments` carry `file_id` references, but they
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue