All checks were successful
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 29s
CI Code / Code Coverage (push) Successful in 2m44s
CI Code / Linux (debian) (push) Successful in 4m46s
CI Code / Linux (ubuntu) (push) Successful in 4m59s
CI Code / Linux (arch) (push) Successful in 5m56s
Add an AI client module that integrates with OpenAI-compatible API providers (OpenAI, Perplexity, and custom endpoints) to provide AI-assisted chat within CProof. Users can start sessions with /ai start, send prompts, receive responses in a dedicated AI window, switch between providers and models, and manage API keys — all with tab-completion. Providers are configured via /ai set commands with per-provider API keys, endpoints, default models, and custom settings. Two default providers (openai, perplexity) are seeded on first use. Provider state persists in [ai/<name>] sections of the preferences keyfile with automatic migration from the previous flat-key format. The /ai command integrates into the existing command system with 8 subcommands covering provider management, session lifecycle, model fetching, and conversation clearing. Autocomplete uses the standard flat prefix-matching chain for reliable tab-completion at every nesting level. A new ProfAiWin window type is added to the window system. Architecture: Async design: HTTP requests run on a background thread (pthread) to avoid blocking the ncurses UI loop; results are displayed on the main thread via direct function calls Thread safety: AIProvider and AISession use atomic ref-counting and mutex-protected session state; the request thread snapshots all session data before making the HTTP call Window validation: wins_ai_exists() prevents use-after-free when the user closes the AI window during an in-flight HTTP request (~60s) Privacy: store:false is sent with every request to prevent providers from persisting conversations or using them for training Response size limit: 10MB cap with immediate curl abort via CURL_WRITEFUNC_ERROR to prevent OOM JSON parsing uses unified helpers for both chat responses and error envelopes with consistent escape decoding. The response parser tries Perplexity /v1/responses "text" field first, then falls back to OpenAI "content". Error parsing extracts provider error.message from the standard envelope format. Model parsing handles multiple API response formats (OpenAI list, Perplexity, array) including edge cases. Tests include 470+ lines of unit tests covering provider management, session lifecycle, JSON parsing (multiple formats), autocomplete cycling, and error handling, plus functional tests for /ai command dispatch. A stub_ai.c module isolates unit tests from UI dependencies.
616 lines
9.9 KiB
C
616 lines
9.9 KiB
C
#include "prof_cmocka.h"
|
|
|
|
#include "xmpp/xmpp.h"
|
|
|
|
// connection functions
|
|
void
|
|
session_init(void)
|
|
{
|
|
}
|
|
void
|
|
session_init_activity(void)
|
|
{
|
|
}
|
|
void
|
|
session_check_autoaway(void)
|
|
{
|
|
}
|
|
|
|
jabber_conn_status_t
|
|
session_connect_with_details(const char* const jid,
|
|
const char* const passwd, const char* const altdomain, const int port, const char* const tls_policy,
|
|
const char* const auth_policy)
|
|
{
|
|
check_expected(jid);
|
|
check_expected(passwd);
|
|
check_expected(altdomain);
|
|
check_expected(port);
|
|
return mock_type(jabber_conn_status_t);
|
|
}
|
|
|
|
jabber_conn_status_t
|
|
session_connect_with_account(const ProfAccount* const account)
|
|
{
|
|
check_expected(account);
|
|
return mock_type(jabber_conn_status_t);
|
|
}
|
|
|
|
void
|
|
session_reconnect_now(void)
|
|
{
|
|
}
|
|
void
|
|
session_disconnect(void)
|
|
{
|
|
}
|
|
void
|
|
session_process_events(void)
|
|
{
|
|
}
|
|
void
|
|
connection_disconnect(void)
|
|
{
|
|
}
|
|
const Jid*
|
|
connection_get_jid(void)
|
|
{
|
|
return mock_ptr_type(Jid*);
|
|
}
|
|
const char*
|
|
connection_get_fulljid(void)
|
|
{
|
|
return mock_ptr_type(char*);
|
|
}
|
|
|
|
const char*
|
|
connection_get_barejid(void)
|
|
{
|
|
return mock_ptr_type(const char*);
|
|
}
|
|
|
|
gboolean
|
|
equals_our_barejid(const char* cmp)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
const char*
|
|
connection_get_user(void)
|
|
{
|
|
return mock_ptr_type(const char*);
|
|
}
|
|
|
|
const char*
|
|
connection_get_domain(void)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
gboolean
|
|
connection_is_secured(void)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
TLSCertificate*
|
|
connection_get_tls_peer_cert(void)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
char*
|
|
connection_create_uuid(void)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
char*
|
|
connection_create_stanza_id(void)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
connection_free_uuid(char* uuid)
|
|
{
|
|
}
|
|
|
|
jabber_conn_status_t
|
|
connection_get_status(void)
|
|
{
|
|
return mock_type(jabber_conn_status_t);
|
|
}
|
|
|
|
const char*
|
|
connection_get_presence_msg(void)
|
|
{
|
|
return mock_ptr_type(const char*);
|
|
}
|
|
|
|
const char*
|
|
session_get_account_name(void)
|
|
{
|
|
return mock_ptr_type(char*);
|
|
}
|
|
|
|
GList*
|
|
session_get_available_resources(void)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
connection_set_presence_msg(const char* const message)
|
|
{
|
|
}
|
|
|
|
gboolean
|
|
connection_send_stanza(const char* const stanza)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
connection_supports(const char* const feature)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
const char*
|
|
connection_get_profanity_identifier(void)
|
|
{
|
|
return "profident";
|
|
}
|
|
|
|
void
|
|
connection_debug_print_features()
|
|
{
|
|
}
|
|
|
|
jabber_conn_status_t
|
|
connection_register(const char* const altdomain, int port, const char* const tls_policy,
|
|
const char* const username, const char* const password)
|
|
{
|
|
check_expected(altdomain);
|
|
check_expected(port);
|
|
check_expected(tls_policy);
|
|
check_expected(username);
|
|
check_expected(password);
|
|
return mock_type(jabber_conn_status_t);
|
|
}
|
|
|
|
// message functions
|
|
char*
|
|
message_send_chat(const char* const barejid, const char* const msg, const char* const oob_url, gboolean request_receipt, const char* const replace_id)
|
|
{
|
|
check_expected(barejid);
|
|
check_expected(msg);
|
|
return NULL;
|
|
}
|
|
|
|
char*
|
|
message_send_chat_otr(const char* const barejid, const char* const msg, gboolean request_receipt, const char* const replace_id)
|
|
{
|
|
check_expected(barejid);
|
|
check_expected(msg);
|
|
return NULL;
|
|
}
|
|
|
|
char*
|
|
message_send_chat_pgp(const char* const barejid, const char* const msg, gboolean request_receipt, const char* const replace_id)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
char*
|
|
message_send_chat_ox(const char* const barejid, const char* const msg, gboolean request_receipt, const char* const replace_id)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
char*
|
|
message_send_private(const char* const fulljid, const char* const msg, const char* const oob_url)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
char*
|
|
message_send_groupchat(const char* const roomjid, const char* const msg, const char* const oob_url, const char* const replace_id)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
message_send_groupchat_subject(const char* const roomjid, const char* const subject)
|
|
{
|
|
}
|
|
|
|
void
|
|
message_send_inactive(const char* const barejid)
|
|
{
|
|
}
|
|
void
|
|
message_send_composing(const char* const barejid)
|
|
{
|
|
}
|
|
void
|
|
message_send_paused(const char* const barejid)
|
|
{
|
|
}
|
|
void
|
|
message_send_gone(const char* const barejid)
|
|
{
|
|
}
|
|
|
|
void
|
|
message_send_invite(const char* const room, const char* const contact,
|
|
const char* const reason)
|
|
{
|
|
}
|
|
|
|
void
|
|
message_request_voice(const char* const roomjid)
|
|
{
|
|
}
|
|
|
|
bool
|
|
message_is_sent_by_us(const ProfMessage* const message, bool checkOID)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
// presence functions
|
|
void
|
|
presence_subscription(const char* const jid, const jabber_subscr_t action)
|
|
{
|
|
}
|
|
|
|
GList*
|
|
presence_get_subscription_requests(void)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
gint
|
|
presence_sub_request_count(void)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
presence_reset_sub_request_search(void)
|
|
{
|
|
}
|
|
|
|
char*
|
|
presence_sub_request_find(const char* const search_str, gboolean previous, void* context)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
presence_join_room(const char* const room, const char* const nick, const char* const passwd)
|
|
{
|
|
check_expected(room);
|
|
check_expected(nick);
|
|
check_expected(passwd);
|
|
}
|
|
|
|
void
|
|
presence_change_room_nick(const char* const room, const char* const nick)
|
|
{
|
|
}
|
|
void
|
|
presence_leave_chat_room(const char* const room_jid)
|
|
{
|
|
}
|
|
|
|
void
|
|
presence_send(resource_presence_t status, int idle, char* signed_status)
|
|
{
|
|
check_expected(status);
|
|
check_expected(idle);
|
|
check_expected(signed_status);
|
|
}
|
|
|
|
gboolean
|
|
presence_sub_request_exists(const char* const bare_jid)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
// iq functions
|
|
void iq_disable_carbons() {};
|
|
void iq_enable_carbons() {};
|
|
void
|
|
iq_send_software_version(const char* const fulljid)
|
|
{
|
|
}
|
|
|
|
void
|
|
iq_room_list_request(const char* conferencejid, char* filter)
|
|
{
|
|
check_expected(conferencejid);
|
|
check_expected(filter);
|
|
}
|
|
|
|
void
|
|
iq_disco_info_request(const char* jid)
|
|
{
|
|
}
|
|
void
|
|
iq_disco_items_request(const char* jid)
|
|
{
|
|
}
|
|
void
|
|
iq_set_autoping(int seconds)
|
|
{
|
|
}
|
|
void
|
|
iq_http_upload_request(HTTPUpload* upload)
|
|
{
|
|
}
|
|
void
|
|
iq_confirm_instant_room(const char* const room_jid)
|
|
{
|
|
}
|
|
void
|
|
iq_destroy_room(const char* const room_jid)
|
|
{
|
|
}
|
|
void
|
|
iq_request_room_config_form(const char* const room_jid)
|
|
{
|
|
}
|
|
void
|
|
iq_submit_room_config(ProfConfWin* confwin)
|
|
{
|
|
}
|
|
void
|
|
iq_room_config_cancel(ProfConfWin* confwin)
|
|
{
|
|
}
|
|
void
|
|
iq_send_ping(const char* const target)
|
|
{
|
|
}
|
|
void
|
|
iq_send_caps_request(const char* const to, const char* const id,
|
|
const char* const node, const char* const ver)
|
|
{
|
|
}
|
|
void
|
|
iq_send_caps_request_for_jid(const char* const to, const char* const id,
|
|
const char* const node, const char* const ver)
|
|
{
|
|
}
|
|
void
|
|
iq_send_caps_request_legacy(const char* const to, const char* const id,
|
|
const char* const node, const char* const ver)
|
|
{
|
|
}
|
|
void
|
|
iq_room_info_request(const char* const room, gboolean display)
|
|
{
|
|
}
|
|
void
|
|
iq_room_affiliation_list(const char* const room, char* affiliation, bool show)
|
|
{
|
|
}
|
|
void
|
|
iq_room_affiliation_set(const char* const room, const char* const jid, char* affiliation,
|
|
const char* const reason)
|
|
{
|
|
}
|
|
void
|
|
iq_room_kick_occupant(const char* const room, const char* const nick, const char* const reason)
|
|
{
|
|
}
|
|
void
|
|
iq_room_role_set(const char* const room, const char* const nick, char* role,
|
|
const char* const reason)
|
|
{
|
|
}
|
|
void
|
|
iq_room_role_list(const char* const room, char* role)
|
|
{
|
|
}
|
|
void
|
|
iq_last_activity_request(const char* jid)
|
|
{
|
|
}
|
|
void
|
|
iq_autoping_timer_cancel(void)
|
|
{
|
|
}
|
|
void
|
|
iq_autoping_check(void)
|
|
{
|
|
}
|
|
void
|
|
iq_rooms_cache_clear(void)
|
|
{
|
|
}
|
|
void
|
|
iq_command_list(const char* const target)
|
|
{
|
|
}
|
|
void
|
|
iq_command_exec(const char* const target, const char* const command)
|
|
{
|
|
}
|
|
void
|
|
iq_register_change_password(const char* const user, const char* const password)
|
|
{
|
|
}
|
|
|
|
void
|
|
iq_muc_register_nick(const char* const roomjid)
|
|
{
|
|
}
|
|
|
|
void
|
|
iq_mam_request(ProfChatWin* win, GDateTime* enddate)
|
|
{
|
|
}
|
|
|
|
void
|
|
iq_feature_retrieval_complete_handler(void)
|
|
{
|
|
}
|
|
|
|
void
|
|
publish_user_mood(const char* const mood, const char* const text)
|
|
{
|
|
}
|
|
|
|
// caps functions
|
|
void
|
|
caps_add_feature(char* feature)
|
|
{
|
|
}
|
|
|
|
void
|
|
caps_remove_feature(char* feature)
|
|
{
|
|
}
|
|
|
|
EntityCapabilities*
|
|
caps_lookup(const char* const jid)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
caps_destroy(EntityCapabilities* caps)
|
|
{
|
|
}
|
|
|
|
void
|
|
caps_reset_ver(void)
|
|
{
|
|
}
|
|
|
|
gboolean
|
|
caps_jid_has_feature(const char* const jid, const char* const feature)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
gboolean
|
|
bookmark_add(const char* jid, const char* nick, const char* password, const char* autojoin_str, const char* name)
|
|
{
|
|
check_expected(jid);
|
|
check_expected(nick);
|
|
check_expected(password);
|
|
check_expected(autojoin_str);
|
|
return mock_type(gboolean);
|
|
}
|
|
|
|
gboolean
|
|
bookmark_update(const char* jid, const char* nick, const char* password, const char* autojoin_str, const char* name)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
gboolean
|
|
bookmark_remove(const char* jid)
|
|
{
|
|
check_expected(jid);
|
|
return mock_type(gboolean);
|
|
}
|
|
|
|
gboolean
|
|
bookmark_join(const char* jid)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
GList*
|
|
bookmark_get_list(void)
|
|
{
|
|
return mock_ptr_type(GList*);
|
|
}
|
|
|
|
Bookmark*
|
|
bookmark_get_by_jid(const char* jid)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
char*
|
|
bookmark_find(const char* const search_str, gboolean previous, void* context)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
bookmark_autocomplete_reset(void)
|
|
{
|
|
}
|
|
|
|
void
|
|
roster_send_name_change(const char* const barejid, const char* const new_name, GSList* groups)
|
|
{
|
|
check_expected(barejid);
|
|
check_expected(new_name);
|
|
check_expected(groups);
|
|
}
|
|
|
|
gboolean
|
|
bookmark_exists(const char* const room)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
void
|
|
roster_send_add_to_group(const char* const group, PContact contact)
|
|
{
|
|
}
|
|
void
|
|
roster_send_remove_from_group(const char* const group, PContact contact)
|
|
{
|
|
}
|
|
|
|
void
|
|
roster_send_add_new(const char* const barejid, const char* const name)
|
|
{
|
|
check_expected(barejid);
|
|
check_expected(name);
|
|
}
|
|
|
|
void
|
|
roster_send_remove(const char* const barejid)
|
|
{
|
|
check_expected(barejid);
|
|
}
|
|
|
|
GList*
|
|
blocked_list(void)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
gboolean
|
|
blocked_add(char* jid, blocked_report reportkind, const char* const message)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
blocked_remove(char* jid)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
char*
|
|
blocked_ac_find(const char* const search_str, gboolean previous, void* context)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
blocked_ac_reset(void)
|
|
{
|
|
}
|