Files
profanity/tests/unittests/test_ai_client.c
Jabber Developer 93ad7379e2 fix(ai): align ai_list_providers with documented contract
Remove ai_provider_ref() from ai_list_providers() so the returned
providers do not need to be unref'd by the caller. This matches the
header docstring which states "caller must not free the list or
providers". Also update all callers (cmd_ai_providers, tests) to
remove redundant ai_provider_unref() calls.
2026-04-30 19:09:46 +00:00

467 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 */
char* result = ai_providers_find("o", FALSE, NULL);
assert_non_null(result);
assert_string_equal("openai", result);
g_free(result);
}
void
test_ai_providers_find_forward_perplexity(void** state)
{
/* Test forward iteration for perplexity */
char* result = ai_providers_find("p", FALSE, NULL);
assert_non_null(result);
assert_string_equal("perplexity", result);
g_free(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);
char* result = ai_providers_find("c", FALSE, NULL);
assert_non_null(result);
assert_string_equal("custom", result);
g_free(result);
}
void
test_ai_providers_find_forward_no_match(void** state)
{
/* Test no match */
char* 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" */
char* result = ai_providers_find("ope", FALSE, NULL);
assert_non_null(result);
assert_string_equal("openai", result);
g_free(result);
}
void
test_ai_providers_find_next(void** state)
{
/* Test that stateless implementation returns same result each call */
char* result1 = ai_providers_find("o", FALSE, NULL);
assert_non_null(result1);
assert_string_equal("openai", result1);
g_free(result1);
/* Second call with same params returns same result (stateless) */
char* result2 = ai_providers_find("o", FALSE, NULL);
assert_non_null(result2);
assert_string_equal("openai", result2);
g_free(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 */
char* result1 = ai_providers_find("o", FALSE, NULL);
assert_non_null(result1);
assert_string_equal("openai", result1);
g_free(result1);
/* previous=TRUE also returns "openai" (only one match, so first==last) */
char* result2 = ai_providers_find("o", TRUE, NULL);
assert_non_null(result2);
assert_string_equal("openai", result2);
g_free(result2);
}
void
test_ai_providers_find_null_search_str(void** state)
{
char* result = ai_providers_find(NULL, FALSE, NULL);
assert_null(result);
}
void
test_ai_providers_find_empty_search_str(void** state)
{
char* result = ai_providers_find("", FALSE, NULL);
assert_null(result);
}
void
test_ai_providers_find_case_sensitive(void** state)
{
/* Test that matching is case-sensitive */
char* result = ai_providers_find("OPENAI", FALSE, NULL);
assert_null(result);
result = ai_providers_find("OpenAI", FALSE, NULL);
assert_null(result);
result = ai_providers_find("openai", FALSE, NULL);
assert_non_null(result);
assert_string_equal("openai", result);
g_free(result);
}