Files
profanity/tests/unittests/test_ai_client.c
Jabber Developer 00f11eb704 fix(ai): guard NULL search_str and update tests for new cycling behavior
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
2026-05-01 13:08:28 +00:00

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);
}