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
This commit is contained in:
2026-05-01 13:08:28 +00:00
parent 4913a3d5a4
commit 00f11eb704
5 changed files with 32 additions and 32 deletions

View File

@@ -360,7 +360,7 @@ ai_list_providers(void)
/* Stateful provider name finder with case-sensitive matching. /* Stateful provider name finder with case-sensitive matching.
* Maintains position in sorted list for deterministic tab-completion cycling. * Maintains position in sorted list for deterministic tab-completion cycling.
* The autocomplete is kept in sync via ai_add_provider/ai_remove_provider. */ * The autocomplete is kept in sync via ai_add_provider/ai_remove_provider. */
char* gchar*
ai_providers_find(const char* const search_str, gboolean previous, void* context) ai_providers_find(const char* const search_str, gboolean previous, void* context)
{ {
/* Initialize autocomplete on first use */ /* Initialize autocomplete on first use */
@@ -368,8 +368,11 @@ ai_providers_find(const char* const search_str, gboolean previous, void* context
providers_ac = autocomplete_new(); providers_ac = autocomplete_new();
} }
/* NULL search_str is treated as empty string for cycling */
const char* effective_search = (search_str != NULL) ? search_str : "";
/* Use stateful autocomplete */ /* Use stateful autocomplete */
return autocomplete_complete(providers_ac, search_str, FALSE, previous); return autocomplete_complete(providers_ac, effective_search, FALSE, previous);
} }
gchar* gchar*

View File

@@ -109,7 +109,7 @@ GList* ai_list_providers(void);
* @param context Unused * @param context Unused
* @return Provider name, or NULL if not found * @return Provider name, or NULL if not found
*/ */
char* ai_providers_find(const char* const search_str, gboolean previous, void* context); gchar* ai_providers_find(const char* const search_str, gboolean previous, void* context);
/** /**
* Get the API key for a provider. * Get the API key for a provider.

View File

@@ -356,20 +356,18 @@ void
test_ai_providers_find_forward(void** state) test_ai_providers_find_forward(void** state)
{ {
/* Test forward iteration - should return first match */ /* Test forward iteration - should return first match */
char* result = ai_providers_find("o", FALSE, NULL); auto_gchar gchar* result = ai_providers_find("o", FALSE, NULL);
assert_non_null(result); assert_non_null(result);
assert_string_equal("openai", result); assert_string_equal("openai", result);
g_free(result);
} }
void void
test_ai_providers_find_forward_perplexity(void** state) test_ai_providers_find_forward_perplexity(void** state)
{ {
/* Test forward iteration for perplexity */ /* Test forward iteration for perplexity */
char* result = ai_providers_find("p", FALSE, NULL); auto_gchar gchar* result = ai_providers_find("p", FALSE, NULL);
assert_non_null(result); assert_non_null(result);
assert_string_equal("perplexity", result); assert_string_equal("perplexity", result);
g_free(result);
} }
void void
@@ -378,17 +376,16 @@ test_ai_providers_find_forward_custom(void** state)
/* Add a custom provider and test */ /* Add a custom provider and test */
ai_add_provider("custom", "https://custom.api.com/v1", NULL); ai_add_provider("custom", "https://custom.api.com/v1", NULL);
char* result = ai_providers_find("c", FALSE, NULL); auto_gchar gchar* result = ai_providers_find("c", FALSE, NULL);
assert_non_null(result); assert_non_null(result);
assert_string_equal("custom", result); assert_string_equal("custom", result);
g_free(result);
} }
void void
test_ai_providers_find_forward_no_match(void** state) test_ai_providers_find_forward_no_match(void** state)
{ {
/* Test no match */ /* Test no match */
char* result = ai_providers_find("z", FALSE, NULL); auto_gchar gchar* result = ai_providers_find("z", FALSE, NULL);
assert_null(result); assert_null(result);
} }
@@ -396,26 +393,23 @@ void
test_ai_providers_find_forward_partial_match(void** state) test_ai_providers_find_forward_partial_match(void** state)
{ {
/* Test partial match - should return providers starting with "ope" */ /* Test partial match - should return providers starting with "ope" */
char* result = ai_providers_find("ope", FALSE, NULL); auto_gchar gchar* result = ai_providers_find("ope", FALSE, NULL);
assert_non_null(result); assert_non_null(result);
assert_string_equal("openai", result); assert_string_equal("openai", result);
g_free(result);
} }
void void
test_ai_providers_find_next(void** state) test_ai_providers_find_next(void** state)
{ {
/* Test that stateless implementation returns same result each call */ /* Test that stateless implementation returns same result each call */
char* result1 = ai_providers_find("o", FALSE, NULL); auto_gchar gchar* result1 = ai_providers_find("o", FALSE, NULL);
assert_non_null(result1); assert_non_null(result1);
assert_string_equal("openai", result1); assert_string_equal("openai", result1);
g_free(result1);
/* Second call with same params returns same result (stateless) */ /* Second call with same params returns same result (stateless) */
char* result2 = ai_providers_find("o", FALSE, NULL); auto_gchar gchar* result2 = ai_providers_find("o", FALSE, NULL);
assert_non_null(result2); assert_non_null(result2);
assert_string_equal("openai", result2); assert_string_equal("openai", result2);
g_free(result2);
} }
void void
@@ -423,44 +417,47 @@ test_ai_providers_find_previous(void** state)
{ {
/* Test that previous=TRUE returns last match in list */ /* Test that previous=TRUE returns last match in list */
/* With only "openai" starting with "o", both FALSE and TRUE return same result */ /* With only "openai" starting with "o", both FALSE and TRUE return same result */
char* result1 = ai_providers_find("o", FALSE, NULL); auto_gchar gchar* result1 = ai_providers_find("o", FALSE, NULL);
assert_non_null(result1); assert_non_null(result1);
assert_string_equal("openai", result1); assert_string_equal("openai", result1);
g_free(result1);
/* previous=TRUE also returns "openai" (only one match, so first==last) */ /* previous=TRUE also returns "openai" (only one match, so first==last) */
char* result2 = ai_providers_find("o", TRUE, NULL); auto_gchar gchar* result2 = ai_providers_find("o", TRUE, NULL);
assert_non_null(result2); assert_non_null(result2);
assert_string_equal("openai", result2); assert_string_equal("openai", result2);
g_free(result2);
} }
void void
test_ai_providers_find_null_search_str(void** state) test_ai_providers_find_null_search_str(void** state)
{ {
char* result = ai_providers_find(NULL, FALSE, NULL); /* NULL search_str triggers cycling: returns first provider in list */
assert_null(result); auto_gchar gchar* result = ai_providers_find(NULL, FALSE, NULL);
assert_non_null(result);
assert_string_equal("openai", result);
} }
void void
test_ai_providers_find_empty_search_str(void** state) test_ai_providers_find_empty_search_str(void** state)
{ {
char* result = ai_providers_find("", FALSE, NULL); /* Empty search_str triggers cycling: returns first provider in list */
assert_null(result); auto_gchar gchar* result = ai_providers_find("", FALSE, NULL);
assert_non_null(result);
assert_string_equal("openai", result);
} }
void void
test_ai_providers_find_case_sensitive(void** state) test_ai_providers_find_case_insensitive(void** state)
{ {
/* Test that matching is case-sensitive */ /* Test that matching is case-insensitive (via g_ascii_strdown) */
char* result = ai_providers_find("OPENAI", FALSE, NULL); auto_gchar gchar* result = ai_providers_find("OPENAI", FALSE, NULL);
assert_null(result); assert_non_null(result);
assert_string_equal("openai", result);
result = ai_providers_find("OpenAI", FALSE, NULL); result = ai_providers_find("OpenAI", FALSE, NULL);
assert_null(result); assert_non_null(result);
assert_string_equal("openai", result);
result = ai_providers_find("openai", FALSE, NULL); result = ai_providers_find("openai", FALSE, NULL);
assert_non_null(result); assert_non_null(result);
assert_string_equal("openai", result); assert_string_equal("openai", result);
g_free(result);
} }

