mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-10 20:48:54 +01:00
🏺 fix: Restore Archive Functionality with Dedicated Endpoint (#11183)
The archive conversation feature was broken after the `/api/convos/update`
route was modified to only handle title updates. The frontend was sending
`{ conversationId, isArchived }` to the update endpoint, but the backend
was only extracting `title` and ignoring the `isArchived` field entirely.
This fix implements a dedicated `/api/convos/archive` endpoint to restore
the archive/unarchive functionality.
Changes:
packages/data-provider/src/api-endpoints.ts:
- Add `archiveConversation()` endpoint returning `/api/convos/archive`
packages/data-provider/src/data-service.ts:
- Update `archiveConversation()` to use dedicated archive endpoint
api/server/routes/convos.js:
- Add `POST /archive` route with validation for `conversationId` (required)
and `isArchived` (must be boolean)
api/server/routes/__tests__/convos.spec.js:
- Add test coverage for archive endpoint (success, validation, error cases)
This commit is contained in:
parent
cda6d589d6
commit
b94388ce9d
4 changed files with 167 additions and 2 deletions
|
|
@ -109,7 +109,7 @@ describe('Convos Routes', () => {
|
|||
let app;
|
||||
let convosRouter;
|
||||
const { deleteAllSharedLinks, deleteConvoSharedLink } = require('~/models');
|
||||
const { deleteConvos } = require('~/models/Conversation');
|
||||
const { deleteConvos, saveConvo } = require('~/models/Conversation');
|
||||
const { deleteToolCalls } = require('~/models/ToolCall');
|
||||
|
||||
beforeAll(() => {
|
||||
|
|
@ -461,6 +461,138 @@ describe('Convos Routes', () => {
|
|||
expect(deleteConvoSharedLink).toHaveBeenCalledAfter(deleteConvos);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /archive', () => {
|
||||
it('should archive a conversation successfully', async () => {
|
||||
const mockConversationId = 'conv-123';
|
||||
const mockArchivedConvo = {
|
||||
conversationId: mockConversationId,
|
||||
title: 'Test Conversation',
|
||||
isArchived: true,
|
||||
user: 'test-user-123',
|
||||
};
|
||||
|
||||
saveConvo.mockResolvedValue(mockArchivedConvo);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/convos/archive')
|
||||
.send({
|
||||
arg: {
|
||||
conversationId: mockConversationId,
|
||||
isArchived: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockArchivedConvo);
|
||||
expect(saveConvo).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ user: { id: 'test-user-123' } }),
|
||||
{ conversationId: mockConversationId, isArchived: true },
|
||||
{ context: `POST /api/convos/archive ${mockConversationId}` },
|
||||
);
|
||||
});
|
||||
|
||||
it('should unarchive a conversation successfully', async () => {
|
||||
const mockConversationId = 'conv-456';
|
||||
const mockUnarchivedConvo = {
|
||||
conversationId: mockConversationId,
|
||||
title: 'Unarchived Conversation',
|
||||
isArchived: false,
|
||||
user: 'test-user-123',
|
||||
};
|
||||
|
||||
saveConvo.mockResolvedValue(mockUnarchivedConvo);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/convos/archive')
|
||||
.send({
|
||||
arg: {
|
||||
conversationId: mockConversationId,
|
||||
isArchived: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockUnarchivedConvo);
|
||||
expect(saveConvo).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ user: { id: 'test-user-123' } }),
|
||||
{ conversationId: mockConversationId, isArchived: false },
|
||||
{ context: `POST /api/convos/archive ${mockConversationId}` },
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 400 when conversationId is missing', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convos/archive')
|
||||
.send({
|
||||
arg: {
|
||||
isArchived: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toEqual({ error: 'conversationId is required' });
|
||||
expect(saveConvo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 400 when isArchived is not a boolean', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convos/archive')
|
||||
.send({
|
||||
arg: {
|
||||
conversationId: 'conv-123',
|
||||
isArchived: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toEqual({ error: 'isArchived must be a boolean' });
|
||||
expect(saveConvo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 400 when isArchived is undefined', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/convos/archive')
|
||||
.send({
|
||||
arg: {
|
||||
conversationId: 'conv-123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toEqual({ error: 'isArchived must be a boolean' });
|
||||
expect(saveConvo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 500 when saveConvo fails', async () => {
|
||||
const mockConversationId = 'conv-error';
|
||||
saveConvo.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/convos/archive')
|
||||
.send({
|
||||
arg: {
|
||||
conversationId: mockConversationId,
|
||||
isArchived: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.text).toBe('Error archiving conversation');
|
||||
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
expect(logger.error).toHaveBeenCalledWith('Error archiving conversation', expect.any(Error));
|
||||
});
|
||||
|
||||
it('should handle empty arg object', async () => {
|
||||
const response = await request(app).post('/api/convos/archive').send({
|
||||
arg: {},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toEqual({ error: 'conversationId is required' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -152,6 +152,37 @@ router.delete('/all', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Archives or unarchives a conversation.
|
||||
* @route POST /archive
|
||||
* @param {string} req.body.arg.conversationId - The conversation ID to archive/unarchive.
|
||||
* @param {boolean} req.body.arg.isArchived - Whether to archive (true) or unarchive (false).
|
||||
* @returns {object} 200 - The updated conversation object.
|
||||
*/
|
||||
router.post('/archive', validateConvoAccess, async (req, res) => {
|
||||
const { conversationId, isArchived } = req.body.arg ?? {};
|
||||
|
||||
if (!conversationId) {
|
||||
return res.status(400).json({ error: 'conversationId is required' });
|
||||
}
|
||||
|
||||
if (typeof isArchived !== 'boolean') {
|
||||
return res.status(400).json({ error: 'isArchived must be a boolean' });
|
||||
}
|
||||
|
||||
try {
|
||||
const dbResponse = await saveConvo(
|
||||
req,
|
||||
{ conversationId, isArchived },
|
||||
{ context: `POST /api/convos/archive ${conversationId}` },
|
||||
);
|
||||
res.status(200).json(dbResponse);
|
||||
} catch (error) {
|
||||
logger.error('Error archiving conversation', error);
|
||||
res.status(500).send('Error archiving conversation');
|
||||
}
|
||||
});
|
||||
|
||||
/** Maximum allowed length for conversation titles */
|
||||
const MAX_CONVO_TITLE_LENGTH = 1024;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue