| <?php |
|
|
| namespace Tests\Feature; |
|
|
| use App\Models\User; |
| use App\Services\OpenAiChatService; |
| use Illuminate\Foundation\Testing\RefreshDatabase; |
| use Tests\TestCase; |
|
|
| class ChatTest extends TestCase |
| { |
| use RefreshDatabase; |
|
|
| public function test_guest_redirected_from_chat_page(): void |
| { |
| $response = $this->get('/chat'); |
|
|
| $response->assertRedirect('/login'); |
| } |
|
|
| public function test_authenticated_user_can_open_chat_page(): void |
| { |
| $user = User::factory()->create(); |
|
|
| $response = $this->actingAs($user)->get('/chat'); |
|
|
| $response->assertRedirect('/tools'); |
| } |
|
|
| public function test_authenticated_user_can_open_chat_assistant_page(): void |
| { |
| $user = User::factory()->create(); |
| config([ |
| 'services.openai.chat_base_url' => 'https://router.huggingface.co/v1', |
| 'services.openai.chat_model' => 'CohereLabs/c4ai-command-r-08-2024:cohere', |
| 'services.openai.assistant_model' => 'gpt-4o', |
| ]); |
|
|
| $response = $this->actingAs($user)->get('/chat/assistant'); |
|
|
| $response->assertOk(); |
| $response->assertSee('What do you want to create?'); |
| $response->assertSee('OpenAI'); |
| $response->assertSee('Create a mockup'); |
| } |
|
|
| public function test_authenticated_user_can_open_chat_assistant_page_with_deepseek_label(): void |
| { |
| $user = User::factory()->create(); |
| config([ |
| 'services.openai.assistant_provider' => 'deepseek', |
| 'services.openai.assistant_model' => 'deepseek-chat', |
| 'services.deepseek.base_url' => 'https://api.deepseek.com', |
| ]); |
|
|
| $response = $this->actingAs($user)->get('/chat/assistant'); |
|
|
| $response->assertOk(); |
| $response->assertSee('DeepSeek'); |
| $response->assertSee('deepseek-chat'); |
| } |
|
|
| public function test_user_can_send_message_and_get_ai_reply(): void |
| { |
| $this->app->instance(OpenAiChatService::class, new class extends OpenAiChatService { |
| public function reply(array $messages): string |
| { |
| return 'Ini balasan dari AI (mock).'; |
| } |
| }); |
|
|
| $user = User::factory()->create(); |
|
|
| $response = $this->actingAs($user)->post('/chat', [ |
| 'prompt' => 'Halo AI', |
| ]); |
|
|
| $response->assertRedirect('/tools'); |
|
|
| $conversation = $user->chatConversations()->with('messages')->first(); |
|
|
| $this->assertNotNull($conversation); |
| $this->assertCount(2, $conversation->messages); |
| $this->assertSame('user', $conversation->messages[0]->role); |
| $this->assertSame('Halo AI', $conversation->messages[0]->content); |
| $this->assertSame('assistant', $conversation->messages[1]->role); |
| $this->assertSame('Ini balasan dari AI (mock).', $conversation->messages[1]->content); |
| } |
|
|
| public function test_user_can_send_message_from_assistant_page_and_get_ai_reply(): void |
| { |
| config([ |
| 'services.openai.base_url' => 'https://api.openai.com/v1', |
| 'services.openai.chat_base_url' => 'https://router.huggingface.co/v1', |
| 'services.openai.chat_model' => 'CohereLabs/c4ai-command-r-08-2024:cohere', |
| 'services.openai.assistant_provider' => 'openai', |
| 'services.openai.assistant_id' => 'asst_medilis_123', |
| 'services.openai.assistant_use_hosted_threads' => true, |
| 'services.openai.assistant_model' => 'gpt-4o', |
| ]); |
|
|
| $capture = new \stdClass(); |
| $capture->threadIds = []; |
| $capture->prompts = []; |
| $capture->instructions = []; |
|
|
| $this->app->instance(OpenAiChatService::class, new class($capture) extends OpenAiChatService { |
| public function __construct(private \stdClass $capture) |
| { |
| } |
|
|
| public function assistantReply( |
| string $prompt, |
| ?string $threadId = null, |
| ?string $additionalInstructions = null, |
| ?string $model = null |
| ): array { |
| \PHPUnit\Framework\Assert::assertSame('https://api.openai.com/v1', rtrim((string) config('services.openai.chat_base_url'), '/')); |
| \PHPUnit\Framework\Assert::assertSame('gpt-4o', (string) $model); |
|
|
| $this->capture->threadIds[] = $threadId; |
| $this->capture->prompts[] = $prompt; |
| $this->capture->instructions[] = $additionalInstructions; |
|
|
| return [ |
| 'thread_id' => $threadId ?: 'thread_medilis_001', |
| 'content' => 'Struktur landing page klinik: hero premium, daftar layanan unggulan, social proof, FAQ singkat, lalu CTA booking yang jelas.', |
| ]; |
| } |
| }); |
|
|
| $user = User::factory()->create(); |
|
|
| $response = $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'Buatkan konsep landing page klinik.', |
| ]); |
|
|
| $response->assertRedirect('/chat/assistant'); |
|
|
| $conversation = $user->chatConversations() |
| ->where('title', 'Assistant') |
| ->with('messages') |
| ->first(); |
|
|
| $this->assertNotNull($conversation); |
| $this->assertCount(2, $conversation->messages); |
| $this->assertSame('thread_medilis_001', $conversation->openai_thread_id); |
| $this->assertSame('user', $conversation->messages[0]->role); |
| $this->assertSame('Buatkan konsep landing page klinik.', $conversation->messages[0]->content); |
| $this->assertSame('assistant', $conversation->messages[1]->role); |
| $this->assertSame('Struktur landing page klinik: hero premium, daftar layanan unggulan, social proof, FAQ singkat, lalu CTA booking yang jelas.', $conversation->messages[1]->content); |
| $this->assertSame([null], $capture->threadIds); |
| $this->assertStringContainsString('Buatkan konsep landing page klinik.', $capture->prompts[0]); |
| $this->assertStringContainsString('jangan jawab dengan template generik', (string) $capture->instructions[0]); |
| } |
|
|
| public function test_assistant_shows_limit_message_when_daily_quota_is_exhausted(): void |
| { |
| config([ |
| 'services.openai.daily_message_limit' => 1, |
| ]); |
|
|
| $this->app->instance(OpenAiChatService::class, new class extends OpenAiChatService { |
| public function reply(array $messages): string |
| { |
| throw new \RuntimeException('reply seharusnya tidak dipanggil ketika limit harian habis.'); |
| } |
| }); |
|
|
| $user = User::factory()->create(); |
| $conversation = $user->chatConversations()->create([ |
| 'title' => 'Assistant', |
| ]); |
|
|
| for ($i = 1; $i <= 5; $i++) { |
| $conversation->messages()->create([ |
| 'role' => 'user', |
| 'content' => "Prompt ke-{$i} hari ini", |
| ]); |
| } |
|
|
| $response = $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'Prompt kedua yang harus kena limit', |
| ]); |
|
|
| $response->assertRedirect('/chat/assistant'); |
| $response->assertSessionHas('chat_error'); |
|
|
| $latestAssistant = $conversation->fresh('messages')->messages |
| ->where('role', 'assistant') |
| ->last(); |
|
|
| $this->assertNotNull($latestAssistant); |
| $this->assertStringContainsString('Batas harian chat (5) sudah tercapai', (string) $latestAssistant->content); |
| } |
|
|
| public function test_assistant_route_reuses_saved_openai_thread_id_for_follow_up_messages(): void |
| { |
| config([ |
| 'services.openai.base_url' => 'https://api.openai.com/v1', |
| 'services.openai.assistant_provider' => 'openai', |
| 'services.openai.assistant_id' => 'asst_medilis_123', |
| 'services.openai.assistant_use_hosted_threads' => true, |
| 'services.openai.assistant_model' => 'gpt-4o', |
| ]); |
|
|
| $capture = new \stdClass(); |
| $capture->threadIds = []; |
|
|
| $this->app->instance(OpenAiChatService::class, new class($capture) extends OpenAiChatService { |
| public function __construct(private \stdClass $capture) |
| { |
| } |
|
|
| public function assistantReply( |
| string $prompt, |
| ?string $threadId = null, |
| ?string $additionalInstructions = null, |
| ?string $model = null |
| ): array { |
| $this->capture->threadIds[] = $threadId; |
|
|
| return [ |
| 'thread_id' => $threadId ?: 'thread_medilis_001', |
| 'content' => 'Tiga CTA klinik gigi: booking konsultasi, cek slot dokter minggu ini, dan mulai treatment estetik dari sesi awal.', |
| ]; |
| } |
| }); |
|
|
| $user = User::factory()->create(); |
|
|
| $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'Buat 3 CTA untuk klinik gigi.', |
| ])->assertRedirect('/chat/assistant'); |
|
|
| $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'jangan template', |
| ])->assertRedirect('/chat/assistant'); |
|
|
| $conversation = $user->chatConversations() |
| ->where('title', 'Assistant') |
| ->first(); |
|
|
| $this->assertNotNull($conversation); |
| $this->assertSame('thread_medilis_001', $conversation->openai_thread_id); |
| $this->assertSame([null, 'thread_medilis_001'], $capture->threadIds); |
| } |
|
|
| public function test_assistant_defaults_to_direct_openai_chat_even_if_assistant_id_exists(): void |
| { |
| config([ |
| 'services.openai.base_url' => 'https://api.openai.com/v1', |
| 'services.openai.chat_base_url' => 'https://router.huggingface.co/v1', |
| 'services.openai.chat_model' => 'CohereLabs/c4ai-command-r-08-2024:cohere', |
| 'services.openai.assistant_provider' => 'openai', |
| 'services.openai.assistant_id' => 'asst_medilis_123', |
| 'services.openai.assistant_use_hosted_threads' => false, |
| 'services.openai.assistant_model' => 'gpt-4o', |
| ]); |
|
|
| $this->app->instance(OpenAiChatService::class, new class extends OpenAiChatService { |
| public function assistantReply( |
| string $prompt, |
| ?string $threadId = null, |
| ?string $additionalInstructions = null, |
| ?string $model = null |
| ): array { |
| throw new \RuntimeException('assistantReply seharusnya tidak dipakai saat hosted threads dimatikan.'); |
| } |
|
|
| public function reply(array $messages): string |
| { |
| \PHPUnit\Framework\Assert::assertSame('https://api.openai.com/v1', rtrim((string) config('services.openai.chat_base_url'), '/')); |
| \PHPUnit\Framework\Assert::assertSame('gpt-4o', (string) config('services.openai.chat_model')); |
|
|
| return 'Ini jawaban OpenAI direct chat yang cukup spesifik, terstruktur, dan tidak berupa template bantuan umum.'; |
| } |
| }); |
|
|
| $user = User::factory()->create(); |
|
|
| $response = $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'cek struktur halaman klinik ini', |
| ]); |
|
|
| $response->assertRedirect('/chat/assistant'); |
|
|
| $conversation = $user->chatConversations() |
| ->where('title', 'Assistant') |
| ->with('messages') |
| ->first(); |
|
|
| $this->assertNotNull($conversation); |
| $this->assertNull($conversation->openai_thread_id); |
| $this->assertSame('Ini jawaban OpenAI direct chat yang cukup spesifik, terstruktur, dan tidak berupa template bantuan umum.', $conversation->messages->last()->content); |
| } |
|
|
| public function test_assistant_can_use_deepseek_direct_chat(): void |
| { |
| config([ |
| 'services.openai.assistant_provider' => 'deepseek', |
| 'services.openai.assistant_model' => 'deepseek-chat', |
| 'services.deepseek.base_url' => 'https://api.deepseek.com', |
| 'services.deepseek.api_key' => 'deepseek_test_key', |
| 'services.openai.assistant_use_hosted_threads' => false, |
| ]); |
|
|
| $this->app->instance(OpenAiChatService::class, new class extends OpenAiChatService { |
| public function assistantReply( |
| string $prompt, |
| ?string $threadId = null, |
| ?string $additionalInstructions = null, |
| ?string $model = null |
| ): array { |
| throw new \RuntimeException('assistantReply tidak boleh dipakai untuk DeepSeek direct chat.'); |
| } |
|
|
| public function reply(array $messages): string |
| { |
| \PHPUnit\Framework\Assert::assertSame('https://api.deepseek.com', rtrim((string) config('services.openai.chat_base_url'), '/')); |
| \PHPUnit\Framework\Assert::assertSame('deepseek-chat', (string) config('services.openai.chat_model')); |
| \PHPUnit\Framework\Assert::assertSame('deepseek_test_key', (string) config('services.openai.chat_api_key')); |
|
|
| return 'Aku belum bisa buka URL itu langsung, tapi aku bisa bantu audit struktur, copy, dan flow fitur dari data yang kamu kirim.'; |
| } |
| }); |
|
|
| $user = User::factory()->create(); |
|
|
| $response = $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'cek halaman klinik ini', |
| ]); |
|
|
| $response->assertRedirect('/chat/assistant'); |
|
|
| $conversation = $user->chatConversations() |
| ->where('title', 'Assistant') |
| ->with('messages') |
| ->first(); |
|
|
| $this->assertNotNull($conversation); |
| $this->assertNull($conversation->openai_thread_id); |
| $this->assertSame('Aku belum bisa buka URL itu langsung, tapi aku bisa bantu audit struktur, copy, dan flow fitur dari data yang kamu kirim.', $conversation->messages->last()->content); |
| } |
|
|
| public function test_assistant_direct_chat_retries_when_first_reply_is_generic_low_signal(): void |
| { |
| config([ |
| 'services.openai.assistant_provider' => 'deepseek', |
| 'services.openai.assistant_model' => 'deepseek-chat', |
| 'services.deepseek.base_url' => 'https://api.deepseek.com', |
| 'services.deepseek.api_key' => 'deepseek_test_key', |
| 'services.openai.assistant_use_hosted_threads' => false, |
| ]); |
|
|
| $counter = new \stdClass(); |
| $counter->value = 0; |
|
|
| $this->app->instance(OpenAiChatService::class, new class($counter) extends OpenAiChatService { |
| public function __construct(private \stdClass $counter) |
| { |
| } |
|
|
| public function reply(array $messages): string |
| { |
| $this->counter->value++; |
|
|
| if ($this->counter->value === 1) { |
| return 'Saya siap membantu dengan berbagai pertanyaan atau tugas terkait coding. Apa yang ingin Anda diskusikan?'; |
| } |
|
|
| $systemMessages = collect($messages)->where('role', 'system')->pluck('content')->implode("\n"); |
| \PHPUnit\Framework\Assert::assertStringContainsString('masih terlalu generik atau mengulang respons lama', $systemMessages); |
|
|
| return 'Bisa. Aku belum bisa akses URL itu langsung dari server ini, tapi aku bisa bantu audit konten, UX, dan copy dari screenshot atau source code yang kamu kirim.'; |
| } |
| }); |
|
|
| $user = User::factory()->create(); |
|
|
| $response = $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'apakah anda bisa cek projek https://medilis.cloud ?', |
| ]); |
|
|
| $response->assertRedirect('/chat/assistant'); |
| $this->assertSame(2, $counter->value); |
|
|
| $conversation = $user->chatConversations() |
| ->where('title', 'Assistant') |
| ->with('messages') |
| ->first(); |
|
|
| $this->assertNotNull($conversation); |
| $this->assertStringContainsString('Aku belum bisa akses URL itu langsung', $conversation->messages->last()->content); |
| } |
|
|
| public function test_assistant_direct_chat_uses_fallback_when_replies_stay_generic_after_retry(): void |
| { |
| config([ |
| 'services.openai.assistant_provider' => 'deepseek', |
| 'services.openai.assistant_model' => 'deepseek-chat', |
| 'services.deepseek.base_url' => 'https://api.deepseek.com', |
| 'services.deepseek.api_key' => 'deepseek_test_key', |
| 'services.openai.assistant_use_hosted_threads' => false, |
| ]); |
|
|
| $counter = new \stdClass(); |
| $counter->value = 0; |
|
|
| $this->app->instance(OpenAiChatService::class, new class($counter) extends OpenAiChatService { |
| public function __construct(private \stdClass $counter) |
| { |
| } |
|
|
| public function reply(array $messages): string |
| { |
| $this->counter->value++; |
|
|
| return 'Saya siap membantu dengan berbagai pertanyaan atau tugas terkait coding. Apa yang ingin Anda diskusikan atau butuhkan bantuan saya?'; |
| } |
| }); |
|
|
| $user = User::factory()->create(); |
|
|
| $response = $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'Bikin versi yang lebih singkat dan jelas', |
| ]); |
|
|
| $response->assertRedirect('/chat/assistant'); |
| $this->assertSame(2, $counter->value); |
|
|
| $conversation = $user->chatConversations() |
| ->where('title', 'Assistant') |
| ->with('messages') |
| ->first(); |
|
|
| $this->assertNotNull($conversation); |
| $latest = (string) $conversation->messages->last()->content; |
| $this->assertStringContainsString('Versi singkat dan jelas', $latest); |
| $this->assertStringNotContainsString('Saya siap membantu dengan berbagai pertanyaan', $latest); |
| } |
|
|
| public function test_assistant_direct_chat_returns_fallback_when_provider_throws_exception(): void |
| { |
| config([ |
| 'services.openai.assistant_provider' => 'deepseek', |
| 'services.openai.assistant_model' => 'deepseek-chat', |
| 'services.deepseek.base_url' => 'https://api.deepseek.com', |
| 'services.deepseek.api_key' => 'deepseek_test_key', |
| 'services.openai.assistant_use_hosted_threads' => false, |
| ]); |
|
|
| $this->app->instance(OpenAiChatService::class, new class extends OpenAiChatService { |
| public function reply(array $messages): string |
| { |
| throw new \RuntimeException('Connection timed out while contacting provider'); |
| } |
| }); |
|
|
| $user = User::factory()->create(); |
|
|
| $response = $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'apakah anda bisa cek projek https://medilis.cloud ?', |
| ]); |
|
|
| $response->assertRedirect('/chat/assistant'); |
| $response->assertSessionHas('chat_error'); |
|
|
| $conversation = $user->chatConversations() |
| ->where('title', 'Assistant') |
| ->with('messages') |
| ->first(); |
|
|
| $this->assertNotNull($conversation); |
| $this->assertStringContainsString('Aku belum bisa akses URL itu langsung', (string) $conversation->messages->last()->content); |
| } |
|
|
| public function test_hosted_assistant_retries_when_first_reply_is_template_like(): void |
| { |
| config([ |
| 'services.openai.base_url' => 'https://api.openai.com/v1', |
| 'services.openai.assistant_provider' => 'openai', |
| 'services.openai.assistant_id' => 'asst_medilis_123', |
| 'services.openai.assistant_use_hosted_threads' => true, |
| 'services.openai.assistant_model' => 'gpt-4o', |
| ]); |
|
|
| $capture = new \stdClass(); |
| $capture->prompts = []; |
| $capture->instructions = []; |
| $capture->calls = 0; |
|
|
| $this->app->instance(OpenAiChatService::class, new class($capture) extends OpenAiChatService { |
| public function __construct(private \stdClass $capture) |
| { |
| } |
|
|
| public function assistantReply( |
| string $prompt, |
| ?string $threadId = null, |
| ?string $additionalInstructions = null, |
| ?string $model = null |
| ): array { |
| $this->capture->calls++; |
| $this->capture->prompts[] = $prompt; |
| $this->capture->instructions[] = $additionalInstructions; |
|
|
| if ($this->capture->calls === 1) { |
| return [ |
| 'thread_id' => 'thread_medilis_001', |
| 'content' => 'Saya selalu siap membantu Anda dengan berbagai pertanyaan dan tugas terkait coding maupun tanya jawab umum. Silakan ajukan pertanyaan atau tugas Anda, dan saya akan berusaha memberikan jawaban terbaik.', |
| ]; |
| } |
|
|
| return [ |
| 'thread_id' => 'thread_medilis_001', |
| 'content' => 'Aku belum bisa cek server atau browse URL itu langsung dari sini. Kalau kamu mau, aku bisa audit struktur halaman, copy, atau flow fitur berdasarkan screenshot, source code, atau error yang kamu kirim.', |
| ]; |
| } |
| }); |
|
|
| $user = User::factory()->create(); |
|
|
| $response = $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'apakah anda bisa cek projek https://medilis.cloud ?', |
| ]); |
|
|
| $response->assertRedirect('/chat/assistant'); |
| $this->assertSame(2, $capture->calls); |
| $this->assertStringContainsString('Jawab pesan user terakhir secara konkret', $capture->prompts[0]); |
| $this->assertStringContainsString('KOREKSI WAJIB', $capture->prompts[1]); |
| $this->assertStringContainsString('Koreksi keras', (string) $capture->instructions[1]); |
|
|
| $conversation = $user->chatConversations() |
| ->where('title', 'Assistant') |
| ->with('messages') |
| ->first(); |
|
|
| $this->assertNotNull($conversation); |
| $this->assertSame('Aku belum bisa cek server atau browse URL itu langsung dari sini. Kalau kamu mau, aku bisa audit struktur halaman, copy, atau flow fitur berdasarkan screenshot, source code, atau error yang kamu kirim.', $conversation->messages->last()->content); |
| } |
|
|
| public function test_assistant_follow_up_critique_rewrites_previous_answer_instead_of_answering_generically(): void |
| { |
| config([ |
| 'services.openai.base_url' => 'https://api.openai.com/v1', |
| 'services.openai.chat_base_url' => 'https://router.huggingface.co/v1', |
| 'services.openai.chat_model' => 'CohereLabs/c4ai-command-r-08-2024:cohere', |
| 'services.openai.assistant_model' => 'gpt-4o', |
| ]); |
|
|
| $user = User::factory()->create(); |
| $conversation = $user->chatConversations()->create([ |
| 'title' => 'Assistant', |
| ]); |
|
|
| $conversation->messages()->create([ |
| 'role' => 'user', |
| 'content' => 'Tulis 3 CTA singkat untuk klinik gigi premium', |
| ]); |
| $conversation->messages()->create([ |
| 'role' => 'assistant', |
| 'content' => "1. Jadwalkan kunjungan Anda sekarang.\n2. Senyum sempurna dengan perawatan premium kami.\n3. Hubungi kami untuk informasi lebih lanjut.", |
| ]); |
|
|
| $capture = new \stdClass(); |
| $capture->lastUserMessage = null; |
|
|
| $this->app->instance(OpenAiChatService::class, new class($capture) extends OpenAiChatService { |
| public function __construct(private \stdClass $capture) |
| { |
| } |
|
|
| public function reply(array $messages): string |
| { |
| $this->capture->lastUserMessage = collect($messages)->reverse()->firstWhere('role', 'user'); |
|
|
| return "1. Booking scaling gigi premium hari ini.\n2. Konsultasi cepat dengan dokter gigi estetik kami.\n3. Senyum rapi dimulai dari sesi pertama Anda."; |
| } |
| }); |
|
|
| $response = $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'apakah anda sudah cukup pintar ? tidak menjawab dengan template', |
| ]); |
|
|
| $response->assertRedirect('/chat/assistant'); |
|
|
| $latestReply = $conversation->fresh('messages')->messages->last(); |
| $lastUserMessage = (string) data_get($capture->lastUserMessage, 'content', ''); |
|
|
| $this->assertSame('assistant', $latestReply->role); |
| $this->assertStringContainsString('Booking scaling gigi premium hari ini.', $latestReply->content); |
| $this->assertStringContainsString('MODE: REWRITE_TOTAL', $lastUserMessage); |
| $this->assertStringContainsString('Tulis 3 CTA singkat untuk klinik gigi premium', $lastUserMessage); |
| $this->assertStringContainsString('tidak menjawab dengan template', strtolower($lastUserMessage)); |
| $this->assertStringContainsString('hubungi kami untuk informasi lebih lanjut', strtolower($lastUserMessage)); |
| } |
|
|
| public function test_assistant_retries_when_first_critique_reply_is_still_generic(): void |
| { |
| config([ |
| 'services.openai.base_url' => 'https://api.openai.com/v1', |
| 'services.openai.assistant_model' => 'gpt-4o', |
| ]); |
|
|
| $user = User::factory()->create(); |
| $conversation = $user->chatConversations()->create([ |
| 'title' => 'Assistant', |
| ]); |
|
|
| $conversation->messages()->create([ |
| 'role' => 'user', |
| 'content' => 'Tulis 3 CTA singkat untuk klinik gigi premium', |
| ]); |
| $conversation->messages()->create([ |
| 'role' => 'assistant', |
| 'content' => '1. Jadwalkan kunjungan Anda sekarang. 2. Hubungi kami untuk informasi lebih lanjut.', |
| ]); |
|
|
| $counter = new \stdClass(); |
| $counter->value = 0; |
|
|
| $this->app->instance(OpenAiChatService::class, new class($counter) extends OpenAiChatService { |
| public function __construct(private \stdClass $counter) |
| { |
| } |
|
|
| public function reply(array $messages): string |
| { |
| $this->counter->value++; |
|
|
| if ($this->counter->value === 1) { |
| return 'Jadwalkan kunjungan Anda sekarang dan hubungi kami untuk informasi lebih lanjut.'; |
| } |
|
|
| $systemMessages = collect($messages)->where('role', 'system')->pluck('content')->implode("\n"); |
| \PHPUnit\Framework\Assert::assertStringContainsString('Jawaban sebelumnya masih terlalu generik', $systemMessages); |
|
|
| return '1. Reservasi veneer premium minggu ini. 2. Konsultasi estetik gigi dalam 15 menit. 3. Mulai treatment senyum rapi dari sesi awal.'; |
| } |
| }); |
|
|
| $response = $this->actingAs($user)->post('/chat/assistant', [ |
| 'prompt' => 'jangan template', |
| ]); |
|
|
| $response->assertRedirect('/chat/assistant'); |
| $this->assertSame(2, $counter->value); |
|
|
| $latestReply = $conversation->fresh('messages')->messages->last(); |
| $this->assertStringContainsString('Reservasi veneer premium minggu ini.', $latestReply->content); |
| } |
| } |
|
|