View File

@@ -39,4 +39,4 @@ void test_ai_providers_find_next(void** state);
void test_ai_providers_find_previous(void** state); void test_ai_providers_find_previous(void** state);
void test_ai_providers_find_null_search_str(void** state); void test_ai_providers_find_null_search_str(void** state);
void test_ai_providers_find_empty_search_str(void** state); void test_ai_providers_find_empty_search_str(void** state);
void test_ai_providers_find_case_sensitive(void** state); void test_ai_providers_find_case_insensitive(void** state);

View File

@@ -687,7 +687,7 @@ main(int argc, char* argv[])
cmocka_unit_test_setup_teardown(test_ai_providers_find_previous, ai_client_setup, ai_client_teardown), cmocka_unit_test_setup_teardown(test_ai_providers_find_previous, ai_client_setup, ai_client_teardown),
cmocka_unit_test_setup_teardown(test_ai_providers_find_null_search_str, ai_client_setup, ai_client_teardown), cmocka_unit_test_setup_teardown(test_ai_providers_find_null_search_str, ai_client_setup, ai_client_teardown),
cmocka_unit_test_setup_teardown(test_ai_providers_find_empty_search_str, ai_client_setup, ai_client_teardown), cmocka_unit_test_setup_teardown(test_ai_providers_find_empty_search_str, ai_client_setup, ai_client_teardown),
cmocka_unit_test_setup_teardown(test_ai_providers_find_case_sensitive, ai_client_setup, ai_client_teardown), cmocka_unit_test_setup_teardown(test_ai_providers_find_case_insensitive, ai_client_setup, ai_client_teardown),
cmocka_unit_test(test_ai_json_escape), cmocka_unit_test(test_ai_json_escape),
cmocka_unit_test(test_ai_json_escape_null), cmocka_unit_test(test_ai_json_escape_null),
cmocka_unit_test(test_ai_json_escape_empty), cmocka_unit_test(test_ai_json_escape_empty),