{"repo": "ayoub-abdallah/al-istikama", "files": {"moodle-docker/moodle/public/ai/provider/deepseek/tests/process_explain_text_test.php": {"1bf62cf189c9fbce38ca65e57a49b56f0cc6e518": {"t": 1785746149.2887547, "n": 21194, "h": "6c44c161db082b32", "c": ".\n\nnamespace aiprovider_deepseek;\n\nuse core_ai\\aiactions\\base;\nuse core_ai\\provider;\nuse GuzzleHttp\\Psr7\\Response;\n\ndefined('MOODLE_INTERNAL') || die();\nrequire_once(__DIR__ . '/testcase_helper_trait.php');\n\n/**\n * Test Explain text provider class for Deepseek provider methods.\n *\n * @package aiprovider_deepseek\n * @copyright 2025 Yusuf Wibisono \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * @covers \\aiprovider_deepseek\\provider\n * @covers \\aiprovider_deepseek\\process_explain_text\n * @covers \\aiprovider_deepseek\\abstract_processor\n */\nfinal class process_explain_text_test extends \\advanced_testcase {\n use testcase_helper_trait;\n\n /** @var string A successful response in JSON format. */\n protected string $responsebodyjson;\n\n /** @var \\core_ai\\manager AI Manager. */\n private $manager;\n\n /** @var provider The provider that will process the action. */\n protected provider $provider;\n\n /** @var base The action to process. */\n protected base $action;\n\n /**\n * Set up the test.\n */\n protected function setUp(): void {\n parent::setUp();\n $this->resetAfterTest();\n // Load a response body from a file.\n $this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_deepseek', 'text_request_success.json'));\n $this->manager = \\core\\di::get(\\core_ai\\manager::class);\n $this->provider = $this->create_provider(\n actionclass: \\core_ai\\aiactions\\explain_text::class,\n actionconfig: [\n 'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),\n ],\n );\n $this->create_action();\n }\n\n /**\n * Create the action object.\n * @param int $userid The user id to use in the action.\n */\n private function create_action(int $userid = 1): void {\n $this->action = new \\core_ai\\aiactions\\explain_text(\n contextid: 1,\n userid: $userid,\n prompttext: 'This is a test prompt',\n );\n }\n\n /**\n * Test create_request_object\n */\n public function test_create_request_object(): void {\n $processor = new process_explain_text($this->provider, $this->action);\n\n // We're working with a private method here, so we need to use reflection.\n $method = new \\ReflectionMethod($processor, 'create_request_object');\n $request = $method->invoke($processor, 1);\n\n $body = (object) json_decode($request->getBody()->getContents());\n\n $this->assertEquals('system', $body->messages[0]->role);\n $this->assertEquals(get_string('action_explain_text_instruction', 'core_ai'), $body->messages[0]->content);\n $this->assertEquals('This is a test prompt', $body->messages[1]->content);\n $this->assertEquals('user', $body->messages[1]->role);\n }\n\n /**\n * Test create_request_object with extra model settings.\n */\n public function test_create_request_object_with_model_settings(): void {\n $this->provider = $this->create_provider(\n actionclass: \\core_ai\\aiactions\\explain_text::class,\n actionconfig: [\n 'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),\n 'temperature' => '0.5',\n 'max_tokens' => '100',\n ],\n );\n $processor = new process_explain_text($this->provider, $this->action);\n\n // We're working with a private method here, so we need to use reflection.\n $method = new \\ReflectionMethod($processor, 'create_request_object');\n $request = $method->invoke($processor, 1);\n\n $body = (object) json_decode($request->getBody()->getContents());\n\n $this->assertEquals('deepseek-chat', $body->model);\n $this->assertEquals('0.5', $body->temperature);\n $this->assertEquals('100', $body->max_tokens);\n\n $this->provider = $this->create_provider(\n actionclass: \\core_ai\\aiactions\\explain_text::class,\n actionconfig: [\n 'model' => 'my-custom-deepseek',\n 'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),\n 'modelextraparams' => '{\"temperature\": 0.5,\"max_tokens\": 100}',\n ],\n );\n $processor = new process_explain_text($this->provider, $this->action);\n\n // We're working with a private method here, so we need to use reflection.\n $method = new \\ReflectionMethod($processor, 'create_request_object');\n $request = $method->invoke($processor, 1);\n\n $body = (object) json_decode($request->getBody()->getContents());\n\n $this->assertEquals('my-custom-deepseek', $body->model);\n $this->assertEquals('0.5', $body->temperature);\n $this->assertEquals('100', $body->max_tokens);\n }\n\n /**\n * Test the API error response handler method.\n */\n public function test_handle_api_error(): void {\n $responses = [\n 500 => new Response(500, ['Content-Type' => 'application/json']),\n 503 => new Response(503, ['Content-Type' => 'application/json']),\n 401 => new Response(\n 401,\n ['Content-Type' => 'application/json'],\n '{\"error\": {\"message\": \"Invalid Authentication\"}}'\n ),\n 404 => new Response(\n 404,\n ['Content-Type' => 'application/json'],\n '{\"error\": {\"message\": \"You must be a member of an organization to use the API\"}}'\n ),\n 429 => new Response(\n 429,\n ['Content-Type' => 'application/json'],\n '{\"error\": {\"message\": \"Rate limit reached for requests\"}}'\n ),\n ];\n\n $processor = new process_explain_text($this->provider, $this->action);\n $method = new \\ReflectionMethod($processor, 'handle_api_error');\n\n foreach ($responses as $status => $response) {\n $result = $method->invoke($processor, $response);\n $this->assertEquals($status, $result['errorcode']);\n if ($status == 500) {\n $this->assertEquals('Internal Server Error', $result['errormessage']);\n } else if ($status == 503) {\n $this->assertEquals('Service Unavailable', $result['errormessage']);\n } else {\n $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']);\n }\n }\n }\n\n /**\n * Test the API success response handler method.\n */\n public function test_handle_api_success(): void {\n $response = new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n );\n\n // We're testing a private method, so we need to set up reflector magic.\n $processor = new process_explain_text($this->provider, $this->action);\n $method = new \\ReflectionMethod($processor, 'handle_api_success');\n\n $result = $method->invoke($processor, $response);\n\n $this->assertTrue($result['success']);\n $this->assertEquals('3a621831-3173-4437-b58a-f16287ecce85', $result['id']);\n $this->assertEquals('fp_8802369eaa_prod0623_fp8_kvcache', $result['fingerprint']);\n $this->assertStringContainsString('Mono Potassium Phosphate (MKP)', $result['generatedcontent']);\n $this->assertEquals('stop', $result['finishreason']);\n $this->assertEquals('23', $result['prompttokens']);\n $this->assertEquals('150', $result['completiontokens']);\n $this->assertEquals('deepseek-chat', $result['model']);\n }\n\n /**\n * Test query_ai_api for a successful call.\n */\n public function test_query_ai_api_success(): void {\n // Mock the http client to return a successful response.\n ['mock' => $mock] = $this->get_mocked_http_client();\n\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n\n $processor = new process_explain_text($this->provider, $this->action);\n $method = new \\ReflectionMethod($processor, 'query_ai_api');\n $result = $method->invoke($processor);\n\n $this->assertTrue($result['success']);\n $this->assertEquals('3a621831-3173-4437-b58a-f16287ecce85', $result['id']);\n $this->assertEquals('fp_8802369eaa_prod0623_fp8_kvcache', $result['fingerprint']);\n $this->assertStringContainsString('Mono Potassium Phosphate (MKP)', $result['generatedcontent']);\n $this->assertEquals('stop', $result['finishreason']);\n $this->assertEquals('23', $result['prompttokens']);\n $this->assertEquals('150', $result['completiontokens']);\n $this->assertEquals('deepseek-chat', $result['model']);\n }\n\n /**\n * Test prepare_response success.\n */\n public function test_prepare_response_success(): void {\n $processor = new process_explain_text($this->provider, $this->action);\n\n // We're working with a private method here, so we need to use reflection.\n $method = new \\ReflectionMethod($processor, 'prepare_response');\n\n $response = [\n 'success' => true,\n 'id' => '3a621831-3173-4437-b58a-f16287ecce85',\n 'fingerprint' => 'fp_8802369eaa_prod0623_fp8_kvcache',\n 'generatedcontent' => 'Mono Potassium Phosphate (MKP)',\n 'finishreason' => 'stop',\n 'prompttokens' => '23',\n 'completiontokens' => '150',\n 'model' => 'deepseek-chat',\n ];\n\n $result = $method->invoke($processor, $response);\n\n $this->assertInstanceOf(\\core_ai\\aiactions\\responses\\response_base::class, $result);\n $this->assertTrue($result->get_success());\n $this->assertEquals('explain_text', $result->get_actionname());\n $this->assertEquals($response['success'], $result->get_success());\n $this->assertEquals($response['generatedcontent'], $result->get_response_data()['generatedcontent']);\n $this->assertEquals($response['model'], $result->get_response_data()['model']);\n }\n\n /**\n * Test prepare_response error.\n */\n public function test_prepare_response_error(): void {\n $processor = new process_explain_text($this->provider, $this->action);\n\n // We're working with a private method here, so we need to use reflection.\n $method = new \\ReflectionMethod($processor, 'prepare_response');\n\n $response = [\n 'success' => false,\n 'errorcode' => 500,\n 'error' => 'Internal server error',\n 'errormessage' => 'Try again later',\n ];\n\n $result = $method->invoke($processor, $response);\n\n $this->assertInstanceOf(\\core_ai\\aiactions\\responses\\response_base::class, $result);\n $this->assertFalse($result->get_success());\n $this->assertEquals('explain_text', $result->get_actionname());\n $this->assertEquals($response['error'], $result->get_error());\n $this->assertEquals($response['errorcode'], $result->get_errorcode());\n $this->assertEquals($response['errormessage'], $result->get_errormessage());\n }\n\n /**\n * Test process method.\n */\n public function test_process(): void {\n // Log in user.\n $this->setUser($this->getDataGenerator()->create_user());\n\n // Mock the http client to return a successful response.\n ['mock' => $mock] = $this->get_mocked_http_client();\n\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n\n $processor = new process_explain_text($this->provider, $this->action);\n $result = $processor->process();\n\n $this->assertInstanceOf(\\core_ai\\aiactions\\responses\\response_base::class, $result);\n $this->assertTrue($result->get_success());\n $this->assertEquals('explain_text', $result->get_actionname());\n }\n\n /**\n * Test process method with error.\n */\n public function test_process_error(): void {\n // Log in user.\n $this->setUser($this->getDataGenerator()->create_user());\n\n // Mock the http client to return a successful response.\n ['mock' => $mock] = $this->get_mocked_http_client();\n\n // The response from Deepseek.\n $mock->append(new Response(\n 401,\n ['Content-Type' => 'application/json'],\n json_encode(['error' => ['message' => 'Invalid Authentication']]),\n ));\n\n $processor = new process_explain_text($this->provider, $this->action);\n $result = $processor->process();\n\n $this->assertInstanceOf(\\core_ai\\aiactions\\responses\\response_base::class, $result);\n $this->assertFalse($result->get_success());\n $this->assertEquals('explain_text', $result->get_actionname());\n $this->assertEquals(401, $result->get_errorcode());\n $this->assertEquals('Invalid Authentication', $result->get_errormessage());\n }\n\n /**\n * Test process method with user rate limiter.\n */\n public function test_process_with_user_rate_limiter(): void {\n // Create users.\n $user1 = $this->getDataGenerator()->create_user();\n $user2 = $this->getDataGenerator()->create_user();\n // Log in user1.\n $this->setUser($user1);\n // Mock clock.\n $clock = $this->mock_clock_with_frozen();\n\n // Set the user rate limiter.\n $config = [\n 'apikey' => '123',\n 'enableuserratelimit' => true,\n 'userratelimit' => 1,\n ];\n $provider = $this->manager->create_provider_instance(\n classname: '\\aiprovider_deepseek\\provider',\n name: 'dummy',\n config: $config,\n actionconfig: [\n \\core_ai\\aiactions\\explain_text::class => [\n 'settings' => [\n 'model' => 'deepseek-chat',\n 'endpoint' => 'https://api.deepseek.com/chat/completions',\n 'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),\n ],\n ],\n ],\n );\n\n // Mock the http client to return a successful response.\n ['mock' => $mock] = $this->get_mocked_http_client();\n\n // Case 1: User rate limit has not been reached.\n $this->create_action($user1->id);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertTrue($result->get_success());\n\n // Case 2: User rate limit has been reached.\n $clock->bump(HOURSECS - 10);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $this->create_action($user1->id);\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertEquals(429, $result->get_errorcode());\n $this->assertEquals(\n expected: 'You have reached the maximum number of AI requests you can make in an hour. Try again later.',\n actual: $result->get_errormessage(),\n );\n $this->assertFalse($result->get_success());\n\n // Case 3: User rate limit has not been reached for a different user.\n // Log in user2.\n $this->setUser($user2);\n $this->create_action($user2->id);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertTrue($result->get_success());\n\n // Case 4: Time window has passed, user rate limit should be reset.\n $clock->bump(11);\n // Log in user1.\n $this->setUser($user1);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $this->create_action($user1->id);\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertTrue($result->get_success());\n }\n\n /**\n * Test process method with global rate limiter.\n */\n public function test_process_with_global_rate_limiter(): void {\n // Create users.\n $user1 = $this->getDataGenerator()->create_user();\n $user2 = $this->getDataGenerator()->create_user();\n // Log in user1.\n $this->setUser($user1);\n // Mock clock.\n $clock = $this->mock_clock_with_frozen();\n\n // Set the global rate limiter.\n $config = [\n 'apikey' => '123',\n 'enableglobalratelimit' => true,\n 'globalratelimit' => 1,\n ];\n $provider = $this->manager->create_provider_instance(\n classname: '\\aiprovider_deepseek\\provider',\n name: 'dummy',\n config: $config,\n actionconfig: [\n \\core_ai\\aiactions\\explain_text::class => [\n 'settings' => [\n 'model' => 'deepseek-chat',\n 'endpoint' => 'https://api.deepseek.com/chat/completions',\n 'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),\n ],\n ],\n ],\n );\n\n // Mock the http client to return a successful response.\n ['mock' => $mock] = $this->get_mocked_http_client();\n\n // Case 1: Global rate limit has not been reached.\n $this->create_action($user1->id);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertTrue($result->get_success());\n\n // Case 2: Global rate limit has been reached.\n $clock->bump(HOURSECS - 10);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $this->create_action($user1->id);\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertEquals(429, $result->get_errorcode());\n $this->assertEquals(\n expected: 'The AI service has reached the maximum number of site-wide requests per hour. Try again later.',\n actual: $result->get_errormessage(),\n );\n $this->assertFalse($result->get_success());\n\n // Case 3: Global rate limit has been reached for a different user too.\n // Log in user2.\n $this->setUser($user2);\n $this->create_action($user2->id);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertFalse($result->get_success());\n\n // Case 4: Time window has passed, global rate limit should be reset.\n $clock->bump(11);\n // Log in user1.\n $this->setUser($user1);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $this->create_action($user1->id);\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertTrue($result->get_success());\n }\n}\n"}, "5a7251ec4488f83432841e55ad01097e47b0286b": {"t": 1785760205.7254364, "n": 21194, "h": "6c44c161db082b32", "c": ".\n\nnamespace aiprovider_deepseek;\n\nuse core_ai\\aiactions\\base;\nuse core_ai\\provider;\nuse GuzzleHttp\\Psr7\\Response;\n\ndefined('MOODLE_INTERNAL') || die();\nrequire_once(__DIR__ . '/testcase_helper_trait.php');\n\n/**\n * Test Explain text provider class for Deepseek provider methods.\n *\n * @package aiprovider_deepseek\n * @copyright 2025 Yusuf Wibisono \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * @covers \\aiprovider_deepseek\\provider\n * @covers \\aiprovider_deepseek\\process_explain_text\n * @covers \\aiprovider_deepseek\\abstract_processor\n */\nfinal class process_explain_text_test extends \\advanced_testcase {\n use testcase_helper_trait;\n\n /** @var string A successful response in JSON format. */\n protected string $responsebodyjson;\n\n /** @var \\core_ai\\manager AI Manager. */\n private $manager;\n\n /** @var provider The provider that will process the action. */\n protected provider $provider;\n\n /** @var base The action to process. */\n protected base $action;\n\n /**\n * Set up the test.\n */\n protected function setUp(): void {\n parent::setUp();\n $this->resetAfterTest();\n // Load a response body from a file.\n $this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_deepseek', 'text_request_success.json'));\n $this->manager = \\core\\di::get(\\core_ai\\manager::class);\n $this->provider = $this->create_provider(\n actionclass: \\core_ai\\aiactions\\explain_text::class,\n actionconfig: [\n 'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),\n ],\n );\n $this->create_action();\n }\n\n /**\n * Create the action object.\n * @param int $userid The user id to use in the action.\n */\n private function create_action(int $userid = 1): void {\n $this->action = new \\core_ai\\aiactions\\explain_text(\n contextid: 1,\n userid: $userid,\n prompttext: 'This is a test prompt',\n );\n }\n\n /**\n * Test create_request_object\n */\n public function test_create_request_object(): void {\n $processor = new process_explain_text($this->provider, $this->action);\n\n // We're working with a private method here, so we need to use reflection.\n $method = new \\ReflectionMethod($processor, 'create_request_object');\n $request = $method->invoke($processor, 1);\n\n $body = (object) json_decode($request->getBody()->getContents());\n\n $this->assertEquals('system', $body->messages[0]->role);\n $this->assertEquals(get_string('action_explain_text_instruction', 'core_ai'), $body->messages[0]->content);\n $this->assertEquals('This is a test prompt', $body->messages[1]->content);\n $this->assertEquals('user', $body->messages[1]->role);\n }\n\n /**\n * Test create_request_object with extra model settings.\n */\n public function test_create_request_object_with_model_settings(): void {\n $this->provider = $this->create_provider(\n actionclass: \\core_ai\\aiactions\\explain_text::class,\n actionconfig: [\n 'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),\n 'temperature' => '0.5',\n 'max_tokens' => '100',\n ],\n );\n $processor = new process_explain_text($this->provider, $this->action);\n\n // We're working with a private method here, so we need to use reflection.\n $method = new \\ReflectionMethod($processor, 'create_request_object');\n $request = $method->invoke($processor, 1);\n\n $body = (object) json_decode($request->getBody()->getContents());\n\n $this->assertEquals('deepseek-chat', $body->model);\n $this->assertEquals('0.5', $body->temperature);\n $this->assertEquals('100', $body->max_tokens);\n\n $this->provider = $this->create_provider(\n actionclass: \\core_ai\\aiactions\\explain_text::class,\n actionconfig: [\n 'model' => 'my-custom-deepseek',\n 'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),\n 'modelextraparams' => '{\"temperature\": 0.5,\"max_tokens\": 100}',\n ],\n );\n $processor = new process_explain_text($this->provider, $this->action);\n\n // We're working with a private method here, so we need to use reflection.\n $method = new \\ReflectionMethod($processor, 'create_request_object');\n $request = $method->invoke($processor, 1);\n\n $body = (object) json_decode($request->getBody()->getContents());\n\n $this->assertEquals('my-custom-deepseek', $body->model);\n $this->assertEquals('0.5', $body->temperature);\n $this->assertEquals('100', $body->max_tokens);\n }\n\n /**\n * Test the API error response handler method.\n */\n public function test_handle_api_error(): void {\n $responses = [\n 500 => new Response(500, ['Content-Type' => 'application/json']),\n 503 => new Response(503, ['Content-Type' => 'application/json']),\n 401 => new Response(\n 401,\n ['Content-Type' => 'application/json'],\n '{\"error\": {\"message\": \"Invalid Authentication\"}}'\n ),\n 404 => new Response(\n 404,\n ['Content-Type' => 'application/json'],\n '{\"error\": {\"message\": \"You must be a member of an organization to use the API\"}}'\n ),\n 429 => new Response(\n 429,\n ['Content-Type' => 'application/json'],\n '{\"error\": {\"message\": \"Rate limit reached for requests\"}}'\n ),\n ];\n\n $processor = new process_explain_text($this->provider, $this->action);\n $method = new \\ReflectionMethod($processor, 'handle_api_error');\n\n foreach ($responses as $status => $response) {\n $result = $method->invoke($processor, $response);\n $this->assertEquals($status, $result['errorcode']);\n if ($status == 500) {\n $this->assertEquals('Internal Server Error', $result['errormessage']);\n } else if ($status == 503) {\n $this->assertEquals('Service Unavailable', $result['errormessage']);\n } else {\n $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']);\n }\n }\n }\n\n /**\n * Test the API success response handler method.\n */\n public function test_handle_api_success(): void {\n $response = new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n );\n\n // We're testing a private method, so we need to set up reflector magic.\n $processor = new process_explain_text($this->provider, $this->action);\n $method = new \\ReflectionMethod($processor, 'handle_api_success');\n\n $result = $method->invoke($processor, $response);\n\n $this->assertTrue($result['success']);\n $this->assertEquals('3a621831-3173-4437-b58a-f16287ecce85', $result['id']);\n $this->assertEquals('fp_8802369eaa_prod0623_fp8_kvcache', $result['fingerprint']);\n $this->assertStringContainsString('Mono Potassium Phosphate (MKP)', $result['generatedcontent']);\n $this->assertEquals('stop', $result['finishreason']);\n $this->assertEquals('23', $result['prompttokens']);\n $this->assertEquals('150', $result['completiontokens']);\n $this->assertEquals('deepseek-chat', $result['model']);\n }\n\n /**\n * Test query_ai_api for a successful call.\n */\n public function test_query_ai_api_success(): void {\n // Mock the http client to return a successful response.\n ['mock' => $mock] = $this->get_mocked_http_client();\n\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n\n $processor = new process_explain_text($this->provider, $this->action);\n $method = new \\ReflectionMethod($processor, 'query_ai_api');\n $result = $method->invoke($processor);\n\n $this->assertTrue($result['success']);\n $this->assertEquals('3a621831-3173-4437-b58a-f16287ecce85', $result['id']);\n $this->assertEquals('fp_8802369eaa_prod0623_fp8_kvcache', $result['fingerprint']);\n $this->assertStringContainsString('Mono Potassium Phosphate (MKP)', $result['generatedcontent']);\n $this->assertEquals('stop', $result['finishreason']);\n $this->assertEquals('23', $result['prompttokens']);\n $this->assertEquals('150', $result['completiontokens']);\n $this->assertEquals('deepseek-chat', $result['model']);\n }\n\n /**\n * Test prepare_response success.\n */\n public function test_prepare_response_success(): void {\n $processor = new process_explain_text($this->provider, $this->action);\n\n // We're working with a private method here, so we need to use reflection.\n $method = new \\ReflectionMethod($processor, 'prepare_response');\n\n $response = [\n 'success' => true,\n 'id' => '3a621831-3173-4437-b58a-f16287ecce85',\n 'fingerprint' => 'fp_8802369eaa_prod0623_fp8_kvcache',\n 'generatedcontent' => 'Mono Potassium Phosphate (MKP)',\n 'finishreason' => 'stop',\n 'prompttokens' => '23',\n 'completiontokens' => '150',\n 'model' => 'deepseek-chat',\n ];\n\n $result = $method->invoke($processor, $response);\n\n $this->assertInstanceOf(\\core_ai\\aiactions\\responses\\response_base::class, $result);\n $this->assertTrue($result->get_success());\n $this->assertEquals('explain_text', $result->get_actionname());\n $this->assertEquals($response['success'], $result->get_success());\n $this->assertEquals($response['generatedcontent'], $result->get_response_data()['generatedcontent']);\n $this->assertEquals($response['model'], $result->get_response_data()['model']);\n }\n\n /**\n * Test prepare_response error.\n */\n public function test_prepare_response_error(): void {\n $processor = new process_explain_text($this->provider, $this->action);\n\n // We're working with a private method here, so we need to use reflection.\n $method = new \\ReflectionMethod($processor, 'prepare_response');\n\n $response = [\n 'success' => false,\n 'errorcode' => 500,\n 'error' => 'Internal server error',\n 'errormessage' => 'Try again later',\n ];\n\n $result = $method->invoke($processor, $response);\n\n $this->assertInstanceOf(\\core_ai\\aiactions\\responses\\response_base::class, $result);\n $this->assertFalse($result->get_success());\n $this->assertEquals('explain_text', $result->get_actionname());\n $this->assertEquals($response['error'], $result->get_error());\n $this->assertEquals($response['errorcode'], $result->get_errorcode());\n $this->assertEquals($response['errormessage'], $result->get_errormessage());\n }\n\n /**\n * Test process method.\n */\n public function test_process(): void {\n // Log in user.\n $this->setUser($this->getDataGenerator()->create_user());\n\n // Mock the http client to return a successful response.\n ['mock' => $mock] = $this->get_mocked_http_client();\n\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n\n $processor = new process_explain_text($this->provider, $this->action);\n $result = $processor->process();\n\n $this->assertInstanceOf(\\core_ai\\aiactions\\responses\\response_base::class, $result);\n $this->assertTrue($result->get_success());\n $this->assertEquals('explain_text', $result->get_actionname());\n }\n\n /**\n * Test process method with error.\n */\n public function test_process_error(): void {\n // Log in user.\n $this->setUser($this->getDataGenerator()->create_user());\n\n // Mock the http client to return a successful response.\n ['mock' => $mock] = $this->get_mocked_http_client();\n\n // The response from Deepseek.\n $mock->append(new Response(\n 401,\n ['Content-Type' => 'application/json'],\n json_encode(['error' => ['message' => 'Invalid Authentication']]),\n ));\n\n $processor = new process_explain_text($this->provider, $this->action);\n $result = $processor->process();\n\n $this->assertInstanceOf(\\core_ai\\aiactions\\responses\\response_base::class, $result);\n $this->assertFalse($result->get_success());\n $this->assertEquals('explain_text', $result->get_actionname());\n $this->assertEquals(401, $result->get_errorcode());\n $this->assertEquals('Invalid Authentication', $result->get_errormessage());\n }\n\n /**\n * Test process method with user rate limiter.\n */\n public function test_process_with_user_rate_limiter(): void {\n // Create users.\n $user1 = $this->getDataGenerator()->create_user();\n $user2 = $this->getDataGenerator()->create_user();\n // Log in user1.\n $this->setUser($user1);\n // Mock clock.\n $clock = $this->mock_clock_with_frozen();\n\n // Set the user rate limiter.\n $config = [\n 'apikey' => '123',\n 'enableuserratelimit' => true,\n 'userratelimit' => 1,\n ];\n $provider = $this->manager->create_provider_instance(\n classname: '\\aiprovider_deepseek\\provider',\n name: 'dummy',\n config: $config,\n actionconfig: [\n \\core_ai\\aiactions\\explain_text::class => [\n 'settings' => [\n 'model' => 'deepseek-chat',\n 'endpoint' => 'https://api.deepseek.com/chat/completions',\n 'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),\n ],\n ],\n ],\n );\n\n // Mock the http client to return a successful response.\n ['mock' => $mock] = $this->get_mocked_http_client();\n\n // Case 1: User rate limit has not been reached.\n $this->create_action($user1->id);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertTrue($result->get_success());\n\n // Case 2: User rate limit has been reached.\n $clock->bump(HOURSECS - 10);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $this->create_action($user1->id);\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertEquals(429, $result->get_errorcode());\n $this->assertEquals(\n expected: 'You have reached the maximum number of AI requests you can make in an hour. Try again later.',\n actual: $result->get_errormessage(),\n );\n $this->assertFalse($result->get_success());\n\n // Case 3: User rate limit has not been reached for a different user.\n // Log in user2.\n $this->setUser($user2);\n $this->create_action($user2->id);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertTrue($result->get_success());\n\n // Case 4: Time window has passed, user rate limit should be reset.\n $clock->bump(11);\n // Log in user1.\n $this->setUser($user1);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $this->create_action($user1->id);\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertTrue($result->get_success());\n }\n\n /**\n * Test process method with global rate limiter.\n */\n public function test_process_with_global_rate_limiter(): void {\n // Create users.\n $user1 = $this->getDataGenerator()->create_user();\n $user2 = $this->getDataGenerator()->create_user();\n // Log in user1.\n $this->setUser($user1);\n // Mock clock.\n $clock = $this->mock_clock_with_frozen();\n\n // Set the global rate limiter.\n $config = [\n 'apikey' => '123',\n 'enableglobalratelimit' => true,\n 'globalratelimit' => 1,\n ];\n $provider = $this->manager->create_provider_instance(\n classname: '\\aiprovider_deepseek\\provider',\n name: 'dummy',\n config: $config,\n actionconfig: [\n \\core_ai\\aiactions\\explain_text::class => [\n 'settings' => [\n 'model' => 'deepseek-chat',\n 'endpoint' => 'https://api.deepseek.com/chat/completions',\n 'systeminstruction' => get_string('action_explain_text_instruction', 'core_ai'),\n ],\n ],\n ],\n );\n\n // Mock the http client to return a successful response.\n ['mock' => $mock] = $this->get_mocked_http_client();\n\n // Case 1: Global rate limit has not been reached.\n $this->create_action($user1->id);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertTrue($result->get_success());\n\n // Case 2: Global rate limit has been reached.\n $clock->bump(HOURSECS - 10);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $this->create_action($user1->id);\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertEquals(429, $result->get_errorcode());\n $this->assertEquals(\n expected: 'The AI service has reached the maximum number of site-wide requests per hour. Try again later.',\n actual: $result->get_errormessage(),\n );\n $this->assertFalse($result->get_success());\n\n // Case 3: Global rate limit has been reached for a different user too.\n // Log in user2.\n $this->setUser($user2);\n $this->create_action($user2->id);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertFalse($result->get_success());\n\n // Case 4: Time window has passed, global rate limit should be reset.\n $clock->bump(11);\n // Log in user1.\n $this->setUser($user1);\n // The response from Deepseek.\n $mock->append(new Response(\n 200,\n ['Content-Type' => 'application/json'],\n $this->responsebodyjson,\n ));\n $this->create_action($user1->id);\n $processor = new process_explain_text($provider, $this->action);\n $result = $processor->process();\n $this->assertTrue($result->get_success());\n }\n}\n"}}}}