fix(ai): bound response parsing and harden AUTO fallback errors
Some checks failed
CI Code / Check spelling (pull_request) Failing after 14s
CI Code / Check coding style (pull_request) Successful in 30s
CI Code / Linux (debian) (pull_request) Successful in 5m0s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m8s
CI Code / Linux (arch) (pull_request) Successful in 6m36s
CI Code / Code Coverage (pull_request) Successful in 3m36s
Some checks failed
CI Code / Check spelling (pull_request) Failing after 14s
CI Code / Check coding style (pull_request) Successful in 30s
CI Code / Linux (debian) (pull_request) Successful in 5m0s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m8s
CI Code / Linux (arch) (pull_request) Successful in 6m36s
CI Code / Code Coverage (pull_request) Successful in 3m36s
- bound _parse_responses scans to the output_text part's object and to
the content array, so a later sibling item's "text" (e.g. a reasoning
summary) can never be returned as the assistant reply
- do not retry the other flavour on a curl timeout: the request likely
reached the server and may still be generating, so a re-POST of the
conversation could trigger a second billed generation
- remember an unparseable 2xx from the first AUTO attempt and surface
stashed first-attempt errors in both error paths, instead of showing
only the final attempt's transport or HTTP error
- recognize Ollama's model-not-found wording in _names_model so a model
typo is not misread as a missing endpoint
- drop the dead, racy provider-lookup fallback in the generic request
thread: a missing provider ref is a caller bug and now fails loudly
- fix ai_providers_lists_defaults to expect the header the command
actually prints ("Configured providers:"); the test was broken since
its introduction but CI never ran it
- add functional test group 5 (AI command surface) to FUNC_TEST_GROUPS
so the CI parallel target runs it; proftest.c port ranges already
account for five groups
This commit is contained in:
@@ -55,11 +55,11 @@ ai_no_args_shows_help(void** state)
|
||||
void
|
||||
ai_providers_lists_defaults(void** state)
|
||||
{
|
||||
/* `/ai providers` (no "list") shows the built-in list with URLs. */
|
||||
/* `/ai providers` (no "list") shows the configured list with URLs. */
|
||||
prof_input("/ai providers");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_exact("Available AI providers:"));
|
||||
assert_true(prof_output_exact("Configured providers:"));
|
||||
/* At least one URL line is rendered — exact name agnostic. */
|
||||
assert_true(prof_output_regex("https?://"));
|
||||
prof_timeout_reset();
|
||||
|
||||
@@ -1087,6 +1087,48 @@ test_ai_parse_response_responses_reasoning_only_returns_null(void** state)
|
||||
assert_null(out);
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_response_responses_reasoning_after_message_skipped(void** state)
|
||||
{
|
||||
/* A reasoning item ordered after the message, with the part's "text"
|
||||
* preceding "type":"output_text": the forward scan must stay inside the
|
||||
* part's object and the backscan must find the real answer. */
|
||||
const gchar* json = "{\"output\":["
|
||||
"{\"type\":\"message\",\"content\":[{\"text\":\"answer\",\"type\":\"output_text\"}]},"
|
||||
"{\"type\":\"reasoning\",\"summary\":[{\"type\":\"summary_text\",\"text\":\"secret\"}]}"
|
||||
"]}";
|
||||
auto_gchar gchar* out = ai_parse_response(json);
|
||||
assert_non_null(out);
|
||||
assert_string_equal("answer", out);
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_response_responses_empty_content_not_leaked(void** state)
|
||||
{
|
||||
/* An empty content array followed by a reasoning item must yield a parse
|
||||
* failure; the scan must not escape the array and return the summary. */
|
||||
const gchar* json = "{\"output\":["
|
||||
"{\"type\":\"message\",\"content\":[]},"
|
||||
"{\"type\":\"reasoning\",\"summary\":[{\"type\":\"summary_text\",\"text\":\"secret\"}]}"
|
||||
"]}";
|
||||
gchar* out = ai_parse_response(json);
|
||||
assert_null(out);
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_response_responses_text_value_before_later_item(void** state)
|
||||
{
|
||||
/* A reply that is exactly "text" plus a reasoning item after the message:
|
||||
* the backscan's own extraction must also stay inside the part's object. */
|
||||
const gchar* json = "{\"output\":["
|
||||
"{\"type\":\"message\",\"content\":[{\"text\":\"text\",\"type\":\"output_text\"}]},"
|
||||
"{\"type\":\"reasoning\",\"summary\":[{\"type\":\"summary_text\",\"text\":\"secret\"}]}"
|
||||
"]}";
|
||||
auto_gchar gchar* out = ai_parse_response(json);
|
||||
assert_non_null(out);
|
||||
assert_string_equal("text", out);
|
||||
}
|
||||
|
||||
void
|
||||
test_ai_parse_response_choices_in_string_value(void** state)
|
||||
{
|
||||
|
||||
@@ -79,6 +79,9 @@ void test_ai_parse_response_responses_brace_in_text(void** state);
|
||||
void test_ai_parse_response_choices_logprobs_before_content(void** state);
|
||||
void test_ai_parse_response_reasoning_not_leaked_without_text(void** state);
|
||||
void test_ai_parse_response_responses_reasoning_only_returns_null(void** state);
|
||||
void test_ai_parse_response_responses_reasoning_after_message_skipped(void** state);
|
||||
void test_ai_parse_response_responses_empty_content_not_leaked(void** state);
|
||||
void test_ai_parse_response_responses_text_value_before_later_item(void** state);
|
||||
void test_ai_parse_response_responses_text_value_text(void** state);
|
||||
void test_ai_parse_response_escaped_quote(void** state);
|
||||
void test_ai_parse_response_newline_escape(void** state);
|
||||
|
||||
@@ -789,6 +789,9 @@ main(int argc, char* argv[])
|
||||
cmocka_unit_test(test_ai_parse_response_choices_logprobs_before_content),
|
||||
cmocka_unit_test(test_ai_parse_response_reasoning_not_leaked_without_text),
|
||||
cmocka_unit_test(test_ai_parse_response_responses_reasoning_only_returns_null),
|
||||
cmocka_unit_test(test_ai_parse_response_responses_reasoning_after_message_skipped),
|
||||
cmocka_unit_test(test_ai_parse_response_responses_empty_content_not_leaked),
|
||||
cmocka_unit_test(test_ai_parse_response_responses_text_value_before_later_item),
|
||||
cmocka_unit_test(test_ai_parse_response_responses_text_value_text),
|
||||
cmocka_unit_test(test_ai_parse_response_escaped_quote),
|
||||
cmocka_unit_test(test_ai_parse_response_newline_escape),
|
||||
|
||||
Reference in New Issue
Block a user