fix(search): omit referenced message from search results (#2172)

This commit is contained in:
Hampus
2026-08-30 22:42:07 +02:00
committed by GitHub
parent 169088df26
commit bf080cb001
2 changed files with 125 additions and 2 deletions
+15 -2
View File
@@ -3,7 +3,7 @@
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
import type {GlobalSearchMessagesRequest} from '@fluxer/schema/src/domains/message/MessageRequestSchemas';
import type {MessageSearchResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
import type {MessageResponse, MessageSearchResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
import type {IWorkerService} from '@pkgs/worker/src/contracts/IWorkerService';
import {createChannelID, createGuildID, type UserID} from '../BrandedTypes';
import type {IChannelRepository} from '../channel/IChannelRepository';
@@ -15,6 +15,19 @@ import type {IUserRepository} from '../user/IUserRepository';
import type {WorkerTaskName} from '../worker/WorkerLaneConfig';
import {GlobalSearchService} from './GlobalSearchService';
function omitReferencedMessages(result: MessageSearchResponse): MessageSearchResponse {
if (!('messages' in result)) {
return result;
}
return {
...result,
messages: result.messages.map((message) => {
const {referenced_message: _referencedMessage, ...rest} = message as MessageResponse;
return rest;
}),
};
}
export class SearchService {
private readonly globalSearch: GlobalSearchService;
private readonly channelService: ChannelService;
@@ -112,6 +125,6 @@ export class SearchService {
}
break;
}
return result;
return omitReferencedMessages(result);
}
}
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {MessageReferenceTypes} from '@fluxer/constants/src/ChannelConstants';
import type {
MessageResponse,
MessageSearchResponse,
MessageSearchResultsResponse,
} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest';
import {createTestAccount} from '../../auth/tests/AuthTestUtils';
import {createGuild} from '../../guild/tests/GuildTestUtils';
import {markChannelAsIndexed, markGuildChannelsAsIndexed, sendMessage} from '../../message/tests/MessageTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
import {HTTP_STATUS} from '../../test/TestConstants';
import {createBuilder} from '../../test/TestRequestBuilder';
import {SearchService} from '../SearchService';
function isSearchResult(response: MessageSearchResponse): response is MessageSearchResultsResponse {
return 'messages' in response;
}
describe('Message Search Referenced Message', () => {
let harness: ApiTestHarness;
beforeEach(async () => {
harness = await createApiTestHarness({search: 'enabled'});
});
afterEach(async () => {
vi.restoreAllMocks();
await harness.shutdown();
});
async function sendReply(
token: string,
channelId: string,
guildId: string,
content: string,
targetMessageId: string,
): Promise<MessageResponse> {
return createBuilder<MessageResponse>(harness, token)
.post(`/channels/${channelId}/messages`)
.body({
content,
message_reference: {
message_id: targetMessageId,
channel_id: channelId,
guild_id: guildId,
type: MessageReferenceTypes.DEFAULT,
},
})
.expect(HTTP_STATUS.OK)
.execute();
}
async function searchAndCaptureServiceResult(
token: string,
body: Record<string, unknown>,
): Promise<MessageSearchResponse> {
const spy = vi.spyOn(SearchService.prototype, 'searchMessages');
try {
await createBuilder(harness, token).post('/search/messages').body(body).expect(HTTP_STATUS.OK).execute();
const [invocation] = spy.mock.results;
if (!invocation || invocation.type !== 'return') {
throw new Error('SearchService.searchMessages did not return a result');
}
return await invocation.value;
} finally {
spy.mockRestore();
}
}
test('channel scope omits referenced_message from reply hits', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Reply Search Channel Scope');
const channelId = guild.system_channel_id!;
const marker = `reply-search-${Date.now()}`;
const original = await sendMessage(harness, account.token, channelId, `${marker} original`);
const reply = await sendReply(account.token, channelId, guild.id, `${marker} answer`, original.id);
await markChannelAsIndexed(harness, channelId);
const result = await searchAndCaptureServiceResult(account.token, {
content: `${marker} answer`,
context_channel_id: channelId,
});
expect(isSearchResult(result)).toBe(true);
if (!isSearchResult(result)) return;
const hit = result.messages.find((message) => message.id === reply.id);
expect(hit).toBeDefined();
expect(hit?.message_reference?.message_id).toBe(original.id);
expect(Object.hasOwn(hit!, 'referenced_message')).toBe(false);
});
test('guild scope omits referenced_message from reply hits', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Reply Search Guild Scope');
const channelId = guild.system_channel_id!;
const marker = `reply-guild-search-${Date.now()}`;
const original = await sendMessage(harness, account.token, channelId, `${marker} original`);
const reply = await sendReply(account.token, channelId, guild.id, `${marker} answer`, original.id);
await markGuildChannelsAsIndexed(harness, account.token, guild.id);
const result = await searchAndCaptureServiceResult(account.token, {
content: `${marker} answer`,
context_guild_id: guild.id,
});
expect(isSearchResult(result)).toBe(true);
if (!isSearchResult(result)) return;
const hit = result.messages.find((message) => message.id === reply.id);
expect(hit).toBeDefined();
expect(hit?.message_reference?.message_id).toBe(original.id);
expect(Object.hasOwn(hit!, 'referenced_message')).toBe(false);
});
});