mirror of
https://git.jabber.space/devs/cproof.git
synced 2026-07-20 18:36:21 +00:00
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.
870 lines
29 KiB
C
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();
|
|
}
|