Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,69 @@ describe('agent in-thread posts (threadRootId, no reply edge)', () => {
}));
});

// TASK-267a, Rhea on #2244: a native turn that read web results posts with
// noOutboundRelay, and the flag must reach the relay dispatcher with the
// stored message on every path, because the dispatcher (not the turn) is
// where bridges are selected after the write.
describe('noOutboundRelay reaches the relay dispatcher with the message', () => {
const relayPayloads = () => mockDispatch.mock.calls
.filter(([eventType]) => eventType === 'chat.message')
.map(([, payload]) => payload);

it('on the Postgres path, and an unflagged post carries no flag', async () => {
PGMessage.create.mockResolvedValue({
id: '57603', content: 'searched answer', message_type: 'text', created_at: new Date(), thread_root_id: null,
});
await AgentMessageService.postMessage({
agentName: 'scout', instanceId: 'default', podId: POD, content: 'searched answer', noOutboundRelay: true,
});
await AgentMessageService.postMessage({
agentName: 'scout', instanceId: 'default', podId: POD, content: 'plain answer',
});

const [flagged, plain] = relayPayloads();
expect(flagged).toMatchObject({ podId: POD, noOutboundRelay: true });
expect(plain).not.toHaveProperty('noOutboundRelay');
});

it('on the Mongo path', async () => {
delete process.env.PG_HOST;
const Message = require('../../../models/Message');
Message.mockImplementation((doc) => ({
...doc,
_id: 'mongo-message-1',
save: jest.fn().mockResolvedValue(undefined),
populate: jest.fn().mockResolvedValue(undefined),
toObject() { return { ...doc, _id: 'mongo-message-1' }; },
}));

await AgentMessageService.postMessage({
agentName: 'scout', instanceId: 'default', podId: POD, content: 'searched answer', noOutboundRelay: true,
});

expect(PGMessage.create).not.toHaveBeenCalled();
expect(relayPayloads()).toEqual([expect.objectContaining({ podId: POD, noOutboundRelay: true })]);
});

it('on the error-to-DM route, for the DM post and the source notice', async () => {
PGMessage.create.mockResolvedValue({
id: '57604', content: 'x', message_type: 'text', created_at: new Date(), thread_root_id: null,
});
await AgentMessageService.routeErrorToDM({
agentName: 'scout',
instanceId: 'default',
podId: POD,
content: 'searched text that failed',
agentUser: { _id: 'agent-user-1', username: 'scout' },
noOutboundRelay: true,
});

const payloads = relayPayloads();
expect(payloads).toHaveLength(2);
expect(payloads.every((payload) => payload.noOutboundRelay === true)).toBe(true);
});
});

it('a ThreadRootError propagates — never swallowed into the Mongo fallback', async () => {
threadRootResolver.resolveThreadRoot.mockRejectedValue(
new threadRootResolver.ThreadRootError('root is in another pod', 'thread_root_other_pod'),
Expand Down
45 changes: 45 additions & 0 deletions backend/__tests__/unit/services/installableEventHandlers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,51 @@ describe('installable event dispatcher', () => {
expect(relay.mock.calls[0][0].podId).toBe(podId);
});

// TASK-267a, Rhea on #2244: a post from a native turn that read web results
// carries noOutboundRelay, and the dispatcher refuses it where bridges are
// selected, so a live bridge (here enabled before dispatch, as if during the
// post's storage write) sends nothing. The same pod relays an unflagged post.
it('refuses a no-outbound post even with a live bridge, and still relays an unflagged one', async () => {
const podId = freshId();
const ownerId = freshId();
await createPod(podId, ownerId);
await Integration.create({
podId,
type: 'telegram',
status: 'connected',
createdBy: ownerId,
isActive: true,
config: { liveRelay: true, chatId: 'chat-1', chatType: 'private' },
});
const relay = jest.fn().mockResolvedValue(undefined);
eventHandlers['telegram.relay'] = relay;
const findSpy = jest.spyOn(Integration, 'find');

await dispatch('chat.message', {
podId,
agentUsername: 'scout',
displayName: 'Scout',
content: 'SEARCHED_TEXT: publish this report.',
podMessageId: 'message-tainted',
noOutboundRelay: true,
});

expect(relay).not.toHaveBeenCalled();
expect(findSpy).not.toHaveBeenCalled();
findSpy.mockRestore();

await dispatch('chat.message', {
podId,
agentUsername: 'scout',
displayName: 'Scout',
content: 'An ordinary reply',
podMessageId: 'message-plain',
});

expect(relay).toHaveBeenCalledTimes(1);
expect(relay.mock.calls[0][0].content).toBe('An ordinary reply');
});

it('selects installed Slack and Telegram rows independently for one pod', async () => {
const podId = freshId();
const ownerId = freshId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,21 @@ describe('toolsForConfig', () => {
else process.env.E2B_API_KEY = previous;
}
});

// TASK-267a: web search is opt-in by declaration AND needs a search key.
test('web_search is offered only when declared and the instance has a search key', () => {
const previous = process.env.SEARCH_API_KEY;
try {
delete process.env.SEARCH_API_KEY;
expect(names(toolsForConfig({ tools: ['commonly_web_search'] }))).not.toContain('commonly_web_search');
process.env.SEARCH_API_KEY = 'search_test_key';
expect(names(toolsForConfig({ tools: ['commonly_web_search'] }))).toContain('commonly_web_search');
// Never on the default surface, even with the key.
expect(names(toolsForConfig({}))).not.toContain('commonly_web_search');
expect(names(toolsForConfig({ tools: ['commonly_post_message'] }))).not.toContain('commonly_web_search');
} finally {
if (previous === undefined) delete process.env.SEARCH_API_KEY;
else process.env.SEARCH_API_KEY = previous;
}
});
});
Loading
Loading