bootCompany('gpt', 'gpt-fake'); Http::fake(['api.openai.com/*' => Http::response([ 'choices' => [['message' => ['content' => 'OpenAI response']]], 'usage' => ['prompt_tokens' => 50, 'completion_tokens' => 12], 'model' => 'gpt-4o-mini', ])]); [$reply, $meta] = app(AiAssistantService::class)->singleShot('system here', 'user prompt', 'gpt'); $this->assertEquals('OpenAI response', $reply); $this->assertEquals('gpt', $meta['provider']); Http::assertSent(function ($req) { if (! str_contains($req->url(), 'openai.com')) return false; $body = json_decode($req->body(), true); return $body['model'] === 'gpt-4o-mini' && $body['messages'][0]['role'] === 'system' && str_contains($body['messages'][0]['content'], 'system here'); }); } public function test_gemini_singleshot_hits_url_with_configured_model(): void { $this->bootCompany('gemini', 'gem-fake'); Http::fake(['generativelanguage.googleapis.com/*' => Http::response([ 'candidates' => [['content' => ['parts' => [['text' => 'Gemini response']]]]], 'usageMetadata' => ['promptTokenCount' => 30], ])]); [$reply, $meta] = app(AiAssistantService::class)->singleShot('s', 'u', 'gemini'); $this->assertEquals('Gemini response', $reply); Http::assertSent(fn ($req) => str_contains($req->url(), 'gemini-1.5-flash:generateContent')); } public function test_no_api_key_returns_friendly_message_without_http(): void { $this->bootCompany('claude', null); Http::fake(); [$reply, $meta] = app(AiAssistantService::class)->singleShot('s', 'u', 'claude'); $this->assertStringContainsString('API key', $reply); $this->assertEquals('no_api_key', $meta['error']); Http::assertNothingSent(); } public function test_tenant_model_override_propagates_into_request(): void { $plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]); $company = Company::create([ 'plan_id' => $plan->id, 'slug' => 'override-' . uniqid(), 'name' => 'O', 'status' => 'active', 'settings' => [ 'ai' => [ 'claude_key' => 'sk-fake', 'models' => ['claude' => 'claude-opus-4-7'], ], ], ]); app(TenantManager::class)->setCurrent($company); Http::fake(['api.anthropic.com/*' => Http::response([ 'content' => [['type' => 'text', 'text' => 'ok']], 'usage' => ['input_tokens' => 1, 'output_tokens' => 1], ])]); app(AiAssistantService::class)->singleShot('s', 'u', 'claude'); Http::assertSent(function ($req) { $body = json_decode($req->body(), true); return $body['model'] === 'claude-opus-4-7'; }); } private function bootCompany(string $provider, ?string $key): Company { $plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]); $company = Company::create([ 'plan_id' => $plan->id, 'slug' => 'ai-' . uniqid(), 'name' => 'AI Co', 'status' => 'active', 'settings' => ['ai' => [ 'default_provider' => $provider, "{$provider}_key" => $key, ]], ]); app(TenantManager::class)->setCurrent($company); return $company; } }