assess([['text' => 'A requisição foi aprovada e paga.', 'source_ids' => ['request']]]); expect($assessment['uncited_fact_ids'])->toBe(['comparison', 'order'])->and($assessment['exact_source_text_matches'])->toBe(0) ->and($assessment['citation_coverage_complete'])->toBeFalse()->and($assessment['requires_human_review'])->toBeTrue() ->and($assessment['semantic_accuracy'])->toBe('not_automatically_verified'); Http::assertNothingSent(); }); test('offline AI assessment counts repeated citations once and distinguishes literal source matches', function () { $facts = app(SyntheticAiRehearsal::class)->preview()['facts']; $claims = array_map(fn ($fact) => ['text' => $fact['text'], 'source_ids' => [$fact['id'], $fact['id']]], $facts); $result = app(SyntheticAiAssessment::class)->assess($claims); expect($result['cited_facts'])->toBe(3)->and($result['exact_source_text_matches'])->toBe(3) ->and($result['citation_coverage_complete'])->toBeTrue()->and($result['requires_human_review'])->toBeTrue(); }); test('offline AI assessment rejects sources outside the built in synthetic scenario', function () { expect(fn () => app(SyntheticAiAssessment::class)->assess([['text' => 'Informação privada.', 'source_ids' => ['bank-account']]]))->toThrow(ValidationException::class); }); test('offline assessment command reads synthetic JSON without contacting a provider', function () { Http::fake(); $path = tempnam(sys_get_temp_dir(), 'sgc-ai-assess-'); try { file_put_contents($path, json_encode(['synthetic_only' => true, 'claims' => [['text' => 'Pedido fictício pendente.', 'source_ids' => ['request']]]], JSON_THROW_ON_ERROR)); $this->artisan('sgc:ai-assess', ['result' => $path])->expectsOutputToContain('uncited_fact_ids')->assertSuccessful(); file_put_contents($path, json_encode(['synthetic_only' => false, 'claims' => []], JSON_THROW_ON_ERROR)); $this->artisan('sgc:ai-assess', ['result' => $path])->assertFailed(); Http::assertNothingSent(); } finally { unlink($path); } });