fix(ai): persistence/UX fixes, parser unification, prefs consolidation, plus test coverage #119
Reference in New Issue
Block a user
No description provided.
Delete Branch "fix/ai-followups"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Followups on
feat/aibased on the manual-testing notes:set default-modeldidn't persist across restart/ai providers(no args) showed a hardcoded openai/perplexity blurb instead of the configured list/ai models <provider>(fresh fetch) printed only a count, not the model nameserror.message/ai set customwas runtime-only and lost on restartPlus the test coverage from
test/ai-coverage-unit-onlyis folded in so this branch lands fixes + the unit/functional tests that exercise them.Source-only commits (no test changes):
de2d21a0afix(ai): persistence and command-output fixes— persist per-provider default model, stop re-seeding defaults after the configured list goes empty, list real configured providers on/ai providers, print actual model names on fresh/ai models, wipe all per-provider keys on/ai remove provider, drop a deadai_models_finddeclaration.659b9d77drefactor(ai): unify response and error JSON parsing— two shared helpers (_json_unescape_substring,_extract_json_string) replace two divergent inline string-scan loops;_parse_error_responseis now wired into_curl_exec_and_handleso models-endpoint errors also get the parsed message instead of the raw body.5104de40drefactor(ai): consolidate per-provider prefs into [ai/<name>] sections— all per-provider state lives in[ai/<provider>]with keysurl,key,default_model,models,setting.<custom>;[ai]keeps only the globaldefaults_initializedsentinel. Idempotent migration in_prefs_load()moves legacy flat-key layouts over. Custom settings now persist (were in-memory only before).Then merged in
test/ai-coverage-unit-only(commitsd676f3b08,befdd0ba1,65c601267) which carries the unit + functional test refactor — tests are defaults-agnostic, set up their own providers, multi-provider tests use distinct URLs, functional tests check forhttps?://rather than specific provider names.Out of scope / follow-up
AI_AUTOCOMPLETE_POSSIBLE_ISSUES.mdontest/ai-coverage-unit-onlyfor the analysis; Issue 2 is parked in the lowest-priority backlog.Test plan
make check-unitgreen in Docker (ci/Dockerfile.debian)make check-functionalgreen/ai set default-model X gpt-4o, restart,/aishows the default/ai set custom openai tools enabled, restart, setting still inprovider->settings/ai remove provider openai, restart, no openai re-seeded/ai models <provider>lists actual model IDs (not just a count)/ai models, error shows the API'serror.message, not raw JSON~/.local/share/profanity/profrcshows[ai/<provider>]sections after first saveUnit 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)Adds 10 tests for ai_models_find and additional edge cases on ai_providers_find. 5 pass as sanity; 5 fail intentionally, documenting two distinct bugs in the autocomplete integration: BUG A — ai_models_find cycling is broken src/ai/ai_client.c ai_models_find() allocates a fresh Autocomplete on every call and frees it before returning. The Autocomplete's last_found cursor is therefore never persisted, so repeated calls with the same prefix always return the first match. Tab+Tab+Tab cannot reach models 2, 3, ... Caught by: - test_ai_models_find_cycles_through_matches - test_ai_models_find_null_search_cycles_all BUG B — providers_ac is never reset between user input cycles src/ai/ai_client.c keeps the static providers_ac AC, but src/command/cmd_ac.c's all_acs[] (which cmd_ac_reset() iterates) does not include &providers_ac. So when the input loop fires the global reset, providers_ac retains its last_found cursor from the previous prefix. Result: tab-completing a new prefix continues from the old cursor position instead of restarting at the head of the new prefix's matches. A provider added mid-session may also be missed depending on cursor position. Caught by: - test_ai_providers_find_state_resets_on_prefix_change - test_ai_providers_find_empty_then_prefix - test_ai_providers_find_after_add_includes_new Sanity (5 passing): - test_ai_models_find_no_models_returns_null - test_ai_models_find_returns_match_for_prefix - test_ai_models_find_no_match_returns_null - test_ai_models_find_previous_direction - test_ai_providers_find_after_remove_skips_removed The failing tests are checked in as-is — they go green when the bugs are fixed. See PR description for suggested fixes.Both the chat response parser and the error envelope parser were doing their own ad-hoc strstr scans and inline unescape loops, with different sets of supported escape sequences (chat: \" \n; error: \" \n \\ \t). Models endpoint errors didn't try to extract error.message at all and surfaced raw response bodies to the user — inconsistent with the chat path which already did parse them. Consolidate around two helpers: _json_unescape_substring: decode \" \\ \/ \n \t \r \b \f into a fresh buffer; pass \uXXXX through verbatim (out of scope here). _extract_json_string: locate "field":"..." via _find_json_field (already handles JSON whitespace around the colon) and return the unescaped value, treating any \X inside the string body as opaque so embedded \" is never misread as the closing quote. _parse_ai_response and _parse_error_response now each collapse to a small wrapper around these helpers. Wire _parse_error_response into the shared _curl_exec_and_handle 4xx branch so models requests also surface the provider's error.message instead of the raw body.