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
67 changes: 29 additions & 38 deletions apps/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,17 @@ interface CheckinResponse {
success: true;
data: {
sessionId: string; // UUID for this session
transcript: string; // Transcribed text
confidence: number; // Transcription confidence (0-1)
transcript: string; // Transcribed text from audio
sentiment: {
score: number; // Sentiment score (0-1)
score: number; // Sentiment score (0-1, lower = negative)
label: 'positive' | 'negative' | 'neutral';
confidence: number; // Sentiment confidence (0-1)
};
coaching: {
breathingExercise: {
title: string;
instructions: string[];
duration: number; // Duration in minutes
duration: number; // Duration in seconds
};
stretchExercise: {
title: string;
Expand All @@ -148,15 +147,9 @@ interface CheckinResponse {
}>;
motivationalMessage: string;
};
audioUrl: string; // URL to generated coaching audio
audioMetadata: {
duration: number; // Audio duration in seconds
fileSize: number; // File size in bytes
format: 'mp3' | 'wav';
};
audioUrl: string; // URL to generated coaching audio (mp3)
};
processingTime: number; // Total processing time in ms
timestamp: string; // ISO timestamp
}
```

Expand Down Expand Up @@ -186,60 +179,58 @@ curl -X POST http://localhost:4000/api/checkin \
{
"success": true,
"data": {
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"transcript": "I'm feeling really stressed about my upcoming exams and I can't seem to focus on studying.",
"confidence": 0.94,
"sessionId": "91edb2fc-ae1b-4dde-a4ca-76b8643dd3c3",
"transcript": "I've been feeling a bit stressed lately with all the assignments and exams coming up.",
"sentiment": {
"score": 0.25,
"score": 0.31,
"label": "negative",
"confidence": 0.89
"confidence": 0.85
},
"coaching": {
"breathingExercise": {
"title": "4-7-8 Calming Breath",
"title": "4-7-8 Breathing Technique",
"instructions": [
"Sit comfortably with your back straight",
"Inhale through your nose for 4 counts",
"Hold your breath for 7 counts",
"Exhale through your mouth for 8 counts",
"Repeat this cycle 4 times"
"Repeat 4 times for maximum effect"
],
"duration": 5
"duration": 120
},
"stretchExercise": {
"title": "Neck and Shoulder Release",
"instructions": [
"Gently tilt your head to the right, hold for 15 seconds",
"Slowly roll your shoulders backward 5 times",
"Gently tilt your head to the right, hold 15 seconds",
"Repeat on the left side",
"Roll your shoulders backward 5 times",
"Take deep breaths throughout"
"Take deep breaths during each stretch"
]
},
"resources": [
{
"title": "Campus Counseling Center",
"description": "Free counseling services for students",
"url": "https://counseling.university.edu",
"title": "University Counseling Center",
"description": "Free confidential counseling services for students",
"url": "https://university.edu/counseling",
"category": "counseling"
},
{
"title": "Headspace - Study Focus",
"description": "Meditation sessions designed for students",
"url": "https://headspace.com/study",
"title": "Headspace for Students",
"description": "Free meditation and mindfulness app",
"url": "https://headspace.com/students",
"category": "meditation"
},
{
"title": "Crisis Text Line",
"description": "24/7 mental health crisis support via text",
"url": "https://crisistextline.org",
"category": "emergency"
}
],
"motivationalMessage": "Remember, feeling stressed about exams is completely normal. You've prepared well, and taking breaks to breathe and stretch will actually help you focus better. You've got this!"
"motivationalMessage": "Thank you for sharing what's on your mind. It's completely normal to feel stressed about exams and assignments - many students experience this. Remember that you have the strength to handle these challenges, and taking time for self-care like breathing exercises can really help. You're taking a positive step by checking in with yourself."
},
"audioUrl": "http://localhost:4000/audio/coaching-550e8400-e29b-41d4.mp3",
"audioMetadata": {
"duration": 45.6,
"fileSize": 1024000,
"format": "mp3"
}
"audioUrl": "https://api.pulsemates.com/audio/91edb2fc-ae1b-4dde-a4ca-76b8643dd3c3.mp3"
},
"processingTime": 2347,
"timestamp": "2025-01-09T10:30:00.000Z"
"processingTime": 3876
}
```

Expand Down
274 changes: 274 additions & 0 deletions apps/server/src/__tests__/checkin-endpoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import request from 'supertest';
import path from 'path';
import fs from 'fs';
import app from '../app';

describe('POST /api/checkin', () => {
// Create a mock audio file for testing
const mockAudioFilePath = path.join(__dirname, 'mock-audio.wav');

beforeAll(() => {
// Create a small mock WAV file for testing
const mockWavContent = Buffer.from([
0x52,
0x49,
0x46,
0x46, // "RIFF"
0x24,
0x00,
0x00,
0x00, // File size
0x57,
0x41,
0x56,
0x45, // "WAVE"
0x66,
0x6d,
0x74,
0x20, // "fmt "
0x10,
0x00,
0x00,
0x00, // Chunk size
0x01,
0x00,
0x01,
0x00, // Audio format and channels
0x44,
0xac,
0x00,
0x00, // Sample rate (44100)
0x88,
0x58,
0x01,
0x00, // Byte rate
0x02,
0x00,
0x10,
0x00, // Block align and bits per sample
0x64,
0x61,
0x74,
0x61, // "data"
0x00,
0x00,
0x00,
0x00, // Data size
]);

fs.writeFileSync(mockAudioFilePath, mockWavContent);
});

afterAll(() => {
// Clean up mock file
if (fs.existsSync(mockAudioFilePath)) {
fs.unlinkSync(mockAudioFilePath);
}
});

it('should successfully process a valid audio file', async () => {
const response = await request(app)
.post('/api/checkin')
.attach('audio', mockAudioFilePath, 'test-audio.wav')
.expect(200);

expect(response.body.success).toBe(true);
expect(response.body.data).toBeDefined();
expect(response.body.data.transcript).toBeDefined();
expect(response.body.data.sentiment).toBeDefined();
expect(response.body.data.coaching).toBeDefined();
expect(response.body.data.audioUrl).toBeDefined();
expect(response.body.data.sessionId).toBeDefined();
expect(response.body.processingTime).toBeGreaterThan(0);

// Check transcript content
expect(typeof response.body.data.transcript).toBe('string');
expect(response.body.data.transcript.length).toBeGreaterThan(0);

// Check sentiment structure
const sentiment = response.body.data.sentiment;
expect(sentiment.score).toBeGreaterThanOrEqual(0);
expect(sentiment.score).toBeLessThanOrEqual(1);
expect(['positive', 'negative', 'neutral']).toContain(sentiment.label);
expect(sentiment.confidence).toBeGreaterThanOrEqual(0);
expect(sentiment.confidence).toBeLessThanOrEqual(1);

// Check coaching structure
const coaching = response.body.data.coaching;
expect(coaching.breathingExercise).toBeDefined();
expect(coaching.breathingExercise.title).toBeDefined();
expect(Array.isArray(coaching.breathingExercise.instructions)).toBe(true);
expect(coaching.breathingExercise.duration).toBeGreaterThan(0);

expect(coaching.stretchExercise).toBeDefined();
expect(coaching.stretchExercise.title).toBeDefined();
expect(Array.isArray(coaching.stretchExercise.instructions)).toBe(true);

expect(Array.isArray(coaching.resources)).toBe(true);
coaching.resources.forEach((resource: any) => {
expect(resource.title).toBeDefined();
expect(resource.description).toBeDefined();
expect(resource.url).toBeDefined();
expect(['counseling', 'meditation', 'emergency']).toContain(
resource.category
);
});

expect(coaching.motivationalMessage).toBeDefined();
expect(typeof coaching.motivationalMessage).toBe('string');

// Check audio URL format
expect(response.body.data.audioUrl).toMatch(
/^https:\/\/api\.pulsemates\.com\/audio\/[a-f0-9-]+\.mp3$/
);
}, 10000); // 10 second timeout for async processing

it('should return 400 when no file is uploaded', async () => {
const response = await request(app).post('/api/checkin').expect(400);

expect(response.body.success).toBe(false);
expect(response.body.error).toContain('No audio file uploaded');
});

it('should return 400 for invalid file format', async () => {
// Create a text file to test invalid format
const invalidFilePath = path.join(__dirname, 'invalid.txt');
fs.writeFileSync(invalidFilePath, 'This is not an audio file');

const response = await request(app)
.post('/api/checkin')
.attach('audio', invalidFilePath, 'invalid.txt')
.expect(400);

expect(response.body.success).toBe(false);
expect(response.body.error).toContain('Invalid file format');

// Clean up
fs.unlinkSync(invalidFilePath);
});

it('should return 400 for oversized file', async () => {
// Create a large file (> 10MB)
const largeFilePath = path.join(__dirname, 'large.wav');
const largeBuffer = Buffer.alloc(11 * 1024 * 1024); // 11MB
fs.writeFileSync(largeFilePath, largeBuffer);

const response = await request(app)
.post('/api/checkin')
.attach('audio', largeFilePath, 'large.wav')
.expect(400);

expect(response.body.success).toBe(false);
expect(response.body.error).toContain('File too large');

// Clean up
fs.unlinkSync(largeFilePath);
});

it('should handle MP3 files correctly', async () => {
// Create a mock MP3 file
const mockMp3FilePath = path.join(__dirname, 'mock-audio.mp3');
const mockMp3Content = Buffer.from([
0xff,
0xfb,
0x90,
0x00, // MP3 header
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
]);

fs.writeFileSync(mockMp3FilePath, mockMp3Content);

const response = await request(app)
.post('/api/checkin')
.attach('audio', mockMp3FilePath, 'test-audio.mp3')
.expect(200);

expect(response.body.success).toBe(true);
expect(response.body.data.transcript).toBeDefined();

// Clean up
fs.unlinkSync(mockMp3FilePath);
}, 10000);

it('should include session ID for tracking', async () => {
const response = await request(app)
.post('/api/checkin')
.attach('audio', mockAudioFilePath, 'test-audio.wav')
.expect(200);

expect(response.body.data.sessionId).toBeDefined();
expect(typeof response.body.data.sessionId).toBe('string');
expect(response.body.data.sessionId).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
);
}, 10000);

it('should have reasonable processing time', async () => {
const response = await request(app)
.post('/api/checkin')
.attach('audio', mockAudioFilePath, 'test-audio.wav')
.expect(200);

expect(response.body.processingTime).toBeDefined();
expect(response.body.processingTime).toBeGreaterThan(1000); // At least 1 second (due to mock delays)
expect(response.body.processingTime).toBeLessThan(6000); // Should be under 6 seconds
}, 10000);

it('should generate sentiment-appropriate coaching', async () => {
// Test multiple times to potentially get different sentiments
const responses = [];

for (let i = 0; i < 3; i++) {
const response = await request(app)
.post('/api/checkin')
.attach('audio', mockAudioFilePath, 'test-audio.wav')
.expect(200);

responses.push(response.body);
}

// Check that we got some variety in sentiments or appropriate coaching
responses.forEach(response => {
const sentiment = response.data.sentiment;
const coaching = response.data.coaching;

expect(['positive', 'negative', 'neutral']).toContain(sentiment.label);

// Coaching should exist regardless of sentiment
expect(coaching.breathingExercise.title).toBeDefined();
expect(coaching.stretchExercise.title).toBeDefined();
expect(coaching.motivationalMessage.length).toBeGreaterThan(10);
});
}, 15000);

it('should include emergency resources for negative sentiment', async () => {
// We can't guarantee negative sentiment with mock data, but we can test the structure
const response = await request(app)
.post('/api/checkin')
.attach('audio', mockAudioFilePath, 'test-audio.wav')
.expect(200);

expect(response.body.success).toBe(true);
const resources = response.body.data.coaching.resources;

// Should have at least 2 base resources
expect(resources.length).toBeGreaterThanOrEqual(2);

// Check resource structure
resources.forEach((resource: any) => {
expect(resource.title).toBeDefined();
expect(resource.description).toBeDefined();
expect(resource.url).toBeDefined();
expect(['counseling', 'meditation', 'emergency']).toContain(
resource.category
);
});
}, 10000);
});
Loading
Loading