Files
profanity/tests/unittests/test_ai_client.c
Jabber Developer a898cb212d feat(ai): implement robust JSON model parsing
Rewrote internal JSON parsing to correctly handle whitespace, escaped
quotes, and strict field context extraction. This prevents incorrect
field names or provider names from being added to the model list.

Added `ai_parse_models_from_json` public API to facilitate testing.

Changed `/ai models` default behavior to always fetch fresh models.
Replaced `--refresh` flag with `--cached` to display local cache.

Added comprehensive unit tests covering OpenAI and Perplexity formats,
array format, empty/null JSON, escaped quotes, and whitespace handling.
2026-05-09 13:56:59 +00:00

870 lines
29 KiB
C

#include "prof_cmocka.h"
#include "common.h"
#include "ai/ai_client.h"
#include <stdlib.h>
/* ========================================================================
* 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();
}