mirror of
https://git.jabber.space/devs/cproof.git
synced 2026-07-20 20:36:21 +00:00
test(ai): expand unit and functional coverage for /ai feature
Unit tests (tests/unittests/test_ai_client.c): +50 cases on top of the
existing 50 — total 100. Covers:
- chat response parser (ai_parse_response): OpenAI content + Perplexity
text formats, escape decoding, empty/null/missing inputs, format-string
safety, multiline content
- error envelope parser (ai_parse_error_message): standard envelope,
nested escapes, missing fields, null/empty
- extended JSON escape: \b, \f, \r, all-specials, UTF-8 pass-through
- provider autocomplete cycling with >=2 matches and wrap-around
- session edge cases: NULL args, 100-message order preservation,
set_model(NULL), ref/unref(NULL)
- provider edge cases: get/remove with NULL, double-remove, survival
via session ref after ai_remove_provider
- settings: multi-key independence, missing key, cross-provider
isolation
- model parsing edges: data not array, empty data, "id" outside data,
multiple models
- prefs round-trip: set token -> shutdown -> init -> token reloaded
from disk (uses load_preferences fixture)
Functional tests:
- Console only (test_ai.c): 15 cases for the /ai command surface that
don't need HTTP. Covers /ai help, providers list, set provider/token,
start with/without key/unknown provider, clear, remove, default
provider/model, switch without window, bad subcommand.
Infrastructure:
- TEST_GROUPS bumped to 5 in proftest.c; AI tests live in their own
Group 5 because mixing them with stabber-driven tests in Group 4
poisoned stbbr_stop() teardown.
- PROF_FUNC_TEST_AI macro in functionaltests.c registers AI tests
with ai_init_test() which wraps init_prof_test with a prof_connect()
so stabber sees a graceful disconnect at teardown.
Source-level changes to enable testing:
- src/ai/ai_client.{c,h}: dropped 'static' from _parse_ai_response
(renamed ai_parse_response) and _parse_error_response (renamed
ai_parse_error_message); declared under a "Parsing helpers (exposed
for testing)" section. Same approach as ai_parse_models_from_json.
Results in Docker (cproof-debian image):
- 595/595 unit tests pass
- 15/15 AI functional tests pass (Group 5)
This commit is contained in:
@@ -1250,8 +1250,8 @@ _build_json_payload_from_list(const gchar* model, GList* history, const gchar* p
|
||||
return json_payload;
|
||||
}
|
||||
|
||||
static gchar*
|
||||
_parse_ai_response(const gchar* response_json)
|
||||
gchar*
|
||||
ai_parse_response(const gchar* response_json)
|
||||
{
|
||||
if (!response_json || strlen(response_json) == 0)
|
||||
return NULL;
|
||||
@@ -1337,8 +1337,8 @@ _parse_ai_response(const gchar* response_json)
|
||||
* Extract error message from an API error JSON response.
|
||||
* Handles format: {"error":{"message":"...","type":"...","code":...}}
|
||||
*/
|
||||
static gchar*
|
||||
_parse_error_response(const gchar* error_json)
|
||||
gchar*
|
||||
ai_parse_error_message(const gchar* error_json)
|
||||
{
|
||||
if (!error_json || strlen(error_json) == 0)
|
||||
return NULL;
|
||||
@@ -1496,7 +1496,7 @@ _ai_request_thread(gpointer data)
|
||||
log_debug("[AI-THREAD] HTTP error response body (%zu bytes): %s",
|
||||
response.size, response.data ? response.data : "NULL");
|
||||
/* Try to extract the actual error message from the JSON response */
|
||||
auto_gchar gchar* parsed_error = _parse_error_response(response.data);
|
||||
auto_gchar gchar* parsed_error = ai_parse_error_message(response.data);
|
||||
auto_gchar gchar* error_msg = parsed_error ? g_strdup_printf("HTTP %ld: %s", http_code, parsed_error) : g_strdup_printf("HTTP %ld: %s", http_code, response.data && strlen(response.data) > 0 ? response.data : "Unknown error");
|
||||
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
||||
_aiwin_display_error(user_data, error_msg);
|
||||
@@ -1505,7 +1505,7 @@ _ai_request_thread(gpointer data)
|
||||
log_debug("[AI-THREAD] Raw API response (%zu bytes): %s", response.size, response.data ? response.data : "NULL");
|
||||
auto_gchar gchar* response_data = response.data;
|
||||
response.data = NULL;
|
||||
auto_gchar gchar* content = _parse_ai_response(response_data);
|
||||
auto_gchar gchar* content = ai_parse_response(response_data);
|
||||
if (content) {
|
||||
/* Add assistant response to history (under lock) */
|
||||
pthread_mutex_lock(&session->lock);
|
||||
|
||||
@@ -181,7 +181,7 @@ gboolean ai_fetch_models(const gchar* provider_name, gpointer user_data);
|
||||
gboolean ai_models_are_fresh(const gchar* provider_name);
|
||||
|
||||
/* ========================================================================
|
||||
* Model Parsing (for testing)
|
||||
* Parsing helpers (exposed for testing)
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
@@ -192,6 +192,24 @@ gboolean ai_models_are_fresh(const gchar* provider_name);
|
||||
*/
|
||||
void ai_parse_models_from_json(AIProvider* provider, const gchar* json);
|
||||
|
||||
/**
|
||||
* Extract assistant content from an LLM response body. Tries Perplexity
|
||||
* /v1/responses "text" first, falls back to OpenAI "content". Decodes
|
||||
* the \" and \n escape sequences only.
|
||||
* @param response_json The JSON body from the provider
|
||||
* @return Newly allocated content string, or NULL if not found (caller frees)
|
||||
*/
|
||||
gchar* ai_parse_response(const gchar* response_json);
|
||||
|
||||
/**
|
||||
* Extract human-readable error message from an API error envelope.
|
||||
* Expected format: {"error":{"message":"...","type":"...","code":...}}
|
||||
* Decodes \" \n \\ \t escapes.
|
||||
* @param error_json The JSON body of the error response
|
||||
* @return Newly allocated error message, or NULL if not parseable (caller frees)
|
||||
*/
|
||||
gchar* ai_parse_error_message(const gchar* error_json);
|
||||
|
||||
/* ========================================================================
|
||||
* Session Management
|
||||
* ======================================================================== */
|
||||
|
||||
Reference in New Issue
Block a user