LibreChat/api/server/routes/__tests__/messages-get.spec.js
Danny Avila d864597731
perf: Append Saved Message Ids Instead of Rebuilding the Conversation Array (#15141)
*  perf: Append Saved Message Ids Instead of Rebuilding the Conversation Array

Every saveConvo read every message id in the conversation (sorted) and
wrote the array back onto the document — twice per chat turn, O(n) in
conversation length, from a write path. The turn's savers know exactly
which message they just wrote, so they now pass it as
metadata.appendMessageIds and saveConvo $addToSet-s it, skipping the
read and the full-array rewrite. Every save without the option — titles,
archive, fork, import, threads — still rebuilds from the database, which
remains the heal point for the drift that message deletion has always
left behind (deletes never ran saveConvo).

The array's consumers read presence or length, or use it as an
optimistic cache placeholder, so incremental maintenance is
behaviorally identical; on traced turns the array stays exactly equal
to the messages collection.

Per-turn queries: 15 -> 13 (two Message.find gone), and the growing
array payload no longer crosses the wire twice per turn.

* 🎯 fix: Brand the Lineage-Only Resolved Conversation Instead of Guessing by Shape

The resolved-conversation files fast path treated an absent files
property as unresolved so the lineage-only partial from a bound
agent-event continuation could not silently hide a conversation's
uploads. But MongoDB never stores an empty files array, so nearly every
real conversation also lacks the property and the fast path never fired
— a follow-up turn on an upload-free conversation still paid the
getConvoFiles round trip.

The synthesized partial is the one object that cannot speak for the
database, so it now carries an explicit symbol brand
(PARTIAL_RESOLVED_CONVERSATION, non-serializing and invisible to key
iteration), and a stored document without files means what it means:
no files. Traced follow-up turns drop from 14 queries to 13.

* 🧪 test: Expect the Appended Message Id in the Route's saveConvo Metadata

messages-get.spec.js pins the exact metadata POST /api/messages passes to
saveConvo; the route now forwards the saved message's _id as
appendMessageIds, which is the behavior the append path depends on.
2026-08-23 16:52:44 -04:00

995 lines
33 KiB
JavaScript

const { CLIENT_MESSAGE_SELECT } = require('@librechat/data-schemas');
const express = require('express');
const request = require('supertest');
jest.mock('@librechat/agents', () => ({
sleep: jest.fn(),
}));
jest.mock('@librechat/api', () => {
const inspectContent = jest.fn(() => null);
const extractChatContent = jest.fn(() => []);
const extractStoredMessageContent = jest.fn(() => []);
const contentFilterBlockResponse = jest.fn();
const getContentTraversalFragments = jest.fn((error) => error.fragments ?? []);
const isContentTraversalLimitError = jest.fn(
(error) => error?.code === 'content_filter_uninspectable',
);
const isContentTraversalProtected = jest.fn(() => false);
const assertModelBoundContent = jest.fn();
const hasActiveFilePolicy = jest.fn(
(filters) => filters?.files?.pii != null && filters.files.pii.starterPatterns?.length !== 0,
);
const resolveCanonicalFileReferences = jest.fn(async ({ input, filters }) => ({
sanitizedInput: input,
hydratedFiles: [],
hydratedFilters: filters,
}));
const assertMutationAllowed = (extract, filters, input) => {
let fragments;
let traversalError;
try {
fragments = extract(input);
} catch (error) {
if (!isContentTraversalLimitError(error)) {
throw error;
}
fragments = getContentTraversalFragments(error);
traversalError = error;
}
const finding = inspectContent(fragments, { filters });
if (finding != null) {
throw Object.assign(new Error('blocked'), {
code: 'content_filter_block',
statusCode: 400,
body: contentFilterBlockResponse(finding),
});
}
if (traversalError != null && isContentTraversalProtected({ error: traversalError, filters })) {
throw traversalError;
}
};
return {
createContentFilter: jest.fn(() => (req, res, next) => next()),
inspectContent,
extractChatContent,
extractFeedbackContent: jest.fn(() => []),
extractStoredMessageContent,
contentFilterBlockResponse,
getContentTraversalFragments,
isContentTraversalLimitError,
isContentTraversalProtected,
assertModelBoundContent,
hasActiveFilePolicy,
isContentFilterError: jest.fn(
(error) =>
error?.code === 'content_filter_block' || error?.code === 'content_filter_uninspectable',
),
resolveCanonicalFileReferences,
assertStoredMessageBranchAllowed: jest.fn(async (input) => {
let storedMessage = input.message;
let resolvedFiles = [];
if (hasActiveFilePolicy(input.filters)) {
const inspection = await resolveCanonicalFileReferences({
filters: input.filters,
input: input.message,
user: input.user,
});
storedMessage = inspection.sanitizedInput;
resolvedFiles = inspection.hydratedFiles;
}
assertModelBoundContent({
filters: input.filters,
legacyPii: input.legacyPii,
storedMessages: [storedMessage],
resolvedFiles,
});
}),
assertStoredMessageMutationAllowed: jest.fn((filters, input) =>
assertMutationAllowed(extractStoredMessageContent, filters, input),
),
assertChatMutationAllowed: jest.fn((filters, input) =>
assertMutationAllowed(extractChatContent, filters, input),
),
mergeUserSubmittedPaths: (...lists) => [...new Set(lists.flat().filter(Boolean))],
mergeUserSubmittedMessageFieldPaths: (...lists) => {
const seen = new Set();
return lists.flat().filter((entry) => {
if (entry == null) {
return false;
}
const key = `${entry.field}:${entry.path}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
},
unescapeLaTeX: jest.fn((x) => x),
countTokens: jest.fn().mockResolvedValue(10),
mergeQuotedTextForCount: jest.fn((text) => text),
sendFeedbackScore: jest.fn().mockResolvedValue(undefined),
traceIdForMessage: jest.fn((messageId) => `trace-${messageId}`),
CHILD_THREAD_READ_ONLY_ERROR: 'Child thread is view-only.',
isSubagentThreadWriteBlocked: jest.fn().mockResolvedValue(false),
requireFeedbackEnabled: (req, res, next) => next(),
};
});
jest.mock('~/server/services/Endpoints/agents/subagentThreadStore', () => ({}));
jest.mock('@librechat/data-schemas', () => ({
...jest.requireActual('@librechat/data-schemas'),
logger: {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
}));
jest.mock('librechat-data-provider', () => ({
...jest.requireActual('librechat-data-provider'),
}));
jest.mock('~/models', () => ({
saveConvo: jest.fn(),
getMessage: jest.fn(),
saveMessage: jest.fn(),
getMessages: jest.fn(),
getConvoOwnership: jest.fn(),
updateMessage: jest.fn(),
deleteMessages: jest.fn(),
getConvosQueried: jest.fn(),
searchMessages: jest.fn(),
getMessagesByCursor: jest.fn(),
getFiles: jest.fn(),
}));
jest.mock('~/server/services/Artifacts/update', () => ({
findAllArtifacts: jest.fn(),
replaceArtifactContent: jest.fn(),
}));
jest.mock('~/server/middleware/requireJwtAuth', () => (req, res, next) => next());
jest.mock('~/server/middleware', () => {
const validateMessageReq = jest.fn((req, res, next) => next());
const canReadActiveJobConversation = jest.fn().mockResolvedValue(false);
const prepareMessageRequestValidation = jest.fn((req, res, next) => {
req.messageRequestValidation = {
conversationId: 'convo-1',
shouldFetchMessages: true,
promise: Promise.resolve({ ok: true }),
};
next();
});
const sendValidationResponse = jest.fn((res, result) => {
if (result.send) {
return res.status(result.status).send(result.body);
}
return res.status(result.status).json(result.body);
});
return {
requireJwtAuth: (req, res, next) => next(),
validateMessageReq,
canReadActiveJobConversation,
sendValidationResponse,
prepareMessageRequestValidation,
configMiddleware: (req, res, next) => {
req.config = {
filters: {
messages: {
pii: {
starterPatterns: [],
},
},
...(req.headers['x-test-inert-file-policy'] === '1' && {
files: { pii: { fields: ['content'], starterPatterns: [] } },
}),
},
};
next();
},
};
});
jest.mock('~/db/models', () => ({
Message: {
findOne: jest.fn(),
find: jest.fn(),
meiliSearch: jest.fn(),
},
}));
describe('message route conversation ownership filters', () => {
let app;
const {
getConvoOwnership,
getMessage,
getMessages,
getMessagesByCursor,
saveConvo,
saveMessage,
updateMessage,
} = require('~/models');
const {
createContentFilter,
inspectContent,
extractChatContent,
extractStoredMessageContent,
contentFilterBlockResponse,
getContentTraversalFragments,
isContentTraversalProtected,
assertModelBoundContent,
hasActiveFilePolicy,
resolveCanonicalFileReferences,
} = require('@librechat/api');
const {
findAllArtifacts,
replaceArtifactContent,
} = require('~/server/services/Artifacts/update');
const {
canReadActiveJobConversation,
prepareMessageRequestValidation,
} = require('~/server/middleware');
let storedMessageFilterOptions;
const authenticatedUserId = 'user-owner-123';
beforeAll(() => {
const messagesRouter = require('../messages');
storedMessageFilterOptions = createContentFilter.mock.calls[0][0];
app = express();
app.use(express.json());
app.use((req, res, next) => {
req.user = { id: authenticatedUserId };
next();
});
app.use('/api/messages', messagesRouter);
});
beforeEach(() => {
jest.clearAllMocks();
canReadActiveJobConversation.mockResolvedValue(false);
prepareMessageRequestValidation.mockImplementation((req, res, next) => {
req.messageRequestValidation = {
conversationId: 'convo-1',
shouldFetchMessages: true,
promise: Promise.resolve({ ok: true }),
};
next();
});
});
it('preflights opaque content from the direct stored-message request body', () => {
const body = { content: [{ type: 'input_file', file_data: 'opaque-data' }] };
expect(storedMessageFilterOptions.getOpaqueFileInput({ body })).toBe(body);
expect(storedMessageFilterOptions.getMessageRoles({ body: { role: 'system' } })).toEqual([
'system',
]);
});
it.each([
{ name: 'marked user-submitted assistant content', isUserSubmitted: true },
{ name: 'legacy unmarked model content', isUserSubmitted: undefined },
])('preserves provenance when branching $name', async ({ isUserSubmitted }) => {
getMessage.mockResolvedValue({
messageId: 'source-message',
conversationId: 'convo-1',
parentMessageId: 'parent-1',
isCreatedByUser: false,
...(typeof isUserSubmitted === 'boolean' && { isUserSubmitted }),
attachments: [{ file_id: 'file-1' }],
userSubmittedPaths: ['/content/0/text', '/content/1/text', '/attachments/0/file_id'],
userSubmittedMessageFieldPaths: [
{ path: '/content/0/text', field: 'answer' },
{ path: '/content/1/text', field: 'decision_response' },
],
content: [
{ type: 'text', text: 'Different agent content', agentId: 'agent-2' },
{ type: 'text', text: 'Assistant content', agentId: 'agent-1' },
],
});
saveMessage.mockImplementation(async (_ctx, message) => message);
const response = await request(app).post('/api/messages/branch').send({
messageId: 'source-message',
agentId: 'agent-1',
});
expect(response.status).toBe(201);
const savedMessage = saveMessage.mock.calls[0][1];
if (typeof isUserSubmitted === 'boolean') {
expect(savedMessage.isUserSubmitted).toBe(isUserSubmitted);
} else {
expect(savedMessage).not.toHaveProperty('isUserSubmitted');
}
expect(savedMessage.userSubmittedPaths).toEqual(['/attachments/0/file_id', '/content/0/text']);
expect(savedMessage.userSubmittedMessageFieldPaths).toEqual([
{ path: '/content/0/text', field: 'decision_response' },
]);
});
it('does not hydrate branch files for an inert file policy', async () => {
getMessage.mockResolvedValue({
messageId: 'source-message',
conversationId: 'convo-1',
parentMessageId: 'parent-1',
isCreatedByUser: false,
content: [{ type: 'text', text: 'Assistant content', agentId: 'agent-1' }],
});
saveMessage.mockImplementation(async (_ctx, message) => message);
const response = await request(app)
.post('/api/messages/branch')
.set('x-test-inert-file-policy', '1')
.send({ messageId: 'source-message', agentId: 'agent-1' });
expect(response.status).toBe(201);
expect(hasActiveFilePolicy).toHaveBeenCalledWith(
expect.objectContaining({
files: { pii: { fields: ['content'], starterPatterns: [] } },
}),
);
expect(resolveCanonicalFileReferences).not.toHaveBeenCalled();
});
it('rechecks branched user-authored content under the current policy before saving', async () => {
getMessage.mockResolvedValue({
messageId: 'source-message',
conversationId: 'convo-1',
parentMessageId: 'parent-1',
isCreatedByUser: false,
userSubmittedPaths: ['/content/0/text'],
content: [{ type: 'text', text: 'PRIVATE-BRANCH', agentId: 'agent-1' }],
});
const filterError = Object.assign(new Error('blocked'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted content is not allowed.',
source: 'message',
field: 'content_part',
},
});
assertModelBoundContent.mockImplementationOnce(() => {
throw filterError;
});
const response = await request(app).post('/api/messages/branch').send({
messageId: 'source-message',
agentId: 'agent-1',
});
expect(response.status).toBe(400);
expect(response.body).toEqual(filterError.body);
expect(assertModelBoundContent).toHaveBeenCalledWith(
expect.objectContaining({
filters: expect.any(Object),
storedMessages: [
expect.objectContaining({
userSubmittedPaths: ['/content/0/text'],
content: [{ type: 'text', text: 'PRIVATE-BRANCH' }],
}),
],
}),
);
expect(saveMessage).not.toHaveBeenCalled();
});
it('should pass only mutable conversation fields to saveConvo', async () => {
const urlConversationId = '11111111-1111-4111-8111-111111111111';
const bodyConversationId = '22222222-2222-4222-8222-222222222222';
const savedMessage = {
_id: 'message-object-id',
__v: 0,
messageId: 'message-1',
conversationId: urlConversationId,
text: 'hello',
endpoint: 'openAI',
model: 'gpt-5',
iconURL: 'https://example.com/icon.png',
isTemporary: false,
files: [{ file_id: 'file-1' }],
user: authenticatedUserId,
};
saveMessage.mockResolvedValue(savedMessage);
saveConvo.mockResolvedValue({ conversationId: urlConversationId });
const response = await request(app)
.post(`/api/messages/${urlConversationId}`)
.send({
messageId: savedMessage.messageId,
conversationId: bodyConversationId,
text: savedMessage.text,
endpoint: savedMessage.endpoint,
model: savedMessage.model,
iconURL: savedMessage.iconURL,
isUserSubmitted: false,
userSubmittedPaths: ['/forged/model/output'],
userSubmittedMessageFieldPaths: [
{ path: '/forged/model/output', field: 'decision_reason' },
],
});
expect(response.status).toBe(201);
expect(saveMessage).toHaveBeenCalledWith(
expect.objectContaining({ userId: authenticatedUserId }),
expect.objectContaining({
messageId: savedMessage.messageId,
conversationId: urlConversationId,
text: savedMessage.text,
isUserSubmitted: true,
user: authenticatedUserId,
}),
{ context: 'POST /api/messages/:conversationId' },
);
expect(saveMessage.mock.calls[0][1].conversationId).not.toBe(bodyConversationId);
expect(saveMessage.mock.calls[0][1]).not.toHaveProperty('userSubmittedPaths');
expect(saveMessage.mock.calls[0][1]).not.toHaveProperty('userSubmittedMessageFieldPaths');
expect(saveConvo).toHaveBeenCalledWith(
expect.objectContaining({ userId: authenticatedUserId }),
{
conversationId: urlConversationId,
endpoint: savedMessage.endpoint,
model: savedMessage.model,
iconURL: savedMessage.iconURL,
},
{
context: 'POST /api/messages/:conversationId',
appendMessageIds: [savedMessage._id],
},
);
});
it('should filter conversation message reads by authenticated user', async () => {
getMessages.mockResolvedValue([{ messageId: 'message-1', conversationId: 'convo-1' }]);
const response = await request(app).get('/api/messages/convo-1');
expect(response.status).toBe(200);
expect(getMessages).toHaveBeenCalledWith(
{ conversationId: 'convo-1', user: authenticatedUserId },
CLIENT_MESSAGE_SELECT,
);
});
it('returns indistinguishable not-found responses for child and missing query reads', async () => {
getConvoOwnership.mockResolvedValueOnce({
user: authenticatedUserId,
subagentThread: { parentConversationId: 'parent-convo' },
});
const childResponse = await request(app).get(
'/api/messages?conversationId=child-convo&messageId=child-message',
);
getConvoOwnership.mockResolvedValueOnce(null);
const missingResponse = await request(app).get(
'/api/messages?conversationId=missing-convo&messageId=missing-message',
);
expect(childResponse.status).toBe(404);
expect(childResponse.body).toEqual({ error: 'Conversation not found' });
expect(childResponse.status).toBe(missingResponse.status);
expect(childResponse.body).toEqual(missingResponse.body);
expect(getMessages).toHaveBeenCalledTimes(2);
});
it.each([
{
name: 'single-message',
path: '/api/messages?conversationId=convo-1&messageId=message-1',
readMock: getMessages,
readResult: [{ messageId: 'message-1', conversationId: 'convo-1' }],
},
{
name: 'cursor',
path: '/api/messages?conversationId=convo-1',
readMock: getMessagesByCursor,
readResult: { messages: [], nextCursor: null },
},
])(
'starts the $name query read before ownership validation resolves',
async ({ path, readMock, readResult }) => {
const events = [];
let resolveOwnership;
const ownershipPromise = new Promise((resolve) => {
resolveOwnership = resolve;
});
getConvoOwnership.mockImplementationOnce(() => {
events.push('ownership-started');
return ownershipPromise;
});
let resolveReadStarted;
const readStartedPromise = new Promise((resolve) => {
resolveReadStarted = resolve;
});
readMock.mockImplementationOnce(() => {
events.push('messages-started');
resolveReadStarted();
return Promise.resolve(readResult);
});
const responsePromise = new Promise((resolve, reject) => {
request(app)
.get(path)
.end((error, response) => (error ? reject(error) : resolve(response)));
});
await Promise.race([readStartedPromise, new Promise((resolve) => setTimeout(resolve, 100))]);
const eventsBeforeValidation = [...events];
resolveOwnership({ user: authenticatedUserId });
const response = await responsePromise;
expect(eventsBeforeValidation).toEqual(['ownership-started', 'messages-started']);
expect(response.status).toBe(200);
},
);
it('allows an ordinary owned conversation query read', async () => {
getConvoOwnership.mockResolvedValue({ user: authenticatedUserId });
getMessages.mockResolvedValue([{ messageId: 'message-1', conversationId: 'convo-1' }]);
const response = await request(app).get(
'/api/messages?conversationId=convo-1&messageId=message-1',
);
expect(response.status).toBe(200);
expect(response.body.messages).toEqual([{ messageId: 'message-1', conversationId: 'convo-1' }]);
});
it('allows an owner-scoped active generation before its conversation row exists', async () => {
getConvoOwnership.mockResolvedValue(null);
canReadActiveJobConversation.mockResolvedValue(true);
getMessagesByCursor.mockResolvedValue({
messages: [{ messageId: 'prompt-1', conversationId: 'active-convo' }],
nextCursor: null,
});
const response = await request(app).get('/api/messages?conversationId=active-convo');
expect(response.status).toBe(200);
expect(response.body.messages).toEqual([
{ messageId: 'prompt-1', conversationId: 'active-convo' },
]);
expect(canReadActiveJobConversation).toHaveBeenCalledWith(
expect.objectContaining({ user: { id: authenticatedUserId } }),
'active-convo',
);
});
it('should start conversation message reads before validation resolves', async () => {
const events = [];
let resolveValidation;
const validationPromise = new Promise((resolve) => {
resolveValidation = resolve;
});
prepareMessageRequestValidation.mockImplementationOnce((req, res, next) => {
req.messageRequestValidation = {
conversationId: 'convo-1',
shouldFetchMessages: true,
promise: validationPromise,
};
next();
});
let resolveMessagesStarted;
const messagesStartedPromise = new Promise((resolve) => {
resolveMessagesStarted = resolve;
});
getMessages.mockImplementation(() => {
events.push('messages-started');
resolveMessagesStarted();
return Promise.resolve([{ messageId: 'message-1', conversationId: 'convo-1' }]);
});
const responsePromise = new Promise((resolve, reject) => {
request(app)
.get('/api/messages/convo-1')
.end((error, response) => (error ? reject(error) : resolve(response)));
});
await Promise.race([
messagesStartedPromise,
new Promise((resolve) => setTimeout(resolve, 100)),
]);
const eventsBeforeValidation = [...events];
resolveValidation({ ok: true });
const response = await responsePromise;
expect(eventsBeforeValidation).toEqual(['messages-started']);
expect(getMessages).toHaveBeenCalledWith(
{ conversationId: 'convo-1', user: authenticatedUserId },
CLIENT_MESSAGE_SELECT,
);
expect(response.status).toBe(200);
expect(response.body).toEqual([{ messageId: 'message-1', conversationId: 'convo-1' }]);
});
it('should not return fetched messages when conversation validation fails', async () => {
prepareMessageRequestValidation.mockImplementationOnce((req, res, next) => {
req.messageRequestValidation = {
conversationId: 'convo-1',
shouldFetchMessages: true,
promise: Promise.resolve({
ok: false,
status: 404,
body: { error: 'Conversation not found' },
}),
};
next();
});
getMessages.mockResolvedValue([{ messageId: 'secret-message', conversationId: 'convo-1' }]);
const response = await request(app).get('/api/messages/convo-1');
expect(getMessages).toHaveBeenCalledWith(
{ conversationId: 'convo-1', user: authenticatedUserId },
CLIENT_MESSAGE_SELECT,
);
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'Conversation not found' });
});
it('should filter single message reads by authenticated user', async () => {
getMessages.mockResolvedValue([{ messageId: 'message-1', conversationId: 'convo-1' }]);
const response = await request(app).get('/api/messages/convo-1/message-1');
expect(response.status).toBe(200);
expect(getMessages).toHaveBeenCalledWith(
{ conversationId: 'convo-1', messageId: 'message-1', user: authenticatedUserId },
CLIENT_MESSAGE_SELECT,
);
});
it('should classify indexed text edits as message content parts', async () => {
getMessages.mockResolvedValue([
{
conversationId: 'convo-1',
content: [{ type: 'text', text: 'old content' }],
tokenCount: 10,
userSubmittedPaths: ['/summary'],
},
]);
updateMessage.mockResolvedValue({ messageId: 'message-1' });
const response = await request(app).put('/api/messages/convo-1/message-1').send({
text: 'new content',
index: 0,
model: 'test-model',
});
expect(response.status).toBe(200);
expect(extractStoredMessageContent).toHaveBeenCalledWith({
content: [{ text: 'new content' }],
});
expect(updateMessage).toHaveBeenCalledWith(authenticatedUserId, {
messageId: 'message-1',
content: [{ type: 'text', text: 'new content' }],
tokenCount: 10,
userSubmittedPaths: ['/summary', '/content/0/text'],
});
});
it('marks direct assistant text edits as user-submitted', async () => {
getMessages.mockResolvedValue([
{
conversationId: 'convo-1',
isCreatedByUser: false,
quotes: [],
userSubmittedPaths: ['/content/0/text'],
},
]);
updateMessage.mockResolvedValue({ messageId: 'message-1' });
const response = await request(app).put('/api/messages/convo-1/message-1').send({
text: 'User replacement for assistant prose',
model: 'test-model',
});
expect(response.status).toBe(200);
expect(updateMessage).toHaveBeenCalledWith(authenticatedUserId, {
messageId: 'message-1',
text: 'User replacement for assistant prose',
tokenCount: 10,
userSubmittedPaths: ['/content/0/text', '/text'],
});
});
it('filters the finalized indexed edit without rescanning persisted siblings', async () => {
const finding = {
detectorId: 'pii-pattern',
ruleId: 'assembled-secret',
label: 'assembled secret',
source: 'assembled_context',
field: 'assembled_context',
};
getMessages.mockResolvedValue([
{
conversationId: 'convo-1',
content: [
{ type: 'text', text: 'api-key:' },
{ type: 'text', text: 'old value' },
],
tokenCount: 10,
},
]);
inspectContent.mockReturnValueOnce(null).mockReturnValueOnce(finding);
contentFilterBlockResponse.mockReturnValue({
error: 'content_filter_block',
message: 'Submitted content is blocked.',
source: 'assembled_context',
field: 'assembled_context',
});
const response = await request(app).put('/api/messages/convo-1/message-1').send({
text: 'secret',
index: 1,
model: 'test-model',
});
expect(response.status).toBe(400);
expect(extractStoredMessageContent).toHaveBeenLastCalledWith({
content: [{ type: 'text', text: 'secret' }],
});
expect(updateMessage).not.toHaveBeenCalled();
});
it('filters partial fragments when indexed stored-message traversal exceeds its bound', async () => {
const partialFragment = {
id: 'stored-message.content.1.nested.0',
path: '/content/1/output/secret',
text: 'PRIVATE-PARTIAL',
source: 'message',
field: 'content_part',
format: 'plain',
treatment: 'send',
};
const traversalError = Object.assign(new Error('Traversal limit exceeded'), {
code: 'content_filter_uninspectable',
statusCode: 400,
body: {
error: 'content_filter_uninspectable',
message: 'Submitted content could not be completely inspected before processing.',
source: 'message',
field: 'content_part',
},
fragments: [partialFragment],
});
const finding = {
detectorId: 'pii-pattern',
ruleId: 'partial-secret',
label: 'protected value',
source: 'message',
field: 'content_part',
};
getMessages.mockResolvedValue([
{
conversationId: 'convo-1',
content: [
{ type: 'text', text: 'old content' },
{ type: 'text', text: 'existing output' },
],
tokenCount: 10,
},
]);
extractStoredMessageContent.mockReturnValueOnce([]).mockImplementationOnce(() => {
throw traversalError;
});
inspectContent.mockReturnValueOnce(null).mockReturnValueOnce(finding);
contentFilterBlockResponse.mockReturnValue({
error: 'content_filter_block',
message: 'Submitted content is blocked.',
source: 'message',
field: 'content_part',
});
const response = await request(app).put('/api/messages/convo-1/message-1').send({
text: 'replacement',
index: 0,
model: 'test-model',
});
expect(response.status).toBe(400);
expect(getContentTraversalFragments).toHaveBeenCalledWith(traversalError);
expect(inspectContent).toHaveBeenLastCalledWith([partialFragment], {
filters: expect.any(Object),
});
expect(isContentTraversalProtected).not.toHaveBeenCalled();
expect(updateMessage).not.toHaveBeenCalled();
});
it('allows indexed edits when a bounded traversal only affects unprotected scopes', async () => {
const traversalError = Object.assign(new Error('Traversal limit exceeded'), {
code: 'content_filter_uninspectable',
statusCode: 400,
body: {
error: 'content_filter_uninspectable',
message: 'Submitted content could not be completely inspected before processing.',
source: 'tool_argument',
field: 'output',
},
fragments: [],
});
getMessages.mockResolvedValue([
{
conversationId: 'convo-1',
content: [
{ type: 'text', text: 'old content' },
{ type: 'text', text: 'existing output' },
],
tokenCount: 10,
},
]);
updateMessage.mockResolvedValue({ messageId: 'message-1' });
extractStoredMessageContent.mockReturnValueOnce([]).mockImplementationOnce(() => {
throw traversalError;
});
isContentTraversalProtected.mockReturnValueOnce(false);
const response = await request(app).put('/api/messages/convo-1/message-1').send({
text: 'replacement',
index: 0,
model: 'test-model',
});
expect(response.status).toBe(200);
expect(isContentTraversalProtected).toHaveBeenCalledWith({
error: traversalError,
filters: expect.any(Object),
});
expect(updateMessage).toHaveBeenCalled();
});
it('filters the final artifact text assembled from existing and submitted content', async () => {
const finding = {
detectorId: 'pii-pattern',
ruleId: 'secret',
label: 'protected value',
source: 'message',
field: 'content_part',
};
getMessage.mockResolvedValue({
conversationId: 'convo-1',
content: [{ type: 'text', text: 'existing artifact' }],
text: '',
userSubmittedPaths: ['/content/1/steer'],
});
findAllArtifacts.mockReturnValue([{ source: 'content', partIndex: 0 }]);
replaceArtifactContent.mockReturnValue('sk-secret');
inspectContent.mockReturnValueOnce(null).mockReturnValueOnce(finding);
contentFilterBlockResponse.mockReturnValue({
error: 'content_filter_block',
message: 'Submitted content is blocked.',
source: 'message',
field: 'content_part',
});
const response = await request(app).post('/api/messages/artifact/message-1').send({
index: 0,
original: 'existing',
updated: 'secret',
});
expect(response.status).toBe(400);
expect(extractStoredMessageContent).toHaveBeenLastCalledWith({
content: [{ text: 'sk-secret' }],
});
expect(saveMessage).not.toHaveBeenCalled();
});
it('returns a raw-free traversal response when artifact content cannot be fully inspected', async () => {
const traversalError = Object.assign(new Error('Traversal limit exceeded'), {
code: 'content_filter_uninspectable',
statusCode: 400,
body: {
error: 'content_filter_uninspectable',
message: 'Submitted content could not be completely inspected before processing.',
source: 'message',
field: 'content_part',
},
fragments: [],
});
getMessage.mockResolvedValue({
conversationId: 'convo-1',
content: [{ type: 'text', text: 'existing artifact' }],
text: '',
});
findAllArtifacts.mockReturnValue([{ source: 'content', partIndex: 0 }]);
replaceArtifactContent.mockReturnValue('updated artifact');
extractStoredMessageContent.mockReturnValueOnce([]).mockImplementationOnce(() => {
throw traversalError;
});
isContentTraversalProtected.mockReturnValueOnce(true);
const response = await request(app).post('/api/messages/artifact/message-1').send({
index: 0,
original: 'existing',
updated: 'replacement',
});
expect(response.status).toBe(400);
expect(response.body).toEqual(traversalError.body);
expect(response.text).not.toContain('updated artifact');
expect(isContentTraversalProtected).toHaveBeenCalledWith({
error: traversalError,
filters: expect.any(Object),
});
expect(saveMessage).not.toHaveBeenCalled();
});
it('marks successful assistant artifact edits as user-submitted', async () => {
getMessage.mockResolvedValue({
conversationId: 'convo-1',
content: [{ type: 'text', text: 'existing artifact' }],
text: '',
userSubmittedPaths: ['/content/1/steer'],
});
findAllArtifacts.mockReturnValue([{ source: 'content', partIndex: 0 }]);
replaceArtifactContent.mockReturnValue('updated artifact');
saveMessage.mockImplementation(async (_ctx, message) => message);
const response = await request(app).post('/api/messages/artifact/message-1').send({
index: 0,
original: 'existing',
updated: 'updated',
});
expect(response.status).toBe(200);
expect(saveMessage).toHaveBeenCalledWith(
expect.objectContaining({ userId: authenticatedUserId }),
expect.objectContaining({
messageId: 'message-1',
content: [{ type: 'text', text: 'updated artifact' }],
userSubmittedPaths: ['/content/1/steer', '/content/0/text'],
}),
{ context: 'POST /api/messages/artifact/:messageId' },
);
});
it('filters edited text after recombining persisted user quotes', async () => {
const finding = {
detectorId: 'pii-pattern',
ruleId: 'assembled-secret',
label: 'assembled secret',
source: 'assembled_context',
field: 'assembled_context',
};
getMessages.mockResolvedValue([
{
conversationId: 'convo-1',
quotes: ['api-key:'],
isCreatedByUser: true,
},
]);
inspectContent.mockReturnValueOnce(null).mockReturnValueOnce(finding);
contentFilterBlockResponse.mockReturnValue({
error: 'content_filter_block',
message: 'Submitted content is blocked.',
source: 'assembled_context',
field: 'assembled_context',
});
const response = await request(app).put('/api/messages/convo-1/message-1').send({
text: 'secret',
model: 'test-model',
});
expect(response.status).toBe(400);
expect(extractChatContent).toHaveBeenCalledWith({
text: 'secret',
quotes: ['api-key:'],
});
expect(updateMessage).not.toHaveBeenCalled();
});
});