mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix: apply retention to forked and duplicated conversations
The fork and duplicate paths created the import batch builder without the runtime interface config, so under retentionMode "ephemeral" a fork or duplicate of an existing permanent conversation skipped the forced isTemporary/expiredAt fields and bypassed the policy. Plumb req.config.interfaceConfig through forkConversation and duplicateConversation into the builder so cloned records honor retention.
This commit is contained in:
parent
06f462ded6
commit
9c71570db1
3 changed files with 52 additions and 5 deletions
|
|
@ -359,6 +359,7 @@ router.post('/fork', forkIpLimiter, forkUserLimiter, async (req, res) => {
|
|||
records: true,
|
||||
splitAtTarget,
|
||||
option,
|
||||
interfaceConfig: req.config?.interfaceConfig,
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
|
|
@ -376,6 +377,7 @@ router.post('/duplicate', forkIpLimiter, forkUserLimiter, async (req, res) => {
|
|||
userId: req.user.id,
|
||||
conversationId,
|
||||
title,
|
||||
interfaceConfig: req.config?.interfaceConfig,
|
||||
});
|
||||
res.status(201).json(result);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ function cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder) {
|
|||
* @param {boolean} [params.records=false] - Optional flag for returning actual database records or resulting conversation and messages.
|
||||
* @param {boolean} [params.splitAtTarget=false] - Optional flag for splitting the messages at the target message level.
|
||||
* @param {string} [params.latestMessageId] - latestMessageId - Required if splitAtTarget is true.
|
||||
* @param {(userId: string) => ImportBatchBuilder} [params.builderFactory] - Optional factory function for creating an ImportBatchBuilder instance.
|
||||
* @param {(userId: string, interfaceConfig?: object) => ImportBatchBuilder} [params.builderFactory] - Optional factory function for creating an ImportBatchBuilder instance.
|
||||
* @param {object} [params.interfaceConfig] - Runtime interface config used to apply retention to cloned records.
|
||||
* @returns {Promise<TForkConvoResponse>} The response after forking the conversation.
|
||||
*/
|
||||
async function forkConversation({
|
||||
|
|
@ -94,6 +95,7 @@ async function forkConversation({
|
|||
splitAtTarget = false,
|
||||
latestMessageId,
|
||||
builderFactory = createImportBatchBuilder,
|
||||
interfaceConfig,
|
||||
}) {
|
||||
try {
|
||||
const originalConvo = await getConvo(requestUserId, originalConvoId);
|
||||
|
|
@ -110,7 +112,7 @@ async function forkConversation({
|
|||
targetMessageId = latestMessageId;
|
||||
}
|
||||
|
||||
const importBatchBuilder = builderFactory(requestUserId);
|
||||
const importBatchBuilder = builderFactory(requestUserId, interfaceConfig);
|
||||
importBatchBuilder.startConversation(originalConvo.endpoint ?? EModelEndpoint.openAI);
|
||||
|
||||
let messagesToClone = [];
|
||||
|
|
@ -500,9 +502,10 @@ async function forkSharedConversation({
|
|||
* @param {string} params.userId - The ID of the user duplicating the conversation.
|
||||
* @param {string} params.conversationId - The ID of the conversation to duplicate.
|
||||
* @param {string} [params.title] - Optional title override for the duplicate.
|
||||
* @param {object} [params.interfaceConfig] - Runtime interface config used to apply retention to cloned records.
|
||||
* @returns {Promise<{ conversation: TConversation, messages: TMessage[] }>} The duplicated conversation and messages.
|
||||
*/
|
||||
async function duplicateConversation({ userId, conversationId, title }) {
|
||||
async function duplicateConversation({ userId, conversationId, title, interfaceConfig }) {
|
||||
const originalConvo = await getConvo(userId, conversationId);
|
||||
if (!originalConvo) {
|
||||
throw new Error('Conversation not found');
|
||||
|
|
@ -518,7 +521,7 @@ async function duplicateConversation({ userId, conversationId, title }) {
|
|||
originalMessages[originalMessages.length - 1].messageId,
|
||||
);
|
||||
|
||||
const importBatchBuilder = createImportBatchBuilder(userId);
|
||||
const importBatchBuilder = createImportBatchBuilder(userId, interfaceConfig);
|
||||
importBatchBuilder.startConversation(originalConvo.endpoint ?? EModelEndpoint.openAI);
|
||||
|
||||
cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const { Constants, ForkOptions } = require('librechat-data-provider');
|
||||
const { Constants, ForkOptions, RetentionMode } = require('librechat-data-provider');
|
||||
|
||||
jest.mock('~/models', () => ({
|
||||
getConvo: jest.fn(),
|
||||
|
|
@ -107,6 +107,28 @@ describe('forkConversation', () => {
|
|||
bulkSaveMessages.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
test('applies ephemeral retention to forked conversation and messages', async () => {
|
||||
await forkConversation({
|
||||
originalConvoId: 'abc123',
|
||||
targetMessageId: '3',
|
||||
requestUserId: 'user1',
|
||||
option: ForkOptions.DIRECT_PATH,
|
||||
interfaceConfig: { retentionMode: RetentionMode.EPHEMERAL, temporaryChatRetention: 1 },
|
||||
});
|
||||
|
||||
expect(bulkSaveConvos).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ isTemporary: true, expiredAt: expect.any(Date) }),
|
||||
]),
|
||||
);
|
||||
expect(bulkSaveMessages).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ isTemporary: true, expiredAt: expect.any(Date) }),
|
||||
]),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('should fork conversation without branches', async () => {
|
||||
const result = await forkConversation({
|
||||
originalConvoId: 'abc123',
|
||||
|
|
@ -264,6 +286,26 @@ describe('duplicateConversation', () => {
|
|||
bulkIncrementTagCounts.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
test('applies ephemeral retention to duplicated conversation and messages', async () => {
|
||||
await duplicateConversation({
|
||||
userId: 'user1',
|
||||
conversationId: 'abc123',
|
||||
interfaceConfig: { retentionMode: RetentionMode.EPHEMERAL, temporaryChatRetention: 1 },
|
||||
});
|
||||
|
||||
expect(bulkSaveConvos).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ isTemporary: true, expiredAt: expect.any(Date) }),
|
||||
]),
|
||||
);
|
||||
expect(bulkSaveMessages).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ isTemporary: true, expiredAt: expect.any(Date) }),
|
||||
]),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('should duplicate conversation and increment tag counts', async () => {
|
||||
const mockConvoWithTags = {
|
||||
...mockConversation,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue