1. Keep session alive with ping-pong. 2. Refreshed tests.

This commit is contained in:
AG
2025-10-16 10:48:11 +03:00
parent 6f64b1daca
commit 95684a34f7
27 changed files with 420 additions and 100 deletions

41
backend/dist/index.js vendored
View File

@@ -12,14 +12,49 @@ const sessions_1 = __importDefault(require("./routes/sessions"));
const auth_1 = __importDefault(require("./api/auth"));
const authMiddleware_1 = require("./middleware/authMiddleware"); // Import the middleware
const cors_1 = __importDefault(require("cors"));
const uuid_1 = require("uuid");
const ws_2 = require("./ws"); // Import sessions and SessionState from ws/index.ts
console.log('index.ts: AUTH_PASSPHRASE:', process.env.AUTH_PASSPHRASE);
console.log('index.ts: SESSION_SECRET:', process.env.SESSION_SECRET);
console.log('index.ts: JWT_SECRET:', process.env.JWT_SECRET);
const app = (0, express_1.default)();
const server = http_1.default.createServer(app);
// Middleware
app.use(express_1.default.json());
app.use((0, cors_1.default)());
// API Routes
app.use('/', authMiddleware_1.authMiddleware, sessions_1.default); // Apply middleware to sessionsRouter
const allowedOrigins = process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : [];
const corsOptions = {
origin: (origin, callback) => {
// Allow same-origin requests (origin is undefined) and requests from the whitelisted origins
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
}
else {
console.warn(`CORS: Blocked request from origin: ${origin}`);
callback(new Error('Not allowed by CORS'));
}
},
};
app.use((0, cors_1.default)(corsOptions));
// Public API Routes
app.use('/api/auth', auth_1.default);
// Public route for creating a new session
app.post('/sessions', (req, res) => {
const sessionId = (0, uuid_1.v4)();
ws_2.sessions.set(sessionId, {
state: ws_2.SessionState.SETUP,
topic: null,
description: null,
expectedResponses: 0,
submittedCount: 0,
responses: new Map(),
clients: new Map(),
finalResult: null,
});
console.log(`New session created: ${sessionId}`);
res.status(201).json({ sessionId });
});
// Protected API Routes
app.use('/sessions', authMiddleware_1.authMiddleware, sessions_1.default);
// Create and attach WebSocket server
(0, ws_1.createWebSocketServer)(server);
const PORT = process.env.PORT || 8000;

View File

@@ -13,24 +13,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const uuid_1 = require("uuid");
const ws_1 = require("../ws"); // Import sessions, SessionState, broadcastToSession, and handleWebSocketMessage from ws/index.ts
const router = express_1.default.Router();
router.post('/sessions', (req, res) => {
const sessionId = (0, uuid_1.v4)();
ws_1.sessions.set(sessionId, {
state: ws_1.SessionState.SETUP,
topic: null,
description: null,
expectedResponses: 0,
submittedCount: 0,
responses: new Map(),
clients: new Map(),
finalResult: null,
});
console.log(`New session created: ${sessionId}`);
res.status(201).json({ sessionId });
});
router.post('/sessions/:sessionId/responses', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const { sessionId } = req.params;
const { userId, wants, accepts, afraidToAsk } = req.body;

View File

@@ -36,6 +36,8 @@ class AuthService {
return !!AuthService.passphrase && AuthService.passphrase.trim() !== '';
}
static validatePassphrase(inputPassphrase) {
console.log('AuthService: AUTH_PASSPHRASE from process.env:', process.env.AUTH_PASSPHRASE);
console.log('AuthService: Stored passphrase:', AuthService.passphrase);
if (!AuthService.isAuthEnabled()) {
return true; // If auth is not enabled, any passphrase is "valid"
}

View File

@@ -24,10 +24,10 @@ class LLMService {
Each participant's desire set includes 'wants', 'accepts', 'noGoes', and an 'afraidToAsk' field. The 'afraidToAsk' field contains a sensitive idea that the participant is hesitant to express publicly.
Here are the rules for categorization and synthesis, with special handling for 'afraidToAsk' ideas:
- "goTo": Synthesize a text describing what ALL participants want without contradictions. This should include 'afraidToAsk' ideas that semantically match all other participant's 'wants' or 'afraidToAsk'. If an 'afraidToAsk' idea matches, it should be treated as a 'want' for the submitting participant. Use the more specific opinions and leave all the specific options if they do not contradict each other drastically.
- "alsoGood": Synthesize a text describing what at least one participant wants (including matched 'afraidToAsk' ideas), not everyone wants but all other participants at least accept, and is not a "noGoes" for anyone. This should reflect a generally agreeable outcome. Use the more specific opinions and leave all the specific options if they do not contradict each other drastically.
- "considerable": Synthesize a text describing what is wanted or accepted by some, but not all, participants (including matched 'afraidToAsk' ideas), and is not a "noGoes" for anyone. This should highlight areas of partial agreement or options that could be explored. Use the more specific opinions and leave all the specific options if they do not contradict each other drastically.
- "noGoes": Synthesize a text describing what at least ONE participant does not want. This should clearly state the collective exclusions. Use the more broad opinions summarizing all the specific options if they do not contradict each other drastically.
- "goTo": Synthesize a text describing what ALL participants want without contradictions. This should include 'afraidToAsk' ideas that semantically match all other participant's 'wants' or 'afraidToAsk'. If an 'afraidToAsk' idea matches, it should be treated as a 'want' for the submitting participant. Use the more specific opinions and keep all the specific options if they do not contradict each other drastically and are not 'noGoes'.
- "alsoGood": Synthesize a text describing what at least one participant wants (including matched 'afraidToAsk' ideas), not everyone wants but all other participants at least accept, and is not a "noGoes" for anyone. This should reflect a generally agreeable outcome. Use the more specific opinions and keep all the specific options if they do not contradict each other drastically and are not 'noGoes'.
- "considerable": Synthesize a text describing what is wanted or accepted by some, but not all, participants (including matched 'afraidToAsk' ideas), and is not a "noGoes" for anyone. This should highlight areas of partial agreement or options that could be explored. Use the more specific opinions and keep all the specific options if they do not contradict each other drastically and are not 'noGoes'.
- "noGoes": Synthesize a text describing what at least ONE participant does not want. This should clearly state the collective exclusions. Use the more broad opinions summarizing more specific options if they do not contradict each other drastically.
- "needsDiscussion": Synthesize a text describing where there is a direct conflict (e.g., one participant wants it, another does not want it). This should highlight areas requiring further negotiation. Do not include 'afraidToAsk' in this category.
'AfraidToAsk' ideas that do NOT semantically match any other participant's 'wants' or 'accepts' very closely should remain private and NOT be included in any of the synthesized categories. Matching must use minimal level of generalization.

View File

@@ -92,6 +92,12 @@ const createWebSocketServer = (server) => {
}
const sessionData = exports.sessions.get(sessionId);
console.log(`Client connecting to session: ${sessionId}`);
// Set up a ping interval to keep the connection alive
const pingInterval = setInterval(() => {
if (ws.readyState === ws_1.WebSocket.OPEN) {
ws.ping();
}
}, 30000); // Send ping every 30 seconds
ws.on('message', (message) => __awaiter(void 0, void 0, void 0, function* () {
const parsedMessage = JSON.parse(message.toString());
const { type, clientId, payload } = parsedMessage;
@@ -109,6 +115,7 @@ const createWebSocketServer = (server) => {
yield (0, exports.handleWebSocketMessage)(ws, sessionId, parsedMessage);
}));
ws.on('close', () => {
clearInterval(pingInterval); // Clear the interval when the connection closes
let disconnectedClientId = null;
for (const [clientId, clientWs] of sessionData.clients.entries()) {
if (clientWs === ws) {
@@ -154,6 +161,12 @@ const handleWebSocketMessage = (ws, sessionId, parsedMessage) => __awaiter(void
case 'REGISTER_CLIENT':
console.log(`Client ${clientId} registered successfully for session ${sessionId}.`);
break;
case 'PING':
// Respond to client pings with a pong
if (ws.readyState === ws_1.WebSocket.OPEN) {
ws.pong();
}
break;
case 'SETUP_SESSION':
if (sessionData.state === SessionState.SETUP) {
const { expectedResponses, topic, description } = payload;

View File

@@ -4,6 +4,21 @@ import { sessions, SessionState, broadcastToSession, handleWebSocketMessage } fr
const router = express.Router();
router.post('/sessions', (req, res) => {
const sessionId = uuidv4();
sessions.set(sessionId, {
state: SessionState.SETUP,
topic: null,
description: null,
expectedResponses: 0,
submittedCount: 0,
responses: new Map(),
clients: new Map(),
finalResult: null,
});
res.status(201).json({ sessionId });
});
router.post('/sessions/:sessionId/responses', async (req, res) => {
const { sessionId } = req.params;
const { userId, wants, accepts, afraidToAsk } = req.body;

View File

@@ -130,6 +130,13 @@ export const createWebSocketServer = (server: any) => {
console.log(`Client connecting to session: ${sessionId}`);
// Set up a ping interval to keep the connection alive
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 30000); // Send ping every 30 seconds
ws.on('message', async (message) => {
const parsedMessage = JSON.parse(message.toString());
const { type, clientId, payload } = parsedMessage;
@@ -151,6 +158,7 @@ export const createWebSocketServer = (server: any) => {
});
ws.on('close', () => {
clearInterval(pingInterval); // Clear the interval when the connection closes
let disconnectedClientId: string | null = null;
for (const [clientId, clientWs] of sessionData.clients.entries()) {
if (clientWs === ws) {
@@ -205,6 +213,13 @@ export const handleWebSocketMessage = async (ws: WebSocket, sessionId: string, p
console.log(`Client ${clientId} registered successfully for session ${sessionId}.`);
break;
case 'PING':
// Respond to client pings with a pong
if (ws.readyState === WebSocket.OPEN) {
ws.pong();
}
break;
case 'SETUP_SESSION':
if (sessionData.state === SessionState.SETUP) {
const { expectedResponses, topic, description } = payload;

View File

@@ -47,7 +47,7 @@ describe('LLMService', () => {
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',

View File

@@ -3,12 +3,25 @@ import cors from 'cors';
// Mock the express request, response, and next function
const mockRequest = (origin: string | undefined) => {
return { header: (name: string) => (name === 'Origin' ? origin : undefined) } as Request;
return {
headers: {
origin: origin,
},
header: (name: string) => (name === 'Origin' ? origin : undefined)
} as Request;
};
const mockResponse = () => {
const headers: { [key: string]: string | string[] | undefined } = {};
const res: Partial<Response> = {};
res.setHeader = jest.fn().mockReturnValue(res as Response);
res.setHeader = jest.fn((name: string, value: string | string[]) => {
headers[name.toLowerCase()] = value;
return res as Response;
});
res.getHeader = jest.fn((name: string) => headers[name.toLowerCase()]);
res.removeHeader = jest.fn((name: string) => {
delete headers[name.toLowerCase()];
});
res.status = jest.fn().mockReturnValue(res as Response);
res.json = jest.fn().mockReturnValue(res as Response);
return res as Response;