All checks were successful
CI Code / Check coding style (pull_request) Successful in 48s
CI Code / Check spelling (pull_request) Successful in 33s
CI Code / Linux (debian) (pull_request) Successful in 4m47s
CI Code / Linux (arch) (pull_request) Successful in 5m29s
CI Code / Code Coverage (pull_request) Successful in 2m45s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m29s
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.
467 lines
13 KiB
C
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);
|
|
}
|