Self-setup providers in tests instead of relying on hardcoded openai/perplexity
defaults. Multi-provider tests now use distinct URLs per provider; functional
tests check for http/https URL presence rather than specific provider names.
Drop ai_models_find tests (function removed upstream in feat/ai). Replace with
reset-hook + persistence coverage: providers_reset_ac restart cycle, provider
add/remove round-trip across init, model cache round-trip across init.
Keep three autocomplete prefix-change tests red in-suite (documented as latent
API-hygiene gap, unreachable from UI today). NULL-search test body retained
but registration commented out — currently SIGSEGVs the cmocka runner.
Add stub_xmpp connection_create_stanza_id to satisfy new cmd_funcs.c call site.
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.
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)
The org_id field was removed from AIProvider as it was no longer
populated through the command surface. The /ai set org command was
previously removed, leaving org_id as dead code.
This change removes org_id from:
- AIProvider struct definition
- ai_provider_new() and ai_provider_unref()
- ai_add_provider() API signature
- _build_curl_headers() (OpenAI-Organization header)
- cmd_ai_set() and cmd_ai_providers()
- All unit tests
Rewrote internal JSON parsing to correctly handle whitespace, escaped
quotes, and strict field context extraction. This prevents incorrect
field names or provider names from being added to the model list.
Added `ai_parse_models_from_json` public API to facilitate testing.
Changed `/ai models` default behavior to always fetch fresh models.
Replaced `--refresh` flag with `--cached` to display local cache.
Added comprehensive unit tests covering OpenAI and Perplexity formats,
array format, empty/null JSON, escaped quotes, and whitespace handling.
- Introduce model caching with persistence to preferences
- Add provider default model and custom settings management
- Implement `/ai switch`, `/ai models`, and improve `/ai start`
- Add model name autocomplete for chat commands
- Update command definitions and help text
- Add unit tests for new functionality
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
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.
Add an AI client module that integrates with OpenAI-compatible API
providers (OpenAI, Perplexity, and custom providers) to provide
AI-assisted responses within the profanity client.
The implementation includes:
- src/ai/ai_client.c/h: Core AI client with provider management,
session handling, and async HTTP request handling via libcurl.
Supports per-provider API keys stored in preferences, reference-
counted sessions, and conversation history tracking.
- src/ui/window.c/window_list.c: New AI window type (ProfAiWin) for
displaying AI conversations, with response streaming and error
display capabilities.
- Command integration: New `/ai` command (cmd_defs.c, cmd_funcs.c)
for creating sessions, sending prompts, and managing providers.
Provider autocomplete support in cmd_ac.c.
- Preferences integration: API keys for providers are persisted in
the preferences system (config/preferences.c).
- Unit tests: 472 lines of comprehensive tests covering provider
management, session lifecycle, JSON escaping, and autocomplete
(tests/unittests/test_ai_client.c).
Architecture decisions:
- Asynchronous design: HTTP requests run on a separate thread to
avoid blocking the main UI loop. Callbacks are invoked on the main
thread via direct function call (profanity uses ncurses, not GLib
main loop).
- Reference counting: Both AIProvider and AISession use ref counting
for safe shared ownership.
- Response size limit: 10MB cap on HTTP responses to prevent OOM.