Skip to content

Commit c30e3b2

Browse files
committed
refactor(server): align transport naming and fix double message processing
1 parent 24e79b1 commit c30e3b2

File tree

9 files changed

+1062
-66
lines changed

9 files changed

+1062
-66
lines changed
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
/**
2+
* HTTP Transport Integration Tests
3+
*
4+
* Tests the HTTP transport for the messaging API:
5+
* - Synchronous request/response flow
6+
* - Agent response included in HTTP response
7+
* - Error handling
8+
*
9+
* HTTP transport is used when clients want immediate synchronous responses
10+
* without WebSocket or SSE streaming.
11+
*
12+
* Note: elizaOS.handleMessage is mocked to avoid LLM costs while testing
13+
* the full request/response flow.
14+
*/
15+
16+
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
17+
import type { UUID, ElizaOS, HandleMessageResult } from '@elizaos/core';
18+
19+
import { TestServerFixture, AgentFixture, CharacterBuilder, stringToUuid } from '../index';
20+
21+
describe('HTTP Transport Integration', () => {
22+
let serverFixture: TestServerFixture;
23+
let agentFixture: AgentFixture;
24+
let baseUrl: string;
25+
let agentId: UUID;
26+
let sessionId: UUID;
27+
let originalHandleMessage: ElizaOS['handleMessage'];
28+
29+
beforeAll(async () => {
30+
// Arrange - Setup server
31+
serverFixture = new TestServerFixture();
32+
const { port } = await serverFixture.setup();
33+
baseUrl = `http://localhost:${port}`;
34+
35+
// Arrange - Create test agent
36+
agentFixture = new AgentFixture(serverFixture.getServer());
37+
const { runtime } = await agentFixture.setup({
38+
character: new CharacterBuilder()
39+
.withName('HTTP Test Agent')
40+
.withBio(['A test agent for HTTP transport'])
41+
.build(),
42+
});
43+
44+
agentId = runtime.agentId;
45+
46+
// Mock elizaOS.handleMessage to avoid LLM calls
47+
// This is the ONLY mock - everything else is real
48+
const elizaOS = serverFixture.getServer().elizaOS!;
49+
originalHandleMessage = elizaOS.handleMessage.bind(elizaOS);
50+
elizaOS.handleMessage = async (
51+
_agentId: UUID,
52+
message: any,
53+
_options?: unknown
54+
): Promise<HandleMessageResult> => {
55+
const mockMessageId = stringToUuid(`mock-msg-${Date.now()}`);
56+
return {
57+
messageId: mockMessageId,
58+
userMessage: {
59+
id: mockMessageId,
60+
entityId: message.entityId,
61+
agentId: _agentId,
62+
roomId: message.roomId,
63+
content: message.content,
64+
createdAt: Date.now(),
65+
} as any,
66+
processing: {
67+
didRespond: true,
68+
responseContent: {
69+
text: 'Mocked HTTP response from agent',
70+
thought: 'Test thought',
71+
},
72+
responseMessages: [],
73+
state: {} as any,
74+
},
75+
};
76+
};
77+
78+
// Create a session for testing
79+
const userId = stringToUuid(`http-test-user-${Date.now()}`);
80+
const sessionResponse = await fetch(`${baseUrl}/api/messaging/sessions`, {
81+
method: 'POST',
82+
headers: { 'Content-Type': 'application/json' },
83+
body: JSON.stringify({
84+
agentId,
85+
userId,
86+
}),
87+
});
88+
89+
const sessionData = await sessionResponse.json();
90+
sessionId = sessionData.sessionId;
91+
92+
await new Promise((resolve) => setTimeout(resolve, 500));
93+
}, 30000);
94+
95+
afterAll(async () => {
96+
// Restore original handleMessage
97+
if (originalHandleMessage && serverFixture.getServer().elizaOS) {
98+
serverFixture.getServer().elizaOS!.handleMessage = originalHandleMessage;
99+
}
100+
await agentFixture.cleanup();
101+
await serverFixture.cleanup();
102+
});
103+
104+
describe('POST /sessions/:sessionId/messages with transport=http', () => {
105+
it('should return synchronous response with agentResponse', async () => {
106+
// Arrange
107+
const authorId = stringToUuid('http-test-user');
108+
109+
// Act
110+
const response = await fetch(`${baseUrl}/api/messaging/sessions/${sessionId}/messages`, {
111+
method: 'POST',
112+
headers: { 'Content-Type': 'application/json' },
113+
body: JSON.stringify({
114+
content: 'Hello via HTTP transport',
115+
author_id: authorId,
116+
transport: 'http',
117+
}),
118+
});
119+
120+
// Assert
121+
expect(response.status).toBe(201);
122+
123+
const data = await response.json();
124+
expect(data.success).toBe(true);
125+
expect(data.userMessage).toBeDefined();
126+
expect(data.userMessage.content).toBe('Hello via HTTP transport');
127+
expect(data.agentResponse).toBeDefined();
128+
expect(data.agentResponse.text).toBe('Mocked HTTP response from agent');
129+
});
130+
131+
it('should handle legacy sync mode for backward compatibility', async () => {
132+
// Arrange
133+
const authorId = stringToUuid('http-legacy-user');
134+
135+
// Act - Using legacy 'mode: sync'
136+
const response = await fetch(`${baseUrl}/api/messaging/sessions/${sessionId}/messages`, {
137+
method: 'POST',
138+
headers: { 'Content-Type': 'application/json' },
139+
body: JSON.stringify({
140+
content: 'Hello via legacy sync mode',
141+
author_id: authorId,
142+
mode: 'sync', // Legacy parameter
143+
}),
144+
});
145+
146+
// Assert - Should work same as transport=http
147+
expect(response.status).toBe(201);
148+
149+
const data = await response.json();
150+
expect(data.success).toBe(true);
151+
expect(data.agentResponse).toBeDefined();
152+
});
153+
154+
it('should return error for invalid transport', async () => {
155+
// Arrange
156+
const authorId = stringToUuid('http-invalid-user');
157+
158+
// Act
159+
const response = await fetch(`${baseUrl}/api/messaging/sessions/${sessionId}/messages`, {
160+
method: 'POST',
161+
headers: { 'Content-Type': 'application/json' },
162+
body: JSON.stringify({
163+
content: 'Test message',
164+
author_id: authorId,
165+
transport: 'invalid_transport',
166+
}),
167+
});
168+
169+
// Assert
170+
expect(response.status).toBe(400);
171+
172+
const data = await response.json();
173+
expect(data.error.message).toContain('Invalid transport');
174+
});
175+
176+
it('should reject non-string transport parameter', async () => {
177+
// Arrange
178+
const authorId = stringToUuid('http-non-string-user');
179+
180+
// Act
181+
const response = await fetch(`${baseUrl}/api/messaging/sessions/${sessionId}/messages`, {
182+
method: 'POST',
183+
headers: { 'Content-Type': 'application/json' },
184+
body: JSON.stringify({
185+
content: 'Test message',
186+
author_id: authorId,
187+
transport: 123, // Invalid - should be string
188+
}),
189+
});
190+
191+
// Assert
192+
expect(response.status).toBe(400);
193+
194+
const data = await response.json();
195+
expect(data.error.message).toContain('Transport must be a string');
196+
});
197+
});
198+
199+
describe('Error Handling', () => {
200+
it('should return 400 for missing content', async () => {
201+
// Arrange
202+
const authorId = stringToUuid('http-no-content-user');
203+
204+
// Act
205+
const response = await fetch(`${baseUrl}/api/messaging/sessions/${sessionId}/messages`, {
206+
method: 'POST',
207+
headers: { 'Content-Type': 'application/json' },
208+
body: JSON.stringify({
209+
author_id: authorId,
210+
transport: 'http',
211+
// Missing content
212+
}),
213+
});
214+
215+
// Assert
216+
expect(response.status).toBe(400);
217+
});
218+
219+
it('should return 404 for non-existent session', async () => {
220+
// Arrange
221+
const fakeSessionId = stringToUuid('non-existent-session');
222+
const authorId = stringToUuid('http-test-user');
223+
224+
// Act
225+
const response = await fetch(`${baseUrl}/api/messaging/sessions/${fakeSessionId}/messages`, {
226+
method: 'POST',
227+
headers: { 'Content-Type': 'application/json' },
228+
body: JSON.stringify({
229+
content: 'Test message',
230+
author_id: authorId,
231+
transport: 'http',
232+
}),
233+
});
234+
235+
// Assert
236+
expect(response.status).toBe(404);
237+
});
238+
});
239+
240+
describe('Response Format', () => {
241+
it('should include userMessage with correct structure', async () => {
242+
// Arrange
243+
const authorId = stringToUuid('http-format-user');
244+
245+
// Act
246+
const response = await fetch(`${baseUrl}/api/messaging/sessions/${sessionId}/messages`, {
247+
method: 'POST',
248+
headers: { 'Content-Type': 'application/json' },
249+
body: JSON.stringify({
250+
content: 'Format test message',
251+
author_id: authorId,
252+
transport: 'http',
253+
}),
254+
});
255+
256+
const data = await response.json();
257+
258+
// Assert
259+
expect(response.status).toBe(201);
260+
expect(data.userMessage).toHaveProperty('id');
261+
expect(data.userMessage).toHaveProperty('content');
262+
expect(data.userMessage).toHaveProperty('authorId');
263+
expect(data.userMessage).toHaveProperty('createdAt');
264+
});
265+
266+
it('should include agentResponse with text', async () => {
267+
// Arrange
268+
const authorId = stringToUuid('http-agent-response-user');
269+
270+
// Act
271+
const response = await fetch(`${baseUrl}/api/messaging/sessions/${sessionId}/messages`, {
272+
method: 'POST',
273+
headers: { 'Content-Type': 'application/json' },
274+
body: JSON.stringify({
275+
content: 'Get agent response',
276+
author_id: authorId,
277+
transport: 'http',
278+
}),
279+
});
280+
281+
const data = await response.json();
282+
283+
// Assert
284+
expect(response.status).toBe(201);
285+
expect(data.agentResponse).toBeDefined();
286+
expect(data.agentResponse).toHaveProperty('text');
287+
});
288+
289+
it('should include sessionStatus in response', async () => {
290+
// Arrange
291+
const authorId = stringToUuid('http-session-status-user');
292+
293+
// Act
294+
const response = await fetch(`${baseUrl}/api/messaging/sessions/${sessionId}/messages`, {
295+
method: 'POST',
296+
headers: { 'Content-Type': 'application/json' },
297+
body: JSON.stringify({
298+
content: 'Check session status',
299+
author_id: authorId,
300+
transport: 'http',
301+
}),
302+
});
303+
304+
const data = await response.json();
305+
306+
// Assert
307+
expect(response.status).toBe(201);
308+
expect(data.sessionStatus).toBeDefined();
309+
expect(data.sessionStatus).toHaveProperty('expiresAt');
310+
});
311+
});
312+
});

packages/server/src/__tests__/integration/socketio-message-flow.test.ts renamed to packages/server/src/__tests__/integration/socketio-infrastructure.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
/**
2-
* Integration tests for Socket.IO end-to-end message flow
2+
* Socket.IO Infrastructure Tests
3+
*
4+
* Tests the Socket.IO server infrastructure:
5+
* - Connection and authentication
6+
* - Room/channel joining and management
7+
* - Message broadcasting between clients
8+
* - Log streaming subscriptions
9+
* - Error handling and disconnection
10+
*
11+
* These tests verify Socket.IO mechanics, NOT the transport layer API.
12+
* For transport-specific tests, see:
13+
* - http-transport.test.ts
14+
* - sse-transport.test.ts
15+
* - websocket-transport.test.ts
316
*/
417

518
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test';

0 commit comments

Comments
 (0)