Files
compras/tests/Feature/SyntheticAiRehearsalTest.php
nala c6373365bf
tests / ci (push) Canceled after 0s
commit9
2026-10-30 20:02:30 +02:00

81 lines
4.6 KiB
PHP

<?php
use App\Services\SyntheticAiRehearsal;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\ValidationException;
beforeEach(function () {
config(['sgc_intelligence.rehearsal.enabled' => 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);
});