create(['title' => 'DNS', 'content' => 'stappen']); $quickReply = QuickReply::query()->create(['title' => 'DNS Quick', 'content' => 'Gebruik DNS quick antwoord', 'is_active' => true]); $article->quickReplies()->sync([$quickReply->id]); $ticket = Ticket::query()->create(['message' => 'DNS vraag', 'status' => 'queued']); $embedding = $this->createMock(EmbeddingService::class); $embedding->method('embed')->willReturn(array_fill(0, 768, 0.1)); $embedding->method('context')->willReturn(['provider_instance_id' => 'p1', 'embedding_model' => 'm1']); $semantic = $this->createMock(SemanticSearchService::class); $semantic->method('findBestArticle')->willReturn([ 'best_article' => $article, 'confidence' => 0.9, 'explanation' => 'match', 'top_3_candidates' => [], 'top_5_candidates' => [], 'retrieval_meta' => [], 'requested_tool_call' => ['action' => 'domain_inf', 'parameters' => ['sld' => 'example', 'tld' => 'nl']], 'classifier_raw_response' => ['mode' => 'llm'], ]); $normalizer = $this->createMock(TicketNormalizationService::class); $normalizer->method('normalize')->willReturn([ 'normalized_message' => 'dns vraag', 'redaction_report' => ['language' => 'nl'], ]); $logger = app(TicketProcessingLoggerService::class); $knowledgeGap = $this->createMock(KnowledgeGapService::class); $knowledgeGap->method('shouldCreateDraft')->willReturn(false); $toolCallService = $this->createMock(TicketToolCallService::class); $toolCallService->expects($this->never())->method('executeRequestedTool'); $quickReplyResolver = $this->createMock(QuickReplyResolver::class); $quickReplyResolver->method('resolveForArticle')->willReturn($quickReply); $supportReply = $this->createMock(SupportReplyService::class); $supportReply->expects($this->never())->method('build'); $job = new ProcessTicketJob($ticket->id); $job->handle( $embedding, $semantic, $normalizer, $logger, $knowledgeGap, $toolCallService, $quickReplyResolver, $supportReply, new TicketResultPayloadBuilder ); $ticket->refresh(); $this->assertSame('completed', $ticket->status); $this->assertSame('Gebruik DNS quick antwoord', $ticket->support_reply); $this->assertSame($quickReply->id, $ticket->result_payload['quick_reply']['id']); } public function test_it_executes_tool_call_when_no_quick_reply_exists(): void { $article = Article::query()->create(['title' => 'DNS', 'content' => 'stappen', 'allowed_actions' => ['domain_inf']]); $ticket = Ticket::query()->create([ 'message' => 'DNS vraag', 'status' => 'queued', 'api_credentials' => ['apiuser' => 'u', 'apipassword' => 'p'], ]); $embedding = $this->createMock(EmbeddingService::class); $embedding->method('embed')->willReturn(array_fill(0, 768, 0.1)); $embedding->method('context')->willReturn(['provider_instance_id' => 'p1', 'embedding_model' => 'm1']); $semantic = $this->createMock(SemanticSearchService::class); $semantic->method('findBestArticle')->willReturn([ 'best_article' => $article, 'confidence' => 0.9, 'explanation' => 'match', 'top_3_candidates' => [], 'top_5_candidates' => [], 'retrieval_meta' => [], 'requested_tool_call' => ['action' => 'domain_inf', 'parameters' => ['sld' => 'example', 'tld' => 'nl']], 'classifier_raw_response' => ['mode' => 'llm'], ]); $normalizer = $this->createMock(TicketNormalizationService::class); $normalizer->method('normalize')->willReturn([ 'normalized_message' => 'dns vraag', 'redaction_report' => ['language' => 'nl'], ]); $knowledgeGap = $this->createMock(KnowledgeGapService::class); $knowledgeGap->method('shouldCreateDraft')->willReturn(false); $quickReplyResolver = $this->createMock(QuickReplyResolver::class); $quickReplyResolver->method('resolveForArticle')->willReturn(null); $toolRecord = new \App\Models\TicketToolCall([ 'action' => 'domain_inf', 'status' => 'success', 'parameters' => ['sld' => 'example', 'tld' => 'nl'], ]); $toolCallService = $this->createMock(TicketToolCallService::class); $toolCallService->method('executeRequestedTool')->willReturn($toolRecord); $supportReply = $this->createMock(SupportReplyService::class); $supportReply->method('build')->willReturn('1. Doe stap 1'); $job = new ProcessTicketJob($ticket->id); $job->handle( $embedding, $semantic, $normalizer, app(TicketProcessingLoggerService::class), $knowledgeGap, $toolCallService, $quickReplyResolver, $supportReply, new TicketResultPayloadBuilder ); $ticket->refresh(); $this->assertSame('completed', $ticket->status); $this->assertSame('1. Doe stap 1', $ticket->support_reply); $this->assertSame('domain_inf', $ticket->result_payload['requested_tool_call']['action']); } public function test_it_marks_knowledge_gap_and_skips_customer_reply(): void { $article = Article::query()->create(['title' => 'DNS', 'content' => 'stappen']); $ticket = Ticket::query()->create(['message' => 'DNS vraag', 'status' => 'queued']); $embedding = $this->createMock(EmbeddingService::class); $embedding->method('embed')->willReturn(array_fill(0, 768, 0.1)); $embedding->method('context')->willReturn(['provider_instance_id' => 'p1', 'embedding_model' => 'm1']); $semantic = $this->createMock(SemanticSearchService::class); $semantic->method('findBestArticle')->willReturn([ 'best_article' => $article, 'confidence' => 0.2, 'explanation' => 'low confidence', 'top_3_candidates' => [], 'top_5_candidates' => [], 'retrieval_meta' => [], 'requested_tool_call' => null, 'classifier_raw_response' => ['mode' => 'llm'], ]); $normalizer = $this->createMock(TicketNormalizationService::class); $normalizer->method('normalize')->willReturn([ 'normalized_message' => 'dns vraag', 'redaction_report' => ['language' => 'nl'], ]); $knowledgeGap = $this->createMock(KnowledgeGapService::class); $knowledgeGap->method('shouldCreateDraft')->willReturn(true); $knowledgeGap->method('suggestArticleDraft')->willReturn([ 'title' => 'Nieuwe DNS handleiding', 'content' => 'Nog aan te vullen', ]); $toolCallService = $this->createMock(TicketToolCallService::class); $toolCallService->expects($this->never())->method('executeRequestedTool'); $quickReplyResolver = $this->createMock(QuickReplyResolver::class); $quickReplyResolver->expects($this->never())->method('resolveForArticle'); $supportReply = $this->createMock(SupportReplyService::class); $supportReply->expects($this->never())->method('build'); $job = new ProcessTicketJob($ticket->id); $job->handle( $embedding, $semantic, $normalizer, app(TicketProcessingLoggerService::class), $knowledgeGap, $toolCallService, $quickReplyResolver, $supportReply, new TicketResultPayloadBuilder ); $ticket->refresh(); $this->assertTrue($ticket->needs_article_draft); $this->assertNull($ticket->support_reply); $this->assertSame('Nieuwe DNS handleiding', $ticket->result_payload['draft_article_suggestion']['title']); } }