pmRole = Role::create(['name' => 'Project Manager', 'description' => 'PM']); $this->devRole = Role::create(['name' => 'Developer', 'description' => 'Dev']); // Create users $this->admin = User::create([ 'name' => 'System Admin', 'email' => 'admin@company.com', 'role' => 'admin', ]); $this->pm = User::create([ 'name' => 'John PM', 'email' => 'john.pm@company.com', 'role' => 'user', ]); $this->pm->roles()->attach($this->pmRole->id); $this->developer = User::create([ 'name' => 'Alex Dev', 'email' => 'alex.dev@company.com', 'role' => 'user', ]); $this->developer->roles()->attach($this->devRole->id); } public function test_pm_and_admin_can_access_client_calls_and_create_client(): void { $this->actingAs($this->pm) ->get(route('client-calls.index')) ->assertStatus(200) ->assertSee('Client Projects', false); $response = $this->actingAs($this->pm) ->post(route('client-calls.store'), [ 'client_name' => 'Alice Walker', 'client_email' => 'alice@innovate.io', 'client_phone' => '+1 555 456 7890', 'company_name' => 'Innovate Tech', 'project_name' => 'AI Analytics Dashboard', 'project_details' => 'Full stack dashboard with live WebRTC meeting rooms and AI transcription.', 'responsible_user_id' => $this->pm->id, 'assigned_team_members' => [$this->pm->id, $this->developer->id], ]); $response->assertRedirect(route('client-calls.index')); $this->assertDatabaseHas('clients', [ 'client_name' => 'Alice Walker', 'client_email' => 'alice@innovate.io', 'project_name' => 'AI Analytics Dashboard', 'responsible_user_id' => $this->pm->id, ]); } public function test_client_hub_page_shows_details_and_opens(): void { $client = Client::create([ 'client_name' => 'Bob Smith', 'client_email' => 'bob@smithco.com', 'project_name' => 'Cloud Migration', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', 'latest_sentiment' => 'happy', ]); $this->actingAs($this->pm) ->get(route('client-calls.show', $client->id)) ->assertStatus(200) ->assertSee('Cloud Migration') ->assertSee('Bob Smith') ->assertSee('Start Instant Meeting') ->assertSee('Schedule Meeting'); } public function test_scheduling_future_meeting_sends_email_invites(): void { Mail::fake(); $client = Client::create([ 'client_name' => 'Bob Smith', 'client_email' => 'bob@smithco.com', 'project_name' => 'Cloud Migration', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $response = $this->actingAs($this->pm) ->post(route('client-calls.schedule-meeting', $client->id), [ 'title' => 'Sprint 1 Architecture Review', 'agenda' => 'Review AWS diagrams and API contracts.', 'scheduled_at' => now()->addDays(2)->format('Y-m-d H:i:s'), 'duration_minutes' => 45, 'client_emails' => 'bob@smithco.com, ctobob@smithco.com', 'team_attendees' => [$this->pm->id, $this->developer->id], ]); $response->assertRedirect(route('client-calls.show', $client->id)); $this->assertDatabaseHas('client_meetings', [ 'client_id' => $client->id, 'title' => 'Sprint 1 Architecture Review', 'meeting_type' => 'scheduled', ]); Mail::assertSent(ClientMeetingInviteMail::class); } public function test_starting_instant_meeting_generates_shareable_link(): void { $client = Client::create([ 'client_name' => 'Bob Smith', 'client_email' => 'bob@smithco.com', 'project_name' => 'Cloud Migration', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $response = $this->actingAs($this->pm) ->postJson(route('client-calls.instant-meeting', $client->id), [ 'title' => 'Quick Alignment Sync', ]); $response->assertStatus(200) ->assertJsonStructure(['success', 'meeting_id', 'meeting_code', 'join_url']); $this->assertDatabaseHas('client_meetings', [ 'client_id' => $client->id, 'meeting_type' => 'instant', 'status' => 'in_progress', ]); } public function test_external_guest_can_join_via_meeting_link(): void { $client = Client::create([ 'client_name' => 'Bob Smith', 'client_email' => 'bob@smithco.com', 'project_name' => 'Cloud Migration', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Instant Sync', 'meeting_code' => 'meet-test-guest-123', 'meeting_type' => 'instant', 'host_user_id' => $this->pm->id, 'status' => 'in_progress', ]); // Unauthenticated guest arrives at room -> gets guest entry screen $this->get(route('client-calls.room', $meeting->meeting_code)) ->assertStatus(200) ->assertSee("What's your name?", false); // Guest submits name $this->post(route('client-calls.join-guest', $meeting->meeting_code), [ 'guest_name' => 'Bob Smith (Client)', 'guest_email' => 'bob@smithco.com', ])->assertRedirect(route('client-calls.room', $meeting->meeting_code)); // Now has session and enters room $this->withSession(["guest_name_{$meeting->meeting_code}" => 'Bob Smith (Client)']) ->get(route('client-calls.room', $meeting->meeting_code)) ->assertStatus(200) ->assertSee('Bob Smith (Client)'); } public function test_ai_emotion_detection_endpoint(): void { $client = Client::create([ 'client_name' => 'Bob Smith', 'client_email' => 'bob@smithco.com', 'project_name' => 'Cloud Migration', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Sync Call', 'meeting_code' => 'meet-emotion-test', 'host_user_id' => $this->pm->id, 'status' => 'in_progress', ]); // Test angry sentiment detection on frustrated speech text $response = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [ 'speech_text' => 'This 2 week delay is totally unacceptable and broken webhooks are terrible.', ]); $response->assertStatus(200) ->assertJson([ 'emotion' => 'angry', ]); // Test happy sentiment detection $happyResponse = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [ 'speech_text' => 'The demo looks fantastic, awesome work on the features!', ]); $happyResponse->assertStatus(200) ->assertJson([ 'emotion' => 'happy', ]); } public function test_ending_call_generates_mom_and_emails_stakeholders(): void { Mail::fake(); $client = Client::create([ 'client_name' => 'Bob Smith', 'client_email' => 'bob@smithco.com', 'project_name' => 'Cloud Migration', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Sprint Wrap-up Call', 'meeting_code' => 'meet-wrapup-456', 'host_user_id' => $this->pm->id, 'status' => 'in_progress', ]); $response = $this->actingAs($this->pm) ->postJson(route('client-calls.end-call', $meeting->meeting_code), [ 'transcript' => "[10:00 AM] PM: Discussing migration progress.\n[10:10 AM] Client: Progress is good.", 'live_notes' => 'Agreed on migration date.', ]); $response->assertStatus(200) ->assertJson(['success' => true]); $this->assertDatabaseHas('client_call_notes', [ 'client_meeting_id' => $meeting->id, 'client_id' => $client->id, ]); $this->assertEquals('completed', $meeting->fresh()->status); Mail::assertSent(ClientCallMomMail::class); } public function test_angry_analysis_is_strictly_restricted_to_admin_and_responsible_lead(): void { $client = Client::create([ 'client_name' => 'Sarah Connor', 'client_email' => 'sarah@cyber.io', 'project_name' => 'Security Portal', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', 'latest_sentiment' => 'angry', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Escalation Review', 'meeting_code' => 'meet-angry-check', 'host_user_id' => $this->pm->id, 'status' => 'completed', ]); $note = ClientCallNote::create([ 'client_meeting_id' => $meeting->id, 'client_id' => $client->id, 'created_by' => $this->pm->id, 'raw_transcript' => 'Client is angry about milestone delays.', 'mom_content' => 'MoM Content', 'summary' => 'Urgent review', 'detected_sentiment' => 'angry', 'angry_analysis' => [ 'is_angry' => true, 'trigger_words' => ['unacceptable', 'delay'], 'summary_why_angry' => 'Client is angry due to milestone delays.', 'grievances' => ['Delays', 'Lack of updates'], 'suggested_remedy' => 'Send remedial roadmap.', ], ]); // 1. Admin CAN view angry analysis $this->actingAs($this->admin) ->getJson(route('client-calls.angry-analysis', $note->id)) ->assertStatus(200) ->assertJson([ 'success' => true, 'detected_sentiment' => 'angry', ]); // 2. Responsible PM CAN view angry analysis $this->actingAs($this->pm) ->getJson(route('client-calls.angry-analysis', $note->id)) ->assertStatus(200) ->assertJson([ 'success' => true, 'detected_sentiment' => 'angry', ]); // 3. Unrelated Developer CANNOT view angry analysis (403 Forbidden) $this->actingAs($this->developer) ->getJson(route('client-calls.angry-analysis', $note->id)) ->assertStatus(403) ->assertJsonStructure(['error']); } public function test_admin_dashboard_displays_client_projects_and_alerts(): void { Client::create([ 'client_name' => 'Sarah Connor', 'client_email' => 'sarah@cyber.io', 'project_name' => 'Security Portal', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', 'latest_sentiment' => 'angry', ]); $this->actingAs($this->admin) ->get(route('admin.dashboard')) ->assertStatus(200) ->assertSee('Client Calls Hub') ->assertSee('Security Portal') ->assertSee('Sarah Connor'); } public function test_can_view_call_logs_and_download_mom_and_todos(): void { $client = Client::create([ 'client_name' => 'Robert Miller', 'client_email' => 'robert@acmecorp.com', 'project_name' => 'Enterprise Cloud Migration', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', 'latest_sentiment' => 'happy', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Sprint 2 Milestone Demo', 'meeting_code' => 'meet-logs-test-1', 'host_user_id' => $this->pm->id, 'status' => 'completed', ]); $note = ClientCallNote::create([ 'client_meeting_id' => $meeting->id, 'client_id' => $client->id, 'created_by' => $this->pm->id, 'raw_transcript' => 'Client is very happy with cloud deployment.', 'mom_content' => "### Minutes of Meeting\nObjectives: Deploy cloud infrastructure.\nNext steps: Testing.", 'summary' => 'Client praised the architecture blueprints.', 'detected_sentiment' => 'happy', 'todo_items' => [ ['id' => 1, 'task' => 'Deploy AWS staging templates', 'owner' => 'Engineering Team', 'priority' => 'High', 'deadline' => 'Aug 22, 2026', 'completed' => false], ['id' => 2, 'task' => 'Provide SSO test credentials', 'owner' => 'Tech Lead', 'priority' => 'Medium', 'deadline' => 'Aug 23, 2026', 'completed' => false], ], 'emotion_details' => [ 'emotion' => 'happy', 'headline' => 'Client Praised Sprint Progress', 'bullet_points' => [ 'Pleased with clear architecture diagram.', 'Praised team responsiveness.' ], 'trigger_quotes' => ['looks great', 'very pleased'], 'suggested_action' => 'Maintain current sprint momentum.' ], ]); // 1. Check Date-wise Call Logs view $this->actingAs($this->pm) ->get(route('client-calls.logs')) ->assertStatus(200) ->assertSee('Date-wise Client Call Logs') ->assertSee('Enterprise Cloud Migration') ->assertSee('Sprint 2 Milestone Demo') ->assertSee('Download MoM (.txt)'); // 2. Test Download MoM as .txt $responseMom = $this->actingAs($this->pm) ->get(route('client-calls.download-mom', $note->id)); $responseMom->assertStatus(200); $this->assertStringContainsString('attachment;', $responseMom->headers->get('content-disposition')); $this->assertStringContainsString('MINUTES OF MEETING (MoM)', $responseMom->getContent()); $this->assertStringContainsString('Enterprise Cloud Migration', $responseMom->getContent()); // 3. Test Download To-Dos as .txt $responseTodos = $this->actingAs($this->pm) ->get(route('client-calls.download-todos', $note->id)); $responseTodos->assertStatus(200); $this->assertStringContainsString('attachment;', $responseTodos->headers->get('content-disposition')); $this->assertStringContainsString('ACTIONABLE TO-DO LIST', $responseTodos->getContent()); $this->assertStringContainsString('Deploy AWS staging templates', $responseTodos->getContent()); // 4. Test Fetch Emotion Details (Why happy) $this->actingAs($this->pm) ->getJson(route('client-calls.emotion-details', $note->id)) ->assertStatus(200) ->assertJson([ 'success' => true, 'detected_sentiment' => 'happy', ]) ->assertJsonStructure([ 'emotion_details' => [ 'emotion', 'headline', 'bullet_points', 'suggested_action', ] ]); // 5. Test Toggle To-Do status $this->actingAs($this->pm) ->postJson(route('client-calls.toggle-todo', ['id' => $note->id, 'todoId' => 1])) ->assertStatus(200) ->assertJson(['success' => true]); $this->assertTrue($note->fresh()->todo_items[0]['completed']); } public function test_client_list_does_not_display_sentiment_badges_on_cards(): void { $client = Client::create([ 'client_name' => 'Alice Wonder', 'client_email' => 'alice@wonderland.com', 'project_name' => 'Wonder App', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', 'latest_sentiment' => 'angry', ]); $response = $this->actingAs($this->pm) ->get(route('client-calls.index')); $response->assertStatus(200) ->assertSee('Wonder App') ->assertSee('Alice Wonder') ->assertDontSee('sentiment-angry') ->assertDontSee('sentiment-happy'); } public function test_client_hub_shows_date_wise_calls_section_and_tasks(): void { $client = Client::create([ 'client_name' => 'David Warner', 'client_email' => 'david@cricket.io', 'project_name' => 'Live Score System', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Initial Sprint Review', 'meeting_code' => 'meet-score-1', 'host_user_id' => $this->pm->id, 'status' => 'completed', ]); $note = ClientCallNote::create([ 'client_meeting_id' => $meeting->id, 'client_id' => $client->id, 'created_by' => $this->pm->id, 'summary' => 'Reviewed live scoring latency.', 'mom_content' => 'Full MoM details here.', 'detected_sentiment' => 'happy', 'todo_items' => [ ['id' => 1, 'task' => 'Optimize websocket latency', 'owner' => 'Dev Team', 'priority' => 'High', 'deadline' => 'Aug 24, 2026', 'completed' => false], ], ]); // Access Client Hub / Show page $response = $this->actingAs($this->pm) ->get(route('client-calls.show', $client->id)); $response->assertStatus(200) ->assertSee('Live Score System') ->assertSee('Date-wise Call History & MoM Details', false) ->assertSee('Initial Sprint Review') ->assertSee('Optimize websocket latency') ->assertSee('Happy / Satisfied'); } public function test_add_and_delete_todo_items(): void { $client = Client::create([ 'client_name' => 'Emma Watson', 'client_email' => 'emma@cinema.com', 'project_name' => 'Film Portal', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Post-Production Sync', 'meeting_code' => 'meet-film-1', 'host_user_id' => $this->pm->id, 'status' => 'completed', ]); $note = ClientCallNote::create([ 'client_meeting_id' => $meeting->id, 'client_id' => $client->id, 'created_by' => $this->pm->id, 'summary' => 'Sync on release roadmap.', 'mom_content' => 'MoM Content.', 'detected_sentiment' => 'neutral', 'todo_items' => [ ['id' => 1, 'task' => 'First Task', 'owner' => 'PM', 'priority' => 'Medium', 'deadline' => 'TBD', 'completed' => false] ], ]); // 1. Add new To-Do $addRes = $this->actingAs($this->pm) ->postJson(route('client-calls.add-todo', $note->id), [ 'task' => 'Deploy video CDN endpoints', 'owner' => 'Alex Dev', 'priority' => 'High', 'deadline' => 'Aug 26, 2026', ]); $addRes->assertStatus(200) ->assertJson([ 'success' => true, 'todo' => [ 'task' => 'Deploy video CDN endpoints', 'owner' => 'Alex Dev', 'priority' => 'High', ] ]); $this->assertCount(2, $note->fresh()->todo_items); // 2. Delete the first To-Do $delRes = $this->actingAs($this->pm) ->deleteJson(route('client-calls.delete-todo', ['id' => $note->id, 'todoId' => 1])); $delRes->assertStatus(200) ->assertJson(['success' => true]); $this->assertCount(1, $note->fresh()->todo_items); $this->assertEquals('Deploy video CDN endpoints', $note->fresh()->todo_items[0]['task']); } public function test_upload_recording_endpoint_saves_file_and_updates_database(): void { \Illuminate\Support\Facades\Storage::fake('public'); $client = Client::create([ 'client_name' => 'Sara Connor', 'client_email' => 'sara@cyberdyne.com', 'project_name' => 'AI Defense System', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Weekly Sync with Cyberdyne', 'meeting_code' => 'meet-cyber-rec-1', 'host_user_id' => $this->pm->id, 'status' => 'in_progress', ]); $note = ClientCallNote::create([ 'client_meeting_id' => $meeting->id, 'client_id' => $client->id, 'created_by' => $this->pm->id, 'summary' => 'Ongoing call notes', 'mom_content' => 'MoM in progress', 'detected_sentiment' => 'happy', ]); $fakeVideo = \Illuminate\Http\UploadedFile::fake()->create('call_recording.webm', 2048, 'video/webm'); $response = $this->post(route('client-calls.upload-recording', $meeting->meeting_code), [ 'video' => $fakeVideo, ]); $response->assertStatus(200) ->assertJson([ 'success' => true, ]); $meeting->refresh(); $note->refresh(); $this->assertNotNull($meeting->recording_path); $this->assertNotEmpty($meeting->recordings); $this->assertNotNull($note->recording_path); $this->assertNotEmpty($note->recordings); $this->assertFileExists(public_path(ltrim($meeting->recording_path, '/'))); // Clean up created test recording file @unlink(public_path(ltrim($meeting->recording_path, '/'))); } public function test_meeting_and_note_recording_downloads(): void { $client = Client::create([ 'client_name' => 'Thomas Anderson', 'client_email' => 'neo@matrix.io', 'project_name' => 'Matrix Simulator', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Matrix Review', 'meeting_code' => 'meet-matrix-99', 'host_user_id' => $this->pm->id, 'status' => 'completed', ]); // Create a dummy video file in public storage $dir = public_path('uploads/client_recordings/' . $meeting->meeting_code); if (!file_exists($dir)) { mkdir($dir, 0777, true); } $testFilePath = $dir . '/test_rec.webm'; file_put_contents($testFilePath, 'dummy webm content'); $relPath = 'uploads/client_recordings/' . $meeting->meeting_code . '/test_rec.webm'; $meeting->update(['recording_path' => $relPath]); $note = ClientCallNote::create([ 'client_meeting_id' => $meeting->id, 'client_id' => $client->id, 'created_by' => $this->pm->id, 'summary' => 'Discussion on simulated physics.', 'mom_content' => 'MoM details here.', 'recording_path' => $relPath, ]); // 1. Download meeting recording $resMeeting = $this->actingAs($this->pm) ->get(route('client-calls.download-meeting-recording', $meeting->id)); $resMeeting->assertStatus(200); // 2. Download note recording $resNote = $this->actingAs($this->pm) ->get(route('client-calls.download-note-recording', $note->id)); $resNote->assertStatus(200); // Clean up @unlink($testFilePath); @rmdir($dir); } public function test_ending_meeting_links_recording_path_to_call_notes(): void { Mail::fake(); $client = Client::create([ 'client_name' => 'Bruce Wayne', 'client_email' => 'bruce@waynecorp.com', 'project_name' => 'Batmobile OS', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Batmobile Specs Call', 'meeting_code' => 'meet-bat-101', 'host_user_id' => $this->pm->id, 'status' => 'in_progress', ]); $response = $this->actingAs($this->pm) ->postJson(route('client-calls.end-call', $meeting->meeting_code), [ 'transcript' => '[10:00 AM] PM: Testing client recording flow.', 'live_notes' => 'Armor upgrades agreed.', 'emotion_timeline' => [['emotion' => 'happy', 'time' => time()]], 'recording_path' => 'uploads/client_recordings/meet-bat-101/rec.webm', ]); $response->assertStatus(200) ->assertJsonStructure(['success', 'redirect_url', 'note_id']); $note = ClientCallNote::where('client_meeting_id', $meeting->id)->first(); $this->assertNotNull($note); $this->assertEquals('uploads/client_recordings/meet-bat-101/rec.webm', $note->recording_path); $this->assertNotEmpty($note->recordings); $meeting->refresh(); $this->assertEquals('uploads/client_recordings/meet-bat-101/rec.webm', $meeting->recording_path); } public function test_client_hub_and_logs_pages_render_recording_actions_when_present(): void { $client = Client::create([ 'client_name' => 'Diana Prince', 'client_email' => 'diana@themyscira.org', 'project_name' => 'Shield Security', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Defense Protocol Sync', 'meeting_code' => 'meet-shield-202', 'host_user_id' => $this->pm->id, 'status' => 'completed', 'recording_path' => 'uploads/client_recordings/meet-shield-202/rec.webm', ]); $note = ClientCallNote::create([ 'client_meeting_id' => $meeting->id, 'client_id' => $client->id, 'created_by' => $this->pm->id, 'summary' => 'Reviewed aegis defense protocols.', 'mom_content' => 'Full MoM details here.', 'detected_sentiment' => 'happy', 'recording_path' => 'uploads/client_recordings/meet-shield-202/rec.webm', ]); // 1. Check Show page renders Video Call Recording $showRes = $this->actingAs($this->pm) ->get(route('client-calls.show', $client->id)); $showRes->assertStatus(200) ->assertSee('Video Call Recording') ->assertSee('Watch Recording') ->assertSee('Download Video'); // 2. Check Logs page renders Recorded badge & Play Video button $logsRes = $this->actingAs($this->pm) ->get(route('client-calls.logs')); $logsRes->assertStatus(200) ->assertSee('Recorded') ->assertSee('Play Video') ->assertSee('Download Video Recording', false); } public function test_ai_emotion_detection_all_scenarios(): void { $client = Client::create([ 'client_name' => 'Emotion Test Client', 'client_email' => 'emotion@test.com', 'project_name' => 'Emotion AI Testing', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Emotion Scenarios Test', 'meeting_code' => 'meet-emotion-scenarios', 'host_user_id' => $this->pm->id, 'status' => 'in_progress', ]); // Scenario 1: Frustrated speech -> Angry $resAngry = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [ 'speech_text' => 'This continuous delay is unacceptable and we missed deadline.', ]); $resAngry->assertStatus(200)->assertJson(['emotion' => 'angry']); // Scenario 2: Negated happy phrase -> Angry $resNegated = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [ 'speech_text' => 'We are not happy and not satisfied with the progress.', ]); $resNegated->assertStatus(200)->assertJson(['emotion' => 'angry']); // Scenario 3: Concern / hesitation -> Sad $resSad = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [ 'speech_text' => 'We are worried and concerned about the upcoming launch date.', ]); $resSad->assertStatus(200)->assertJson(['emotion' => 'sad']); // Scenario 4: Praise and delight -> Happy $resHappy = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [ 'speech_text' => 'The sprint demo looks fantastic and we love the new dashboard.', ]); $resHappy->assertStatus(200)->assertJson(['emotion' => 'happy']); // Scenario 5: Dialogue with words like "made" and "message" (formerly false positive triggers) -> Neutral $resNeutral = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [ 'speech_text' => 'We made a roadmap plan and I will send you a message shortly.', ]); $resNeutral->assertStatus(200)->assertJson(['emotion' => 'neutral']); // Scenario 6: Empty speech -> Neutral $resEmpty = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [ 'speech_text' => '', ]); $resEmpty->assertStatus(200)->assertJson(['emotion' => 'neutral']); } public function test_ai_service_mom_generation_and_fallback_engine(): void { $aiService = app(\App\Services\AiClientCallService::class); // Test happy conversation MoM $happyMoM = $aiService->generateMoM( 'Client: The release looks great, wonderful job team!', 'Reviewed sprint deliverables demo.', 'Payment Gateway', 'Fintech Corp' ); $this->assertEquals('happy', $happyMoM['detected_sentiment']); $this->assertNotEmpty($happyMoM['summary']); $this->assertNotEmpty($happyMoM['todo_items']); $this->assertNotEmpty($happyMoM['emotion_details']); // Test angry conversation MoM $angryMoM = $aiService->generateMoM( 'Client: This broken feature and missed deadline is unacceptable!', 'Client escalated timeline delay.', 'Payment Gateway', 'Fintech Corp' ); $this->assertEquals('angry', $angryMoM['detected_sentiment']); $this->assertTrue($angryMoM['angry_analysis']['is_angry']); $this->assertNotEmpty($angryMoM['angry_analysis']['trigger_words']); // Test sad conversation MoM $sadMoM = $aiService->generateMoM( 'Client: We are disappointed and worried about budget allocations.', 'Discussion on risk factors.', 'Payment Gateway', 'Fintech Corp' ); $this->assertEquals('sad', $sadMoM['detected_sentiment']); // Test neutral conversation MoM $neutralMoM = $aiService->generateMoM( 'Client: We made notes and will review the API contracts next week.', 'Standard sync.', 'Payment Gateway', 'Fintech Corp' ); $this->assertEquals('neutral', $neutralMoM['detected_sentiment']); } public function test_resend_mom_email_endpoint(): void { Mail::fake(); $client = Client::create([ 'client_name' => 'Email Test Client', 'client_email' => 'emailclient@test.com', 'project_name' => 'Email Dispatches', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'MoM Dispatch Sync', 'meeting_code' => 'meet-dispatch-789', 'host_user_id' => $this->pm->id, 'status' => 'completed', ]); $note = ClientCallNote::create([ 'client_meeting_id' => $meeting->id, 'client_id' => $client->id, 'created_by' => $this->pm->id, 'summary' => 'MoM dispatch testing', 'mom_content' => 'Full MoM details here.', 'detected_sentiment' => 'happy', ]); $response = $this->actingAs($this->pm) ->postJson(route('client-calls.resend-email', $note->id)); $response->assertStatus(200) ->assertJson([ 'success' => true, ]); Mail::assertSent(ClientCallMomMail::class); } public function test_signaling_heartbeat_and_chat_messages(): void { $client = Client::create([ 'client_name' => 'Signal Client', 'client_email' => 'signal@test.com', 'project_name' => 'Signal Project', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'WebRTC Signal Test', 'meeting_code' => 'meet-signal-test-321', 'host_user_id' => $this->pm->id, 'status' => 'in_progress', ]); $response = $this->postJson(route('client-calls.heartbeat', $meeting->meeting_code), [ 'peer_id' => 'peer_test_1', 'peer_name' => 'Alice Tester', 'chat_message' => ['text' => 'Hello everyone!'], ]); $response->assertStatus(200) ->assertJsonStructure(['active_peers', 'signals', 'chat_messages']); $meeting->refresh(); $this->assertNotEmpty($meeting->active_peers); $this->assertNotEmpty($meeting->chat_messages); $this->assertEquals('Hello everyone!', $meeting->chat_messages[0]['text']); } public function test_actual_discussed_tasks_and_mom_are_extracted_from_notes_and_transcript(): void { Mail::fake(); $client = Client::create([ 'client_name' => 'Michael Scott', 'client_email' => 'michael@dunder.com', 'project_name' => 'Paper Ordering Portal', 'responsible_user_id' => $this->pm->id, 'created_by' => $this->admin->id, 'status' => 'active', ]); $meeting = ClientMeeting::create([ 'client_id' => $client->id, 'title' => 'Feature Review Call', 'meeting_code' => 'meet-paper-portal-77', 'host_user_id' => $this->pm->id, 'status' => 'in_progress', ]); $response = $this->actingAs($this->pm) ->postJson(route('client-calls.end-call', $meeting->meeting_code), [ 'transcript' => "[10:01 AM] Michael: We need to change the logo to navy blue and add PayPal checkout by Friday.\n[10:02 AM] Alex Dev: I will implement PayPal checkout by Friday and update the logo.", 'live_notes' => "- Fix shopping cart checkout discount code bug\n- Sarah will send updated invoice by tomorrow\n- Client approved the new homepage layout", ]); $response->assertStatus(200) ->assertJson(['success' => true]); $note = ClientCallNote::where('client_meeting_id', $meeting->id)->first(); $this->assertNotNull($note); // Verify that extracted tasks contain the actual discussed items $tasks = collect($note->todo_items)->pluck('task')->implode(' '); $this->assertStringContainsString('shopping cart checkout discount code bug', strtolower($tasks)); $this->assertStringContainsString('invoice by tomorrow', strtolower($tasks)); // Verify MoM content contains the actual discussion points $this->assertStringContainsString('shopping cart checkout discount code bug', strtolower($note->mom_content)); $this->assertStringContainsString('paypal', strtolower($note->mom_content)); // Verify that generic placeholder tasks are NOT present $this->assertStringNotContainsString('deploy latest sprint milestone updates to staging environment', strtolower($tasks)); $this->assertStringNotContainsString('provide updated api contracts and sandbox test credentials', strtolower($tasks)); } }