#include "prof_cmocka.h" #include "common.h" #include "ai/ai_client.h" #include /* ======================================================================== * Setup/Teardown * ======================================================================== */ int ai_client_setup(void** state) { ai_client_init(); return 0; } int ai_client_teardown(void** state) { ai_client_shutdown(); return 0; } /* ======================================================================== * Provider Management Tests * ======================================================================== */ void test_ai_client_init(void** state) { /* After init, default providers should exist */ AIProvider* openai = ai_get_provider("openai"); assert_non_null(openai); assert_string_equal("openai", openai->name); assert_string_equal("https://api.openai.com/", openai->api_url); AIProvider* perplexity = ai_get_provider("perplexity"); assert_non_null(perplexity); assert_string_equal("perplexity", perplexity->name); assert_string_equal("https://api.perplexity.ai/", perplexity->api_url); } void test_ai_add_provider(void** state) { /* Add a custom provider (hash table owns ref; caller gets non-owning pointer) */ AIProvider* provider = ai_add_provider("custom", "https://custom.api.com/v1", "my-org"); assert_non_null(provider); assert_string_equal("custom", provider->name); assert_string_equal("https://custom.api.com/v1", provider->api_url); assert_string_equal("my-org", provider->org_id); /* Update existing provider (returns ref; caller owns it) */ AIProvider* updated = ai_add_provider("custom", "https://new.api.com/v1", NULL); assert_non_null(updated); assert_string_equal("https://new.api.com/v1", updated->api_url); assert_null(updated->org_id); ai_provider_unref(updated); } void test_ai_remove_provider(void** state) { /* Remove default provider should fail */ assert_false(ai_remove_provider("nonexistent")); /* Add and remove custom provider */ ai_add_provider("temp", "https://temp.api.com/v1", NULL); assert_true(ai_remove_provider("temp")); assert_null(ai_get_provider("temp")); } void test_ai_list_providers(void** state) { GList* providers = ai_list_providers(); assert_non_null(providers); assert_int_equal(2, g_list_length(providers)); /* openai and perplexity */ /* Free list (ai_list_providers returns non-ref'd providers; caller must not unref) */ g_list_free(providers); /* Add another provider */ ai_add_provider("test", "https://test.api.com/v1", NULL); providers = ai_list_providers(); assert_int_equal(3, g_list_length(providers)); /* Free list */ g_list_free(providers); } /* ======================================================================== * API Key Tests * ======================================================================== */ void test_ai_set_provider_key(void** state) { ai_set_provider_key("openai", "sk-test-key-123"); { auto_gchar gchar* key = ai_get_provider_key("openai"); assert_non_null(key); assert_string_equal("sk-test-key-123", key); } /* Update key */ ai_set_provider_key("openai", "sk-new-key-456"); { auto_gchar gchar* key = ai_get_provider_key("openai"); assert_string_equal("sk-new-key-456", key); } /* Remove key */ ai_set_provider_key("openai", NULL); { auto_gchar gchar* key = ai_get_provider_key("openai"); assert_null(key); } } void test_ai_get_provider_key(void** state) { /* No key set initially */ { auto_gchar gchar* key = ai_get_provider_key("openai"); assert_null(key); } /* Set and get key */ ai_set_provider_key("perplexity", "pplx-abc123"); { auto_gchar gchar* key = ai_get_provider_key("perplexity"); assert_non_null(key); assert_string_equal("pplx-abc123", key); } /* Wrong provider returns null */ { auto_gchar gchar* key = ai_get_provider_key("openai"); assert_null(key); } } /* ======================================================================== * Session Tests * ======================================================================== */ void test_ai_session_create(void** state) { AISession* session = ai_session_create("openai", "gpt-4"); assert_non_null(session); assert_string_equal("openai", session->provider_name); assert_string_equal("gpt-4", session->model); assert_null(session->api_key); /* No key set */ ai_session_unref(session); } void test_ai_session_ref_unref(void** state) { AISession* session = ai_session_create("openai", "gpt-4"); assert_non_null(session); /* Reference */ AISession* ref = ai_session_ref(session); assert_true(ref == session); /* Unreference twice */ ai_session_unref(session); ai_session_unref(ref); /* Should free here */ } void test_ai_session_add_message(void** state) { AISession* session = ai_session_create("openai", "gpt-4"); assert_non_null(session); ai_session_add_message(session, "user", "Hello"); ai_session_add_message(session, "assistant", "Hi there!"); assert_int_equal(2, g_list_length(session->history)); AIMessage* first = session->history->data; assert_string_equal("user", first->role); assert_string_equal("Hello", first->content); AIMessage* second = g_list_next(session->history)->data; assert_string_equal("assistant", second->role); assert_string_equal("Hi there!", second->content); ai_session_unref(session); } void test_ai_session_clear_history(void** state) { AISession* session = ai_session_create("openai", "gpt-4"); ai_session_add_message(session, "user", "Message 1"); ai_session_add_message(session, "user", "Message 2"); ai_session_add_message(session, "assistant", "Response"); assert_int_equal(3, g_list_length(session->history)); ai_session_clear_history(session); assert_int_equal(0, g_list_length(session->history)); ai_session_unref(session); } void test_ai_session_set_model(void** state) { AISession* session = ai_session_create("openai", "gpt-4"); assert_string_equal("gpt-4", session->model); ai_session_set_model(session, "gpt-3.5-turbo"); assert_string_equal("gpt-3.5-turbo", session->model); ai_session_unref(session); } /* ======================================================================== * JSON Escape Tests * ======================================================================== */ void test_ai_json_escape(void** state) { gchar* escaped = ai_json_escape("hello \"world\""); assert_string_equal("hello \\\"world\\\"", escaped); g_free(escaped); } void test_ai_json_escape_null(void** state) { gchar* escaped = ai_json_escape(NULL); assert_string_equal("", escaped); g_free(escaped); } void test_ai_json_escape_empty(void** state) { gchar* escaped = ai_json_escape(""); assert_string_equal("", escaped); g_free(escaped); } void test_ai_json_escape_special_chars(void** state) { gchar* escaped = ai_json_escape("line1\nline2\ttab\\backslash\"quote"); assert_string_equal("line1\\nline2\\ttab\\\\backslash\\\"quote", escaped); g_free(escaped); } void test_ai_json_escape_percent_signs(void** state) { /* Critical: % characters in content must be escaped for JSON, not treated as format specifiers */ gchar* escaped = ai_json_escape("100% complete with %s and %d format strings"); assert_string_equal("100% complete with %s and %d format strings", escaped); g_free(escaped); } void test_ai_json_escape_backslash_quote(void** state) { /* Test escaped quote handling */ gchar* escaped = ai_json_escape("He said \"hello\" and \\ goodbye"); assert_string_equal("He said \\\"hello\\\" and \\\\ goodbye", escaped); g_free(escaped); } void test_ai_session_api_key_is_copied(void** state) { /* Verify that session owns its own copy of the API key */ ai_set_provider_key("openai", "sk-test-key-123"); AISession* session = ai_session_create("openai", "gpt-4"); assert_non_null(session); assert_string_equal("sk-test-key-123", session->api_key); /* Remove the provider key - session should still have its copy */ ai_set_provider_key("openai", NULL); assert_non_null(session->api_key); assert_string_equal("sk-test-key-123", session->api_key); ai_session_unref(session); } void test_ai_add_provider_update_existing(void** state) { /* Add a provider (hash table owns ref) */ AIProvider* provider = ai_add_provider("custom", "https://first.api.com/v1", "org1"); assert_non_null(provider); assert_string_equal("https://first.api.com/v1", provider->api_url); assert_string_equal("org1", provider->org_id); /* Update the same provider (returns ref) */ provider = ai_add_provider("custom", "https://second.api.com/v1", "org2"); assert_non_null(provider); assert_string_equal("https://second.api.com/v1", provider->api_url); assert_string_equal("org2", provider->org_id); ai_provider_unref(provider); } void test_ai_add_provider_null_name_returns_null(void** state) { assert_null(ai_add_provider(NULL, "https://api.com/v1", NULL)); } void test_ai_add_provider_null_url_returns_null(void** state) { assert_null(ai_add_provider("test", NULL, NULL)); } void test_ai_session_create_null_provider_returns_null(void** state) { assert_null(ai_session_create("nonexistent", "gpt-4")); } void test_ai_session_create_null_model_returns_null(void** state) { assert_null(ai_session_create("openai", NULL)); } void test_ai_session_api_key_null_when_no_key_set(void** state) { /* openai has no key set by default */ AISession* session = ai_session_create("openai", "gpt-4"); assert_non_null(session); assert_null(session->api_key); ai_session_unref(session); } /* ======================================================================== * Provider Autocomplete Tests * ======================================================================== */ void test_ai_providers_find_forward(void** state) { /* Test forward iteration - should return first match */ auto_gchar gchar* result = ai_providers_find("o", FALSE, NULL); assert_non_null(result); assert_string_equal("openai", result); } void test_ai_providers_find_forward_perplexity(void** state) { /* Test forward iteration for perplexity */ auto_gchar gchar* result = ai_providers_find("p", FALSE, NULL); assert_non_null(result); assert_string_equal("perplexity", result); } void test_ai_providers_find_forward_custom(void** state) { /* Add a custom provider and test */ ai_add_provider("custom", "https://custom.api.com/v1", NULL); auto_gchar gchar* result = ai_providers_find("c", FALSE, NULL); assert_non_null(result); assert_string_equal("custom", result); } void test_ai_providers_find_forward_no_match(void** state) { /* Test no match */ auto_gchar gchar* result = ai_providers_find("z", FALSE, NULL); assert_null(result); } void test_ai_providers_find_forward_partial_match(void** state) { /* Test partial match - should return providers starting with "ope" */ auto_gchar gchar* result = ai_providers_find("ope", FALSE, NULL); assert_non_null(result); assert_string_equal("openai", result); } void test_ai_providers_find_next(void** state) { /* Test that stateless implementation returns same result each call */ auto_gchar gchar* result1 = ai_providers_find("o", FALSE, NULL); assert_non_null(result1); assert_string_equal("openai", result1); /* Second call with same params returns same result (stateless) */ auto_gchar gchar* result2 = ai_providers_find("o", FALSE, NULL); assert_non_null(result2); assert_string_equal("openai", result2); } void test_ai_providers_find_previous(void** state) { /* Test that previous=TRUE returns last match in list */ /* With only "openai" starting with "o", both FALSE and TRUE return same result */ auto_gchar gchar* result1 = ai_providers_find("o", FALSE, NULL); assert_non_null(result1); assert_string_equal("openai", result1); /* previous=TRUE also returns "openai" (only one match, so first==last) */ auto_gchar gchar* result2 = ai_providers_find("o", TRUE, NULL); assert_non_null(result2); assert_string_equal("openai", result2); } void test_ai_providers_find_null_search_str(void** state) { /* NULL search_str triggers cycling: returns first provider in list */ auto_gchar gchar* result = ai_providers_find(NULL, FALSE, NULL); assert_non_null(result); assert_string_equal("openai", result); } void test_ai_providers_find_empty_search_str(void** state) { /* Empty search_str triggers cycling: returns first provider in list */ auto_gchar gchar* result = ai_providers_find("", FALSE, NULL); assert_non_null(result); assert_string_equal("openai", result); } void test_ai_providers_find_case_insensitive(void** state) { /* Test that matching is case-insensitive (via g_ascii_strdown) */ auto_gchar gchar* result = ai_providers_find("OPENAI", FALSE, NULL); assert_non_null(result); assert_string_equal("openai", result); result = ai_providers_find("OpenAI", FALSE, NULL); assert_non_null(result); assert_string_equal("openai", result); result = ai_providers_find("openai", FALSE, NULL); assert_non_null(result); assert_string_equal("openai", result); } /* ======================================================================== * AI Autocomplete Integration Tests * ======================================================================== */ void test_ai_start_provider_autocomplete_only_on_exact(void** state) { auto_gchar gchar* result = ai_providers_find("", FALSE, NULL); assert_non_null(result); assert_string_equal("openai", result); } void test_ai_models_find_null_session(void** state) { auto_gchar gchar* result = ai_models_find("gpt", FALSE, NULL); assert_null(result); } void test_ai_models_find_null_provider(void** state) { ai_add_provider("temp", "https://temp.api.com/v1", NULL); AISession* session = ai_session_create("temp", "gpt-4"); assert_non_null(session); ai_session_unref(session); } void test_ai_providers_find_cycling(void** state) { /* NULL search_str should cycle through providers */ auto_gchar gchar* result1 = ai_providers_find(NULL, FALSE, NULL); assert_non_null(result1); auto_gchar gchar* result2 = ai_providers_find(NULL, FALSE, NULL); assert_non_null(result2); /* Cycling: consecutive calls with NULL should return different providers */ assert_string_not_equal(result1, result2); } /* ======================================================================== * Provider Default Model and Settings Tests * ======================================================================== */ void test_ai_set_provider_default_model(void** state) { /* Set default model for openai provider */ ai_set_provider_default_model("openai", "gpt-5"); /* Verify the model was set */ const gchar* model = ai_get_provider_default_model("openai"); assert_non_null(model); assert_string_equal("gpt-5", model); /* Update default model */ ai_set_provider_default_model("openai", "gpt-4o"); model = ai_get_provider_default_model("openai"); assert_non_null(model); assert_string_equal("gpt-4o", model); /* Set default model for perplexity */ ai_set_provider_default_model("perplexity", "sonar-pro"); model = ai_get_provider_default_model("perplexity"); assert_non_null(model); assert_string_equal("sonar-pro", model); } void test_ai_get_provider_default_model(void** state) { /* NULL provider returns NULL */ assert_null(ai_get_provider_default_model(NULL)); /* Non-existent provider returns NULL */ assert_null(ai_get_provider_default_model("nonexistent")); /* Default providers may or may not have default models set initially */ /* After setting, they should return the model */ ai_set_provider_default_model("openai", "test-model"); assert_string_equal("test-model", ai_get_provider_default_model("openai")); /* NULL model argument should be ignored (no change) */ ai_set_provider_default_model("openai", NULL); /* After setting NULL, the model should still be "test-model" because * ai_set_provider_default_model returns early on NULL model */ assert_string_equal("test-model", ai_get_provider_default_model("openai")); } void test_ai_set_provider_setting(void** state) { /* NULL provider should be ignored */ ai_set_provider_setting(NULL, "tools", "enabled"); /* NULL setting should be ignored */ ai_set_provider_setting("openai", NULL, "enabled"); /* Non-existent provider should log warning but not crash */ ai_set_provider_setting("nonexistent", "tools", "enabled"); /* Set a setting for openai */ ai_set_provider_setting("openai", "tools", "enabled"); ai_set_provider_setting("openai", "search", "disabled"); /* Verify settings were set */ gchar* tools = ai_get_provider_setting("openai", "tools"); assert_non_null(tools); assert_string_equal("enabled", tools); g_free(tools); gchar* search = ai_get_provider_setting("openai", "search"); assert_non_null(search); assert_string_equal("disabled", search); g_free(search); /* NULL value removes the setting */ ai_set_provider_setting("openai", "tools", NULL); tools = ai_get_provider_setting("openai", "tools"); assert_null(tools); /* Setting that was never set returns NULL */ assert_null(ai_get_provider_setting("openai", "nonexistent_setting")); } void test_ai_get_provider_setting(void** state) { /* NULL provider returns NULL */ assert_null(ai_get_provider_setting(NULL, "tools")); /* NULL setting returns NULL */ assert_null(ai_get_provider_setting("openai", NULL)); /* Non-existent provider returns NULL */ assert_null(ai_get_provider_setting("nonexistent", "tools")); /* Provider without settings returns NULL */ assert_null(ai_get_provider_setting("perplexity", "tools")); /* Set and get setting */ ai_set_provider_setting("openai", "temperature", "0.7"); gchar* temp = ai_get_provider_setting("openai", "temperature"); assert_non_null(temp); assert_string_equal("0.7", temp); g_free(temp); /* Setting value is a copy (caller must free) */ ai_set_provider_setting("openai", "max_tokens", "2048"); temp = ai_get_provider_setting("openai", "max_tokens"); assert_non_null(temp); assert_string_equal("2048", temp); g_free(temp); } /* ======================================================================== * Model Caching Tests * ======================================================================== */ void test_ai_models_are_fresh_initial(void** state) { /* After init, models should be fresh (or at least not crash) */ /* The ai_client_init() loads models from prefs, which may be empty initially */ /* So we just verify the function works correctly */ /* NULL provider returns FALSE */ assert_false(ai_models_are_fresh(NULL)); /* Non-existent provider returns FALSE */ assert_false(ai_models_are_fresh("nonexistent")); /* Default providers - check that function doesn't crash */ /* Freshness depends on whether models were loaded from prefs */ gboolean fresh_openai = ai_models_are_fresh("openai"); assert_true(fresh_openai == TRUE || fresh_openai == FALSE); /* Just verify no crash */ gboolean fresh_perplexity = ai_models_are_fresh("perplexity"); assert_true(fresh_perplexity == TRUE || fresh_perplexity == FALSE); /* Just verify no crash */ } /* ======================================================================== * Model Parsing Tests * ======================================================================== */ void test_ai_parse_models_openai_format(void** state) { /* Test parsing OpenAI-compatible API response format */ /* Format: {"object":"list","data":[{"id":"model1",...},...]} */ ai_client_init(); AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL); /* JSON response from Perplexity/OpenAI API */ const gchar* json = "{\"object\":\"list\"," "\"data\":[" "{\"id\":\"anthropic/claude-haiku-4-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"}," "{\"id\":\"anthropic/claude-opus-4-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"}," "{\"id\":\"openai/gpt-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"}," "{\"id\":\"google/gemini-3-flash-preview\",\"object\":\"model\",\"created\":0,\"owned_by\":\"google\"}" "]}"; ai_parse_models_from_json(provider, json); /* Verify models were parsed correctly */ assert_int_equal(4, g_list_length(provider->models)); /* Check specific models */ GList* models = provider->models; assert_string_equal("anthropic/claude-haiku-4-5", models->data); models = g_list_next(models); assert_string_equal("anthropic/claude-opus-4-5", models->data); models = g_list_next(models); assert_string_equal("openai/gpt-5", models->data); models = g_list_next(models); assert_string_equal("google/gemini-3-flash-preview", models->data); ai_provider_unref(provider); ai_client_shutdown(); } void test_ai_parse_models_perplexity_format(void** state) { /* Test parsing actual Perplexity API response with all models from curl test */ ai_client_init(); AIProvider* provider = ai_add_provider("perplexity", "https://api.perplexity.ai/", NULL); const gchar* json = "{\"object\":\"list\"," "\"data\":[" "{\"id\":\"anthropic/claude-haiku-4-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"}," "{\"id\":\"anthropic/claude-opus-4-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"}," "{\"id\":\"anthropic/claude-opus-4-6\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"}," "{\"id\":\"anthropic/claude-opus-4-7\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"}," "{\"id\":\"anthropic/claude-sonnet-4-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"}," "{\"id\":\"anthropic/claude-sonnet-4-6\",\"object\":\"model\",\"created\":0,\"owned_by\":\"anthropic\"}," "{\"id\":\"google/gemini-3-flash-preview\",\"object\":\"model\",\"created\":0,\"owned_by\":\"google\"}," "{\"id\":\"google/gemini-3.1-pro-preview\",\"object\":\"model\",\"created\":0,\"owned_by\":\"google\"}," "{\"id\":\"nvidia/nemotron-3-super-120b-a12b\",\"object\":\"model\",\"created\":0,\"owned_by\":\"nvidia\"}," "{\"id\":\"openai/gpt-5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"}," "{\"id\":\"openai/gpt-5-mini\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"}," "{\"id\":\"openai/gpt-5.1\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"}," "{\"id\":\"openai/gpt-5.2\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"}," "{\"id\":\"openai/gpt-5.4\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"}," "{\"id\":\"openai/gpt-5.4-mini\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"}," "{\"id\":\"openai/gpt-5.4-nano\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"}," "{\"id\":\"openai/gpt-5.5\",\"object\":\"model\",\"created\":0,\"owned_by\":\"openai\"}," "{\"id\":\"perplexity/sonar\",\"object\":\"model\",\"created\":0,\"owned_by\":\"perplexity\"}," "{\"id\":\"xai/grok-4-1-fast-non-reasoning\",\"object\":\"model\",\"created\":0,\"owned_by\":\"xai\"}," "{\"id\":\"xai/grok-4.20-multi-agent\",\"object\":\"model\",\"created\":0,\"owned_by\":\"xai\"}," "{\"id\":\"xai/grok-4.20-non-reasoning\",\"object\":\"model\",\"created\":0,\"owned_by\":\"xai\"}," "{\"id\":\"xai/grok-4.20-reasoning\",\"object\":\"model\",\"created\":0,\"owned_by\":\"xai\"}," "{\"id\":\"xai/grok-4.3\",\"object\":\"model\",\"created\":0,\"owned_by\":\"xai\"}" "]}"; ai_parse_models_from_json(provider, json); /* Verify all 23 models were parsed correctly */ assert_int_equal(23, g_list_length(provider->models)); /* Verify NO field names or owned_by values are in the model list */ /* The bug was that "id", "object", "model", "created", "owned_by" and provider names * like "anthropic", "google", "openai", "nvidia", "perplexity", "xai" were extracted */ GList* models = provider->models; while (models) { const gchar* model = (const gchar*)models->data; /* These should never be model IDs */ assert_false(g_strcmp0(model, "id") == 0); assert_false(g_strcmp0(model, "object") == 0); assert_false(g_strcmp0(model, "model") == 0); assert_false(g_strcmp0(model, "created") == 0); assert_false(g_strcmp0(model, "owned_by") == 0); assert_false(g_strcmp0(model, "list") == 0); /* These are owned_by values, not model IDs */ assert_false(g_strcmp0(model, "anthropic") == 0); assert_false(g_strcmp0(model, "google") == 0); assert_false(g_strcmp0(model, "openai") == 0); assert_false(g_strcmp0(model, "nvidia") == 0); assert_false(g_strcmp0(model, "perplexity") == 0); assert_false(g_strcmp0(model, "xai") == 0); models = g_list_next(models); } /* Verify first and last model IDs */ models = provider->models; assert_string_equal("anthropic/claude-haiku-4-5", models->data); models = g_list_last(models); assert_string_equal("xai/grok-4.3", models->data); ai_provider_unref(provider); ai_client_shutdown(); } void test_ai_parse_models_array_format(void** state) { /* Test parsing simple array format: ["model1", "model2", ...] */ ai_client_init(); AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL); const gchar* json = "[\"model1\", \"model2\", \"model3\"]"; ai_parse_models_from_json(provider, json); assert_int_equal(3, g_list_length(provider->models)); GList* models = provider->models; assert_string_equal("model1", models->data); models = g_list_next(models); assert_string_equal("model2", models->data); models = g_list_next(models); assert_string_equal("model3", models->data); ai_provider_unref(provider); ai_client_shutdown(); } void test_ai_parse_models_empty_json(void** state) { /* Test parsing empty/invalid JSON */ ai_client_init(); AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL); ai_parse_models_from_json(provider, ""); assert_null(provider->models); assert_int_equal(0, g_list_length(provider->models)); ai_parse_models_from_json(provider, "{\"invalid\": true}"); assert_null(provider->models); assert_int_equal(0, g_list_length(provider->models)); ai_provider_unref(provider); ai_client_shutdown(); } void test_ai_parse_models_null_handling(void** state) { /* Test NULL handling */ ai_client_init(); AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL); /* NULL json should not crash */ ai_parse_models_from_json(provider, NULL); assert_null(provider->models); ai_provider_unref(provider); ai_client_shutdown(); } void test_ai_parse_models_escaped_quotes(void** state) { /* Test parsing model IDs with escaped quotes */ ai_client_init(); AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL); const gchar* json = "{\"object\":\"list\"," "\"data\":[" "{\"id\":\"test/model\\\"with\\\"quotes\",\"object\":\"model\",\"created\":0,\"owned_by\":\"test\"}" "]}"; ai_parse_models_from_json(provider, json); assert_int_equal(1, g_list_length(provider->models)); /* Model ID should have the escaped quotes handled */ GList* models = provider->models; assert_non_null(models); ai_provider_unref(provider); ai_client_shutdown(); } void test_ai_parse_models_with_whitespace(void** state) { /* Test parsing JSON with whitespace after colons */ ai_client_init(); AIProvider* provider = ai_add_provider("test", "https://test.api.com/v1", NULL); /* JSON with spaces after colons (common in formatted JSON) */ const gchar* json = "{ \"object\": \"list\", " "\"data\": [ " "{ \"id\": \"model-with-spaces\", \"object\": \"model\", \"created\": 0, \"owned_by\": \"test\" }" "]}"; ai_parse_models_from_json(provider, json); assert_int_equal(1, g_list_length(provider->models)); GList* models = provider->models; assert_string_equal("model-with-spaces", models->data); ai_provider_unref(provider); ai_client_shutdown(); }