'Afraid to Ask' implemented
This commit is contained in:
101
backend/tests/LLMService.test.ts
Normal file
101
backend/tests/LLMService.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { LLMService } from '../src/services/LLMService';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
|
||||
// Mock the GoogleGenerativeAI module
|
||||
jest.mock('@google/generative-ai');
|
||||
|
||||
describe('LLMService', () => {
|
||||
let llmService: LLMService;
|
||||
let mockGenerateContent: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Mock the generateContent method of the GenerativeModel
|
||||
mockGenerateContent = jest.fn();
|
||||
(GoogleGenerativeAI as jest.Mock).mockImplementation(() => ({
|
||||
getGenerativeModel: jest.fn(() => ({
|
||||
generateContent: mockGenerateContent,
|
||||
})),
|
||||
}));
|
||||
|
||||
llmService = new LLMService('test-api-key');
|
||||
});
|
||||
|
||||
describe('analyzeDesires', () => {
|
||||
it('should call the LLM with the correct prompt and handle afraidToAsk ideas', async () => {
|
||||
const desireSets = [
|
||||
{ wants: ['apple'], accepts: [], noGoes: [], afraidToAsk: 'banana' },
|
||||
{ wants: ['orange'], accepts: ['banana'], noGoes: [], afraidToAsk: 'grape' },
|
||||
];
|
||||
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
response: {
|
||||
text: () => JSON.stringify({
|
||||
goTo: 'apple',
|
||||
alsoGood: 'banana',
|
||||
considerable: 'orange',
|
||||
noGoes: '',
|
||||
needsDiscussion: '',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await llmService.analyzeDesires(desireSets);
|
||||
|
||||
expect(mockGenerateContent).toHaveBeenCalledTimes(1);
|
||||
const prompt = mockGenerateContent.mock.calls[0][0];
|
||||
expect(prompt).toContain(JSON.stringify(desireSets));
|
||||
expect(prompt).toContain('afraidToAsk' in each desire set);
|
||||
expect(prompt).toContain('If an \'afraidToAsk\' idea matches, it should be treated as a \'want\'');
|
||||
expect(result).toEqual({
|
||||
goTo: 'apple',
|
||||
alsoGood: 'banana',
|
||||
considerable: 'orange',
|
||||
noGoes: '',
|
||||
needsDiscussion: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if LLM response is not valid JSON', async () => {
|
||||
const desireSets = [
|
||||
{ wants: ['apple'], accepts: [], noGoes: [], afraidToAsk: 'banana' },
|
||||
];
|
||||
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
response: {
|
||||
text: () => 'invalid json',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(llmService.analyzeDesires(desireSets)).rejects.toThrow('Failed to parse LLM response as JSON.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkForInnerContradictions', () => {
|
||||
it('should return null if no contradictions are found', async () => {
|
||||
const desireSet = { wants: ['apple'], accepts: ['banana'], noGoes: ['grape'], afraidToAsk: 'kiwi' };
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
response: {
|
||||
text: () => 'null',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await llmService.checkForInnerContradictions(desireSet);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return a contradiction message if contradictions are found', async () => {
|
||||
const desireSet = { wants: ['apple', 'no apple'], accepts: [], noGoes: [], afraidToAsk: '' };
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
response: {
|
||||
text: () => 'Contradiction: apple and no apple in wants.',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await llmService.checkForInnerContradictions(desireSet);
|
||||
expect(result).toBe('Contradiction: apple and no apple in wants.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,30 +6,37 @@ import { LLMService } from '../src/services/LLMService';
|
||||
// Mock the LLMService
|
||||
jest.mock('../src/services/LLMService');
|
||||
|
||||
// Mock the ws module
|
||||
const mockHandleWebSocketMessage = jest.fn();
|
||||
const mockSessions = new Map<string, any>();
|
||||
const mockBroadcastToSession = jest.fn();
|
||||
|
||||
jest.mock('../src/ws', () => ({
|
||||
sessions: mockSessions,
|
||||
handleWebSocketMessage: mockHandleWebSocketMessage,
|
||||
broadcastToSession: mockBroadcastToSession,
|
||||
SessionState: {
|
||||
SETUP: 'SETUP',
|
||||
GATHERING: 'GATHERING',
|
||||
HARMONIZING: 'HARMONIZING',
|
||||
FINAL: 'FINAL',
|
||||
ERROR: 'ERROR',
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the routes
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Mock session storage for testing analyze endpoint
|
||||
const mockSessions = new Map<string, any>();
|
||||
mockSessions.set('test-session-id', { /* session data */ });
|
||||
|
||||
app.post('/sessions', (req, res) => {
|
||||
res.status(201).json({ sessionId: 'mock-session-id' });
|
||||
});
|
||||
|
||||
app.post('/sessions/:sessionId/analyze', async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
if (!mockSessions.has(sessionId)) {
|
||||
return res.status(404).send('Session not found');
|
||||
}
|
||||
// Mock LLMService call
|
||||
const mockLLMService = new LLMService('mock-api-key');
|
||||
const analysisResult = await mockLLMService.analyzeDesires(req.body.allDesires);
|
||||
res.status(202).json({ message: 'Analysis triggered', result: analysisResult });
|
||||
});
|
||||
// Import the actual router after mocks are set up
|
||||
import sessionsRouter from '../src/routes/sessions';
|
||||
app.use('/', sessionsRouter);
|
||||
|
||||
describe('POST /sessions', () => {
|
||||
beforeEach(() => {
|
||||
mockSessions.clear();
|
||||
});
|
||||
|
||||
it('should create a new session and return a session ID', async () => {
|
||||
const response = await request(app)
|
||||
.post('/sessions')
|
||||
@@ -38,42 +45,136 @@ describe('POST /sessions', () => {
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty('sessionId');
|
||||
expect(typeof response.body.sessionId).toBe('string');
|
||||
expect(mockSessions.has(response.body.sessionId)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /sessions/:sessionId/analyze', () => {
|
||||
it('should trigger analysis for a valid session', async () => {
|
||||
const mockDesires = [
|
||||
{ wants: ['item1'], accepts: [], noGoes: [] },
|
||||
{ wants: ['item2'], accepts: [], noGoes: [] },
|
||||
];
|
||||
describe('POST /sessions/:sessionId/responses', () => {
|
||||
const testSessionId = 'test-session-id';
|
||||
const testUserId = 'test-user-id';
|
||||
|
||||
// Mock the analyzeDesires method to return a predictable result
|
||||
(LLMService as jest.Mock).mockImplementation(() => ({
|
||||
analyzeDesires: jest.fn().mockResolvedValue({
|
||||
"item1": "Concept A",
|
||||
"item2": "Concept A"
|
||||
}),
|
||||
}));
|
||||
beforeEach(() => {
|
||||
mockSessions.clear();
|
||||
mockHandleWebSocketMessage.mockClear();
|
||||
mockBroadcastToSession.mockClear();
|
||||
|
||||
// Initialize a session for testing
|
||||
mockSessions.set(testSessionId, {
|
||||
state: 'GATHERING',
|
||||
topic: 'Test Topic',
|
||||
expectedResponses: 1,
|
||||
submittedCount: 0,
|
||||
responses: new Map(),
|
||||
clients: new Map(),
|
||||
finalResult: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept a response with afraidToAsk and call handleWebSocketMessage', async () => {
|
||||
const responsePayload = {
|
||||
userId: testUserId,
|
||||
wants: ['More features'],
|
||||
accepts: ['Bug fixes'],
|
||||
afraidToAsk: 'A raise',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/sessions/test-session-id/analyze')
|
||||
.send({ allDesires: mockDesires });
|
||||
.post(`/sessions/${testSessionId}/responses`)
|
||||
.send(responsePayload);
|
||||
|
||||
expect(response.status).toBe(202);
|
||||
expect(response.body).toHaveProperty('message', 'Analysis triggered');
|
||||
expect(response.body).toHaveProperty('result');
|
||||
expect(response.body.result).toEqual({
|
||||
"item1": "Concept A",
|
||||
"item2": "Concept A"
|
||||
expect(response.body).toEqual({ message: 'Response submission acknowledged and processed.' });
|
||||
expect(mockHandleWebSocketMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockHandleWebSocketMessage).toHaveBeenCalledWith(
|
||||
expect.any(Object), // dummyWs
|
||||
testSessionId,
|
||||
{
|
||||
type: 'SUBMIT_RESPONSE',
|
||||
clientId: testUserId,
|
||||
payload: {
|
||||
response: {
|
||||
wants: ['More features'],
|
||||
accepts: ['Bug fixes'],
|
||||
afraidToAsk: 'A raise',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 404 if session is not found', async () => {
|
||||
const responsePayload = {
|
||||
userId: testUserId,
|
||||
wants: ['More features'],
|
||||
accepts: ['Bug fixes'],
|
||||
afraidToAsk: 'A raise',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/sessions/non-existent-session/responses`)
|
||||
.send(responsePayload);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toEqual({ message: 'Session not found.' });
|
||||
expect(mockHandleWebSocketMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 500 if handleWebSocketMessage throws an error', async () => {
|
||||
mockHandleWebSocketMessage.mockRejectedValueOnce(new Error('WebSocket processing error'));
|
||||
|
||||
const responsePayload = {
|
||||
userId: testUserId,
|
||||
wants: ['More features'],
|
||||
accepts: ['Bug fixes'],
|
||||
afraidToAsk: 'A raise',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/sessions/${testSessionId}/responses`)
|
||||
.send(responsePayload);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toHaveProperty('message', 'Error processing response.');
|
||||
expect(response.body).toHaveProperty('error', 'WebSocket processing error');
|
||||
expect(mockHandleWebSocketMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /sessions/:sessionId/terminate', () => {
|
||||
const testSessionId = 'session-to-terminate';
|
||||
|
||||
beforeEach(() => {
|
||||
mockSessions.clear();
|
||||
// Initialize a session for testing termination
|
||||
mockSessions.set(testSessionId, {
|
||||
state: 'FINAL',
|
||||
topic: 'Test Topic',
|
||||
expectedResponses: 1,
|
||||
submittedCount: 1,
|
||||
responses: new Map([['client1', { wants: ['test'], accepts: [], noGoes: [], afraidToAsk: 'secret' }]]),
|
||||
clients: new Map(),
|
||||
finalResult: { goTo: 'test' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should terminate the session and purge its data', async () => {
|
||||
expect(mockSessions.has(testSessionId)).toBe(true);
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/sessions/${testSessionId}/terminate`)
|
||||
.send();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ message: 'Session terminated and data purged successfully.' });
|
||||
expect(mockSessions.has(testSessionId)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return 404 if session is not found', async () => {
|
||||
const response = await request(app)
|
||||
.post('/sessions/non-existent-session/analyze')
|
||||
.send({ allDesires: [] });
|
||||
.post(`/sessions/non-existent-session/terminate`)
|
||||
.send();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toEqual({ message: 'Session not found.' });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user