Some checks failed
CI Code / Check spelling (pull_request) Successful in 21s
CI Code / Check coding style (pull_request) Successful in 37s
CI Code / Linux (debian) (pull_request) Failing after 2m56s
CI Code / Linux (ubuntu) (pull_request) Failing after 3m7s
CI Code / Code Coverage (pull_request) Successful in 2m55s
CI Code / Linux (arch) (pull_request) Failing after 3m45s
Update ai_providers_find() to handle NULL search_str safely by treating it as an empty string, preventing a segfault in strdup() within autocomplete_complete(). Rename test_ai_providers_find_case_sensitive to test_ai_providers_find_case_insensitive to reflect the new case-insensitive matching contract. Update all affected tests to expect cycling behavior (NULL/empty returns first provider) and use auto_gchar for automatic memory management. Use `gchar` instead of `char` in ai_providers_find for consistency
464 lines
13 KiB
C
464 lines
13 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);
|
|
}
|