true, 'sgc_intelligence.rehearsal.api_key' => 'synthetic-key-not-a-real-credential']); Http::preventStrayRequests(); }); function syntheticAiResponse(array $result): array { return ['status' => 'completed', 'output' => [['type' => 'message', 'role' => 'assistant', 'content' => [ ['type' => 'output_text', 'text' => json_encode($result, JSON_THROW_ON_ERROR)], ]]]]; } test('AI rehearsal defaults to a reviewable synthetic preview with no network or database reads', function () { Http::fake(); $queries = 0; DB::listen(function () use (&$queries) { $queries++; }); $preview = app(SyntheticAiRehearsal::class)->preview(); expect($preview['synthetic_only'])->toBeTrue()->and($preview['external_api_called'])->toBeFalse() ->and($preview['facts'])->toHaveCount(3)->and($queries)->toBe(0); $this->artisan('sgc:ai-rehearsal')->assertSuccessful(); Http::assertNothingSent(); }); test('AI rehearsal cannot call a provider while disabled or without a server credential', function (bool $enabled, ?string $key) { config(['sgc_intelligence.rehearsal.enabled' => $enabled, 'sgc_intelligence.rehearsal.api_key' => $key]); Http::fake(); expect(fn () => app(SyntheticAiRehearsal::class)->run())->toThrow(RuntimeException::class); $this->artisan('sgc:ai-rehearsal --live')->assertFailed(); Http::assertNothingSent(); })->with([[false, 'test'], [true, null], [true, '']]); test('AI adapter makes one bounded synthetic request and validates source membership without claiming factual correctness', function () { Http::fake([SyntheticAiRehearsal::ENDPOINT => Http::response(syntheticAiResponse(['claims' => [['text' => 'Pedido fictício ainda em aprovação.', 'source_ids' => ['request']]]]))]); $result = app(SyntheticAiRehearsal::class)->run(); expect($result['requires_human_review'])->toBeTrue()->and($result['citation_membership'])->toBe('validated') ->and($result['factual_accuracy'])->toBe('not_automatically_verified'); Http::assertSentCount(1); Http::assertSent(fn (Request $request) => $request->url() === SyntheticAiRehearsal::ENDPOINT && $request['store'] === false && $request['max_output_tokens'] === 1000 && $request['text']['format']['strict'] === true && ! isset($request['tools']) && ! isset($request['previous_response_id']) && count(json_decode($request['input'], true)['facts']) === 3 && $request['reasoning']['effort'] === 'none'); }); test('AI adapter rejects unknown or uncited sources and unexpected fields after structured generation', function (array $claim) { Http::fake([SyntheticAiRehearsal::ENDPOINT => Http::response(syntheticAiResponse(['claims' => [$claim]]))]); expect(fn () => app(SyntheticAiRehearsal::class)->run())->toThrow(ValidationException::class); Http::assertSentCount(1); })->with([ [['text' => 'Alegação sem suporte.', 'source_ids' => ['private-finance']]], [['text' => 'Alegação sem fonte.', 'source_ids' => []]], [['text' => 'Alegação inválida.', 'source_ids' => ['request'], 'approved' => true]], ]); test('AI adapter handles refusal truncation malformed content and provider failures without retry', function (array $body, int $status) { Http::fake([SyntheticAiRehearsal::ENDPOINT => Http::response($body, $status)]); expect(fn () => app(SyntheticAiRehearsal::class)->run())->toThrow(RuntimeException::class); Http::assertSentCount(1); })->with([ [['status' => 'incomplete', 'output' => []], 200], [['status' => 'completed', 'output' => [['type' => 'message', 'content' => [['type' => 'refusal', 'refusal' => 'No']]]]], 200], [['status' => 'completed', 'output' => [['type' => 'message', 'content' => [['type' => 'output_text', 'text' => 'not JSON']]]]], 200], [['status' => 'completed', 'output' => []], 200], [['error' => ['message' => 'synthetic-key-not-a-real-credential']], 429], [['error' => ['message' => 'server failure']], 500], ]); test('AI rehearsal command does not print provider error payloads or credentials', function () { Http::fake([SyntheticAiRehearsal::ENDPOINT => Http::response(['error' => ['message' => 'synthetic-key-not-a-real-credential']], 401)]); $this->artisan('sgc:ai-rehearsal --live')->doesntExpectOutputToContain('synthetic-key-not-a-real-credential')->assertFailed(); Http::assertSentCount(1); });