Compare commits
35 Commits
feat/no-db
...
feat/no-db
| Author | SHA1 | Date | |
|---|---|---|---|
|
bdbd588d90
|
|||
|
26db671a38
|
|||
|
2990ec795a
|
|||
|
b8a4900393
|
|||
|
3daee1216b
|
|||
|
024e4b2336
|
|||
|
09b2295414
|
|||
|
16aa569329
|
|||
|
48b89460b7
|
|||
|
a9b181beb7
|
|||
|
52f97f7195
|
|||
|
352eb26f71
|
|||
|
875f8f4100
|
|||
|
a7da9e2add
|
|||
|
1fe3808b23
|
|||
|
555faaa232
|
|||
|
b1d820462a
|
|||
|
8868d58920
|
|||
|
7ba7e53291
|
|||
|
a96a4ad41e
|
|||
|
58002409ff
|
|||
|
2b1b70665b
|
|||
|
0d38a0a2cb
|
|||
|
71f9ab5007
|
|||
|
06463b75b4
|
|||
|
507ef91b5f
|
|||
|
9f3020e40a
|
|||
|
7e320ccdb0
|
|||
|
b5fe714a89
|
|||
|
1f3d3117bf
|
|||
|
23723376c6
|
|||
|
16ae7fb85c
|
|||
|
d25f90899c
|
|||
|
89b90e5cf2
|
|||
|
31e8c30c34
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,8 +17,6 @@ compile_commands.json
|
||||
.project
|
||||
.settings/
|
||||
.vscode/
|
||||
.kilo/
|
||||
.roo/
|
||||
*.plist/
|
||||
|
||||
# autotools
|
||||
|
||||
@@ -37,7 +37,6 @@ core_sources = \
|
||||
src/ui/window_list.c src/ui/window_list.h \
|
||||
src/ui/rosterwin.c src/ui/occupantswin.c \
|
||||
src/ui/buffer.c src/ui/buffer.h \
|
||||
src/ui/aiwin.c \
|
||||
src/ui/chatwin.c \
|
||||
src/ui/mucwin.c \
|
||||
src/ui/privwin.c \
|
||||
@@ -79,8 +78,7 @@ core_sources = \
|
||||
src/plugins/themes.c src/plugins/themes.h \
|
||||
src/plugins/settings.c src/plugins/settings.h \
|
||||
src/plugins/disco.c src/plugins/disco.h \
|
||||
src/ui/tray.h src/ui/tray.c \
|
||||
src/ai/ai_client.h src/ai/ai_client.c
|
||||
src/ui/tray.h src/ui/tray.c
|
||||
|
||||
unittest_sources = \
|
||||
src/xmpp/contact.c src/xmpp/contact.h src/common.c \
|
||||
@@ -91,7 +89,6 @@ unittest_sources = \
|
||||
src/xmpp/chat_state.h src/xmpp/chat_state.c \
|
||||
src/xmpp/roster_list.c src/xmpp/roster_list.h \
|
||||
src/xmpp/xmpp.h src/xmpp/form.c \
|
||||
src/ai/ai_client.h src/ai/ai_client.c \
|
||||
src/ui/ui.h \
|
||||
src/otr/otr.h \
|
||||
src/pgp/gpg.h \
|
||||
@@ -141,7 +138,6 @@ unittest_sources = \
|
||||
tests/unittests/xmpp/stub_message.c \
|
||||
tests/unittests/ui/stub_ui.c tests/unittests/ui/stub_ui.h \
|
||||
tests/unittests/ui/stub_vcardwin.c \
|
||||
tests/unittests/ui/stub_ai.c \
|
||||
tests/unittests/log/stub_log.c \
|
||||
tests/unittests/chatlog/stub_chatlog.c \
|
||||
tests/unittests/database/stub_database.c \
|
||||
@@ -178,7 +174,6 @@ unittest_sources = \
|
||||
tests/unittests/test_callbacks.c tests/unittests/test_callbacks.h \
|
||||
tests/unittests/test_plugins_disco.c tests/unittests/test_plugins_disco.h \
|
||||
tests/unittests/test_forced_encryption.c tests/unittests/test_forced_encryption.h \
|
||||
tests/unittests/test_ai_client.c tests/unittests/test_ai_client.h \
|
||||
tests/unittests/test_database_export.c tests/unittests/test_database_export.h \
|
||||
tests/unittests/test_database_stress.c tests/unittests/test_database_stress.h \
|
||||
tests/unittests/unittests.c
|
||||
@@ -203,7 +198,6 @@ functionaltest_sources = \
|
||||
tests/functionaltests/test_autoping.c tests/functionaltests/test_autoping.h \
|
||||
tests/functionaltests/test_disco.c tests/functionaltests/test_disco.h \
|
||||
tests/functionaltests/test_export_import.c tests/functionaltests/test_export_import.h \
|
||||
tests/functionaltests/test_ai.c tests/functionaltests/test_ai.h \
|
||||
tests/functionaltests/functionaltests.c
|
||||
|
||||
main_source = src/main.c
|
||||
|
||||
@@ -204,13 +204,6 @@ case "$ARCH" in
|
||||
""
|
||||
)
|
||||
source /etc/profile.d/debuginfod.sh 2>/dev/null || true
|
||||
|
||||
if grep -q 'ID=arch' /etc/os-release 2>/dev/null && [ -f /etc/makepkg.conf ]; then
|
||||
echo "--> [Parity Mode] Simulating Pikaur collision..."
|
||||
set -a
|
||||
source /etc/makepkg.conf
|
||||
set +a
|
||||
fi
|
||||
;;
|
||||
darwin*)
|
||||
# 4 configurations for parallel CI
|
||||
|
||||
@@ -396,11 +396,12 @@ AC_SUBST([FORKPTY_LIB])
|
||||
## Default parameters
|
||||
AM_CFLAGS="$AM_CFLAGS -Wall -Wextra -Wformat=2 -Wno-format-zero-length"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wno-deprecated-declarations -Wno-unused-parameter -Wno-missing-field-initializers -Wno-sign-compare -Wno-cast-function-type"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wpointer-arith"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wnull-dereference -Wpointer-arith"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wimplicit-function-declaration"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wundef"
|
||||
AM_CFLAGS="$AM_CFLAGS -Wfloat-equal -Wredundant-decls"
|
||||
AM_CFLAGS="$AM_CFLAGS -fstack-protector-strong -fno-common"
|
||||
AM_CFLAGS="$AM_CFLAGS -D_FORTIFY_SOURCE=2"
|
||||
AM_CFLAGS="$AM_CFLAGS -std=gnu99 -ggdb3"
|
||||
|
||||
# GCC-specific warnings (not supported by clang) — test each one
|
||||
|
||||
1574
src/ai/ai_client.c
1574
src/ai/ai_client.c
File diff suppressed because it is too large
Load Diff
@@ -1,291 +0,0 @@
|
||||
#ifndef AI_CLIENT_H
|
||||
#define AI_CLIENT_H
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
/**
|
||||
* @brief AI message structure for conversation history.
|
||||
*/
|
||||
typedef struct ai_message_t
|
||||
{
|
||||
gchar* role; /* "user" or "assistant" */
|
||||
gchar* content; /* Message content */
|
||||
} AIMessage;
|
||||
|
||||
/**
|
||||
* @brief AI provider configuration.
|
||||
*/
|
||||
typedef struct ai_provider_t
|
||||
{
|
||||
gchar* name; /* Provider name (e.g., "openai", "perplexity") */
|
||||
gchar* api_url; /* API endpoint URL */
|
||||
gchar* project_id; /* Optional project ID (for some providers) */
|
||||
gchar* default_model; /* Default model for this provider */
|
||||
GHashTable* settings; /* Extensible per-provider settings (e.g., tools=enabled, search=disabled) */
|
||||
GList* models; /* Cached models (gchar*) */
|
||||
gboolean models_fresh; /* Whether models cache is current */
|
||||
gint32 ref_count; /* Reference count (atomic) */
|
||||
} AIProvider;
|
||||
|
||||
/**
|
||||
* @brief AI chat session structure.
|
||||
*/
|
||||
typedef struct ai_session_t
|
||||
{
|
||||
pthread_mutex_t lock; /* Protects all session fields below */
|
||||
gchar* provider_name; /* Provider name */
|
||||
AIProvider* provider; /* Provider configuration */
|
||||
gchar* model; /* Model identifier (e.g., "gpt-4", "sonar") */
|
||||
gchar* api_key; /* API key for this session */
|
||||
GList* history; /* Conversation history (GList of AIMessage*) */
|
||||
gint32 ref_count; /* Reference count (atomic) */
|
||||
} AISession;
|
||||
|
||||
/* ========================================================================
|
||||
* Provider Management
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Initialize the AI client and load default providers.
|
||||
*/
|
||||
void ai_client_init(void);
|
||||
|
||||
/**
|
||||
* Shutdown the AI client and free resources.
|
||||
*/
|
||||
void ai_client_shutdown(void);
|
||||
|
||||
/**
|
||||
* Get a provider by name.
|
||||
* @param name The provider name (e.g., "openai", "perplexity")
|
||||
* @return AIProvider*, or NULL if not found
|
||||
*/
|
||||
AIProvider* ai_get_provider(const gchar* name);
|
||||
|
||||
/**
|
||||
* Add or update a provider configuration.
|
||||
* @param name The provider name
|
||||
* @param api_url The API endpoint URL
|
||||
* @return New AIProvider* (caller must unref when done)
|
||||
*/
|
||||
AIProvider* ai_add_provider(const gchar* name, const gchar* api_url);
|
||||
|
||||
/**
|
||||
* Remove a provider by name.
|
||||
* @param name The provider name
|
||||
* @return TRUE if provider was removed, FALSE if not found
|
||||
*/
|
||||
gboolean ai_remove_provider(const gchar* name);
|
||||
|
||||
/**
|
||||
* Increment the reference count of a provider.
|
||||
* @param provider The provider to reference
|
||||
* @return The same provider pointer
|
||||
*/
|
||||
AIProvider* ai_provider_ref(AIProvider* provider);
|
||||
|
||||
/**
|
||||
* Decrement the reference count of a provider.
|
||||
* @param provider The provider to unreference
|
||||
*/
|
||||
void ai_provider_unref(AIProvider* provider);
|
||||
|
||||
/**
|
||||
* List all configured providers.
|
||||
* @return GList of AIProvider* (caller must not free the list or providers,
|
||||
* and must not unref the returned providers)
|
||||
*/
|
||||
GList* ai_list_providers(void);
|
||||
|
||||
/**
|
||||
* Find a provider name for autocomplete.
|
||||
* @param search_str The search string
|
||||
* @param previous Whether to go to previous match
|
||||
* @param context Unused
|
||||
* @return Provider name, or NULL if not found
|
||||
*/
|
||||
gchar* ai_providers_find(const char* const search_str, gboolean previous, void* context);
|
||||
|
||||
/**
|
||||
* Reset the provider autocomplete state.
|
||||
* Called from cmd_ac_reset() to clear autocomplete state.
|
||||
*/
|
||||
void ai_providers_reset_ac(void);
|
||||
|
||||
/**
|
||||
* Get the API key for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @return The API key, or NULL if not set (caller must free)
|
||||
*/
|
||||
gchar* ai_get_provider_key(const gchar* provider_name);
|
||||
|
||||
/**
|
||||
* Set the API key for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @param api_key The API key to set
|
||||
*/
|
||||
void ai_set_provider_key(const gchar* provider_name, const gchar* api_key);
|
||||
|
||||
/**
|
||||
* Set the default model for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @param model The default model name
|
||||
*/
|
||||
void ai_set_provider_default_model(const gchar* provider_name, const gchar* model);
|
||||
|
||||
/**
|
||||
* Get the default model for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @return The default model name (caller must not free), or NULL if not set
|
||||
*/
|
||||
const gchar* ai_get_provider_default_model(const gchar* provider_name);
|
||||
|
||||
/**
|
||||
* Set a custom setting for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @param setting The setting key (e.g., "tools", "search")
|
||||
* @param value The setting value
|
||||
*/
|
||||
void ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const gchar* value);
|
||||
|
||||
/**
|
||||
* Get a custom setting for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @param setting The setting key
|
||||
* @return The setting value (caller must free), or NULL if not set
|
||||
*/
|
||||
gchar* ai_get_provider_setting(const gchar* provider_name, const gchar* setting);
|
||||
|
||||
/**
|
||||
* Fetch available models from a provider's API.
|
||||
* @param provider_name The provider name
|
||||
* @param user_data User data (ProfAiWin* for UI display)
|
||||
* @return TRUE if the request was successfully queued, FALSE otherwise
|
||||
*/
|
||||
gboolean ai_fetch_models(const gchar* provider_name, gpointer user_data);
|
||||
|
||||
/**
|
||||
* Check if models cache is fresh for a provider.
|
||||
* @param provider_name The provider name
|
||||
* @return TRUE if models are fresh, FALSE otherwise
|
||||
*/
|
||||
gboolean ai_models_are_fresh(const gchar* provider_name);
|
||||
|
||||
/* ========================================================================
|
||||
* Parsing helpers (exposed for testing)
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Parse model IDs from an OpenAI-compatible API response.
|
||||
* Expected format: {"object":"list","data":[{"id":"model1",...},...]}
|
||||
* @param provider The provider to add models to
|
||||
* @param json The JSON response from the API
|
||||
*/
|
||||
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
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Create a new AI session with the specified provider and model.
|
||||
* @param provider_name The provider name (e.g., "openai")
|
||||
* @param model The model identifier (e.g., "gpt-4")
|
||||
* @return New AISession*, or NULL on failure
|
||||
*/
|
||||
AISession* ai_session_create(const gchar* provider_name, const gchar* model);
|
||||
|
||||
/**
|
||||
* Increment the reference count of an AI session.
|
||||
* @param session The session to reference
|
||||
* @return The same session pointer
|
||||
*/
|
||||
AISession* ai_session_ref(AISession* session);
|
||||
|
||||
/**
|
||||
* Decrement the reference count and free the session when it reaches zero.
|
||||
* @param session The session to unreference
|
||||
*/
|
||||
void ai_session_unref(AISession* session);
|
||||
|
||||
/**
|
||||
* Add a message to the session history.
|
||||
* @param session The session
|
||||
* @param role The message role ("user" or "assistant")
|
||||
* @param content The message content
|
||||
*/
|
||||
void ai_session_add_message(AISession* session, const gchar* role, const gchar* content);
|
||||
|
||||
/**
|
||||
* Clear the conversation history.
|
||||
* @param session The session
|
||||
*/
|
||||
void ai_session_clear_history(AISession* session);
|
||||
|
||||
/**
|
||||
* Get the current model for a session.
|
||||
* @param session The session
|
||||
* @return The model name (caller must not free)
|
||||
*/
|
||||
const gchar* ai_session_get_model(AISession* session);
|
||||
|
||||
/**
|
||||
* Set the model for a session.
|
||||
* @param session The session
|
||||
* @param model The model name
|
||||
*/
|
||||
void ai_session_set_model(AISession* session, const gchar* model);
|
||||
|
||||
/**
|
||||
* Atomically switch session provider, model, and API key.
|
||||
* All mutations happen under the session lock to prevent races with
|
||||
* _ai_request_thread() which snapshots session state before making requests.
|
||||
*
|
||||
* @param session The session
|
||||
* @param provider_name New provider name
|
||||
* @param model New model identifier
|
||||
* @param api_key New API key (caller must free after calling this)
|
||||
*/
|
||||
void ai_session_switch(AISession* session, const gchar* provider_name,
|
||||
const gchar* model, gchar* api_key);
|
||||
|
||||
/* ========================================================================
|
||||
* Request Handling
|
||||
* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Send a prompt to the AI provider asynchronously.
|
||||
* @param session The AI session containing provider and model
|
||||
* @param prompt The prompt to send
|
||||
* @param user_data User data (ProfAiWin* for UI display)
|
||||
* @return TRUE if the request was successfully queued, FALSE otherwise
|
||||
*/
|
||||
gboolean ai_send_prompt(AISession* session, const gchar* prompt,
|
||||
gpointer user_data);
|
||||
|
||||
/**
|
||||
* Escape a string for JSON embedding.
|
||||
* @param str The string to escape
|
||||
* @return Newly allocated escaped string (caller must free)
|
||||
*/
|
||||
gchar* ai_json_escape(const gchar* str);
|
||||
|
||||
#endif /* AI_CLIENT_H */
|
||||
@@ -66,8 +66,6 @@
|
||||
#include "omemo/omemo.h"
|
||||
#endif
|
||||
|
||||
#include "ai/ai_client.h"
|
||||
|
||||
static char* _sub_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _notify_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _theme_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
@@ -139,7 +137,6 @@ static char* _strophe_autocomplete(ProfWin* window, const char* const input, gbo
|
||||
static char* _adhoc_cmd_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _force_encryption_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
|
||||
static char* _script_autocomplete_func(const char* const prefix, gboolean previous, void* context);
|
||||
|
||||
@@ -283,16 +280,11 @@ static Autocomplete history_switch_ac;
|
||||
static Autocomplete color_ac;
|
||||
static Autocomplete correction_ac;
|
||||
static Autocomplete avatar_ac;
|
||||
static Autocomplete ai_subcommands_ac;
|
||||
static Autocomplete ai_set_subcommands_ac;
|
||||
static Autocomplete ai_set_custom_subcommands_ac;
|
||||
static Autocomplete ai_remove_subcommands_ac;
|
||||
static Autocomplete url_ac;
|
||||
static Autocomplete executable_ac;
|
||||
static Autocomplete executable_param_ac;
|
||||
static Autocomplete intype_ac;
|
||||
static Autocomplete mood_ac;
|
||||
static Autocomplete ai_models_ac;
|
||||
static Autocomplete mood_type_ac;
|
||||
static Autocomplete strophe_ac;
|
||||
static Autocomplete strophe_sm_ac;
|
||||
@@ -464,12 +456,7 @@ static Autocomplete* all_acs[] = {
|
||||
&vcard_togglable_param_ac,
|
||||
&vcard_address_type_ac,
|
||||
&force_encryption_ac,
|
||||
&force_encryption_policy_ac,
|
||||
&ai_subcommands_ac,
|
||||
&ai_set_subcommands_ac,
|
||||
&ai_set_custom_subcommands_ac,
|
||||
&ai_remove_subcommands_ac,
|
||||
&ai_models_ac
|
||||
&force_encryption_policy_ac
|
||||
};
|
||||
|
||||
static GHashTable* ac_funcs = NULL;
|
||||
@@ -1132,7 +1119,6 @@ cmd_ac_init(void)
|
||||
autocomplete_add(statusbar_show_ac, "name");
|
||||
autocomplete_add(statusbar_show_ac, "number");
|
||||
autocomplete_add(statusbar_show_ac, "read");
|
||||
autocomplete_add(statusbar_show_ac, "dbbackend");
|
||||
|
||||
autocomplete_add(statusbar_tabmode_ac, "actlist");
|
||||
autocomplete_add(statusbar_tabmode_ac, "dynamic");
|
||||
@@ -1147,6 +1133,9 @@ cmd_ac_init(void)
|
||||
autocomplete_add(status_state_ac, "xa");
|
||||
autocomplete_add(status_state_ac, "dnd");
|
||||
|
||||
autocomplete_add(logging_ac, "chat");
|
||||
autocomplete_add(logging_ac, "group");
|
||||
|
||||
autocomplete_add(privacy_ac, "logging");
|
||||
autocomplete_add(privacy_ac, "os");
|
||||
|
||||
@@ -1180,26 +1169,6 @@ cmd_ac_init(void)
|
||||
autocomplete_add(correction_ac, "off");
|
||||
autocomplete_add(correction_ac, "char");
|
||||
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "set", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "remove", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "start", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "clear", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "providers", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "switch", FALSE);
|
||||
autocomplete_add_unsorted(ai_subcommands_ac, "models", FALSE);
|
||||
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "provider", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "token", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "default-model", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_subcommands_ac, "custom", FALSE);
|
||||
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "tools", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "search", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "memory", FALSE);
|
||||
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "plugins", FALSE);
|
||||
|
||||
autocomplete_add_unsorted(ai_remove_subcommands_ac, "provider", FALSE);
|
||||
|
||||
autocomplete_add(avatar_ac, "set");
|
||||
autocomplete_add(avatar_ac, "disable");
|
||||
autocomplete_add(avatar_ac, "get");
|
||||
@@ -1475,7 +1444,6 @@ cmd_ac_init(void)
|
||||
g_hash_table_insert(ac_funcs, "/wins", _wins_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/wintitle", _wintitle_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/force-encryption", _force_encryption_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/ai", _ai_autocomplete);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1694,7 +1662,6 @@ cmd_ac_reset(ProfWin* window)
|
||||
win_reset_search_attempts();
|
||||
win_close_reset_search_attempts();
|
||||
plugins_reset_autocomplete();
|
||||
ai_providers_reset_ac();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4077,6 +4044,11 @@ _logging_autocomplete(ProfWin* window, const char* const input, gboolean previou
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_func(input, "/logging chat", prefs_autocomplete_boolean_choice, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/logging group", logging_group_ac, TRUE, previous);
|
||||
return result;
|
||||
}
|
||||
@@ -4478,163 +4450,4 @@ _force_encryption_autocomplete(ProfWin* window, const char* const input, gboolea
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/force-encryption", force_encryption_ac, TRUE, previous);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Forward declaration */
|
||||
static char* _ai_provider_and_model_autocomplete(ProfWin* window, const char* const input,
|
||||
gboolean previous, const char* cmd);
|
||||
|
||||
static char*
|
||||
_ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
char* result = NULL;
|
||||
|
||||
/* Parse once for reuse - /ai <subcommand> [<arg1>] [<arg2>] */
|
||||
gboolean parse_result = FALSE;
|
||||
auto_gcharv gchar** args = parse_args(input, 1, 4, &parse_result);
|
||||
int num_args = g_strv_length(args);
|
||||
|
||||
/* Top-level /ai <subcommand> autocomplete (e.g., /ai s<tab> -> /ai set) */
|
||||
result = autocomplete_param_with_func(input, "/ai set provider", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set token <provider> <token> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set token", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set default-model <provider> <model> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set default-model", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set custom <provider> <setting> <value> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set custom", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set custom <provider> <setting> - autocomplete settings
|
||||
/* args[0]="set", args[1]="custom", args[2]=provider (if typed), args[3]=setting (if typed) */
|
||||
if (num_args == 3 && g_strcmp0(args[1], "custom") == 0) {
|
||||
/* /ai set custom <provider> - check if provider is valid */
|
||||
if (ai_get_provider(args[2])) {
|
||||
/* Valid provider, try settings autocomplete */
|
||||
result = autocomplete_param_with_ac(input, "/ai set custom ", ai_set_custom_subcommands_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
// /ai set <subcommand> - autocomplete subcommands
|
||||
result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai remove provider <name> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai remove provider", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/ai remove", ai_remove_subcommands_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai start <provider> <model> - autocomplete provider names and model names
|
||||
result = _ai_provider_and_model_autocomplete(window, input, previous, "/ai start");
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai switch <provider> <model> - autocomplete provider names and model names
|
||||
result = _ai_provider_and_model_autocomplete(window, input, previous, "/ai switch");
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai set default-model <provider> <model> - autocomplete provider names and model names
|
||||
result = _ai_provider_and_model_autocomplete(window, input, previous, "/ai set default-model");
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai models <provider> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai models", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// /ai clear - no autocomplete
|
||||
// /ai providers - no autocomplete
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/ai", ai_subcommands_ac, FALSE, previous);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Autocomplete provider names and model names for /ai <cmd> <provider> <model> patterns.
|
||||
* First tries to autocomplete provider names, then model names if provider is valid.
|
||||
* Uses static ai_models_ac to preserve cycling state (last_found) across calls.
|
||||
* The cmd_prefix should NOT include trailing space (e.g., "/ai start" not "/ai start ").
|
||||
*/
|
||||
static char*
|
||||
_ai_provider_and_model_autocomplete(ProfWin* window, const char* const input, gboolean previous, const char* cmd)
|
||||
{
|
||||
char* result;
|
||||
|
||||
/* First, try provider name autocomplete */
|
||||
result = autocomplete_param_with_func(input, cmd, ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Provider was specified, try model name autocomplete */
|
||||
/* Build the full command prefix with space */
|
||||
auto_gchar gchar* cmd_prefix = g_strdup_printf("%s ", cmd);
|
||||
|
||||
if (!g_str_has_prefix(input, cmd_prefix)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Extract provider name from input (after cmd_prefix) */
|
||||
auto_gchar gchar* rest = g_strdup(input + strlen(cmd_prefix));
|
||||
char* space = strchr(rest, ' ');
|
||||
if (space) {
|
||||
*space = '\0';
|
||||
}
|
||||
|
||||
/* Look up the provider by name and get its models */
|
||||
AIProvider* prov = ai_get_provider(rest);
|
||||
if (!prov || !prov->models) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!ai_models_ac) {
|
||||
ai_models_ac = autocomplete_new();
|
||||
}
|
||||
/* Convert GList* to char** for autocomplete_update */
|
||||
int model_count = g_list_length(prov->models);
|
||||
char** model_array = g_new0(char*, model_count + 1);
|
||||
GList* curr = prov->models;
|
||||
int i = 0;
|
||||
while (curr) {
|
||||
model_array[i++] = curr->data;
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
|
||||
autocomplete_update(ai_models_ac, model_array);
|
||||
g_free(model_array);
|
||||
|
||||
auto_gchar gchar* full_prefix = g_strdup_printf("%s%s", cmd_prefix, rest);
|
||||
|
||||
result = autocomplete_param_with_ac(input, full_prefix, ai_models_ac, TRUE, previous);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1289,8 +1289,8 @@ static const struct cmd_t command_defs[] = {
|
||||
CMD_TAGS(
|
||||
CMD_TAG_UI)
|
||||
CMD_SYN(
|
||||
"/statusbar show name|number|read|dbbackend",
|
||||
"/statusbar hide name|number|read|dbbackend",
|
||||
"/statusbar show name|number|read",
|
||||
"/statusbar hide name|number|read",
|
||||
"/statusbar maxtabs <value>",
|
||||
"/statusbar tablen <value>",
|
||||
"/statusbar tabmode default|dynamic|actlist",
|
||||
@@ -1308,7 +1308,6 @@ static const struct cmd_t command_defs[] = {
|
||||
{ "show|hide name", "Show or hide names in tabs." },
|
||||
{ "show|hide number", "Show or hide numbers in tabs." },
|
||||
{ "show|hide read", "Show or hide inactive tabs." },
|
||||
{ "show|hide dbbackend", "Show or hide the database backend indicator [sqlite] or [flatfile]." },
|
||||
{ "self user|barejid|fulljid", "Show account user name, barejid, fulljid as status bar title." },
|
||||
{ "self off", "Disable showing self as status bar title." },
|
||||
{ "chat user|jid", "Show users name, or fulljid. Change needs a redraw/restart to take effect." },
|
||||
@@ -1660,14 +1659,23 @@ static const struct cmd_t command_defs[] = {
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/logging",
|
||||
parse_args, 0, 0, NULL)
|
||||
parse_args, 2, 3, &cons_logging_setting)
|
||||
CMD_MAINFUNC(cmd_logging)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_CHAT)
|
||||
CMD_SYN(
|
||||
"/logging")
|
||||
"/logging chat|group on|off")
|
||||
CMD_DESC(
|
||||
"Deprecated. Use '/history' instead.")
|
||||
"Configure chat logging. "
|
||||
"Switch logging on or off. "
|
||||
"Chat logging will be enabled if /history is set to on. "
|
||||
"When disabling this option, /history will also be disabled. ")
|
||||
CMD_ARGS(
|
||||
{ "chat on|off", "Enable/Disable regular chat logging." },
|
||||
{ "group on|off", "Enable/Disable groupchat (room) logging." })
|
||||
CMD_EXAMPLES(
|
||||
"/logging chat on",
|
||||
"/logging group off")
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/states",
|
||||
@@ -1880,16 +1888,15 @@ static const struct cmd_t command_defs[] = {
|
||||
"/history export [<jid>]",
|
||||
"/history import [<jid>]")
|
||||
CMD_DESC(
|
||||
"Enable or disable chat history. "
|
||||
"When on, messages are persisted via the active database backend and shown in chat windows. "
|
||||
"When off, persistence stops and old history is hidden on chat-window open. "
|
||||
"Switch chat history on or off, /logging chat will automatically be enabled when this setting is on. "
|
||||
"When history is enabled, previous messages are shown in chat windows. "
|
||||
"Use 'backend' to show the active database backend. "
|
||||
"Use 'switch' to change the active database backend at runtime without reconnecting. "
|
||||
"Use 'verify' to check integrity of stored message history. "
|
||||
"Use 'export' to copy messages from SQLite to flat-file format, or 'import' to copy from flat-file to SQLite. "
|
||||
"Both export and import merge with existing data (duplicates are skipped).")
|
||||
CMD_ARGS(
|
||||
{ "on|off", "Enable or disable persistence and showing chat history." },
|
||||
{ "on|off", "Enable or disable showing chat history." },
|
||||
{ "backend", "Show the name of the active database backend." },
|
||||
{ "switch sqlite|flatfile", "Switch the active database backend at runtime." },
|
||||
{ "verify [<jid>]", "Verify integrity of message history. Optionally specify a JID to check only one contact." },
|
||||
@@ -2734,7 +2741,7 @@ static const struct cmd_t command_defs[] = {
|
||||
"clientid to set the client identification name "
|
||||
"session_alarm to configure an alarm when more clients log in.")
|
||||
CMD_ARGS(
|
||||
{ "logging on|redact|off|flatfile", "Switch chat logging backend. 'on' uses the SQLite database (default). 'flatfile' stores messages as plain-text files under ~/.local/share/profanity/flatlog/ that can be edited with any text editor. 'off' disables logging entirely. 'redact' keeps logging but replaces every message body with '[REDACTED]'. Note: 'off' may have side effects such as not being able to decrypt OMEMO messages received later via MAM. Backend changes ('on' / 'flatfile') take effect immediately when connected, otherwise on next connect." },
|
||||
{ "logging on|redact|off|flatfile", "Switch chat logging. 'on' uses SQLite database (default). 'flatfile' stores messages as plain text files in ~/.local/share/profanity/flatlog/ that can be manually edited with any text editor. 'off' disables logging entirely. 'redact' stores messages with content replaced by '[redacted]'. Note: 'off' might have unintended consequences, such as not being able to decrypt OMEMO encrypted messages received later via MAM. The 'flatfile' setting takes effect on next connection." },
|
||||
{ "os on|off", "Choose whether to include the OS name if a user asks for software information (XEP-0092)." }
|
||||
)
|
||||
CMD_EXAMPLES(
|
||||
@@ -2780,67 +2787,6 @@ static const struct cmd_t command_defs[] = {
|
||||
"/force-encryption policy block")
|
||||
},
|
||||
|
||||
{ CMD_PREAMBLE("/ai",
|
||||
parse_args, 0, 5, NULL)
|
||||
CMD_SUBFUNCS(
|
||||
{ "set", cmd_ai_set },
|
||||
{ "remove", cmd_ai_remove },
|
||||
{ "start", cmd_ai_start },
|
||||
{ "clear", cmd_ai_clear },
|
||||
{ "providers", cmd_ai_providers },
|
||||
{ "switch", cmd_ai_switch },
|
||||
{ "models", cmd_ai_models })
|
||||
CMD_MAINFUNC(cmd_ai)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_CHAT)
|
||||
CMD_SYN(
|
||||
"/ai",
|
||||
"/ai set provider <name> <url>",
|
||||
"/ai set token <provider> <token>",
|
||||
"/ai set default-model <provider> <model>",
|
||||
"/ai set custom <provider> <setting> <value>",
|
||||
"/ai remove provider <name>",
|
||||
"/ai providers",
|
||||
"/ai start [<provider>] [<model>]",
|
||||
"/ai switch <provider> [<model>]",
|
||||
"/ai switch <model>",
|
||||
"/ai models <provider>",
|
||||
"/ai clear")
|
||||
CMD_DESC(
|
||||
"Interact with AI models via OpenAI-compatible APIs. "
|
||||
"Supports multiple providers (openai, perplexity, custom). "
|
||||
"Each provider has its own API key, endpoint, default model, and settings. "
|
||||
"Chat history is maintained per session and not persisted locally.")
|
||||
CMD_ARGS(
|
||||
{ "", "Display current AI settings and configured providers" },
|
||||
{ "set provider <name> <url>", "Add or update a provider with custom API endpoint" },
|
||||
{ "set token <provider> <token>", "Set API token for a provider (e.g., openai, perplexity)" },
|
||||
{ "set default-model <provider> <model>", "Set default model for a provider" },
|
||||
{ "set custom <provider> <setting> <value>", "Set provider-level setting (e.g., tools, search)" },
|
||||
{ "remove provider <name>", "Remove a custom provider" },
|
||||
{ "providers", "List configured providers with full details" },
|
||||
{ "start <provider> [<model>]", "Start new AI chat (space-separated, uses defaults if omitted)" },
|
||||
{ "switch <provider> [<model>]", "Switch to different provider (and optionally model)" },
|
||||
{ "models <provider>", "Fetch and display available models for provider" },
|
||||
{ "clear", "Clear current chat history" })
|
||||
CMD_EXAMPLES(
|
||||
"/ai",
|
||||
"/ai set token openai sk-xxx",
|
||||
"/ai set token perplexity pplx-xxx",
|
||||
"/ai set provider custom https://my-api.com/v1/chat/completions",
|
||||
"/ai set default-model perplexity sonar",
|
||||
"/ai set custom perplexity tools enabled",
|
||||
"/ai remove provider custom",
|
||||
"/ai start",
|
||||
"/ai start perplexity",
|
||||
"/ai start perplexity sonar",
|
||||
"/ai start openai gpt-4o",
|
||||
"/ai switch gpt-4o",
|
||||
"/ai switch openai gpt-5.4-nano",
|
||||
"/ai models perplexity",
|
||||
"/ai clear")
|
||||
},
|
||||
|
||||
// NEXT-COMMAND (search helper)
|
||||
};
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
#include "tools/bookmark_ignore.h"
|
||||
#include "tools/editor.h"
|
||||
#include "plugins/plugins.h"
|
||||
#include "ui/inputwin.h"
|
||||
#include "ui/ui.h"
|
||||
#include "ui/window_list.h"
|
||||
#include "xmpp/avatar.h"
|
||||
@@ -116,8 +117,6 @@
|
||||
#include "tools/clipboard.h"
|
||||
#endif
|
||||
|
||||
#include "ai/ai_client.h"
|
||||
|
||||
#ifdef HAVE_PYTHON
|
||||
#include "plugins/python_plugins.h"
|
||||
#endif
|
||||
@@ -6002,12 +6001,6 @@ cmd_statusbar(ProfWin* window, const char* const command, gchar** args)
|
||||
ui_resize();
|
||||
return TRUE;
|
||||
}
|
||||
if (g_strcmp0(args[1], "dbbackend") == 0) {
|
||||
prefs_set_boolean(PREF_STATUSBAR_SHOW_DBBACKEND, TRUE);
|
||||
cons_show("Enabled showing database backend indicator.");
|
||||
ui_resize();
|
||||
return TRUE;
|
||||
}
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -6041,12 +6034,6 @@ cmd_statusbar(ProfWin* window, const char* const command, gchar** args)
|
||||
ui_resize();
|
||||
return TRUE;
|
||||
}
|
||||
if (g_strcmp0(args[1], "dbbackend") == 0) {
|
||||
prefs_set_boolean(PREF_STATUSBAR_SHOW_DBBACKEND, FALSE);
|
||||
cons_show("Disabled showing database backend indicator.");
|
||||
ui_resize();
|
||||
return TRUE;
|
||||
}
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -6680,48 +6667,36 @@ cmd_privacy(ProfWin* window, const char* const command, gchar** args)
|
||||
}
|
||||
|
||||
if (g_strcmp0(args[0], "logging") == 0) {
|
||||
const gchar* arg = args[1];
|
||||
gchar* arg = args[1];
|
||||
if (arg == NULL) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
const gboolean is_on = (g_strcmp0(arg, "on") == 0);
|
||||
const gboolean is_off = (g_strcmp0(arg, "off") == 0);
|
||||
const gboolean is_redact = (g_strcmp0(arg, "redact") == 0);
|
||||
const gboolean is_flatfile = (g_strcmp0(arg, "flatfile") == 0);
|
||||
|
||||
if (!is_on && !is_off && !is_redact && !is_flatfile) {
|
||||
if (g_strcmp0(arg, "on") == 0) {
|
||||
cons_show("Logging enabled.");
|
||||
prefs_set_string(PREF_DBLOG, arg);
|
||||
prefs_set_boolean(PREF_CHLOG, TRUE);
|
||||
prefs_set_boolean(PREF_HISTORY, TRUE);
|
||||
} else if (g_strcmp0(arg, "off") == 0) {
|
||||
cons_show("Logging disabled.");
|
||||
prefs_set_string(PREF_DBLOG, arg);
|
||||
prefs_set_boolean(PREF_CHLOG, FALSE);
|
||||
prefs_set_boolean(PREF_HISTORY, FALSE);
|
||||
} else if (g_strcmp0(arg, "redact") == 0) {
|
||||
cons_show("Messages are going to be redacted.");
|
||||
prefs_set_string(PREF_DBLOG, arg);
|
||||
} else if (g_strcmp0(arg, "flatfile") == 0) {
|
||||
auto_gchar gchar* ff_path = files_get_data_path(DIR_FLATLOG);
|
||||
cons_show("Using flat-file backend for message logging. Takes effect on next connection.");
|
||||
cons_show("Flatfile directory: %s", ff_path);
|
||||
prefs_set_string(PREF_DBLOG, arg);
|
||||
prefs_set_boolean(PREF_CHLOG, TRUE);
|
||||
prefs_set_boolean(PREF_HISTORY, TRUE);
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
prefs_set_string(PREF_DBLOG, arg);
|
||||
prefs_set_boolean(PREF_CHLOG, !is_off);
|
||||
prefs_set_boolean(PREF_HISTORY, !is_off);
|
||||
|
||||
// For backend-switching values (on = sqlite, flatfile), apply the
|
||||
// change immediately when connected so the user does not have to
|
||||
// reconnect to see the new backend take effect. off/redact change
|
||||
// write semantics but keep the currently-open backend alive.
|
||||
if ((is_on || is_flatfile)
|
||||
&& active_db_backend
|
||||
&& connection_get_status() == JABBER_CONNECTED
|
||||
&& g_strcmp0(active_db_backend->name, is_on ? "sqlite" : "flatfile") != 0) {
|
||||
log_database_switch_backend(is_on ? "on" : "flatfile");
|
||||
}
|
||||
|
||||
if (is_on) {
|
||||
cons_show("Database logging enabled (sqlite).");
|
||||
} else if (is_off) {
|
||||
cons_show("Database logging disabled.");
|
||||
} else if (is_redact) {
|
||||
cons_show("Database logging set to redact (message bodies replaced with [REDACTED]).");
|
||||
} else if (is_flatfile) {
|
||||
auto_gchar gchar* ff_path = files_get_data_path(DIR_FLATLOG);
|
||||
cons_show("Database logging set to flat-file backend.");
|
||||
cons_show("Flatfile directory: %s", ff_path);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -6731,7 +6706,26 @@ cmd_privacy(ProfWin* window, const char* const command, gchar** args)
|
||||
gboolean
|
||||
cmd_logging(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
cons_show("The '/logging' command is deprecated, use '/history' instead.");
|
||||
if (args[0] == NULL) {
|
||||
cons_logging_setting();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (strcmp(args[0], "chat") == 0 && args[1] != NULL) {
|
||||
_cmd_set_boolean_preference(args[1], "Chat logging", PREF_CHLOG);
|
||||
|
||||
// if set to off, disable history
|
||||
if (strcmp(args[1], "off") == 0) {
|
||||
prefs_set_boolean(PREF_HISTORY, FALSE);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
} else if (g_strcmp0(args[0], "group") == 0 && args[1] != NULL) {
|
||||
_cmd_set_boolean_preference(args[1], "Groupchat logging", PREF_GRLOG);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -6878,20 +6872,9 @@ cmd_history(ProfWin* window, const char* const command, gchar** args)
|
||||
|
||||
_cmd_set_boolean_preference(args[0], "Chat history", PREF_HISTORY);
|
||||
|
||||
// /history off should stop persisting messages too, not just hide
|
||||
// existing history on chat-window open. /history on resumes
|
||||
// persistence (default backend) and parallel text chatlogs.
|
||||
// if set to on, set chlog (/logging chat on)
|
||||
if (strcmp(args[0], "on") == 0) {
|
||||
prefs_set_boolean(PREF_CHLOG, TRUE);
|
||||
auto_gchar gchar* dblog = prefs_get_string(PREF_DBLOG);
|
||||
if (g_strcmp0(dblog, "off") == 0) {
|
||||
prefs_set_string(PREF_DBLOG, "on");
|
||||
cons_show("Database logging re-enabled (sqlite).");
|
||||
}
|
||||
} else if (strcmp(args[0], "off") == 0) {
|
||||
prefs_set_boolean(PREF_CHLOG, FALSE);
|
||||
prefs_set_string(PREF_DBLOG, "off");
|
||||
cons_show("Database logging disabled.");
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
@@ -7308,7 +7291,7 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_PGP_LOG, "on");
|
||||
cons_show("PGP messages will be logged as plaintext.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_PGP_LOG, "off");
|
||||
@@ -7317,7 +7300,7 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_PGP_LOG, "redact");
|
||||
cons_show("PGP messages will be logged as '[redacted]'.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -7804,7 +7787,7 @@ cmd_ox_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OX_LOG, "on");
|
||||
cons_show("OX messages will be logged as plaintext.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_OX_LOG, "off");
|
||||
@@ -7813,7 +7796,7 @@ cmd_ox_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OX_LOG, "redact");
|
||||
cons_show("OX messages will be logged as '[redacted]'.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -7853,7 +7836,7 @@ cmd_otr_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OTR_LOG, "on");
|
||||
cons_show("OTR messages will be logged as plaintext.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_OTR_LOG, "off");
|
||||
@@ -7862,7 +7845,7 @@ cmd_otr_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OTR_LOG, "redact");
|
||||
cons_show("OTR messages will be logged as '[redacted]'.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -8483,7 +8466,7 @@ _cmd_execute_default(ProfWin* window, const char* inp)
|
||||
}
|
||||
|
||||
// handle non commands in non chat or plugin windows
|
||||
if (window->type != WIN_CHAT && window->type != WIN_MUC && window->type != WIN_PRIVATE && window->type != WIN_PLUGIN && window->type != WIN_XML && window->type != WIN_AI) {
|
||||
if (window->type != WIN_CHAT && window->type != WIN_MUC && window->type != WIN_PRIVATE && window->type != WIN_PLUGIN && window->type != WIN_XML) {
|
||||
cons_show("Unknown command: %s", inp);
|
||||
cons_alert(NULL);
|
||||
return TRUE;
|
||||
@@ -8529,13 +8512,6 @@ _cmd_execute_default(ProfWin* window, const char* inp)
|
||||
connection_send_stanza(inp);
|
||||
break;
|
||||
}
|
||||
case WIN_AI:
|
||||
{
|
||||
ProfAiWin* aiwin = (ProfAiWin*)window;
|
||||
auto_gchar gchar* stanza_id = connection_create_stanza_id();
|
||||
cl_ev_send_ai_msg(aiwin, inp, stanza_id);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -8799,7 +8775,7 @@ cmd_omemo_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OMEMO_LOG, "on");
|
||||
cons_show("OMEMO messages will be logged as plaintext.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else if (g_strcmp0(choice, "off") == 0) {
|
||||
prefs_set_string(PREF_OMEMO_LOG, "off");
|
||||
@@ -8808,7 +8784,7 @@ cmd_omemo_log(ProfWin* window, const char* const command, gchar** args)
|
||||
prefs_set_string(PREF_OMEMO_LOG, "redact");
|
||||
cons_show("OMEMO messages will be logged as '[redacted]'.");
|
||||
if (!prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
}
|
||||
} else {
|
||||
cons_bad_cmd_usage(command);
|
||||
@@ -10804,419 +10780,6 @@ cmd_vcard_save(ProfWin* window, const char* const command, gchar** args)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
if (args[0] == NULL) {
|
||||
// Display current AI settings
|
||||
cons_show("AI Chat - OpenAI-compatible API client");
|
||||
cons_show("");
|
||||
|
||||
// List configured providers with full details
|
||||
GList* providers = ai_list_providers();
|
||||
cons_show("Configured providers:");
|
||||
for (GList* curr = providers; curr; curr = g_list_next(curr)) {
|
||||
AIProvider* provider = curr->data;
|
||||
auto_gchar gchar* key = ai_get_provider_key(provider->name);
|
||||
const gchar* default_model = ai_get_provider_default_model(provider->name);
|
||||
cons_show(" %s", provider->name);
|
||||
cons_show(" URL: %s", provider->api_url);
|
||||
cons_show(" Key: %s", key ? "configured" : "NOT configured");
|
||||
if (default_model) {
|
||||
cons_show(" Default model: %s", default_model);
|
||||
}
|
||||
if (provider->models && g_list_length(provider->models) > 0) {
|
||||
cons_show(" Cached models: %d", g_list_length(provider->models));
|
||||
}
|
||||
cons_show("");
|
||||
}
|
||||
g_list_free(providers);
|
||||
|
||||
cons_show("Use '/ai start' to begin a chat (uses default provider/model).");
|
||||
cons_show("Use '/ai models <provider>' to fetch available models.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_set(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
if (args[1] == NULL) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (g_strcmp0(args[1], "provider") == 0) {
|
||||
// /ai set provider <name> <url>
|
||||
if (g_strv_length(args) < 4) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
if (ai_add_provider(args[2], args[3])) {
|
||||
cons_show("Provider '%s' configured with URL: %s", args[2], args[3]);
|
||||
} else {
|
||||
cons_show_error("Failed to configure provider '%s'.", args[2]);
|
||||
}
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
} else if (g_strcmp0(args[1], "token") == 0) {
|
||||
// /ai set token <provider> <token>
|
||||
if (g_strv_length(args) < 4) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
ai_set_provider_key(args[2], args[3]);
|
||||
cons_show("API token set for provider: %s", args[2]);
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
} else if (g_strcmp0(args[1], "default-model") == 0) {
|
||||
// /ai set default-model <provider> <model>
|
||||
if (g_strv_length(args) < 4) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
ai_set_provider_default_model(args[2], args[3]);
|
||||
cons_show("Default model for provider '%s' set to: %s", args[2], args[3]);
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
} else if (g_strcmp0(args[1], "custom") == 0) {
|
||||
// /ai set custom <provider> <setting> <value>
|
||||
if (g_strv_length(args) < 5) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
ai_set_provider_setting(args[2], args[3], args[4]);
|
||||
cons_show("Setting '%s' for provider '%s' set to: %s", args[3], args[2], args[4]);
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_remove(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai remove provider <name>
|
||||
if (g_strcmp0(args[1], "provider") != 0) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (g_strv_length(args) < 3) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (ai_remove_provider(args[2])) {
|
||||
cons_show("Provider '%s' removed.", args[2]);
|
||||
} else {
|
||||
cons_show("Provider '%s' not found.", args[2]);
|
||||
}
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai start [<provider>] [<model>]
|
||||
// Space-separated, no slash syntax
|
||||
const gchar* provider_name = NULL;
|
||||
const gchar* model = NULL;
|
||||
|
||||
if (g_strv_length(args) >= 2) {
|
||||
provider_name = args[1];
|
||||
}
|
||||
if (g_strv_length(args) >= 3) {
|
||||
model = args[2];
|
||||
}
|
||||
|
||||
// Resolve defaults
|
||||
auto_gchar gchar* owned_provider_name = NULL;
|
||||
if (!provider_name) {
|
||||
// Use default provider from preferences
|
||||
auto_gchar gchar* default_provider = prefs_get_string(PREF_AI_PROVIDER);
|
||||
if (default_provider && strlen(default_provider) > 0) {
|
||||
provider_name = default_provider;
|
||||
}
|
||||
}
|
||||
if (!provider_name) {
|
||||
provider_name = "openai"; // Fallback
|
||||
}
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider) {
|
||||
cons_show_error("Provider '%s' not found. Use '/ai set provider %s <url>' to add it.",
|
||||
provider_name, provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Get model: explicit > provider default > hardcoded fallback
|
||||
if (!model) {
|
||||
model = ai_get_provider_default_model(provider_name);
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
cons_show_error("No model specified and no default model set for provider '%s'.", provider_name);
|
||||
cons_show("Use '/ai set default-model %s <model>'.", provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Check for API key
|
||||
gchar* api_key = ai_get_provider_key(provider_name);
|
||||
if (!api_key || strlen(api_key) == 0) {
|
||||
cons_show_error("No API key set for provider '%s'. Use '/ai set token %s <key>' first.",
|
||||
provider_name, provider_name);
|
||||
g_free(api_key);
|
||||
return TRUE;
|
||||
}
|
||||
g_free(api_key);
|
||||
|
||||
// Create new AI session
|
||||
AISession* session = ai_session_create(provider_name, model);
|
||||
if (!session) {
|
||||
log_error("[AI-CMD] Failed to create AI session");
|
||||
cons_show_error("Failed to create AI session for %s/%s.", provider_name, model);
|
||||
return TRUE;
|
||||
}
|
||||
log_debug("[AI-CMD] AI session created successfully");
|
||||
|
||||
// Create AI chat window
|
||||
log_debug("[AI-CMD] Calling wins_new_ai()...");
|
||||
ProfWin* ai_win = wins_new_ai(session);
|
||||
if (!ai_win) {
|
||||
log_error("[AI-CMD] wins_new_ai() returned NULL");
|
||||
cons_show_error("Failed to create AI chat window.");
|
||||
ai_session_unref(session);
|
||||
return TRUE;
|
||||
}
|
||||
log_debug("[AI-CMD] wins_new_ai() returned successfully, window type: %d", ai_win->type);
|
||||
|
||||
// Release the reference held by cmd_ai_start, since the window now holds one
|
||||
ai_session_unref(session);
|
||||
|
||||
// Add welcome message to the AI window
|
||||
log_debug("[AI-CMD] Adding welcome messages...");
|
||||
win_println(ai_win, THEME_DEFAULT, "-", "AI Chat: %s/%s", provider_name, model);
|
||||
win_println(ai_win, THEME_DEFAULT, "-", "Type your message and press Enter to start a conversation.");
|
||||
log_debug("[AI-CMD] Welcome messages added");
|
||||
|
||||
// Focus the new window
|
||||
log_debug("[AI-CMD] Calling ui_focus_win()...");
|
||||
ui_focus_win(ai_win);
|
||||
log_debug("[AI-CMD] ui_focus_win() returned");
|
||||
|
||||
cons_show("Started AI chat: %s/%s", provider_name, model);
|
||||
cons_show("");
|
||||
log_debug("[AI-CMD] AI start command completed");
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_model(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai model <model>
|
||||
if (args[1] == NULL) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
const gchar* model = args[1];
|
||||
|
||||
// Must be in an AI window to use this command
|
||||
if (!window || window->type != WIN_AI) {
|
||||
cons_show_error("Must be in an AI chat window to use this command.");
|
||||
return TRUE;
|
||||
}
|
||||
ProfAiWin* aiwin = (ProfAiWin*)window;
|
||||
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
|
||||
if (!aiwin->session) {
|
||||
cons_show("No active session in this chat window.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
ai_session_set_model(aiwin->session, model);
|
||||
cons_show("Session model changed to: %s", model);
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_switch(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai switch <provider> [<model>] - Change provider and optionally model
|
||||
// /ai switch <model> - Change model only (keeps current provider)
|
||||
// Modifies the existing session's provider and model instead of recreating it
|
||||
if (g_strv_length(args) < 2) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Must be in an AI window to use this command
|
||||
if (!window || window->type != WIN_AI) {
|
||||
cons_show_error("Must be in an AI chat window to use this command.");
|
||||
return TRUE;
|
||||
}
|
||||
ProfAiWin* aiwin = (ProfAiWin*)window;
|
||||
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
|
||||
if (!aiwin->session) {
|
||||
cons_show("No active session in this chat window.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
const gchar* arg1 = args[1];
|
||||
const gchar* arg2 = (g_strv_length(args) >= 3) ? args[2] : NULL;
|
||||
|
||||
const gchar* provider_name;
|
||||
const gchar* model;
|
||||
|
||||
// Check if arg1 is a known provider
|
||||
AIProvider* provider = ai_get_provider(arg1);
|
||||
if (provider) {
|
||||
// arg1 is a provider name
|
||||
provider_name = arg1;
|
||||
|
||||
// Get model: arg2 > provider default > error
|
||||
if (arg2) {
|
||||
model = arg2;
|
||||
} else {
|
||||
model = ai_get_provider_default_model(provider_name);
|
||||
}
|
||||
if (!model) {
|
||||
cons_show_error("No model specified and no default model set for provider '%s'.", provider_name);
|
||||
cons_show("Use '/ai set default-model %s <model>' or '/ai switch %s <model>'.", provider_name, provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
} else {
|
||||
// arg1 is a model name, keep current provider
|
||||
provider_name = aiwin->session->provider_name;
|
||||
if (!provider_name) {
|
||||
cons_show_error("Session has no provider name.");
|
||||
return TRUE;
|
||||
}
|
||||
model = arg1;
|
||||
}
|
||||
|
||||
// Check for API key
|
||||
auto_gchar gchar* api_key = ai_get_provider_key(provider_name);
|
||||
if (!api_key || strlen(api_key) == 0) {
|
||||
cons_show_error("No API key set for provider '%s'. Use '/ai set token %s <key>' first.",
|
||||
provider_name, provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Atomically switch session provider, model, and API key (thread-safe)
|
||||
ai_session_switch(aiwin->session, provider_name, model, g_steal_pointer(&api_key));
|
||||
|
||||
// Update window title
|
||||
win_println((ProfWin*)aiwin, THEME_DEFAULT, "-", "AI Chat: %s/%s", provider_name, model);
|
||||
|
||||
cons_show("Switched to %s/%s", provider_name, model);
|
||||
cons_show("");
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_models(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai models <provider> [--cached]
|
||||
// Default: fetch fresh models from API
|
||||
// --cached: display cached models (if available)
|
||||
if (args[1] == NULL) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
const gchar* provider_name = args[1];
|
||||
gboolean use_cached = (g_strv_length(args) >= 3) && (g_strcmp0(args[2], "--cached") == 0);
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
if (!provider) {
|
||||
cons_show_error("Provider '%s' not found.", provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (use_cached) {
|
||||
// Display cached models (if available)
|
||||
if (!provider->models) {
|
||||
cons_show("No cached models for provider '%s'. Use '/ai models %s' to fetch from API.", provider_name, provider_name);
|
||||
return TRUE;
|
||||
}
|
||||
cons_show("Cached models for provider '%s':", provider_name);
|
||||
GList* curr = provider->models;
|
||||
while (curr) {
|
||||
cons_show(" %s", (gchar*)curr->data);
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
cons_show("");
|
||||
cons_show("Use '/ai models %s' to fetch fresh models from the API.", provider_name);
|
||||
} else {
|
||||
// Default: fetch fresh models from API
|
||||
ai_fetch_models(provider_name, window);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_clear(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// /ai clear
|
||||
// Must be in an AI window to use this command
|
||||
if (!window || window->type != WIN_AI) {
|
||||
cons_show_error("Must be in an AI chat window to use this command.");
|
||||
return TRUE;
|
||||
}
|
||||
ProfAiWin* aiwin = (ProfAiWin*)window;
|
||||
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
if (aiwin->session) {
|
||||
ai_session_clear_history(aiwin->session);
|
||||
}
|
||||
|
||||
win_clear(window);
|
||||
cons_show("AI Chat history cleared.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_ai_providers(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
cons_show("Configured providers:");
|
||||
cons_show("");
|
||||
|
||||
GList* providers = ai_list_providers();
|
||||
if (!providers) {
|
||||
cons_show(" (none configured)");
|
||||
cons_show("");
|
||||
cons_show("Add a provider with: /ai set provider <name> <url>");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
for (GList* curr = providers; curr; curr = g_list_next(curr)) {
|
||||
AIProvider* provider = curr->data;
|
||||
auto_gchar gchar* key = ai_get_provider_key(provider->name);
|
||||
cons_show(" %s", provider->name);
|
||||
cons_show(" URL: %s", provider->api_url);
|
||||
cons_show(" Key: %s", key ? "configured" : "NOT configured");
|
||||
cons_show("");
|
||||
}
|
||||
g_list_free(providers);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
cmd_force_encryption(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
|
||||
@@ -166,14 +166,6 @@ gboolean cmd_console(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_command_list(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_command_exec(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_change_password(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_set(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_remove(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_start(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_clear(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_providers(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_switch(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_ai_models(ProfWin* window, const char* const command, gchar** args);
|
||||
|
||||
gboolean cmd_plugins(ProfWin* window, const char* const command, gchar** args);
|
||||
gboolean cmd_plugins_sourcepath(ProfWin* window, const char* const command, gchar** args);
|
||||
|
||||
@@ -66,9 +66,6 @@
|
||||
#define PREF_GROUP_MUC "muc"
|
||||
#define PREF_GROUP_PLUGINS "plugins"
|
||||
#define PREF_GROUP_EXECUTABLES "executables"
|
||||
#define PREF_GROUP_AI "ai"
|
||||
#define PREF_GROUP_AI_PREFIX "ai/" /* per-provider subsection prefix: [ai/<provider>] */
|
||||
#define PREF_AI_SETTING_PREFIX "setting." /* per-provider custom setting key prefix */
|
||||
|
||||
#define INPBLOCK_DEFAULT 1000
|
||||
|
||||
@@ -245,69 +242,6 @@ _prefs_load(void)
|
||||
g_key_file_remove_key(prefs, PREF_GROUP_UI, "titlebar.muc.title.jid", NULL);
|
||||
g_key_file_remove_key(prefs, PREF_GROUP_UI, "titlebar.muc.title.name", NULL);
|
||||
}
|
||||
|
||||
/* Migrate legacy flat [ai] layout
|
||||
* openai_url=..., openai=<token>, openai_models=..., openai_default_model=...
|
||||
* into per-provider subsections
|
||||
* [ai/openai] url=..., key=..., models=..., default_model=...
|
||||
* Idempotent: once the flat keys are gone, this is a no-op. */
|
||||
if (g_key_file_has_group(prefs, PREF_GROUP_AI)) {
|
||||
gsize key_count = 0;
|
||||
auto_gcharv gchar** all_keys = g_key_file_get_keys(prefs, PREF_GROUP_AI, &key_count, NULL);
|
||||
|
||||
/* Collect provider names from <name>_url keys first so we can migrate
|
||||
* the whole bundle (url + token + models + default_model) atomically
|
||||
* per provider, without misclassifying unrelated flat keys. */
|
||||
GHashTable* to_migrate = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
||||
for (gsize i = 0; i < key_count; i++) {
|
||||
const gchar* k = all_keys[i];
|
||||
if (g_str_has_suffix(k, "_url") && !g_str_has_suffix(k, "_models_url")) {
|
||||
gsize klen = strlen(k);
|
||||
if (klen > 4) {
|
||||
g_hash_table_add(to_migrate, g_strndup(k, klen - 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GList* provider_names = g_hash_table_get_keys(to_migrate);
|
||||
for (GList* curr = provider_names; curr; curr = g_list_next(curr)) {
|
||||
const gchar* name = (const gchar*)curr->data;
|
||||
auto_gchar gchar* group = g_strconcat(PREF_GROUP_AI_PREFIX, name, NULL);
|
||||
|
||||
const struct
|
||||
{
|
||||
const gchar* flat_suffix;
|
||||
const gchar* new_key;
|
||||
} map[] = {
|
||||
{ "_url", "url" },
|
||||
{ "_models", "models" },
|
||||
{ "_default_model", "default_model" },
|
||||
{ NULL, NULL },
|
||||
};
|
||||
for (int j = 0; map[j].flat_suffix; j++) {
|
||||
auto_gchar gchar* flat_key = g_strconcat(name, map[j].flat_suffix, NULL);
|
||||
if (g_key_file_has_key(prefs, PREF_GROUP_AI, flat_key, NULL)) {
|
||||
auto_gchar gchar* val = g_key_file_get_string(prefs, PREF_GROUP_AI, flat_key, NULL);
|
||||
if (val) {
|
||||
g_key_file_set_string(prefs, group, map[j].new_key, val);
|
||||
}
|
||||
g_key_file_remove_key(prefs, PREF_GROUP_AI, flat_key, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/* Token was stored as the bare provider name in [ai]. */
|
||||
if (g_key_file_has_key(prefs, PREF_GROUP_AI, name, NULL)) {
|
||||
auto_gchar gchar* val = g_key_file_get_string(prefs, PREF_GROUP_AI, name, NULL);
|
||||
if (val) {
|
||||
g_key_file_set_string(prefs, group, "key", val);
|
||||
}
|
||||
g_key_file_remove_key(prefs, PREF_GROUP_AI, name, NULL);
|
||||
}
|
||||
}
|
||||
g_list_free(provider_names);
|
||||
g_hash_table_destroy(to_migrate);
|
||||
}
|
||||
|
||||
_save_prefs();
|
||||
|
||||
boolean_choice_ac = autocomplete_new();
|
||||
@@ -1772,366 +1706,6 @@ _save_prefs(void)
|
||||
save_keyfile(&prefs_prof_keyfile);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* AI provider preferences
|
||||
*
|
||||
* Each provider lives in its own [ai/<name>] subsection of the keyfile with:
|
||||
* url=...
|
||||
* key=<api token>
|
||||
* default_model=...
|
||||
* models=model1;model2;...
|
||||
* setting.<custom>=value (e.g. setting.tools=enabled)
|
||||
* The bare [ai] section keeps only the global "defaults_initialized" sentinel.
|
||||
* Legacy flat-key layouts are migrated on first load in _prefs_load().
|
||||
* ======================================================================== */
|
||||
|
||||
static gchar*
|
||||
_ai_section(const char* const provider)
|
||||
{
|
||||
return g_strconcat(PREF_GROUP_AI_PREFIX, provider, NULL);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_ai_set_string(const char* const provider, const char* const key, const char* const value)
|
||||
{
|
||||
if (!provider || !key || !value || !prefs)
|
||||
return FALSE;
|
||||
|
||||
auto_gchar gchar* group = _ai_section(provider);
|
||||
g_key_file_set_string(prefs, group, key, value);
|
||||
_save_prefs();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static char*
|
||||
_ai_get_string(const char* const provider, const char* const key)
|
||||
{
|
||||
if (!provider || !key || !prefs)
|
||||
return NULL;
|
||||
|
||||
auto_gchar gchar* group = _ai_section(provider);
|
||||
if (!g_key_file_has_key(prefs, group, key, NULL))
|
||||
return NULL;
|
||||
return g_key_file_get_string(prefs, group, key, NULL);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_ai_remove_string(const char* const provider, const char* const key)
|
||||
{
|
||||
if (!provider || !key || !prefs)
|
||||
return FALSE;
|
||||
|
||||
auto_gchar gchar* group = _ai_section(provider);
|
||||
if (!g_key_file_has_key(prefs, group, key, NULL))
|
||||
return FALSE;
|
||||
|
||||
g_key_file_remove_key(prefs, group, key, NULL);
|
||||
_save_prefs();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* --- Token --- */
|
||||
|
||||
gboolean
|
||||
prefs_ai_set_token(const char* const provider, const char* const token)
|
||||
{
|
||||
if (!provider || !prefs)
|
||||
return FALSE;
|
||||
if (!token)
|
||||
return prefs_ai_remove_token(provider);
|
||||
return _ai_set_string(provider, "key", token);
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_token(const char* const provider)
|
||||
{
|
||||
return _ai_remove_string(provider, "key");
|
||||
}
|
||||
|
||||
char*
|
||||
prefs_ai_get_token(const char* const provider)
|
||||
{
|
||||
return _ai_get_string(provider, "key");
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_list_tokens(void)
|
||||
{
|
||||
if (!prefs)
|
||||
return NULL;
|
||||
|
||||
GList* result = NULL;
|
||||
GList* providers = prefs_ai_list_providers();
|
||||
for (GList* curr = providers; curr; curr = g_list_next(curr)) {
|
||||
const gchar* name = (const gchar*)curr->data;
|
||||
auto_gchar gchar* tok = prefs_ai_get_token(name);
|
||||
if (tok && tok[0] != '\0') {
|
||||
result = g_list_append(result, g_strdup(name));
|
||||
}
|
||||
}
|
||||
prefs_free_ai_providers(providers);
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
prefs_free_ai_tokens(GList* tokens)
|
||||
{
|
||||
if (tokens) {
|
||||
g_list_free_full(tokens, g_free);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Models cache --- */
|
||||
|
||||
gboolean
|
||||
prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count)
|
||||
{
|
||||
if (!provider || count <= 0 || !prefs)
|
||||
return FALSE;
|
||||
|
||||
GString* model_str = g_string_new("");
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (i > 0)
|
||||
g_string_append_c(model_str, ';');
|
||||
g_string_append(model_str, models[i]);
|
||||
}
|
||||
|
||||
gboolean ok = _ai_set_string(provider, "models", model_str->str);
|
||||
g_string_free(model_str, TRUE);
|
||||
return ok;
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_models(const char* const provider)
|
||||
{
|
||||
return _ai_remove_string(provider, "models");
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_get_models(const char* const provider)
|
||||
{
|
||||
auto_gchar gchar* model_str = _ai_get_string(provider, "models");
|
||||
if (!model_str || model_str[0] == '\0')
|
||||
return NULL;
|
||||
|
||||
GList* result = NULL;
|
||||
gchar** models = g_strsplit(model_str, ";", -1);
|
||||
for (int i = 0; models[i] != NULL; i++) {
|
||||
result = g_list_append(result, g_strdup(models[i]));
|
||||
}
|
||||
g_strfreev(models);
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
prefs_free_ai_models(GList* models)
|
||||
{
|
||||
if (models) {
|
||||
g_list_free_full(models, g_free);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Default model --- */
|
||||
|
||||
gboolean
|
||||
prefs_ai_set_default_model(const char* const provider, const char* const model)
|
||||
{
|
||||
if (!provider || !model || !prefs)
|
||||
return FALSE;
|
||||
if (model[0] == '\0')
|
||||
return prefs_ai_remove_default_model(provider);
|
||||
return _ai_set_string(provider, "default_model", model);
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_default_model(const char* const provider)
|
||||
{
|
||||
return _ai_remove_string(provider, "default_model");
|
||||
}
|
||||
|
||||
char*
|
||||
prefs_ai_get_default_model(const char* const provider)
|
||||
{
|
||||
return _ai_get_string(provider, "default_model");
|
||||
}
|
||||
|
||||
/* --- Provider URL --- */
|
||||
|
||||
gboolean
|
||||
prefs_ai_set_provider(const char* const provider, const char* const url)
|
||||
{
|
||||
if (!provider || !url)
|
||||
return FALSE;
|
||||
return _ai_set_string(provider, "url", url);
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_provider(const char* const provider)
|
||||
{
|
||||
/* Provider presence is identified by the url key; removing it is what
|
||||
* makes prefs_ai_list_providers stop returning this name. The remaining
|
||||
* sub-keys are wiped through prefs_ai_remove_section(). */
|
||||
return _ai_remove_string(provider, "url");
|
||||
}
|
||||
|
||||
char*
|
||||
prefs_ai_get_provider_url(const char* const provider)
|
||||
{
|
||||
return _ai_get_string(provider, "url");
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_section(const char* const provider)
|
||||
{
|
||||
if (!provider || !prefs)
|
||||
return FALSE;
|
||||
|
||||
auto_gchar gchar* group = _ai_section(provider);
|
||||
if (!g_key_file_has_group(prefs, group))
|
||||
return FALSE;
|
||||
|
||||
g_key_file_remove_group(prefs, group, NULL);
|
||||
_save_prefs();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_list_providers(void)
|
||||
{
|
||||
if (!prefs)
|
||||
return NULL;
|
||||
|
||||
GList* result = NULL;
|
||||
gsize len = 0;
|
||||
auto_gcharv gchar** groups = g_key_file_get_groups(prefs, &len);
|
||||
if (!groups)
|
||||
return NULL;
|
||||
|
||||
gsize prefix_len = strlen(PREF_GROUP_AI_PREFIX);
|
||||
for (gsize i = 0; i < len; i++) {
|
||||
const gchar* g = groups[i];
|
||||
if (g_str_has_prefix(g, PREF_GROUP_AI_PREFIX) && strlen(g) > prefix_len) {
|
||||
/* Only count groups that actually identify a provider (have url). */
|
||||
if (g_key_file_has_key(prefs, g, "url", NULL)) {
|
||||
result = g_list_append(result, g_strdup(g + prefix_len));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
prefs_free_ai_providers(GList* providers)
|
||||
{
|
||||
if (providers) {
|
||||
g_list_free_full(providers, g_free);
|
||||
}
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_get_providers(void)
|
||||
{
|
||||
GList* configured = prefs_ai_list_providers();
|
||||
GList* result = NULL;
|
||||
|
||||
for (GList* curr = configured; curr; curr = g_list_next(curr)) {
|
||||
const gchar* name = (const gchar*)curr->data;
|
||||
auto_gchar gchar* url = prefs_ai_get_provider_url(name);
|
||||
if (url && strlen(url) > 0) {
|
||||
result = g_list_append(result, g_strdup(name));
|
||||
result = g_list_append(result, g_strdup(url));
|
||||
}
|
||||
}
|
||||
prefs_free_ai_providers(configured);
|
||||
|
||||
/* Seed openai/perplexity into the in-memory result on very first init
|
||||
* only. In production the "defaults_initialized" sentinel in [ai] is what
|
||||
* tells us we've already seeded — a user who removes every provider must
|
||||
* not get them silently re-added on the next start. When prefs is NULL
|
||||
* (e.g. unit tests that bring up ai_client without prefs_load) we still
|
||||
* seed the in-memory list so behaviour matches a fresh first run, but
|
||||
* skip the persistence write. */
|
||||
gboolean already_seeded = prefs && g_key_file_get_boolean(prefs, PREF_GROUP_AI, "defaults_initialized", NULL);
|
||||
if (!already_seeded && !result) {
|
||||
if (prefs) {
|
||||
prefs_ai_set_provider("openai", "https://api.openai.com/");
|
||||
prefs_ai_set_provider("perplexity", "https://api.perplexity.ai/");
|
||||
}
|
||||
result = g_list_append(result, g_strdup("openai"));
|
||||
result = g_list_append(result, g_strdup("https://api.openai.com/"));
|
||||
result = g_list_append(result, g_strdup("perplexity"));
|
||||
result = g_list_append(result, g_strdup("https://api.perplexity.ai/"));
|
||||
}
|
||||
if (prefs && !already_seeded) {
|
||||
g_key_file_set_boolean(prefs, PREF_GROUP_AI, "defaults_initialized", TRUE);
|
||||
_save_prefs();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* --- Custom per-provider settings (setting.<name>) --- */
|
||||
|
||||
gboolean
|
||||
prefs_ai_set_setting(const char* const provider, const char* const setting, const char* const value)
|
||||
{
|
||||
if (!provider || !setting)
|
||||
return FALSE;
|
||||
auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL);
|
||||
if (!value)
|
||||
return _ai_remove_string(provider, key);
|
||||
return _ai_set_string(provider, key, value);
|
||||
}
|
||||
|
||||
gboolean
|
||||
prefs_ai_remove_setting(const char* const provider, const char* const setting)
|
||||
{
|
||||
if (!provider || !setting)
|
||||
return FALSE;
|
||||
auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL);
|
||||
return _ai_remove_string(provider, key);
|
||||
}
|
||||
|
||||
char*
|
||||
prefs_ai_get_setting(const char* const provider, const char* const setting)
|
||||
{
|
||||
if (!provider || !setting)
|
||||
return NULL;
|
||||
auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL);
|
||||
return _ai_get_string(provider, key);
|
||||
}
|
||||
|
||||
GList*
|
||||
prefs_ai_list_settings(const char* const provider)
|
||||
{
|
||||
if (!provider || !prefs)
|
||||
return NULL;
|
||||
|
||||
auto_gchar gchar* group = _ai_section(provider);
|
||||
if (!g_key_file_has_group(prefs, group))
|
||||
return NULL;
|
||||
|
||||
GList* result = NULL;
|
||||
gsize len = 0;
|
||||
auto_gcharv gchar** keys = g_key_file_get_keys(prefs, group, &len, NULL);
|
||||
gsize prefix_len = strlen(PREF_AI_SETTING_PREFIX);
|
||||
for (gsize i = 0; i < len; i++) {
|
||||
if (g_str_has_prefix(keys[i], PREF_AI_SETTING_PREFIX) && strlen(keys[i]) > prefix_len) {
|
||||
result = g_list_append(result, g_strdup(keys[i] + prefix_len));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
prefs_free_ai_settings(GList* settings)
|
||||
{
|
||||
if (settings) {
|
||||
g_list_free_full(settings, g_free);
|
||||
}
|
||||
}
|
||||
|
||||
// get the preference group for a specific preference
|
||||
// for example the PREF_BEEP setting ("beep" in .profrc, see _get_key) belongs
|
||||
// to the [ui] section.
|
||||
@@ -2208,7 +1782,6 @@ _get_group(preference_t pref)
|
||||
case PREF_STATUSBAR_SHOW_NAME:
|
||||
case PREF_STATUSBAR_SHOW_NUMBER:
|
||||
case PREF_STATUSBAR_SHOW_READ:
|
||||
case PREF_STATUSBAR_SHOW_DBBACKEND:
|
||||
case PREF_STATUSBAR_SELF:
|
||||
case PREF_STATUSBAR_CHAT:
|
||||
case PREF_STATUSBAR_ROOM_TITLE:
|
||||
@@ -2292,9 +1865,6 @@ _get_group(preference_t pref)
|
||||
return PREF_GROUP_OMEMO;
|
||||
case PREF_OX_LOG:
|
||||
return PREF_GROUP_OX;
|
||||
case PREF_AI_PROVIDER:
|
||||
case PREF_AI_API_KEY:
|
||||
return PREF_GROUP_AI;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
@@ -2536,8 +2106,6 @@ _get_key(preference_t pref)
|
||||
return "statusbar.show.number";
|
||||
case PREF_STATUSBAR_SHOW_READ:
|
||||
return "statusbar.show.read";
|
||||
case PREF_STATUSBAR_SHOW_DBBACKEND:
|
||||
return "statusbar.show.dbbackend";
|
||||
case PREF_STATUSBAR_SELF:
|
||||
return "statusbar.self";
|
||||
case PREF_STATUSBAR_CHAT:
|
||||
@@ -2574,10 +2142,6 @@ _get_key(preference_t pref)
|
||||
return "stamp.incoming";
|
||||
case PREF_OX_LOG:
|
||||
return "log";
|
||||
case PREF_AI_PROVIDER:
|
||||
return "provider";
|
||||
case PREF_AI_API_KEY:
|
||||
return "api_key";
|
||||
case PREF_MOOD:
|
||||
return "mood";
|
||||
case PREF_VCARD_PHOTO_CMD:
|
||||
@@ -2631,7 +2195,6 @@ _get_default_boolean(preference_t pref)
|
||||
case PREF_ROOM_LIST_CACHE:
|
||||
case PREF_STATUSBAR_SHOW_NUMBER:
|
||||
case PREF_STATUSBAR_SHOW_READ:
|
||||
case PREF_STATUSBAR_SHOW_DBBACKEND:
|
||||
case PREF_REVEAL_OS:
|
||||
case PREF_CORRECTION_ALLOW:
|
||||
case PREF_RECEIPTS_SEND:
|
||||
@@ -2756,10 +2319,6 @@ _get_default_string(preference_t pref)
|
||||
return "on";
|
||||
case PREF_FORCE_ENCRYPTION_MODE:
|
||||
return "resend-to-confirm";
|
||||
case PREF_AI_PROVIDER:
|
||||
return "openai";
|
||||
case PREF_AI_API_KEY:
|
||||
return "";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -162,7 +162,6 @@ typedef enum {
|
||||
PREF_STATUSBAR_SHOW_NAME,
|
||||
PREF_STATUSBAR_SHOW_NUMBER,
|
||||
PREF_STATUSBAR_SHOW_READ,
|
||||
PREF_STATUSBAR_SHOW_DBBACKEND,
|
||||
PREF_STATUSBAR_SELF,
|
||||
PREF_STATUSBAR_CHAT,
|
||||
PREF_STATUSBAR_ROOM_TITLE,
|
||||
@@ -184,9 +183,6 @@ typedef enum {
|
||||
PREF_OX_LOG,
|
||||
PREF_MOOD,
|
||||
PREF_STROPHE_VERBOSITY,
|
||||
PREF_AI_PROVIDER,
|
||||
PREF_AI_API_KEY,
|
||||
PREF_AI_DEFAULT_MODEL,
|
||||
PREF_STROPHE_SM_ENABLED,
|
||||
PREF_STROPHE_SM_RESEND,
|
||||
PREF_VCARD_PHOTO_CMD,
|
||||
@@ -360,42 +356,4 @@ gboolean prefs_get_room_notify(const char* const roomjid);
|
||||
gboolean prefs_get_room_notify_mention(const char* const roomjid);
|
||||
gboolean prefs_get_room_notify_trigger(const char* const roomjid);
|
||||
|
||||
/* AI token management */
|
||||
gboolean prefs_ai_set_token(const char* const provider, const char* const token);
|
||||
gboolean prefs_ai_remove_token(const char* const provider);
|
||||
char* prefs_ai_get_token(const char* const provider);
|
||||
GList* prefs_ai_list_tokens(void);
|
||||
void prefs_free_ai_tokens(GList* tokens);
|
||||
|
||||
/* AI provider management */
|
||||
gboolean prefs_ai_set_provider(const char* const provider, const char* const url);
|
||||
gboolean prefs_ai_remove_provider(const char* const provider);
|
||||
char* prefs_ai_get_provider_url(const char* const provider);
|
||||
GList* prefs_ai_list_providers(void);
|
||||
void prefs_free_ai_providers(GList* providers);
|
||||
GList* prefs_ai_get_providers(void);
|
||||
|
||||
/* Wipe the whole [ai/<provider>] subsection (url, key, models, default_model,
|
||||
* custom settings) in one call. */
|
||||
gboolean prefs_ai_remove_section(const char* const provider);
|
||||
|
||||
/* AI model cache management */
|
||||
gboolean prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count);
|
||||
gboolean prefs_ai_remove_models(const char* const provider);
|
||||
GList* prefs_ai_get_models(const char* const provider);
|
||||
void prefs_free_ai_models(GList* models);
|
||||
|
||||
/* AI default model per provider */
|
||||
gboolean prefs_ai_set_default_model(const char* const provider, const char* const model);
|
||||
gboolean prefs_ai_remove_default_model(const char* const provider);
|
||||
char* prefs_ai_get_default_model(const char* const provider);
|
||||
|
||||
/* AI per-provider custom settings (e.g. tools, search) — persisted as
|
||||
* setting.<name>=<value> inside [ai/<provider>]. Pass NULL value to remove. */
|
||||
gboolean prefs_ai_set_setting(const char* const provider, const char* const setting, const char* const value);
|
||||
gboolean prefs_ai_remove_setting(const char* const provider, const char* const setting);
|
||||
char* prefs_ai_get_setting(const char* const provider, const char* const setting);
|
||||
GList* prefs_ai_list_settings(const char* const provider);
|
||||
void prefs_free_ai_settings(GList* settings);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -88,8 +88,9 @@ _ff_cache_line_ids(ff_contact_state_t* state, const char* line)
|
||||
return;
|
||||
|
||||
// Split metadata on unescaped '|'
|
||||
auto_gchar gchar* meta_str = g_strndup(bracket + 1, close - bracket - 1);
|
||||
char* meta_str = g_strndup(bracket + 1, close - bracket - 1);
|
||||
char** parts = ff_split_meta(meta_str);
|
||||
g_free(meta_str);
|
||||
|
||||
char* stanza_id = NULL;
|
||||
char* archive_id = NULL;
|
||||
@@ -140,13 +141,15 @@ _ff_maybe_index_line(ff_contact_state_t* state, const char* buf, off_t pos)
|
||||
if (!space)
|
||||
return;
|
||||
|
||||
auto_gchar gchar* ts_str = g_strndup(buf, space - buf);
|
||||
char* ts_str = g_strndup(buf, space - buf);
|
||||
GDateTime* dt = g_date_time_new_from_iso8601(ts_str, NULL);
|
||||
if (!dt) {
|
||||
log_warning("flatfile: unparsable timestamp in %s at offset %ld: %s",
|
||||
state->filepath, (long)pos, ts_str);
|
||||
g_free(ts_str);
|
||||
return;
|
||||
}
|
||||
g_free(ts_str);
|
||||
|
||||
if (state->n_entries >= state->cap_entries) {
|
||||
state->cap_entries = state->cap_entries ? state->cap_entries * 2 : 64;
|
||||
|
||||
@@ -310,9 +310,11 @@ _verify_contact_dir(const char* cdir_path, GSList** issues)
|
||||
// identify which contact the problem belongs to (every contact has its
|
||||
// own history.log, so the bare filename is ambiguous).
|
||||
auto_gchar gchar* dir_name = g_path_get_basename(cdir_path);
|
||||
auto_gcharv gchar** parts = g_strsplit(dir_name, "_at_", -1);
|
||||
auto_gchar gchar* contact_jid = g_strjoinv("@", parts);
|
||||
auto_gchar gchar* basename = g_strdup_printf("%s/history.log", contact_jid);
|
||||
char* dir_unescaped = str_replace(dir_name, "_at_", "@");
|
||||
auto_gchar gchar* basename_owned = g_strdup_printf("%s/history.log",
|
||||
dir_unescaped ? dir_unescaped : dir_name);
|
||||
free(dir_unescaped);
|
||||
const char* basename = basename_owned;
|
||||
|
||||
_check_permissions(filepath, basename, issues);
|
||||
|
||||
|
||||
@@ -510,10 +510,10 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
||||
|
||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||
issue->level = INTEGRITY_WARNING;
|
||||
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
|
||||
from_a ? from_a : "?", to_a ? to_a : "?");
|
||||
issue->file = g_strdup("chatlog.db");
|
||||
issue->line = id_a;
|
||||
issue->message = g_strdup_printf("Timestamp out of order: row %d (%s) > row %d (%s)",
|
||||
issue->message = g_strdup_printf("Timestamp out of order in %s↔%s: row %d (%s) > row %d (%s)",
|
||||
from_a ? from_a : "?", to_a ? to_a : "?",
|
||||
id_a, ts_a ? ts_a : "NULL",
|
||||
id_b, ts_b ? ts_b : "NULL");
|
||||
issues = g_slist_append(issues, issue);
|
||||
@@ -537,10 +537,10 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
||||
|
||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||
issue->level = INTEGRITY_ERROR;
|
||||
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
|
||||
from ? from : "?", to ? to : "?");
|
||||
issue->file = g_strdup("chatlog.db");
|
||||
issue->line = id;
|
||||
issue->message = g_strdup_printf("Broken LMC reference: row %d references non-existent row %d",
|
||||
issue->message = g_strdup_printf("Broken LMC reference in %s↔%s: row %d references non-existent row %d",
|
||||
from ? from : "?", to ? to : "?",
|
||||
id, replaces_id);
|
||||
issues = g_slist_append(issues, issue);
|
||||
}
|
||||
|
||||
@@ -37,10 +37,8 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <assert.h>
|
||||
#include <glib.h>
|
||||
|
||||
#include "ai/ai_client.h"
|
||||
#include "log.h"
|
||||
#include "chatlog.h"
|
||||
#include "database.h"
|
||||
@@ -49,7 +47,7 @@
|
||||
#include "event/common.h"
|
||||
#include "plugins/plugins.h"
|
||||
#include "ui/inputwin.h"
|
||||
#include "ui/window.h"
|
||||
#include "ui/window_list.h"
|
||||
#include "xmpp/chat_session.h"
|
||||
#include "xmpp/session.h"
|
||||
#include "xmpp/xmpp.h"
|
||||
@@ -286,30 +284,3 @@ allow_unencrypted_message(ProfChatWin* chatwin, const char* const msg)
|
||||
win_println((ProfWin*)chatwin, THEME_ERROR, "-", "Message not sent: invalid encryption mode (%s). Use '/force-encryption policy resend-to-confirm'.", force_enc_mode);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
cl_ev_send_ai_msg(ProfAiWin* aiwin, const char* const message, const char* const id)
|
||||
{
|
||||
if (!aiwin || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
|
||||
if (!aiwin->session) {
|
||||
log_error("[AI] AI session not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Reset paged flag before printing user message.
|
||||
* If the user scrolled up to view history, paged=1 would suppress
|
||||
* the message in _win_printf(). Reset it here so the message displays. */
|
||||
aiwin->window.layout->paged = 0;
|
||||
aiwin->window.layout->unread_msg = 0;
|
||||
|
||||
// Display user message in AI window.
|
||||
win_print_outgoing(&aiwin->window, ">>", id, NULL, message);
|
||||
|
||||
// Send to AI provider.
|
||||
ai_send_prompt(aiwin->session, message, aiwin);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ void cl_ev_send_msg(ProfChatWin* chatwin, const char* const msg, const char* con
|
||||
void cl_ev_send_muc_msg_corrected(ProfMucWin* mucwin, const char* const msg, const char* const oob_url, gboolean correct_last_msg);
|
||||
void cl_ev_send_muc_msg(ProfMucWin* mucwin, const char* const msg, const char* const oob_url);
|
||||
void cl_ev_send_priv_msg(ProfPrivateWin* privwin, const char* const msg, const char* const oob_url);
|
||||
void cl_ev_send_ai_msg(ProfAiWin* aiwin, const char* const message, const char* const id);
|
||||
|
||||
// Checks if an unencrypted message can be sent based on encryption preferences.
|
||||
// Returns TRUE if allowed, FALSE if blocked.
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
#include "xmpp/xmpp.h"
|
||||
#include "xmpp/muc.h"
|
||||
#include "xmpp/chat_session.h"
|
||||
#include "ai/ai_client.h"
|
||||
#include "xmpp/chat_state.h"
|
||||
#include "xmpp/contact.h"
|
||||
#include "xmpp/roster_list.h"
|
||||
@@ -250,7 +249,6 @@ _init(char* log_level, char* config_file, char* log_file, char* theme_name)
|
||||
}
|
||||
session_init();
|
||||
cmd_init();
|
||||
ai_client_init();
|
||||
log_info("Initialising contact list");
|
||||
muc_init();
|
||||
tlscerts_init();
|
||||
|
||||
@@ -260,7 +260,7 @@ autocomplete_complete(Autocomplete ac, const gchar* search_str, gboolean quote,
|
||||
FREE_SET_NULL(ac->search_str);
|
||||
}
|
||||
|
||||
ac->search_str = g_strdup(search_str);
|
||||
ac->search_str = strdup(search_str);
|
||||
found = _search(ac, ac->items, quote, NEXT);
|
||||
|
||||
return found;
|
||||
@@ -305,7 +305,7 @@ autocomplete_complete(Autocomplete ac, const gchar* search_str, gboolean quote,
|
||||
// autocomplete_func func is used -> autocomplete_param_with_func
|
||||
// Autocomplete ac, gboolean quote are used -> autocomplete_param_with_ac
|
||||
static char*
|
||||
_autocomplete_param_common(const char* const input, const char* command, autocomplete_func func, Autocomplete ac, gboolean quote, gboolean previous, void* context)
|
||||
_autocomplete_param_common(const char* const input, char* command, autocomplete_func func, Autocomplete ac, gboolean quote, gboolean previous, void* context)
|
||||
{
|
||||
int len;
|
||||
auto_gchar gchar* command_cpy = g_strdup_printf("%s ", command);
|
||||
@@ -344,7 +344,7 @@ _autocomplete_param_common(const char* const input, const char* command, autocom
|
||||
}
|
||||
|
||||
char*
|
||||
autocomplete_param_with_func(const char* const input, const char* command, autocomplete_func func, gboolean previous, void* context)
|
||||
autocomplete_param_with_func(const char* const input, char* command, autocomplete_func func, gboolean previous, void* context)
|
||||
{
|
||||
return _autocomplete_param_common(input, command, func, NULL, FALSE, previous, context);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ gchar* autocomplete_complete(Autocomplete ac, const gchar* search_str, gboolean
|
||||
GList* autocomplete_create_list(Autocomplete ac);
|
||||
gint autocomplete_length(Autocomplete ac);
|
||||
|
||||
char* autocomplete_param_with_func(const char* const input, const char* command,
|
||||
char* autocomplete_param_with_func(const char* const input, char* command,
|
||||
autocomplete_func func, gboolean previous, void* context);
|
||||
|
||||
char* autocomplete_param_with_ac(const char* const input, char* command,
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* aiwin.c - AI Chat Window Management
|
||||
*
|
||||
* Provides functions for managing the AI chat window (ProfAiWin) in the
|
||||
* profanity client. This includes retrieving window strings for identification,
|
||||
* displaying AI/LLM responses in the chat view, and handling error messages
|
||||
* from AI interactions.
|
||||
*
|
||||
* Key functions:
|
||||
* aiwin_get_string() - Returns a descriptive string for the window
|
||||
* aiwin_display_response() - Displays an AI/LLM response and appends it
|
||||
* to the conversation history
|
||||
* aiwin_display_error() - Displays an error message from AI operations
|
||||
*
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "ai/ai_client.h"
|
||||
#include "log.h"
|
||||
#include "ui/ui.h"
|
||||
#include "ui/win_types.h"
|
||||
|
||||
char*
|
||||
aiwin_get_string(ProfAiWin* win)
|
||||
{
|
||||
assert(win->memcheck == PROFAIWIN_MEMCHECK);
|
||||
return g_strdup_printf("AI Chat (%s: %s)", win->session->provider_name, win->session->model);
|
||||
}
|
||||
|
||||
void
|
||||
aiwin_display_response(ProfAiWin* win, const char* response)
|
||||
{
|
||||
log_debug("[AI-WIN] aiwin_display_response ENTER: win=%p, response='%s'", (void*)win, response);
|
||||
assert(win->memcheck == PROFAIWIN_MEMCHECK);
|
||||
|
||||
if (!response) {
|
||||
log_error("[AI-WIN] FAIL: null response");
|
||||
return;
|
||||
}
|
||||
|
||||
// Display AI response
|
||||
win_println(&win->window, THEME_DEFAULT, "<< LLM:", "%s", response);
|
||||
log_debug("[AI-WIN] Displayed AI response");
|
||||
}
|
||||
|
||||
void
|
||||
aiwin_display_error(ProfAiWin* win, const char* error_msg)
|
||||
{
|
||||
log_debug("[AI-WIN] aiwin_display_error ENTER: win=%p, error='%s'", (void*)win, error_msg);
|
||||
assert(win->memcheck == PROFAIWIN_MEMCHECK);
|
||||
|
||||
const char* msg = error_msg ? error_msg : "Unknown error";
|
||||
win_println(&win->window, THEME_ERROR, "[ERROR]", "%s", msg);
|
||||
|
||||
// Also show error in console so user sees it regardless of active window
|
||||
cons_show_error("AI Error: %s", msg);
|
||||
}
|
||||
@@ -1884,12 +1884,6 @@ cons_statusbar_setting(void)
|
||||
cons_show("Show tab with no actions (/statusbar) : OFF");
|
||||
}
|
||||
|
||||
if (prefs_get_boolean(PREF_STATUSBAR_SHOW_DBBACKEND)) {
|
||||
cons_show("Show database backend (/statusbar) : ON");
|
||||
} else {
|
||||
cons_show("Show database backend (/statusbar) : OFF");
|
||||
}
|
||||
|
||||
cons_show("Max tabs (/statusbar) : %d", prefs_get_statusbartabs());
|
||||
|
||||
gint pref_len = prefs_get_statusbartablen();
|
||||
@@ -1948,12 +1942,27 @@ cons_log_setting(void)
|
||||
cons_show("Log level (/log level) : %s", level);
|
||||
}
|
||||
|
||||
void
|
||||
cons_logging_setting(void)
|
||||
{
|
||||
if (prefs_get_boolean(PREF_CHLOG))
|
||||
cons_show("Chat logging (/logging chat) : ON");
|
||||
else
|
||||
cons_show("Chat logging (/logging chat) : OFF");
|
||||
|
||||
if (prefs_get_boolean(PREF_GRLOG))
|
||||
cons_show("Groupchat logging (/logging group) : ON");
|
||||
else
|
||||
cons_show("Groupchat logging (/logging group) : OFF");
|
||||
}
|
||||
|
||||
void
|
||||
cons_show_log_prefs(void)
|
||||
{
|
||||
cons_show("Logging preferences:");
|
||||
cons_show("");
|
||||
cons_log_setting();
|
||||
cons_logging_setting();
|
||||
|
||||
cons_alert(NULL);
|
||||
}
|
||||
@@ -2847,6 +2856,11 @@ cons_privacy_setting(void)
|
||||
{
|
||||
cons_show("Database logging : %s", prefs_get_string(PREF_DBLOG));
|
||||
|
||||
if (prefs_get_boolean(PREF_CHLOG)) {
|
||||
cons_show("Chat logging (/logging chat) : ON");
|
||||
} else {
|
||||
cons_show("Chat logging (/logging chat) : OFF");
|
||||
}
|
||||
if (prefs_get_boolean(PREF_HISTORY)) {
|
||||
cons_show("Chat history (/history) : ON");
|
||||
} else {
|
||||
|
||||
@@ -609,8 +609,6 @@ _status_bar_draw_maintext(int pos)
|
||||
static int
|
||||
_status_bar_draw_dbbackend(int pos)
|
||||
{
|
||||
if (!prefs_get_boolean(PREF_STATUSBAR_SHOW_DBBACKEND))
|
||||
return pos;
|
||||
if (!active_db_backend || !active_db_backend->name)
|
||||
return pos;
|
||||
|
||||
|
||||
@@ -330,6 +330,7 @@ void cons_history_setting(void);
|
||||
void cons_carbons_setting(void);
|
||||
void cons_receipts_setting(void);
|
||||
void cons_log_setting(void);
|
||||
void cons_logging_setting(void);
|
||||
void cons_autoaway_setting(void);
|
||||
void cons_reconnect_setting(void);
|
||||
void cons_autoping_setting(void);
|
||||
@@ -385,8 +386,6 @@ ProfWin* win_create_config(const char* const title, DataForm* form, ProfConfWinC
|
||||
ProfWin* win_create_private(const char* const fulljid);
|
||||
ProfWin* win_create_plugin(const char* const plugin_name, const char* const tag);
|
||||
ProfWin* win_create_vcard(vCard* vcard);
|
||||
ProfWin* win_create_ai(AISession* session);
|
||||
ProfWin* wins_new_ai(AISession* session);
|
||||
void win_update_virtual(ProfWin* window);
|
||||
void win_free(ProfWin* window);
|
||||
gboolean win_notify_remind(ProfWin* window);
|
||||
@@ -446,9 +445,4 @@ void notify_invite(const char* const from, const char* const room, const char* c
|
||||
void notify(const char* const message, int timeout, const char* const category);
|
||||
void notify_subscription(const char* const from);
|
||||
|
||||
/* AI window */
|
||||
char* aiwin_get_string(ProfAiWin* win);
|
||||
void aiwin_display_response(ProfAiWin* win, const char* response);
|
||||
void aiwin_display_error(ProfAiWin* win, const char* error_msg);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
#define PROFXMLWIN_MEMCHECK 87333463
|
||||
#define PROFPLUGINWIN_MEMCHECK 43434777
|
||||
#define PROFVCARDWIN_MEMCHECK 68947523
|
||||
#define PROFAIWIN_MEMCHECK 91827364
|
||||
|
||||
typedef enum {
|
||||
FIELD_HIDDEN,
|
||||
@@ -145,8 +144,7 @@ typedef enum {
|
||||
WIN_PRIVATE,
|
||||
WIN_XML,
|
||||
WIN_PLUGIN,
|
||||
WIN_VCARD,
|
||||
WIN_AI
|
||||
WIN_VCARD
|
||||
} win_type_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -260,16 +258,4 @@ typedef struct prof_vcard_win_t
|
||||
unsigned long memcheck;
|
||||
} ProfVcardWin;
|
||||
|
||||
/* Forward declaration */
|
||||
typedef struct ai_session_t AISession;
|
||||
|
||||
typedef struct prof_ai_win_t
|
||||
{
|
||||
ProfWin window;
|
||||
ProfBuff message_buffer;
|
||||
GString* conversation_history;
|
||||
AISession* session;
|
||||
unsigned long memcheck;
|
||||
} ProfAiWin;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "database.h"
|
||||
#include "ui/win_types.h"
|
||||
#include "ui/window_list.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
@@ -63,7 +62,6 @@
|
||||
#include "ui/screen.h"
|
||||
#include "xmpp/xmpp.h"
|
||||
#include "xmpp/roster_list.h"
|
||||
#include "ai/ai_client.h"
|
||||
|
||||
static const char* LOADING_MESSAGE = "Loading older messages…";
|
||||
static const char* CONS_WIN_TITLE = "CProof. Type /help for help information.";
|
||||
@@ -331,22 +329,6 @@ win_create_vcard(vCard* vcard)
|
||||
return &new_win->window;
|
||||
}
|
||||
|
||||
ProfWin*
|
||||
win_create_ai(AISession* session)
|
||||
{
|
||||
ProfAiWin* new_win = g_new0(ProfAiWin, 1);
|
||||
new_win->window.type = WIN_AI;
|
||||
new_win->window.scroll_state = WIN_SCROLL_INNER;
|
||||
new_win->window.layout = _win_create_simple_layout();
|
||||
|
||||
new_win->message_buffer = buffer_create();
|
||||
new_win->conversation_history = g_string_new("");
|
||||
new_win->session = session ? ai_session_ref(session) : NULL;
|
||||
new_win->memcheck = PROFAIWIN_MEMCHECK;
|
||||
|
||||
return &new_win->window;
|
||||
}
|
||||
|
||||
gchar*
|
||||
win_get_title(ProfWin* window)
|
||||
{
|
||||
@@ -425,13 +407,6 @@ win_get_title(ProfWin* window)
|
||||
|
||||
return g_string_free(title, FALSE);
|
||||
}
|
||||
case WIN_AI:
|
||||
{
|
||||
const ProfAiWin* aiwin = (ProfAiWin*)window;
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
|
||||
return g_strdup_printf("AI Assistant (%s: %s)", aiwin->session->provider_name, aiwin->session->model);
|
||||
}
|
||||
}
|
||||
assert(FALSE);
|
||||
}
|
||||
@@ -479,10 +454,6 @@ win_get_tab_identifier(ProfWin* window)
|
||||
{
|
||||
return strdup("vcard");
|
||||
}
|
||||
case WIN_AI:
|
||||
{
|
||||
return strdup("ai");
|
||||
}
|
||||
}
|
||||
assert(FALSE);
|
||||
}
|
||||
@@ -565,12 +536,6 @@ win_to_string(ProfWin* window)
|
||||
ProfVcardWin* vcardwin = (ProfVcardWin*)window;
|
||||
return vcardwin_get_string(vcardwin);
|
||||
}
|
||||
case WIN_AI:
|
||||
{
|
||||
ProfAiWin* aiwin = (ProfAiWin*)window;
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
return aiwin_get_string(aiwin);
|
||||
}
|
||||
}
|
||||
assert(FALSE);
|
||||
}
|
||||
@@ -697,17 +662,6 @@ win_free(ProfWin* window)
|
||||
free(pluginwin->plugin_name);
|
||||
break;
|
||||
}
|
||||
case WIN_AI:
|
||||
{
|
||||
ProfAiWin* aiwin = (ProfAiWin*)window;
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
buffer_free(aiwin->message_buffer);
|
||||
g_string_free(aiwin->conversation_history, TRUE);
|
||||
if (aiwin->session) {
|
||||
ai_session_unref(aiwin->session);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -1600,7 +1554,7 @@ win_print_outgoing(ProfWin* window, const char* show_char, const char* const id,
|
||||
GDateTime* timestamp = g_date_time_new_now_local();
|
||||
|
||||
const char* myjid = connection_get_fulljid();
|
||||
if (!prefs_get_boolean(PREF_CORRECTION_ALLOW) || !_win_correct(window, message, id, replace_id, myjid)) {
|
||||
if (!_win_correct(window, message, id, replace_id, myjid)) {
|
||||
auto_gchar gchar* outgoing_str = prefs_get_string(PREF_OUTGOING_STAMP);
|
||||
_win_printf(window, show_char, 0, timestamp, 0, THEME_TEXT_ME, outgoing_str, myjid, id, "%s", message);
|
||||
}
|
||||
@@ -1747,7 +1701,7 @@ win_print_outgoing_with_receipt(ProfWin* window, const char* show_char, const ch
|
||||
receipt->received = FALSE;
|
||||
|
||||
const char* myjid = connection_get_fulljid();
|
||||
if (prefs_get_boolean(PREF_CORRECTION_ALLOW) && _win_correct(window, message, id, replace_id, myjid)) {
|
||||
if (_win_correct(window, message, id, replace_id, myjid)) {
|
||||
free(receipt); // TODO: probably we should use this in _win_correct()
|
||||
} else {
|
||||
int y_start_pos = getcury(window->layout->win);
|
||||
|
||||
@@ -44,11 +44,9 @@
|
||||
|
||||
#include "common.h"
|
||||
#include "config/preferences.h"
|
||||
#include "log.h"
|
||||
#include "config/theme.h"
|
||||
#include "plugins/plugins.h"
|
||||
#include "ui/ui.h"
|
||||
#include "ui/window.h"
|
||||
#include "ui/window_list.h"
|
||||
#include "xmpp/xmpp.h"
|
||||
#include "xmpp/roster_list.h"
|
||||
@@ -376,8 +374,6 @@ wins_set_current_by_num(int i)
|
||||
status_bar_active(1, conswin->type, "console");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log_error("Window not found for index %d", i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,20 +714,6 @@ wins_new_vcard(vCard* vcard)
|
||||
return newwin;
|
||||
}
|
||||
|
||||
ProfWin*
|
||||
wins_new_ai(AISession* session)
|
||||
{
|
||||
int result = _wins_get_next_available_num(keys);
|
||||
ProfWin* newwin = win_create_ai(session);
|
||||
if (!newwin) {
|
||||
return NULL;
|
||||
}
|
||||
_wins_htable_insert(windows, GINT_TO_POINTER(result), newwin);
|
||||
autocomplete_add(wins_ac, "ai");
|
||||
autocomplete_add(wins_close_ac, "ai");
|
||||
return newwin;
|
||||
}
|
||||
|
||||
gboolean
|
||||
wins_do_notify_remind(void)
|
||||
{
|
||||
@@ -859,7 +841,7 @@ wins_get_prune_wins(void)
|
||||
|
||||
while (curr) {
|
||||
ProfWin* window = curr->data;
|
||||
if (win_unread(window) == 0 && window->type != WIN_MUC && window->type != WIN_CONFIG && window->type != WIN_XML && window->type != WIN_CONSOLE && window->type != WIN_AI) {
|
||||
if (win_unread(window) == 0 && window->type != WIN_MUC && window->type != WIN_CONFIG && window->type != WIN_XML && window->type != WIN_CONSOLE) {
|
||||
result = g_slist_append(result, window);
|
||||
}
|
||||
curr = g_list_next(curr);
|
||||
@@ -867,27 +849,6 @@ wins_get_prune_wins(void)
|
||||
return result;
|
||||
}
|
||||
|
||||
gboolean
|
||||
wins_ai_exists(ProfAiWin* const aiwin)
|
||||
{
|
||||
if (!aiwin) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
GList* curr = values;
|
||||
while (curr) {
|
||||
ProfWin* window = curr->data;
|
||||
if (window->type == WIN_AI) {
|
||||
if ((ProfAiWin*)window == aiwin) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
curr = g_list_next(curr);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
wins_lost_connection(void)
|
||||
{
|
||||
|
||||
@@ -63,7 +63,6 @@ ProfPrivateWin* wins_get_private(const char* const fulljid);
|
||||
ProfPluginWin* wins_get_plugin(const char* const tag);
|
||||
ProfXMLWin* wins_get_xmlconsole(void);
|
||||
ProfVcardWin* wins_get_vcard(void);
|
||||
gboolean wins_ai_exists(ProfAiWin* const aiwin);
|
||||
|
||||
void wins_close_plugin(char* tag);
|
||||
|
||||
|
||||
@@ -70,12 +70,9 @@ const char*
|
||||
bench_volume_name(bench_volume_t v)
|
||||
{
|
||||
switch (v) {
|
||||
case BENCH_VOLUME_SMALL:
|
||||
return "small";
|
||||
case BENCH_VOLUME_MEDIUM:
|
||||
return "medium";
|
||||
case BENCH_VOLUME_MAX:
|
||||
return "max";
|
||||
case BENCH_VOLUME_SMALL: return "small";
|
||||
case BENCH_VOLUME_MEDIUM: return "medium";
|
||||
case BENCH_VOLUME_MAX: return "max";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ char* bench_fmt_bytes(uint64_t bytes);
|
||||
char* bench_fmt_ms(double ms);
|
||||
|
||||
// Resolve BENCH_VOLUME env: "small" / "medium" / "max" / unset.
|
||||
typedef enum {
|
||||
typedef enum
|
||||
{
|
||||
BENCH_VOLUME_SMALL, // 10k lines / 1 contact / 1 year
|
||||
BENCH_VOLUME_MEDIUM, // 500k lines / 1 contact / 5 years
|
||||
BENCH_VOLUME_MAX, // 5M lines / 1 contact / 10 years
|
||||
|
||||
@@ -105,25 +105,26 @@ db_path_for(const char* account_jid)
|
||||
// Schema (lifted from database_sqlite.c so the seeder can populate without
|
||||
// going through _sqlite_init's full machinery)
|
||||
|
||||
static const char* SCHEMA_DDL = "CREATE TABLE IF NOT EXISTS `ChatLogs` ("
|
||||
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"`from_jid` TEXT NOT NULL, "
|
||||
"`to_jid` TEXT NOT NULL, "
|
||||
"`from_resource` TEXT, `to_resource` TEXT, "
|
||||
"`message` TEXT, `timestamp` TEXT, `type` TEXT, "
|
||||
"`stanza_id` TEXT, `archive_id` TEXT, "
|
||||
"`encryption` TEXT, `marked_read` INTEGER, "
|
||||
"`replace_id` TEXT, "
|
||||
"`replaces_db_id` INTEGER, `replaced_by_db_id` INTEGER);"
|
||||
"CREATE TABLE IF NOT EXISTS `DbVersion` ("
|
||||
"`dv_id` INTEGER PRIMARY KEY, `version` INTEGER UNIQUE);"
|
||||
"INSERT OR IGNORE INTO `DbVersion` (`version`) VALUES ('2');"
|
||||
"CREATE INDEX IF NOT EXISTS ChatLogs_timestamp_IDX ON `ChatLogs` (`timestamp`);"
|
||||
"CREATE INDEX IF NOT EXISTS ChatLogs_to_from_jid_IDX ON `ChatLogs` (`to_jid`, `from_jid`);"
|
||||
"CREATE TRIGGER IF NOT EXISTS update_corrected_message "
|
||||
"AFTER INSERT ON ChatLogs FOR EACH ROW WHEN NEW.replaces_db_id IS NOT NULL "
|
||||
"BEGIN UPDATE ChatLogs SET replaced_by_db_id = NEW.id "
|
||||
"WHERE id = NEW.replaces_db_id; END;";
|
||||
static const char* SCHEMA_DDL =
|
||||
"CREATE TABLE IF NOT EXISTS `ChatLogs` ("
|
||||
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"`from_jid` TEXT NOT NULL, "
|
||||
"`to_jid` TEXT NOT NULL, "
|
||||
"`from_resource` TEXT, `to_resource` TEXT, "
|
||||
"`message` TEXT, `timestamp` TEXT, `type` TEXT, "
|
||||
"`stanza_id` TEXT, `archive_id` TEXT, "
|
||||
"`encryption` TEXT, `marked_read` INTEGER, "
|
||||
"`replace_id` TEXT, "
|
||||
"`replaces_db_id` INTEGER, `replaced_by_db_id` INTEGER);"
|
||||
"CREATE TABLE IF NOT EXISTS `DbVersion` ("
|
||||
"`dv_id` INTEGER PRIMARY KEY, `version` INTEGER UNIQUE);"
|
||||
"INSERT OR IGNORE INTO `DbVersion` (`version`) VALUES ('2');"
|
||||
"CREATE INDEX IF NOT EXISTS ChatLogs_timestamp_IDX ON `ChatLogs` (`timestamp`);"
|
||||
"CREATE INDEX IF NOT EXISTS ChatLogs_to_from_jid_IDX ON `ChatLogs` (`to_jid`, `from_jid`);"
|
||||
"CREATE TRIGGER IF NOT EXISTS update_corrected_message "
|
||||
"AFTER INSERT ON ChatLogs FOR EACH ROW WHEN NEW.replaces_db_id IS NOT NULL "
|
||||
"BEGIN UPDATE ChatLogs SET replaced_by_db_id = NEW.id "
|
||||
"WHERE id = NEW.replaces_db_id; END;";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Seed: open sqlite3 directly, build schema, batched-INSERT N rows.
|
||||
@@ -159,15 +160,14 @@ do_seed(const seed_opts_t* o)
|
||||
|
||||
if (sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, &err) != SQLITE_OK) {
|
||||
fprintf(stderr, "seed: BEGIN failed: %s\n", err);
|
||||
sqlite3_free(err);
|
||||
sqlite3_close(db);
|
||||
return 2;
|
||||
sqlite3_free(err); sqlite3_close(db); return 2;
|
||||
}
|
||||
|
||||
const char* INSERT_SQL = "INSERT INTO ChatLogs ("
|
||||
"from_jid, to_jid, from_resource, to_resource, message, timestamp, type, "
|
||||
"stanza_id, archive_id, encryption, marked_read, replace_id"
|
||||
") VALUES (?, ?, ?, ?, ?, ?, 'chat', ?, ?, 'none', -1, ?);";
|
||||
const char* INSERT_SQL =
|
||||
"INSERT INTO ChatLogs ("
|
||||
"from_jid, to_jid, from_resource, to_resource, message, timestamp, type, "
|
||||
"stanza_id, archive_id, encryption, marked_read, replace_id"
|
||||
") VALUES (?, ?, ?, ?, ?, ?, 'chat', ?, ?, 'none', -1, ?);";
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(db, INSERT_SQL, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
fprintf(stderr, "seed: prepare failed: %s\n", sqlite3_errmsg(db));
|
||||
@@ -180,8 +180,7 @@ do_seed(const seed_opts_t* o)
|
||||
int64_t span_secs = (int64_t)5 * 365 * 24 * 3600; // 5 years
|
||||
char* prev_sid_per_contact[1024] = { 0 };
|
||||
int n_contacts = o->contacts > 0 ? o->contacts : 1;
|
||||
if (n_contacts > 1024)
|
||||
n_contacts = 1024;
|
||||
if (n_contacts > 1024) n_contacts = 1024;
|
||||
|
||||
double t0 = bench_now_ms();
|
||||
|
||||
@@ -210,14 +209,10 @@ do_seed(const seed_opts_t* o)
|
||||
sqlite3_bind_text(stmt, 5, body, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 6, iso, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 7, sid, -1, SQLITE_TRANSIENT);
|
||||
if (aid)
|
||||
sqlite3_bind_text(stmt, 8, aid, -1, SQLITE_TRANSIENT);
|
||||
else
|
||||
sqlite3_bind_null(stmt, 8);
|
||||
if (is_lmc)
|
||||
sqlite3_bind_text(stmt, 9, prev_sid_per_contact[ci], -1, SQLITE_TRANSIENT);
|
||||
else
|
||||
sqlite3_bind_null(stmt, 9);
|
||||
if (aid) sqlite3_bind_text(stmt, 8, aid, -1, SQLITE_TRANSIENT);
|
||||
else sqlite3_bind_null(stmt, 8);
|
||||
if (is_lmc) sqlite3_bind_text(stmt, 9, prev_sid_per_contact[ci], -1, SQLITE_TRANSIENT);
|
||||
else sqlite3_bind_null(stmt, 9);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
fprintf(stderr, "seed: insert failed at row %" PRId64 ": %s\n",
|
||||
@@ -314,29 +309,21 @@ db_row_count(const char* db_path)
|
||||
static int64_t
|
||||
db_diff_full(const char* a_path, const char* b_path, int64_t* total_a, int64_t* total_b)
|
||||
{
|
||||
sqlite3* da = NULL;
|
||||
sqlite3* db = NULL;
|
||||
if (sqlite3_open(a_path, &da) != SQLITE_OK)
|
||||
return -1;
|
||||
if (sqlite3_open(b_path, &db) != SQLITE_OK) {
|
||||
sqlite3_close(da);
|
||||
return -1;
|
||||
}
|
||||
sqlite3* da = NULL; sqlite3* db = NULL;
|
||||
if (sqlite3_open(a_path, &da) != SQLITE_OK) return -1;
|
||||
if (sqlite3_open(b_path, &db) != SQLITE_OK) { sqlite3_close(da); return -1; }
|
||||
|
||||
const char* SQL = "SELECT IFNULL(from_jid,''), IFNULL(to_jid,''), IFNULL(message,''), "
|
||||
"IFNULL(timestamp,''), IFNULL(type,''), IFNULL(stanza_id,''), "
|
||||
"IFNULL(archive_id,''), IFNULL(encryption,''), IFNULL(replace_id,'') "
|
||||
"FROM ChatLogs ORDER BY timestamp ASC, stanza_id ASC;";
|
||||
sqlite3_stmt* sa = NULL;
|
||||
sqlite3_stmt* sb = NULL;
|
||||
const char* SQL =
|
||||
"SELECT IFNULL(from_jid,''), IFNULL(to_jid,''), IFNULL(message,''), "
|
||||
"IFNULL(timestamp,''), IFNULL(type,''), IFNULL(stanza_id,''), "
|
||||
"IFNULL(archive_id,''), IFNULL(encryption,''), IFNULL(replace_id,'') "
|
||||
"FROM ChatLogs ORDER BY timestamp ASC, stanza_id ASC;";
|
||||
sqlite3_stmt* sa = NULL; sqlite3_stmt* sb = NULL;
|
||||
if (sqlite3_prepare_v2(da, SQL, -1, &sa, NULL) != SQLITE_OK
|
||||
|| sqlite3_prepare_v2(db, SQL, -1, &sb, NULL) != SQLITE_OK) {
|
||||
if (sa)
|
||||
sqlite3_finalize(sa);
|
||||
if (sb)
|
||||
sqlite3_finalize(sb);
|
||||
sqlite3_close(da);
|
||||
sqlite3_close(db);
|
||||
if (sa) sqlite3_finalize(sa);
|
||||
if (sb) sqlite3_finalize(sb);
|
||||
sqlite3_close(da); sqlite3_close(db);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -345,16 +332,14 @@ db_diff_full(const char* a_path, const char* b_path, int64_t* total_a, int64_t*
|
||||
while (1) {
|
||||
int ra = sqlite3_step(sa);
|
||||
int rb = sqlite3_step(sb);
|
||||
if (ra != SQLITE_ROW && rb != SQLITE_ROW)
|
||||
break;
|
||||
if (ra == SQLITE_ROW)
|
||||
na++;
|
||||
if (rb == SQLITE_ROW)
|
||||
nb++;
|
||||
if (ra != SQLITE_ROW && rb != SQLITE_ROW) break;
|
||||
if (ra == SQLITE_ROW) na++;
|
||||
if (rb == SQLITE_ROW) nb++;
|
||||
if (ra != rb) {
|
||||
mismatches++;
|
||||
if (shown < 5) {
|
||||
fprintf(stderr, " diff: row count drift at ra_row=%" PRId64 " rb_row=%" PRId64 " ra=%d rb=%d\n", na, nb, ra, rb);
|
||||
fprintf(stderr, " diff: row count drift at ra_row=%" PRId64
|
||||
" rb_row=%" PRId64 " ra=%d rb=%d\n", na, nb, ra, rb);
|
||||
shown++;
|
||||
}
|
||||
continue;
|
||||
@@ -379,10 +364,8 @@ db_diff_full(const char* a_path, const char* b_path, int64_t* total_a, int64_t*
|
||||
sqlite3_finalize(sb);
|
||||
sqlite3_close(da);
|
||||
sqlite3_close(db);
|
||||
if (total_a)
|
||||
*total_a = na;
|
||||
if (total_b)
|
||||
*total_b = nb;
|
||||
if (total_a) *total_a = na;
|
||||
if (total_b) *total_b = nb;
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
@@ -416,23 +399,15 @@ cmd_seed(int argc, char** argv)
|
||||
char* db_path = NULL;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
const char* a = argv[i];
|
||||
if (strncmp(a, "--db=", 5) == 0)
|
||||
db_path = g_strdup(a + 5);
|
||||
else if (strncmp(a, "--rows=", 7) == 0)
|
||||
o.rows = strtoll(a + 7, NULL, 10);
|
||||
else if (strncmp(a, "--contacts=", 11) == 0)
|
||||
o.contacts = atoi(a + 11);
|
||||
else if (strncmp(a, "--lmc=", 6) == 0)
|
||||
o.lmc_pct = atoi(a + 6);
|
||||
else if (strncmp(a, "--body=", 7) == 0)
|
||||
o.avg_body = strtoull(a + 7, NULL, 10);
|
||||
else if (strncmp(a, "--seed=", 7) == 0)
|
||||
o.seed = strtoull(a + 7, NULL, 10);
|
||||
else if (strncmp(a, "--account=", 10) == 0)
|
||||
o.account_jid = a + 10;
|
||||
if (strncmp(a, "--db=", 5) == 0) db_path = g_strdup(a + 5);
|
||||
else if (strncmp(a, "--rows=", 7) == 0) o.rows = strtoll(a + 7, NULL, 10);
|
||||
else if (strncmp(a, "--contacts=", 11) == 0) o.contacts = atoi(a + 11);
|
||||
else if (strncmp(a, "--lmc=", 6) == 0) o.lmc_pct = atoi(a + 6);
|
||||
else if (strncmp(a, "--body=", 7) == 0) o.avg_body = strtoull(a + 7, NULL, 10);
|
||||
else if (strncmp(a, "--seed=", 7) == 0) o.seed = strtoull(a + 7, NULL, 10);
|
||||
else if (strncmp(a, "--account=", 10) == 0) o.account_jid = a + 10;
|
||||
}
|
||||
if (!db_path)
|
||||
db_path = db_path_for(o.account_jid);
|
||||
if (!db_path) db_path = db_path_for(o.account_jid);
|
||||
o.db_path = db_path;
|
||||
int r = do_seed(&o);
|
||||
g_free(db_path);
|
||||
@@ -448,14 +423,10 @@ cmd_export(int argc, char** argv)
|
||||
const char* volume = "export";
|
||||
for (int i = 0; i < argc; i++) {
|
||||
const char* a = argv[i];
|
||||
if (strncmp(a, "--account=", 10) == 0)
|
||||
account = a + 10;
|
||||
else if (strncmp(a, "--csv=", 6) == 0)
|
||||
csv = a + 6;
|
||||
else if (strncmp(a, "--label=", 8) == 0)
|
||||
label = a + 8;
|
||||
else if (strncmp(a, "--volume=", 9) == 0)
|
||||
volume = a + 9;
|
||||
if (strncmp(a, "--account=", 10) == 0) account = a + 10;
|
||||
else if (strncmp(a, "--csv=", 6) == 0) csv = a + 6;
|
||||
else if (strncmp(a, "--label=", 8) == 0) label = a + 8;
|
||||
else if (strncmp(a, "--volume=", 9) == 0) volume = a + 9;
|
||||
}
|
||||
if (!init_sqlite_backend(account))
|
||||
return 2;
|
||||
@@ -490,14 +461,10 @@ cmd_import(int argc, char** argv)
|
||||
const char* volume = "import";
|
||||
for (int i = 0; i < argc; i++) {
|
||||
const char* a = argv[i];
|
||||
if (strncmp(a, "--account=", 10) == 0)
|
||||
account = a + 10;
|
||||
else if (strncmp(a, "--csv=", 6) == 0)
|
||||
csv = a + 6;
|
||||
else if (strncmp(a, "--label=", 8) == 0)
|
||||
label = a + 8;
|
||||
else if (strncmp(a, "--volume=", 9) == 0)
|
||||
volume = a + 9;
|
||||
if (strncmp(a, "--account=", 10) == 0) account = a + 10;
|
||||
else if (strncmp(a, "--csv=", 6) == 0) csv = a + 6;
|
||||
else if (strncmp(a, "--label=", 8) == 0) label = a + 8;
|
||||
else if (strncmp(a, "--volume=", 9) == 0) volume = a + 9;
|
||||
}
|
||||
if (!init_sqlite_backend(account))
|
||||
return 2;
|
||||
@@ -540,36 +507,24 @@ cmd_roundtrip(int argc, char** argv)
|
||||
int do_diff = 0;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
const char* a = argv[i];
|
||||
if (strncmp(a, "--rows=", 7) == 0)
|
||||
rows = strtoll(a + 7, NULL, 10);
|
||||
else if (strncmp(a, "--csv=", 6) == 0)
|
||||
csv = a + 6;
|
||||
else if (strncmp(a, "--label=", 8) == 0)
|
||||
label = a + 8;
|
||||
else if (strncmp(a, "--volume=", 9) == 0)
|
||||
volume = a + 9;
|
||||
else if (strncmp(a, "--lmc=", 6) == 0)
|
||||
lmc = atoi(a + 6);
|
||||
else if (strncmp(a, "--body=", 7) == 0)
|
||||
body = strtoull(a + 7, NULL, 10);
|
||||
else if (strcmp(a, "--full-diff") == 0)
|
||||
do_diff = 1;
|
||||
if (strncmp(a, "--rows=", 7) == 0) rows = strtoll(a + 7, NULL, 10);
|
||||
else if (strncmp(a, "--csv=", 6) == 0) csv = a + 6;
|
||||
else if (strncmp(a, "--label=", 8) == 0) label = a + 8;
|
||||
else if (strncmp(a, "--volume=", 9) == 0) volume = a + 9;
|
||||
else if (strncmp(a, "--lmc=", 6) == 0) lmc = atoi(a + 6);
|
||||
else if (strncmp(a, "--body=", 7) == 0) body = strtoull(a + 7, NULL, 10);
|
||||
else if (strcmp(a, "--full-diff") == 0) do_diff = 1;
|
||||
}
|
||||
|
||||
const char* account_a = "rt-a@bench.example";
|
||||
const char* account_b = "rt-b@bench.example";
|
||||
auto_gchar gchar* db_a = db_path_for(account_a);
|
||||
auto_gchar gchar* db_b = db_path_for(account_b);
|
||||
unlink(db_a);
|
||||
unlink(db_b);
|
||||
auto_gchar gchar* wal_a = g_strdup_printf("%s-wal", db_a);
|
||||
unlink(wal_a);
|
||||
auto_gchar gchar* shm_a = g_strdup_printf("%s-shm", db_a);
|
||||
unlink(shm_a);
|
||||
auto_gchar gchar* wal_b = g_strdup_printf("%s-wal", db_b);
|
||||
unlink(wal_b);
|
||||
auto_gchar gchar* shm_b = g_strdup_printf("%s-shm", db_b);
|
||||
unlink(shm_b);
|
||||
unlink(db_a); unlink(db_b);
|
||||
auto_gchar gchar* wal_a = g_strdup_printf("%s-wal", db_a); unlink(wal_a);
|
||||
auto_gchar gchar* shm_a = g_strdup_printf("%s-shm", db_a); unlink(shm_a);
|
||||
auto_gchar gchar* wal_b = g_strdup_printf("%s-wal", db_b); unlink(wal_b);
|
||||
auto_gchar gchar* shm_b = g_strdup_printf("%s-shm", db_b); unlink(shm_b);
|
||||
|
||||
fprintf(stderr, "===== roundtrip: rows=%" PRId64 " account_a=%s account_b=%s =====\n",
|
||||
rows, account_a, account_b);
|
||||
@@ -584,13 +539,11 @@ cmd_roundtrip(int argc, char** argv)
|
||||
so.seed = 42;
|
||||
so.account_jid = account_a;
|
||||
double seed_ms_t0 = bench_now_ms();
|
||||
if (do_seed(&so) != 0)
|
||||
return 2;
|
||||
if (do_seed(&so) != 0) return 2;
|
||||
double seed_ms = bench_now_ms() - seed_ms_t0;
|
||||
|
||||
// 2) Export DB_A → flatlog under account_a
|
||||
if (!init_sqlite_backend(account_a))
|
||||
return 2;
|
||||
if (!init_sqlite_backend(account_a)) return 2;
|
||||
double export_t0 = bench_now_ms();
|
||||
int exported = log_database_export_to_flatfile(NULL);
|
||||
double export_ms = bench_now_ms() - export_t0;
|
||||
@@ -599,8 +552,7 @@ cmd_roundtrip(int argc, char** argv)
|
||||
|
||||
// 3) Cross-mount the flatlog tree at account_b's expected location
|
||||
const char* base = getenv("BENCH_DATA_DIR");
|
||||
if (!base || !base[0])
|
||||
base = "/tmp/cproof-bench-export";
|
||||
if (!base || !base[0]) base = "/tmp/cproof-bench-export";
|
||||
auto_gchar gchar* dir_a = g_strdup_printf("%s/flatlog/%s", base, ff_jid_to_dir(account_a));
|
||||
auto_gchar gchar* dir_b = g_strdup_printf("%s/flatlog/%s", base, ff_jid_to_dir(account_b));
|
||||
auto_gchar gchar* parent_b = g_path_get_dirname(dir_b);
|
||||
@@ -613,8 +565,7 @@ cmd_roundtrip(int argc, char** argv)
|
||||
}
|
||||
|
||||
// 4) Import flatlog → DB_B
|
||||
if (!init_sqlite_backend(account_b))
|
||||
return 2;
|
||||
if (!init_sqlite_backend(account_b)) return 2;
|
||||
double import_t0 = bench_now_ms();
|
||||
int imported = log_database_import_from_flatfile(NULL);
|
||||
double import_ms = bench_now_ms() - import_t0;
|
||||
@@ -632,7 +583,8 @@ cmd_roundtrip(int argc, char** argv)
|
||||
int64_t na = 0, nb = 0;
|
||||
mismatches = db_diff_full(db_a, db_b, &na, &nb);
|
||||
diff_ms = bench_now_ms() - diff_t0;
|
||||
fprintf(stderr, " full content diff: a_rows=%" PRId64 " b_rows=%" PRId64 " mismatches=%" PRId64 " (%.2fs)\n",
|
||||
fprintf(stderr, " full content diff: a_rows=%" PRId64 " b_rows=%" PRId64
|
||||
" mismatches=%" PRId64 " (%.2fs)\n",
|
||||
na, nb, mismatches, diff_ms / 1000.0);
|
||||
}
|
||||
|
||||
@@ -662,19 +614,13 @@ cmd_roundtrip(int argc, char** argv)
|
||||
static int
|
||||
cmd_verify(int argc, char** argv)
|
||||
{
|
||||
const char* a = NULL;
|
||||
const char* b = NULL;
|
||||
const char* a = NULL; const char* b = NULL;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
const char* x = argv[i];
|
||||
if (strncmp(x, "--db-a=", 7) == 0)
|
||||
a = x + 7;
|
||||
else if (strncmp(x, "--db-b=", 7) == 0)
|
||||
b = x + 7;
|
||||
}
|
||||
if (!a || !b) {
|
||||
usage();
|
||||
return 2;
|
||||
if (strncmp(x, "--db-a=", 7) == 0) a = x + 7;
|
||||
else if (strncmp(x, "--db-b=", 7) == 0) b = x + 7;
|
||||
}
|
||||
if (!a || !b) { usage(); return 2; }
|
||||
int64_t na = 0, nb = 0;
|
||||
int64_t m = db_diff_full(a, b, &na, &nb);
|
||||
fprintf(stderr, "verify: a_rows=%" PRId64 " b_rows=%" PRId64 " mismatches=%" PRId64 "\n",
|
||||
@@ -685,23 +631,15 @@ cmd_verify(int argc, char** argv)
|
||||
int
|
||||
main(int argc, char** argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
usage();
|
||||
return 2;
|
||||
}
|
||||
if (argc < 2) { usage(); return 2; }
|
||||
const char* sub = argv[1];
|
||||
int sub_argc = argc - 2;
|
||||
char** sub_argv = argv + 2;
|
||||
if (g_strcmp0(sub, "seed") == 0)
|
||||
return cmd_seed(sub_argc, sub_argv);
|
||||
if (g_strcmp0(sub, "export") == 0)
|
||||
return cmd_export(sub_argc, sub_argv);
|
||||
if (g_strcmp0(sub, "import") == 0)
|
||||
return cmd_import(sub_argc, sub_argv);
|
||||
if (g_strcmp0(sub, "roundtrip") == 0)
|
||||
return cmd_roundtrip(sub_argc, sub_argv);
|
||||
if (g_strcmp0(sub, "verify") == 0)
|
||||
return cmd_verify(sub_argc, sub_argv);
|
||||
if (g_strcmp0(sub, "seed") == 0) return cmd_seed(sub_argc, sub_argv);
|
||||
if (g_strcmp0(sub, "export") == 0) return cmd_export(sub_argc, sub_argv);
|
||||
if (g_strcmp0(sub, "import") == 0) return cmd_import(sub_argc, sub_argv);
|
||||
if (g_strcmp0(sub, "roundtrip") == 0) return cmd_roundtrip(sub_argc, sub_argv);
|
||||
if (g_strcmp0(sub, "verify") == 0) return cmd_verify(sub_argc, sub_argv);
|
||||
usage();
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
#include "config/account.h"
|
||||
#include "database_flatfile.h"
|
||||
|
||||
#define ACCOUNT_JID "fail@bench.example"
|
||||
#define CONTACT_JID "peer@bench.example"
|
||||
#define ACCOUNT_JID "fail@bench.example"
|
||||
#define CONTACT_JID "peer@bench.example"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test framework
|
||||
@@ -109,8 +109,7 @@ typedef struct
|
||||
static void
|
||||
verify_summary_free(verify_summary_t* s)
|
||||
{
|
||||
if (!s)
|
||||
return;
|
||||
if (!s) return;
|
||||
g_free(s->first_err);
|
||||
g_free(s->first_warn);
|
||||
}
|
||||
@@ -125,18 +124,15 @@ run_verify(verify_summary_t* out)
|
||||
out->total = g_slist_length(issues);
|
||||
for (GSList* l = issues; l; l = l->next) {
|
||||
integrity_issue_t* i = (integrity_issue_t*)l->data;
|
||||
if (!i)
|
||||
continue;
|
||||
if (!i) continue;
|
||||
switch (i->level) {
|
||||
case INTEGRITY_ERROR:
|
||||
out->errors++;
|
||||
if (!out->first_err)
|
||||
out->first_err = g_strdup(i->message ? i->message : "");
|
||||
if (!out->first_err) out->first_err = g_strdup(i->message ? i->message : "");
|
||||
break;
|
||||
case INTEGRITY_WARNING:
|
||||
out->warnings++;
|
||||
if (!out->first_warn)
|
||||
out->first_warn = g_strdup(i->message ? i->message : "");
|
||||
if (!out->first_warn) out->first_warn = g_strdup(i->message ? i->message : "");
|
||||
break;
|
||||
case INTEGRITY_INFO:
|
||||
out->infos++;
|
||||
@@ -152,10 +148,7 @@ report(fail_ctx_t* ctx, const char* tag, gboolean ok, double wall_ms,
|
||||
{
|
||||
fprintf(stderr, " %s %-3s %.1fms %s\n",
|
||||
ok ? "PASS" : "FAIL", tag, wall_ms, note ? note : "");
|
||||
if (ok)
|
||||
ctx->passed++;
|
||||
else
|
||||
ctx->failed++;
|
||||
if (ok) ctx->passed++; else ctx->failed++;
|
||||
if (ctx->csv_path) {
|
||||
struct stat st;
|
||||
uint64_t sz = (path && stat(path, &st) == 0) ? (uint64_t)st.st_size : 0;
|
||||
@@ -189,8 +182,7 @@ write_normal_line(FILE* fp, int seq, const char* body)
|
||||
static void
|
||||
test_F1(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F1"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F1")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F1");
|
||||
|
||||
// Write 10 normal lines; chop the trailing \n off the last.
|
||||
@@ -235,8 +227,7 @@ test_F1(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F2(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F2"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F2")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F2");
|
||||
|
||||
FILE* fp = fopen(path, "w");
|
||||
@@ -271,17 +262,14 @@ test_F2(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F3(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F3"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F3")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F3");
|
||||
|
||||
FILE* fp = fopen(path, "w");
|
||||
fprintf(fp, "%s", FLATFILE_HEADER);
|
||||
write_normal_line(fp, 0, "before bom");
|
||||
// Inject BOM bytes at the start of a line in the middle of the file.
|
||||
fputc(0xEF, fp);
|
||||
fputc(0xBB, fp);
|
||||
fputc(0xBF, fp);
|
||||
fputc(0xEF, fp); fputc(0xBB, fp); fputc(0xBF, fp);
|
||||
fprintf(fp,
|
||||
"2025-06-15T12:00:01Z [chat|none|id:after-bom] peer@bench.example/phone: after bom\n");
|
||||
write_normal_line(fp, 2, "trailing");
|
||||
@@ -308,8 +296,7 @@ test_F3(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F7(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F7"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F7")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F7");
|
||||
|
||||
// A: stanza_id="A", no replace
|
||||
@@ -350,8 +337,7 @@ test_F7(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F8(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F8"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F8")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F8");
|
||||
|
||||
FILE* fp = fopen(path, "w");
|
||||
@@ -390,8 +376,7 @@ test_F8(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F9(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F9"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F9")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F9");
|
||||
|
||||
FILE* fp = fopen(path, "w");
|
||||
@@ -427,8 +412,7 @@ test_F9(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F10(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F10"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F10")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F10");
|
||||
|
||||
FILE* fp = fopen(path, "w");
|
||||
@@ -466,8 +450,7 @@ test_F10(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F11(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F11"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F11")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F11");
|
||||
|
||||
FILE* fp = fopen(path, "w");
|
||||
@@ -505,8 +488,7 @@ test_F11(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F12(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F12"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F12")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F12");
|
||||
|
||||
FILE* fp = fopen(path, "w");
|
||||
@@ -536,8 +518,7 @@ test_F12(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F14(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F14"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F14")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F14");
|
||||
|
||||
FILE* fp = fopen(path, "w");
|
||||
@@ -582,8 +563,7 @@ test_F14(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F15(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F15"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F15")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F15");
|
||||
FILE* fp = fopen(path, "w");
|
||||
fclose(fp);
|
||||
@@ -620,8 +600,7 @@ test_F15(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F16(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F16"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F16")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F16");
|
||||
|
||||
// Write enough messages that we cross at least 3 index entries
|
||||
@@ -736,8 +715,7 @@ test_F16(fail_ctx_t* ctx)
|
||||
static void
|
||||
test_F17(fail_ctx_t* ctx)
|
||||
{
|
||||
if (!test_enabled(ctx->tests, "F17"))
|
||||
return;
|
||||
if (!test_enabled(ctx->tests, "F17")) return;
|
||||
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F17");
|
||||
|
||||
const int total_msgs = 2000;
|
||||
@@ -834,12 +812,9 @@ main(int argc, char** argv)
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
const char* a = argv[i];
|
||||
if (strncmp(a, "--tmp=", 6) == 0)
|
||||
ctx.tmp_dir = a + 6;
|
||||
else if (strncmp(a, "--csv=", 6) == 0)
|
||||
ctx.csv_path = a + 6;
|
||||
else if (strncmp(a, "--tests=", 8) == 0)
|
||||
ctx.tests = a + 8;
|
||||
if (strncmp(a, "--tmp=", 6) == 0) ctx.tmp_dir = a + 6;
|
||||
else if (strncmp(a, "--csv=", 6) == 0) ctx.csv_path = a + 6;
|
||||
else if (strncmp(a, "--tests=", 8) == 0) ctx.tests = a + 8;
|
||||
else {
|
||||
fprintf(stderr, "unknown option: %s\n", a);
|
||||
return 2;
|
||||
|
||||
@@ -68,12 +68,9 @@ parse_args(int argc, char** argv, cli_t* c)
|
||||
c->tests = "all";
|
||||
for (int i = 1; i < argc; i++) {
|
||||
const char* a = argv[i];
|
||||
if (strncmp(a, "--tmp=", 6) == 0)
|
||||
c->tmp_dir = a + 6;
|
||||
else if (strncmp(a, "--csv=", 6) == 0)
|
||||
c->csv_path = a + 6;
|
||||
else if (strncmp(a, "--tests=", 8) == 0)
|
||||
c->tests = a + 8;
|
||||
if (strncmp(a, "--tmp=", 6) == 0) c->tmp_dir = a + 6;
|
||||
else if (strncmp(a, "--csv=", 6) == 0) c->csv_path = a + 6;
|
||||
else if (strncmp(a, "--tests=", 8) == 0) c->tests = a + 8;
|
||||
else {
|
||||
fprintf(stderr, "unknown option: %s\n", a);
|
||||
return -1;
|
||||
@@ -98,7 +95,8 @@ test_enabled(const char* list, const char* name)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Body factories
|
||||
|
||||
typedef enum {
|
||||
typedef enum
|
||||
{
|
||||
PAT_FILLER, // deterministic alpha-num
|
||||
PAT_EMBEDDED_LF, // filler with newlines every 100 bytes
|
||||
PAT_EMBEDDED_PIPE, // filler with '|' sprinkled
|
||||
@@ -157,8 +155,7 @@ typedef struct
|
||||
static void
|
||||
roundtrip_cleanup(roundtrip_result_t* r)
|
||||
{
|
||||
if (!r)
|
||||
return;
|
||||
if (!r) return;
|
||||
if (r->path && r->path[0])
|
||||
unlink(r->path);
|
||||
g_free(r->path);
|
||||
@@ -170,8 +167,7 @@ write_n_messages(const char* path, int n, size_t body_len, pattern_t pat,
|
||||
int64_t* bytes_written_out)
|
||||
{
|
||||
FILE* fp = fopen(path, "w");
|
||||
if (!fp)
|
||||
return -1;
|
||||
if (!fp) return -1;
|
||||
fprintf(fp, "%s", FLATFILE_HEADER);
|
||||
|
||||
GDateTime* base = g_date_time_new_utc(2025, 6, 15, 12, 0, 0.0);
|
||||
@@ -180,7 +176,7 @@ write_n_messages(const char* path, int n, size_t body_len, pattern_t pat,
|
||||
auto_gchar gchar* iso = g_date_time_format_iso8601(t);
|
||||
g_date_time_unref(t);
|
||||
auto_gchar gchar* sid = g_strdup_printf("long-%d-%08x", i,
|
||||
(unsigned)(rand_r(&(unsigned){ i + 1 })));
|
||||
(unsigned)(rand_r(&(unsigned){i + 1})));
|
||||
auto_gchar gchar* body = make_body(body_len, pat);
|
||||
ff_write_line(fp, iso, "chat", "none",
|
||||
sid, NULL, NULL,
|
||||
@@ -204,8 +200,7 @@ read_and_validate(const char* path, size_t expected_body_len, int n,
|
||||
int64_t* parsed_out, int64_t* mismatched_out, int64_t* parse_fail_out)
|
||||
{
|
||||
FILE* fp = fopen(path, "r");
|
||||
if (!fp)
|
||||
return -1;
|
||||
if (!fp) return -1;
|
||||
ff_skip_bom(fp);
|
||||
|
||||
int64_t parsed = 0, mismatched = 0, failures = 0;
|
||||
@@ -229,12 +224,9 @@ read_and_validate(const char* path, size_t expected_body_len, int n,
|
||||
ff_parsed_line_free(pl);
|
||||
}
|
||||
fclose(fp);
|
||||
if (parsed_out)
|
||||
*parsed_out = parsed;
|
||||
if (mismatched_out)
|
||||
*mismatched_out = mismatched;
|
||||
if (parse_fail_out)
|
||||
*parse_fail_out = failures;
|
||||
if (parsed_out) *parsed_out = parsed;
|
||||
if (mismatched_out) *mismatched_out = mismatched;
|
||||
if (parse_fail_out) *parse_fail_out = failures;
|
||||
(void)n;
|
||||
return 0;
|
||||
}
|
||||
@@ -265,8 +257,7 @@ static void
|
||||
csv_emit(const char* csv_path, const char* tag, size_t body_len, int n,
|
||||
const roundtrip_result_t* r, const char* note)
|
||||
{
|
||||
if (!csv_path)
|
||||
return;
|
||||
if (!csv_path) return;
|
||||
auto_gchar gchar* full_note = g_strdup_printf(
|
||||
"n=%d body=%zu write=%.1fms read=%.1fms parsed=%" PRId64
|
||||
" mismatch=%" PRId64 " parse_fail=%" PRId64 " %s",
|
||||
@@ -346,11 +337,8 @@ run_oversized(const char* tmp, const char* csv)
|
||||
}
|
||||
ff_parsed_line_t* pl = ff_parse_line(buf);
|
||||
free(buf);
|
||||
if (pl) {
|
||||
parsed++;
|
||||
ff_parsed_line_free(pl);
|
||||
} else
|
||||
failures++;
|
||||
if (pl) { parsed++; ff_parsed_line_free(pl); }
|
||||
else failures++;
|
||||
}
|
||||
fclose(fp);
|
||||
double read_ms = bench_now_ms() - t0;
|
||||
@@ -385,8 +373,7 @@ run_pagination_with_huge(const char* tmp, const char* csv)
|
||||
int huge_indices[5] = { 950, 970, 985, 992, 998 }; // five 1MB bodies near end
|
||||
|
||||
FILE* fp = fopen(path, "w");
|
||||
if (!fp)
|
||||
return;
|
||||
if (!fp) return;
|
||||
fprintf(fp, "%s", FLATFILE_HEADER);
|
||||
GDateTime* base = g_date_time_new_utc(2025, 6, 15, 12, 0, 0.0);
|
||||
for (int i = 0; i < total; i++) {
|
||||
@@ -394,11 +381,7 @@ run_pagination_with_huge(const char* tmp, const char* csv)
|
||||
auto_gchar gchar* iso = g_date_time_format_iso8601(t);
|
||||
g_date_time_unref(t);
|
||||
size_t blen = 100;
|
||||
for (int k = 0; k < 5; k++)
|
||||
if (huge_indices[k] == i) {
|
||||
blen = MB;
|
||||
break;
|
||||
}
|
||||
for (int k = 0; k < 5; k++) if (huge_indices[k] == i) { blen = MB; break; }
|
||||
auto_gchar gchar* sid = g_strdup_printf("page-%d", i);
|
||||
auto_gchar gchar* body = make_body(blen, PAT_FILLER);
|
||||
ff_write_line(fp, iso, "chat", "none",
|
||||
@@ -430,17 +413,12 @@ run_pagination_with_huge(const char* tmp, const char* csv)
|
||||
int64_t parsed = 0;
|
||||
char* buf;
|
||||
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
||||
if (buf[0] == '\0' || buf[0] == '#') {
|
||||
free(buf);
|
||||
continue;
|
||||
}
|
||||
if (buf[0] == '\0' || buf[0] == '#') { free(buf); continue; }
|
||||
ff_parsed_line_t* pl = ff_parse_line(buf);
|
||||
free(buf);
|
||||
if (!pl)
|
||||
continue;
|
||||
if (!pl) continue;
|
||||
parsed++;
|
||||
if (ring[rpos])
|
||||
g_free(ring[rpos]);
|
||||
if (ring[rpos]) g_free(ring[rpos]);
|
||||
ring[rpos] = g_strdup(pl->message ? pl->message : "");
|
||||
rpos = (rpos + 1) % 100;
|
||||
ff_parsed_line_free(pl);
|
||||
@@ -448,8 +426,7 @@ run_pagination_with_huge(const char* tmp, const char* csv)
|
||||
fclose(fp);
|
||||
double dt = bench_now_ms() - t0;
|
||||
long rss = bench_peak_rss_kb();
|
||||
for (int i = 0; i < 100; i++)
|
||||
g_free(ring[i]);
|
||||
for (int i = 0; i < 100; i++) g_free(ring[i]);
|
||||
|
||||
fprintf(stderr,
|
||||
" L12 scrollback w/ 5×1MB in last 100 read=%.1fms parsed=%" PRId64 " rss=%ldKB\n",
|
||||
|
||||
@@ -58,7 +58,7 @@ typedef struct
|
||||
const char* csv_path;
|
||||
const char* scenarios; // comma-separated list, or "all"
|
||||
const char* account_jid;
|
||||
int extend_lines; // for S5
|
||||
int extend_lines; // for S5
|
||||
} cli_t;
|
||||
|
||||
static void
|
||||
@@ -77,16 +77,11 @@ parse_args(int argc, char** argv, cli_t* c)
|
||||
cli_default(c);
|
||||
for (int i = 1; i < argc; i++) {
|
||||
const char* a = argv[i];
|
||||
if (strncmp(a, "--data=", 7) == 0)
|
||||
c->data_dir = a + 7;
|
||||
else if (strncmp(a, "--csv=", 6) == 0)
|
||||
c->csv_path = a + 6;
|
||||
else if (strncmp(a, "--scenarios=", 12) == 0)
|
||||
c->scenarios = a + 12;
|
||||
else if (strncmp(a, "--account=", 10) == 0)
|
||||
c->account_jid = a + 10;
|
||||
else if (strncmp(a, "--extend-lines=", 15) == 0)
|
||||
c->extend_lines = atoi(a + 15);
|
||||
if (strncmp(a, "--data=", 7) == 0) c->data_dir = a + 7;
|
||||
else if (strncmp(a, "--csv=", 6) == 0) c->csv_path = a + 6;
|
||||
else if (strncmp(a, "--scenarios=", 12) == 0) c->scenarios = a + 12;
|
||||
else if (strncmp(a, "--account=", 10) == 0) c->account_jid = a + 10;
|
||||
else if (strncmp(a, "--extend-lines=", 15) == 0) c->extend_lines = atoi(a + 15);
|
||||
else {
|
||||
fprintf(stderr, "unknown option: %s\n", a);
|
||||
return -1;
|
||||
@@ -126,8 +121,7 @@ static int64_t
|
||||
count_lines(const char* path)
|
||||
{
|
||||
FILE* fp = fopen(path, "r");
|
||||
if (!fp)
|
||||
return -1;
|
||||
if (!fp) return -1;
|
||||
int64_t n = 0;
|
||||
int c;
|
||||
while ((c = fgetc(fp)) != EOF) {
|
||||
@@ -197,8 +191,7 @@ run_tail_access(const char* path, int drop_cache, long* peak_rss_kb, int64_t* li
|
||||
ff_contact_state_t* state = ff_state_new(path);
|
||||
if (!ff_state_ensure_fresh(state)) {
|
||||
ff_state_free(state);
|
||||
if (lines_read)
|
||||
*lines_read = 0;
|
||||
if (lines_read) *lines_read = 0;
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
@@ -236,8 +229,7 @@ run_tail_access(const char* path, int drop_cache, long* peak_rss_kb, int64_t* li
|
||||
if (!pl)
|
||||
continue;
|
||||
parsed++;
|
||||
if (ring[ring_pos])
|
||||
g_free(ring[ring_pos]);
|
||||
if (ring[ring_pos]) g_free(ring[ring_pos]);
|
||||
ring[ring_pos] = g_strdup(pl->message ? pl->message : "");
|
||||
ring_pos = (ring_pos + 1) % TAIL_PAGE;
|
||||
ff_parsed_line_free(pl);
|
||||
@@ -305,10 +297,8 @@ run_first_build(const char* path, long* peak_rss_kb, size_t* idx_entries)
|
||||
ff_contact_state_t* state = ff_state_new(path);
|
||||
int ok = ff_state_ensure_fresh(state);
|
||||
double dt = bench_now_ms() - t0;
|
||||
if (idx_entries)
|
||||
*idx_entries = ok ? state->n_entries : 0;
|
||||
if (peak_rss_kb)
|
||||
*peak_rss_kb = bench_peak_rss_kb();
|
||||
if (idx_entries) *idx_entries = ok ? state->n_entries : 0;
|
||||
if (peak_rss_kb) *peak_rss_kb = bench_peak_rss_kb();
|
||||
ff_state_free(state);
|
||||
return ok ? dt : -1.0;
|
||||
}
|
||||
@@ -340,8 +330,7 @@ run_incremental_extend(const char* path, int n_lines, long* peak_rss_kb,
|
||||
int64_t before_size = -1;
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0)
|
||||
before_size = st.st_size;
|
||||
if (stat(path, &st) == 0) before_size = st.st_size;
|
||||
}
|
||||
GDateTime* now = g_date_time_new_now_utc();
|
||||
for (int i = 0; i < n_lines; i++) {
|
||||
@@ -370,8 +359,7 @@ run_incremental_extend(const char* path, int n_lines, long* peak_rss_kb,
|
||||
else
|
||||
*added_bytes = -1;
|
||||
}
|
||||
if (peak_rss_kb)
|
||||
*peak_rss_kb = bench_peak_rss_kb();
|
||||
if (peak_rss_kb) *peak_rss_kb = bench_peak_rss_kb();
|
||||
|
||||
fprintf(stderr, " S5: lines before=%zu after=%zu (delta=%zu)\n",
|
||||
before_lines, state->total_lines,
|
||||
@@ -392,8 +380,7 @@ _count_level(GSList* issues, integrity_level_t level)
|
||||
int n = 0;
|
||||
for (GSList* l = issues; l; l = l->next) {
|
||||
integrity_issue_t* i = (integrity_issue_t*)l->data;
|
||||
if (i && i->level == level)
|
||||
n++;
|
||||
if (i && i->level == level) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
@@ -419,8 +406,7 @@ run_verify(long* peak_rss_kb, int64_t* total_issues, int* errors, int* warnings,
|
||||
int shown_err = 0, shown_warn = 0;
|
||||
for (GSList* l = issues; l; l = l->next) {
|
||||
integrity_issue_t* iss = (integrity_issue_t*)l->data;
|
||||
if (!iss)
|
||||
continue;
|
||||
if (!iss) continue;
|
||||
if (iss->level == INTEGRITY_ERROR && shown_err < 5) {
|
||||
fprintf(stderr, " ERR %s:%d %s\n",
|
||||
iss->file ? iss->file : "?", iss->line, iss->message ? iss->message : "");
|
||||
@@ -433,16 +419,11 @@ run_verify(long* peak_rss_kb, int64_t* total_issues, int* errors, int* warnings,
|
||||
}
|
||||
g_slist_free_full(issues, (GDestroyNotify)integrity_issue_free);
|
||||
|
||||
if (peak_rss_kb)
|
||||
*peak_rss_kb = bench_peak_rss_kb();
|
||||
if (total_issues)
|
||||
*total_issues = total;
|
||||
if (errors)
|
||||
*errors = e;
|
||||
if (warnings)
|
||||
*warnings = w;
|
||||
if (infos)
|
||||
*infos = i;
|
||||
if (peak_rss_kb) *peak_rss_kb = bench_peak_rss_kb();
|
||||
if (total_issues) *total_issues = total;
|
||||
if (errors) *errors = e;
|
||||
if (warnings) *warnings = w;
|
||||
if (infos) *infos = i;
|
||||
return dt;
|
||||
}
|
||||
|
||||
@@ -464,8 +445,7 @@ main(int argc, char** argv)
|
||||
cli.data_dir, account_dir);
|
||||
if (!g_file_test(contacts_root, G_FILE_TEST_IS_DIR)) {
|
||||
fprintf(stderr, "ERROR: %s does not exist. Run gen_history first "
|
||||
"(make sure --account matches).\n",
|
||||
contacts_root);
|
||||
"(make sure --account matches).\n", contacts_root);
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -474,8 +454,7 @@ main(int argc, char** argv)
|
||||
return 2;
|
||||
|
||||
const char* volume_name = getenv("BENCH_VOLUME");
|
||||
if (!volume_name)
|
||||
volume_name = "medium";
|
||||
if (!volume_name) volume_name = "medium";
|
||||
|
||||
fprintf(stderr, "===== bench_runner: data=%s volume=%s =====\n", cli.data_dir, volume_name);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
#include "config/files.h"
|
||||
#include "config/preferences.h"
|
||||
#include "database.h"
|
||||
#include "database_flatfile.h" // for ff_jid_to_dir() in stub
|
||||
#include "database_flatfile.h" // for ff_jid_to_dir() in stub
|
||||
#include "xmpp/xmpp.h"
|
||||
#include "xmpp/jid.h"
|
||||
#include "xmpp/message.h"
|
||||
@@ -100,50 +100,14 @@ log_error(const char* const msg, ...)
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
void
|
||||
log_init(log_level_t filter, const char* const log_file)
|
||||
{
|
||||
(void)filter;
|
||||
(void)log_file;
|
||||
}
|
||||
void
|
||||
log_close(void)
|
||||
{
|
||||
}
|
||||
void
|
||||
log_msg(log_level_t level, const char* const area, const char* const msg)
|
||||
{
|
||||
(void)level;
|
||||
(void)area;
|
||||
(void)msg;
|
||||
}
|
||||
const char*
|
||||
get_log_file_location(void)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
log_level_t
|
||||
log_get_filter(void)
|
||||
{
|
||||
return PROF_LEVEL_INFO;
|
||||
}
|
||||
int
|
||||
log_level_from_string(char* log_level, log_level_t* level)
|
||||
{
|
||||
(void)log_level;
|
||||
if (level)
|
||||
*level = PROF_LEVEL_INFO;
|
||||
return 0;
|
||||
}
|
||||
void
|
||||
log_stderr_init(log_level_t level)
|
||||
{
|
||||
(void)level;
|
||||
}
|
||||
void
|
||||
log_stderr_handler(void)
|
||||
{
|
||||
}
|
||||
void log_init(log_level_t filter, const char* const log_file) { (void)filter; (void)log_file; }
|
||||
void log_close(void) {}
|
||||
void log_msg(log_level_t level, const char* const area, const char* const msg) { (void)level; (void)area; (void)msg; }
|
||||
const char* get_log_file_location(void) { return ""; }
|
||||
log_level_t log_get_filter(void) { return PROF_LEVEL_INFO; }
|
||||
int log_level_from_string(char* log_level, log_level_t* level) { (void)log_level; if (level) *level = PROF_LEVEL_INFO; return 0; }
|
||||
void log_stderr_init(log_level_t level) { (void)level; }
|
||||
void log_stderr_handler(void) {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// prefs / files / xmpp / ui — never reached by the harness, but database_flatfile.c
|
||||
|
||||
@@ -57,14 +57,16 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI options
|
||||
|
||||
typedef enum {
|
||||
typedef enum
|
||||
{
|
||||
SID_UUID,
|
||||
SID_LIBPURPLE,
|
||||
SID_CONVERSATIONS,
|
||||
SID_MIXED,
|
||||
} sid_mode_t;
|
||||
|
||||
typedef enum {
|
||||
typedef enum
|
||||
{
|
||||
LEN_SHORT,
|
||||
LEN_MIXED,
|
||||
LEN_LONG,
|
||||
@@ -107,44 +109,20 @@ opts_default(opts_t* o)
|
||||
static int
|
||||
parse_sid_mode(const char* s, sid_mode_t* out)
|
||||
{
|
||||
if (g_strcmp0(s, "uuid") == 0) {
|
||||
*out = SID_UUID;
|
||||
return 0;
|
||||
}
|
||||
if (g_strcmp0(s, "libpurple") == 0) {
|
||||
*out = SID_LIBPURPLE;
|
||||
return 0;
|
||||
}
|
||||
if (g_strcmp0(s, "conversations") == 0) {
|
||||
*out = SID_CONVERSATIONS;
|
||||
return 0;
|
||||
}
|
||||
if (g_strcmp0(s, "mixed") == 0) {
|
||||
*out = SID_MIXED;
|
||||
return 0;
|
||||
}
|
||||
if (g_strcmp0(s, "uuid") == 0) { *out = SID_UUID; return 0; }
|
||||
if (g_strcmp0(s, "libpurple") == 0) { *out = SID_LIBPURPLE; return 0; }
|
||||
if (g_strcmp0(s, "conversations") == 0) { *out = SID_CONVERSATIONS; return 0; }
|
||||
if (g_strcmp0(s, "mixed") == 0) { *out = SID_MIXED; return 0; }
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
parse_len_profile(const char* s, len_profile_t* out)
|
||||
{
|
||||
if (g_strcmp0(s, "short") == 0) {
|
||||
*out = LEN_SHORT;
|
||||
return 0;
|
||||
}
|
||||
if (g_strcmp0(s, "mixed") == 0) {
|
||||
*out = LEN_MIXED;
|
||||
return 0;
|
||||
}
|
||||
if (g_strcmp0(s, "long") == 0) {
|
||||
*out = LEN_LONG;
|
||||
return 0;
|
||||
}
|
||||
if (g_strcmp0(s, "extreme") == 0) {
|
||||
*out = LEN_EXTREME;
|
||||
return 0;
|
||||
}
|
||||
if (g_strcmp0(s, "short") == 0) { *out = LEN_SHORT; return 0; }
|
||||
if (g_strcmp0(s, "mixed") == 0) { *out = LEN_MIXED; return 0; }
|
||||
if (g_strcmp0(s, "long") == 0) { *out = LEN_LONG; return 0; }
|
||||
if (g_strcmp0(s, "extreme") == 0) { *out = LEN_EXTREME; return 0; }
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -226,26 +204,8 @@ print_help(void)
|
||||
// Body / stanza-id / resource generators (deterministic)
|
||||
|
||||
static const char* MSG_BANK_TINY[] = {
|
||||
"ok",
|
||||
"yes",
|
||||
"no",
|
||||
"ack",
|
||||
"thx",
|
||||
"ttyl",
|
||||
"k",
|
||||
"lol",
|
||||
"+1",
|
||||
"yep",
|
||||
"nope",
|
||||
"sure",
|
||||
"got it",
|
||||
"great",
|
||||
"afk",
|
||||
"brb",
|
||||
"bye",
|
||||
"hi",
|
||||
"hello",
|
||||
"hey",
|
||||
"ok", "yes", "no", "ack", "thx", "ttyl", "k", "lol", "+1", "yep", "nope",
|
||||
"sure", "got it", "great", "afk", "brb", "bye", "hi", "hello", "hey",
|
||||
};
|
||||
static const int MSG_BANK_TINY_N = sizeof(MSG_BANK_TINY) / sizeof(MSG_BANK_TINY[0]);
|
||||
|
||||
@@ -305,35 +265,24 @@ pick_body_len(uint64_t* rng, len_profile_t profile)
|
||||
return 5 + (xorshift64(rng) % 96);
|
||||
}
|
||||
if (profile == LEN_LONG) {
|
||||
if (r < 200)
|
||||
return 5 + (xorshift64(rng) % 96);
|
||||
if (r < 600)
|
||||
return 100 + (xorshift64(rng) % 400);
|
||||
if (r < 850)
|
||||
return 1000 + (xorshift64(rng) % 9000); // 1-10 KB
|
||||
if (r < 980)
|
||||
return 10000 + (xorshift64(rng) % 90000); // 10-100 KB
|
||||
return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
|
||||
if (r < 200) return 5 + (xorshift64(rng) % 96);
|
||||
if (r < 600) return 100 + (xorshift64(rng) % 400);
|
||||
if (r < 850) return 1000 + (xorshift64(rng) % 9000); // 1-10 KB
|
||||
if (r < 980) return 10000 + (xorshift64(rng) % 90000); // 10-100 KB
|
||||
return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
|
||||
}
|
||||
if (profile == LEN_EXTREME) {
|
||||
if (r < 100)
|
||||
return 5 + (xorshift64(rng) % 96);
|
||||
if (r < 400)
|
||||
return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
|
||||
if (r < 800)
|
||||
return 1000000 + (xorshift64(rng) % 4000000); // 1-5 MB
|
||||
return 5000000 + (xorshift64(rng) % 4000000); // 5-9 MB
|
||||
if (r < 100) return 5 + (xorshift64(rng) % 96);
|
||||
if (r < 400) return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
|
||||
if (r < 800) return 1000000 + (xorshift64(rng) % 4000000); // 1-5 MB
|
||||
return 5000000 + (xorshift64(rng) % 4000000); // 5-9 MB
|
||||
}
|
||||
// LEN_MIXED — realistic distribution
|
||||
if (r < 500)
|
||||
return 5 + (xorshift64(rng) % 96);
|
||||
if (r < 900)
|
||||
return 100 + (xorshift64(rng) % 400);
|
||||
if (r < 980)
|
||||
return 500 + (xorshift64(rng) % 4500); // 500B-5KB
|
||||
if (r < 995)
|
||||
return 5000 + (xorshift64(rng) % 95000); // 5-100 KB
|
||||
return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
|
||||
if (r < 500) return 5 + (xorshift64(rng) % 96);
|
||||
if (r < 900) return 100 + (xorshift64(rng) % 400);
|
||||
if (r < 980) return 500 + (xorshift64(rng) % 4500); // 500B-5KB
|
||||
if (r < 995) return 5000 + (xorshift64(rng) % 95000); // 5-100 KB
|
||||
return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
|
||||
}
|
||||
|
||||
static char*
|
||||
@@ -392,15 +341,13 @@ make_stanza_id(uint64_t* rng, sid_mode_t mode, int64_t global_seq, int contact_i
|
||||
sid_mode_t effective = mode;
|
||||
if (mode == SID_MIXED) {
|
||||
int r = (int)(xorshift64(rng) % 3);
|
||||
effective = (r == 0) ? SID_UUID : (r == 1) ? SID_LIBPURPLE
|
||||
: SID_CONVERSATIONS;
|
||||
effective = (r == 0) ? SID_UUID : (r == 1) ? SID_LIBPURPLE : SID_CONVERSATIONS;
|
||||
}
|
||||
switch (effective) {
|
||||
case SID_LIBPURPLE:
|
||||
// Per-contact incremental — collides across contacts (intentional, mirrors libpurple).
|
||||
return g_strdup_printf("%" PRId64, global_seq);
|
||||
case SID_CONVERSATIONS:
|
||||
{
|
||||
case SID_CONVERSATIONS: {
|
||||
// Conversations-style: 22 base32-ish chars
|
||||
static const char alpha[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
char buf[24];
|
||||
@@ -411,8 +358,7 @@ make_stanza_id(uint64_t* rng, sid_mode_t mode, int64_t global_seq, int contact_i
|
||||
}
|
||||
case SID_UUID:
|
||||
case SID_MIXED:
|
||||
default:
|
||||
{
|
||||
default: {
|
||||
// RFC4122-ish UUID v4 (deterministic from RNG, not cryptographic)
|
||||
uint64_t a = xorshift64(rng);
|
||||
uint64_t b = xorshift64(rng);
|
||||
@@ -433,12 +379,12 @@ make_stanza_id(uint64_t* rng, sid_mode_t mode, int64_t global_seq, int contact_i
|
||||
typedef struct
|
||||
{
|
||||
char* jid;
|
||||
char** resources; // n = opts.resources_per_contact
|
||||
char** resources; // n = opts.resources_per_contact
|
||||
int n_resources;
|
||||
char* dir;
|
||||
char* path;
|
||||
FILE* fp;
|
||||
int64_t seq; // libpurple-style counter
|
||||
int64_t seq; // libpurple-style counter
|
||||
int64_t lines_written;
|
||||
int64_t bytes_written;
|
||||
char* last_stanza_id; // for LMC corrections
|
||||
|
||||
@@ -59,21 +59,9 @@
|
||||
#ifdef HAVE_SQLITE
|
||||
#include "test_export_import.h"
|
||||
#endif
|
||||
#include "test_ai.h"
|
||||
|
||||
/* Macro to wrap each test with setup/teardown functions */
|
||||
#define PROF_FUNC_TEST(test) \
|
||||
{ \
|
||||
#test, test, init_prof_test, close_prof_test, #test \
|
||||
}
|
||||
|
||||
/* AI tests use a custom init that primes stabber via prof_connect so
|
||||
* stbbr_stop() in close_prof_test() doesn't hang on a never-connected
|
||||
* server thread. See ai_init_test() in test_ai.c. */
|
||||
#define PROF_FUNC_TEST_AI(test) \
|
||||
{ \
|
||||
#test, test, ai_init_test, close_prof_test, #test \
|
||||
}
|
||||
#define PROF_FUNC_TEST(test) { #test, test, init_prof_test, close_prof_test, #test }
|
||||
|
||||
#ifdef HAVE_SQLITE
|
||||
/* Custom init that sets a non-UTC timezone (POSIX TST-3 = UTC+3) */
|
||||
@@ -93,9 +81,9 @@ main(int argc, char* argv[])
|
||||
int group = 0; /* 0 = all groups */
|
||||
if (argc > 1) {
|
||||
group = atoi(argv[1]);
|
||||
if (group < 1 || group > 5) {
|
||||
if (group < 1 || group > 4) {
|
||||
fprintf(stderr, "Usage: %s [group]\n", argv[0]);
|
||||
fprintf(stderr, " group: 1-5 to run specific group, or omit for all\n");
|
||||
fprintf(stderr, " group: 1-4 to run specific group, or omit for all\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -316,31 +304,6 @@ main(int argc, char* argv[])
|
||||
#endif
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* GROUP 5: AI command surface (console + HTTP stub)
|
||||
* Standalone because AI tests don't use stabber's XMPP path; mixing
|
||||
* them with XMPP-bound tests in the same group poisons stabber state
|
||||
* between fixture init/teardown cycles and hangs subsequent
|
||||
* prof_connect() calls.
|
||||
* ============================================================ */
|
||||
const struct CMUnitTest group5_tests[] = {
|
||||
/* Console-only — no network calls */
|
||||
PROF_FUNC_TEST_AI(ai_no_args_shows_help),
|
||||
PROF_FUNC_TEST_AI(ai_providers_lists_defaults),
|
||||
PROF_FUNC_TEST_AI(ai_providers_list_shows_key_status),
|
||||
PROF_FUNC_TEST_AI(ai_set_provider_adds_custom),
|
||||
PROF_FUNC_TEST_AI(ai_set_token_marks_key_set),
|
||||
PROF_FUNC_TEST_AI(ai_start_unknown_provider_errors),
|
||||
PROF_FUNC_TEST_AI(ai_start_without_key_errors),
|
||||
PROF_FUNC_TEST_AI(ai_start_with_key_opens_window),
|
||||
PROF_FUNC_TEST_AI(ai_clear_without_window_errors),
|
||||
PROF_FUNC_TEST_AI(ai_remove_provider_works),
|
||||
PROF_FUNC_TEST_AI(ai_remove_provider_unknown_errors),
|
||||
PROF_FUNC_TEST_AI(ai_set_default_model_updates_provider),
|
||||
PROF_FUNC_TEST_AI(ai_switch_without_window_errors),
|
||||
PROF_FUNC_TEST_AI(ai_bad_subcommand_shows_usage),
|
||||
};
|
||||
|
||||
/* Test group registry for easy extension */
|
||||
struct
|
||||
{
|
||||
@@ -352,7 +315,6 @@ main(int argc, char* argv[])
|
||||
{ "Group 2: ExportImport/Receipts", group2_tests, ARRAY_SIZE(group2_tests) },
|
||||
{ "Group 3: Presence/Disconnect/DBHistory/Message/MUC-DB", group3_tests, ARRAY_SIZE(group3_tests) },
|
||||
{ "Group 4: Session/MUC/Carbons/MigrationStress", group4_tests, ARRAY_SIZE(group4_tests) },
|
||||
{ "Group 5: AI command surface (console only)", group5_tests, ARRAY_SIZE(group5_tests) },
|
||||
};
|
||||
const int num_groups = ARRAY_SIZE(groups);
|
||||
|
||||
|
||||
@@ -20,47 +20,47 @@
|
||||
#include "proftest.h"
|
||||
|
||||
/* Number of parallel test groups for CI builds */
|
||||
#define TEST_GROUPS 5
|
||||
#define TEST_GROUPS 4
|
||||
|
||||
/* Number of ports reserved per test group; allows skipping TIME_WAIT ports */
|
||||
#define PORTS_PER_GROUP 50
|
||||
|
||||
/* Base port and fallback scan range for stabber */
|
||||
#define BASE_PORT 5230
|
||||
#define FALLBACK_PORT_RANGE (TEST_GROUPS * PORTS_PER_GROUP)
|
||||
#define BASE_PORT 5230
|
||||
#define FALLBACK_PORT_RANGE (TEST_GROUPS * PORTS_PER_GROUP)
|
||||
|
||||
/* Shutdown polling: interval and maximum attempts before escalating signals */
|
||||
#define MS_TO_US 1000
|
||||
#define SHUTDOWN_POLL_MS 50
|
||||
#define SHUTDOWN_POLL_MAX 100 /* 100 x 50ms = 5s */
|
||||
#define SIGTERM_POLL_MAX 20 /* 20 x 50ms = 1s */
|
||||
#define QUIT_GRACE_MS 100 /* grace for /quit to be read from pty */
|
||||
#define INIT_WAIT_MS 50 /* wait for child process to initialize */
|
||||
#define INPUT_DELAY_MS 10 /* let profanity process input */
|
||||
#define OUTPUT_POLL_MS 50 /* polling interval for output checks */
|
||||
#define READ_TIMEOUT_MS 100 /* select() timeout for pty reads */
|
||||
#define MS_TO_US 1000
|
||||
#define SHUTDOWN_POLL_MS 50
|
||||
#define SHUTDOWN_POLL_MAX 100 /* 100 x 50ms = 5s */
|
||||
#define SIGTERM_POLL_MAX 20 /* 20 x 50ms = 1s */
|
||||
#define QUIT_GRACE_MS 100 /* grace for /quit to be read from pty */
|
||||
#define INIT_WAIT_MS 50 /* wait for child process to initialize */
|
||||
#define INPUT_DELAY_MS 10 /* let profanity process input */
|
||||
#define OUTPUT_POLL_MS 50 /* polling interval for output checks */
|
||||
#define READ_TIMEOUT_MS 100 /* select() timeout for pty reads */
|
||||
|
||||
/* Expect timeouts (seconds) */
|
||||
#define EXPECT_TIMEOUT_DEFAULT 30
|
||||
#define EXPECT_TIMEOUT_CONNECT 60
|
||||
#define EXPECT_TIMEOUT_DEFAULT 30
|
||||
#define EXPECT_TIMEOUT_CONNECT 60
|
||||
|
||||
/* Test duration thresholds (seconds) */
|
||||
#define SLOW_TEST_THRESHOLD_S 20
|
||||
#define FAST_TEST_THRESHOLD_S 0.3
|
||||
#define SLOW_TEST_THRESHOLD_S 20
|
||||
#define FAST_TEST_THRESHOLD_S 0.3
|
||||
|
||||
/* Sentinel "infinity" for min-elapsed tracking (any real test is faster) */
|
||||
#define MIN_ELAPSED_INIT 1e9
|
||||
#define MIN_ELAPSED_INIT 1e9
|
||||
|
||||
/* Terminal dimensions for forkpty */
|
||||
#define PTY_ROWS 24
|
||||
#define PTY_COLS 300
|
||||
#define PTY_ROWS 24
|
||||
#define PTY_COLS 300
|
||||
|
||||
/* Preprocessor stringification (for setenv from numeric #define) */
|
||||
#define STRINGIFY_(x) #x
|
||||
#define STRINGIFY(x) STRINGIFY_(x)
|
||||
|
||||
char* config_orig;
|
||||
char* data_orig;
|
||||
char *config_orig;
|
||||
char *data_orig;
|
||||
|
||||
int fd = 0;
|
||||
int stub_port = BASE_PORT;
|
||||
@@ -87,19 +87,19 @@ static int expect_timeout = EXPECT_TIMEOUT_DEFAULT;
|
||||
|
||||
/* Per-test wall-clock timer */
|
||||
static struct timespec test_start_ts;
|
||||
static const char* current_test_name;
|
||||
static const char *current_test_name;
|
||||
static double min_elapsed = MIN_ELAPSED_INIT;
|
||||
static const char* min_test_name = "(none)";
|
||||
static const char *min_test_name = "(none)";
|
||||
|
||||
/* Per-test threshold overrides (0 = use default) */
|
||||
double prof_test_slow_threshold = 0;
|
||||
double prof_test_fast_threshold = 0;
|
||||
|
||||
/* Timezone for profanity child process (see proftest.h) */
|
||||
const char* prof_test_tz = "UTC";
|
||||
const char *prof_test_tz = "UTC";
|
||||
|
||||
gboolean
|
||||
_create_dir(const char* name)
|
||||
_create_dir(const char *name)
|
||||
{
|
||||
struct stat sb;
|
||||
|
||||
@@ -117,14 +117,14 @@ _create_dir(const char* name)
|
||||
}
|
||||
|
||||
gboolean
|
||||
_mkdir_recursive(const char* dir)
|
||||
_mkdir_recursive(const char *dir)
|
||||
{
|
||||
int i;
|
||||
gboolean result = TRUE;
|
||||
|
||||
for (i = 1; i <= strlen(dir); i++) {
|
||||
if (dir[i] == '/' || dir[i] == '\0') {
|
||||
gchar* next_dir = g_strndup(dir, i);
|
||||
gchar *next_dir = g_strndup(dir, i);
|
||||
result = _create_dir(next_dir);
|
||||
g_free(next_dir);
|
||||
if (!result) {
|
||||
@@ -139,7 +139,7 @@ _mkdir_recursive(const char* dir)
|
||||
void
|
||||
_create_config_dir(void)
|
||||
{
|
||||
GString* profanity_dir = g_string_new(xdg_config_home);
|
||||
GString *profanity_dir = g_string_new(xdg_config_home);
|
||||
g_string_append(profanity_dir, "/profanity");
|
||||
|
||||
if (!_mkdir_recursive(profanity_dir->str)) {
|
||||
@@ -152,7 +152,7 @@ _create_config_dir(void)
|
||||
void
|
||||
_create_data_dir(void)
|
||||
{
|
||||
GString* profanity_dir = g_string_new(xdg_data_home);
|
||||
GString *profanity_dir = g_string_new(xdg_data_home);
|
||||
g_string_append(profanity_dir, "/profanity");
|
||||
|
||||
if (!_mkdir_recursive(profanity_dir->str)) {
|
||||
@@ -165,7 +165,7 @@ _create_data_dir(void)
|
||||
void
|
||||
_create_chatlogs_dir(void)
|
||||
{
|
||||
GString* chatlogs_dir = g_string_new(xdg_data_home);
|
||||
GString *chatlogs_dir = g_string_new(xdg_data_home);
|
||||
g_string_append(chatlogs_dir, "/profanity/chatlogs");
|
||||
|
||||
if (!_mkdir_recursive(chatlogs_dir->str)) {
|
||||
@@ -178,7 +178,7 @@ _create_chatlogs_dir(void)
|
||||
void
|
||||
_create_logs_dir(void)
|
||||
{
|
||||
GString* logs_dir = g_string_new(xdg_data_home);
|
||||
GString *logs_dir = g_string_new(xdg_data_home);
|
||||
g_string_append(logs_dir, "/profanity/logs");
|
||||
|
||||
if (!_mkdir_recursive(logs_dir->str)) {
|
||||
@@ -191,12 +191,12 @@ _create_logs_dir(void)
|
||||
void
|
||||
_cleanup_dirs(void)
|
||||
{
|
||||
const char* group_env = getenv("PROF_TEST_GROUP");
|
||||
const char *group_env = getenv("PROF_TEST_GROUP");
|
||||
int group = group_env ? atoi(group_env) : 0;
|
||||
int dir_id = (group >= 1 && group <= TEST_GROUPS) ? group : stub_port;
|
||||
|
||||
|
||||
printf("[PROF_TEST] Cleaning up directories for group %d (dir_id %d)\n", group, dir_id);
|
||||
|
||||
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf ./test-files/%d", dir_id);
|
||||
int res = system(cmd);
|
||||
@@ -220,26 +220,26 @@ _read_output(int timeout_ms)
|
||||
{
|
||||
fd_set readfds;
|
||||
struct timeval tv;
|
||||
|
||||
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(fd, &readfds);
|
||||
|
||||
|
||||
tv.tv_sec = timeout_ms / MS_TO_US;
|
||||
tv.tv_usec = (timeout_ms % MS_TO_US) * MS_TO_US;
|
||||
|
||||
|
||||
int ret = select(fd + 1, &readfds, NULL, NULL, &tv);
|
||||
if (ret <= 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
size_t space = OUTPUT_BUF_SIZE - output_len - 1;
|
||||
if (space <= 0) {
|
||||
/* Buffer full, shift content */
|
||||
memmove(output_buffer, output_buffer + OUTPUT_BUF_SIZE / 2, OUTPUT_BUF_SIZE / 2);
|
||||
output_len = OUTPUT_BUF_SIZE / 2;
|
||||
memmove(output_buffer, output_buffer + OUTPUT_BUF_SIZE/2, OUTPUT_BUF_SIZE/2);
|
||||
output_len = OUTPUT_BUF_SIZE/2;
|
||||
space = OUTPUT_BUF_SIZE - output_len - 1;
|
||||
}
|
||||
|
||||
|
||||
ssize_t n = read(fd, output_buffer + output_len, space);
|
||||
if (n > 0) {
|
||||
output_len += n;
|
||||
@@ -260,7 +260,7 @@ prof_start(void)
|
||||
ws.ws_col = PTY_COLS;
|
||||
ws.ws_xpixel = 0;
|
||||
ws.ws_ypixel = 0;
|
||||
|
||||
|
||||
/* Reset output buffer */
|
||||
output_len = 0;
|
||||
output_buffer[0] = '\0';
|
||||
@@ -272,48 +272,48 @@ prof_start(void)
|
||||
tzset();
|
||||
|
||||
child_pid = forkpty(&fd, NULL, NULL, &ws);
|
||||
|
||||
|
||||
if (child_pid < 0) {
|
||||
fd = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (child_pid == 0) {
|
||||
/* Child process */
|
||||
setenv("COLUMNS", STRINGIFY(PTY_COLS), 1);
|
||||
setenv("TERM", "xterm", 1);
|
||||
|
||||
|
||||
execl("./profanity", "./profanity", "-l", "DEBUG", NULL);
|
||||
|
||||
|
||||
/* If exec fails */
|
||||
fprintf(stderr, "execl failed: %s\n", strerror(errno));
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
|
||||
/* Parent process */
|
||||
/* Set non-blocking mode for reading */
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
|
||||
/* Brief wait for process to initialize */
|
||||
sleep_ms(INIT_WAIT_MS);
|
||||
}
|
||||
|
||||
int
|
||||
init_prof_test(void** state)
|
||||
init_prof_test(void **state)
|
||||
{
|
||||
clock_gettime(CLOCK_MONOTONIC, &test_start_ts);
|
||||
current_test_name = state && *state ? (const char*)*state : "unknown";
|
||||
current_test_name = state && *state ? (const char *)*state : "unknown";
|
||||
prof_test_slow_threshold = 0;
|
||||
prof_test_fast_threshold = 0;
|
||||
/* Get test group from environment for static resource allocation */
|
||||
const char* group_env = getenv("PROF_TEST_GROUP");
|
||||
const char *group_env = getenv("PROF_TEST_GROUP");
|
||||
int group = group_env ? atoi(group_env) : 0;
|
||||
|
||||
|
||||
/* Get build index for port offset (for parallel CI builds) */
|
||||
const char* build_env = getenv("PROF_BUILD_INDEX");
|
||||
const char *build_env = getenv("PROF_BUILD_INDEX");
|
||||
int build_idx = build_env ? atoi(build_env) : 0;
|
||||
|
||||
|
||||
/* Calculate port base: each build uses a different range of
|
||||
* TEST_GROUPS * PORTS_PER_GROUP ports.
|
||||
* Build 0 (local/default): 5230-5429, Full: 5230-5429, Minimal: 5430-5629, etc.
|
||||
@@ -321,7 +321,7 @@ init_prof_test(void** state)
|
||||
* or sequential run (no parallel builds), while Full/Minimal/NoEncrypt/Default
|
||||
* are used in CI where they run in parallel. */
|
||||
int port_base = BASE_PORT + ((build_idx > 0 ? build_idx - 1 : 0) * TEST_GROUPS * PORTS_PER_GROUP);
|
||||
|
||||
|
||||
/* Each group gets a dedicated range of ports for parallel execution.
|
||||
* Group 1: port_base..port_base+PORTS_PER_GROUP-1
|
||||
* Group 2: port_base+PORTS_PER_GROUP..port_base+2*PORTS_PER_GROUP-1, etc.
|
||||
@@ -353,7 +353,7 @@ init_prof_test(void** state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!started) {
|
||||
fprintf(stderr, "[PROF_TEST] ERROR: could not start stabber on any port\n");
|
||||
return -1;
|
||||
@@ -366,8 +366,8 @@ init_prof_test(void** state)
|
||||
"./test-files/%d/xdg_config_home", dir_id);
|
||||
snprintf(xdg_data_home, sizeof(xdg_data_home),
|
||||
"./test-files/%d/xdg_data_home", dir_id);
|
||||
|
||||
printf("[PROF_TEST] Group %d using directories: config=%s, data=%s\n",
|
||||
|
||||
printf("[PROF_TEST] Group %d using directories: config=%s, data=%s\n",
|
||||
group, xdg_config_home, xdg_data_home);
|
||||
|
||||
config_orig = getenv("XDG_CONFIG_HOME");
|
||||
@@ -418,7 +418,7 @@ init_prof_test(void** state)
|
||||
}
|
||||
|
||||
int
|
||||
close_prof_test(void** state)
|
||||
close_prof_test(void **state)
|
||||
{
|
||||
if (fd > 0 && child_pid > 0) {
|
||||
prof_input("/quit");
|
||||
@@ -439,10 +439,7 @@ close_prof_test(void** state)
|
||||
if (!exited) {
|
||||
kill(child_pid, SIGTERM);
|
||||
for (int i = 0; i < SIGTERM_POLL_MAX; i++) {
|
||||
if (waitpid(child_pid, NULL, WNOHANG) != 0) {
|
||||
exited = 1;
|
||||
break;
|
||||
}
|
||||
if (waitpid(child_pid, NULL, WNOHANG) != 0) { exited = 1; break; }
|
||||
sleep_ms(SHUTDOWN_POLL_MS);
|
||||
}
|
||||
if (!exited) {
|
||||
@@ -465,7 +462,7 @@ close_prof_test(void** state)
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
double elapsed = (now.tv_sec - test_start_ts.tv_sec)
|
||||
+ (now.tv_nsec - test_start_ts.tv_nsec) / 1e9;
|
||||
+ (now.tv_nsec - test_start_ts.tv_nsec) / 1e9;
|
||||
if (elapsed < min_elapsed) {
|
||||
min_elapsed = elapsed;
|
||||
min_test_name = current_test_name;
|
||||
@@ -486,14 +483,14 @@ close_prof_test(void** state)
|
||||
}
|
||||
|
||||
void
|
||||
prof_input(const char* input)
|
||||
prof_input(const char *input)
|
||||
{
|
||||
GString* inp_str = g_string_new(input);
|
||||
GString *inp_str = g_string_new(input);
|
||||
g_string_append(inp_str, "\r");
|
||||
ssize_t _wn = write(fd, inp_str->str, inp_str->len);
|
||||
(void)_wn;
|
||||
g_string_free(inp_str, TRUE);
|
||||
|
||||
|
||||
/* Small delay to let profanity process input */
|
||||
sleep_ms(INPUT_DELAY_MS);
|
||||
}
|
||||
@@ -503,24 +500,24 @@ prof_input(const char* input)
|
||||
* Returns 1 if found, 0 if timeout.
|
||||
*/
|
||||
int
|
||||
prof_output_exact(const char* text)
|
||||
prof_output_exact(const char *text)
|
||||
{
|
||||
time_t start = time(NULL);
|
||||
|
||||
|
||||
while (time(NULL) - start < expect_timeout) {
|
||||
/* Read any available output */
|
||||
while (_read_output(READ_TIMEOUT_MS) > 0) {
|
||||
/* Keep reading while data available */
|
||||
}
|
||||
|
||||
|
||||
/* Check if text is in buffer */
|
||||
if (strstr(output_buffer, text) != NULL) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
sleep_ms(OUTPUT_POLL_MS);
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -529,34 +526,34 @@ prof_output_exact(const char* text)
|
||||
* Returns 1 if found, 0 if timeout.
|
||||
*/
|
||||
int
|
||||
prof_output_regex(const char* pattern)
|
||||
prof_output_regex(const char *pattern)
|
||||
{
|
||||
regex_t regex;
|
||||
int ret;
|
||||
|
||||
|
||||
ret = regcomp(®ex, pattern, REG_EXTENDED | REG_NOSUB);
|
||||
if (ret != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
time_t start = time(NULL);
|
||||
|
||||
|
||||
while (time(NULL) - start < expect_timeout) {
|
||||
/* Read any available output */
|
||||
while (_read_output(READ_TIMEOUT_MS) > 0) {
|
||||
/* Keep reading while data available */
|
||||
}
|
||||
|
||||
|
||||
/* Check if pattern matches */
|
||||
ret = regexec(®ex, output_buffer, 0, NULL, 0);
|
||||
if (ret == 0) {
|
||||
regfree(®ex);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
sleep_ms(OUTPUT_POLL_MS);
|
||||
}
|
||||
|
||||
|
||||
/* Timeout reached - log diagnostic info */
|
||||
fprintf(stderr, "Timeout waiting for regex '%s' after %d seconds. Last output:\n", pattern, expect_timeout);
|
||||
size_t len = strlen(output_buffer);
|
||||
@@ -566,21 +563,23 @@ prof_output_regex(const char* pattern)
|
||||
fprintf(stderr, "%s", output_buffer);
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
|
||||
|
||||
regfree(®ex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
prof_connect_with_roster(const char* roster)
|
||||
prof_connect_with_roster(const char *roster)
|
||||
{
|
||||
GString* roster_str = g_string_new(
|
||||
GString *roster_str = g_string_new(
|
||||
"<iq type='result' to='stabber@localhost/profanity'>"
|
||||
"<query xmlns='jabber:iq:roster' ver='362'>");
|
||||
"<query xmlns='jabber:iq:roster' ver='362'>"
|
||||
);
|
||||
g_string_append(roster_str, roster);
|
||||
g_string_append(roster_str,
|
||||
"</query>"
|
||||
"</iq>");
|
||||
"</query>"
|
||||
"</iq>"
|
||||
);
|
||||
|
||||
stbbr_for_query("jabber:iq:roster", roster_str->str);
|
||||
g_string_free(roster_str, TRUE);
|
||||
@@ -590,7 +589,7 @@ prof_connect_with_roster(const char* roster)
|
||||
char connect_cmd[128];
|
||||
snprintf(connect_cmd, sizeof(connect_cmd), "/connect stabber@localhost server 127.0.0.1 port %d tls disable auth legacy", stub_port);
|
||||
prof_input(connect_cmd);
|
||||
|
||||
|
||||
assert_true(prof_output_regex("password:"));
|
||||
prof_input("password");
|
||||
|
||||
@@ -598,14 +597,15 @@ prof_connect_with_roster(const char* roster)
|
||||
assert_true(prof_output_regex("Connecting as stabber@localhost"));
|
||||
assert_true(prof_output_regex("logged in successfully"));
|
||||
assert_true(prof_output_regex(".+online.+ \\(priority 0\\)\\."));
|
||||
|
||||
|
||||
expect_timeout = EXPECT_TIMEOUT_CONNECT;
|
||||
// Wait for presence stanza to be sent (content-based, not ID-based)
|
||||
// Match the actual attribute order from stanza_attach_caps
|
||||
assert_true(stbbr_received(
|
||||
"<presence id=\"*\">"
|
||||
"<c xmlns=\"http://jabber.org/protocol/caps\" hash=\"sha-1\" node=\"http://profanity-im.github.io\" ver=\"*\"/>"
|
||||
"</presence>"));
|
||||
"<c xmlns=\"http://jabber.org/protocol/caps\" hash=\"sha-1\" node=\"http://profanity-im.github.io\" ver=\"*\"/>"
|
||||
"</presence>"
|
||||
));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -625,5 +625,6 @@ prof_connect(void)
|
||||
{
|
||||
prof_connect_with_roster(
|
||||
"<item jid='buddy1@localhost' subscription='both' name='Buddy1'/>"
|
||||
"<item jid='buddy2@localhost' subscription='both' name='Buddy2'/>");
|
||||
"<item jid='buddy2@localhost' subscription='both' name='Buddy2'/>"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,6 @@ extern double prof_test_slow_threshold;
|
||||
extern double prof_test_fast_threshold;
|
||||
|
||||
/* Timezone used for profanity child process (default "UTC") */
|
||||
extern const char* prof_test_tz;
|
||||
extern const char *prof_test_tz;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
/*
|
||||
* test_ai.c
|
||||
*
|
||||
* Functional tests for the /ai command surface (Tier A — no network).
|
||||
*
|
||||
* These tests interact with profanity through its PTY console and exercise
|
||||
* paths that do not reach libcurl: command parsing, provider registration,
|
||||
* key/setting/default storage, error messages, and AI window creation.
|
||||
*
|
||||
* Tests that require an actual provider HTTP exchange (prompt -> response,
|
||||
* /ai models <provider>, HTTP error envelopes) are out of scope here and
|
||||
* belong in a separate suite backed by a local HTTP stub.
|
||||
*/
|
||||
|
||||
#include <glib.h>
|
||||
#include "prof_cmocka.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "proftest.h"
|
||||
#include "test_ai.h"
|
||||
|
||||
int
|
||||
ai_init_test(void** state)
|
||||
{
|
||||
int ret = init_prof_test(state);
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
/* Connect to stabber even though AI tests don't use XMPP. Without this
|
||||
* stabber's accept loop has no client to disconnect on /quit, and
|
||||
* stbbr_stop() in close_prof_test() hangs uninterruptibly when joining
|
||||
* the server thread. */
|
||||
prof_connect();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
ai_no_args_shows_help(void** state)
|
||||
{
|
||||
/* `/ai` with no arguments lists the built-in providers and a usage hint.
|
||||
* We don't pin specific provider names — defaults may change. Verify the
|
||||
* header, the "Configured providers" section, that *some* provider line
|
||||
* carries an http(s) URL, and the usage hint. */
|
||||
prof_input("/ai");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_exact("AI Chat - OpenAI-compatible API client"));
|
||||
assert_true(prof_output_exact("Configured providers:"));
|
||||
assert_true(prof_output_regex("URL: https?://"));
|
||||
assert_true(prof_output_regex("Use '/ai start' to begin a chat"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_providers_lists_defaults(void** state)
|
||||
{
|
||||
/* `/ai providers` (no "list") shows the built-in list with URLs. */
|
||||
prof_input("/ai providers");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_exact("Available AI providers:"));
|
||||
/* At least one URL line is rendered — exact name agnostic. */
|
||||
assert_true(prof_output_regex("https?://"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_providers_list_shows_key_status(void** state)
|
||||
{
|
||||
/* `/ai providers list` lists each configured provider with key status. */
|
||||
prof_input("/ai providers list");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_exact("Configured providers:"));
|
||||
/* No tokens have been set yet in this fresh session. */
|
||||
assert_true(prof_output_regex("Key: NOT configured"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_set_provider_adds_custom(void** state)
|
||||
{
|
||||
/* Adding a custom provider should make it appear in /ai providers list. */
|
||||
prof_input("/ai set provider mock http://127.0.0.1:1/");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Provider 'mock' configured"));
|
||||
prof_timeout_reset();
|
||||
|
||||
prof_input("/ai providers list");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("mock"));
|
||||
assert_true(prof_output_regex("http://127.0.0.1:1/"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_set_token_marks_key_set(void** state)
|
||||
{
|
||||
/* Use a self-set-up provider so the test doesn't pin a default name. */
|
||||
prof_input("/ai set provider testprov https://example.test/");
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Provider 'testprov' configured"));
|
||||
prof_timeout_reset();
|
||||
|
||||
prof_input("/ai set token testprov sk-fake-test-key");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("API token set for provider: testprov"));
|
||||
prof_timeout_reset();
|
||||
|
||||
prof_input("/ai providers list");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Key: configured"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_start_unknown_provider_errors(void** state)
|
||||
{
|
||||
/* Unknown provider name should error out without creating a window. */
|
||||
prof_input("/ai start nope_provider somemodel");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Provider 'nope_provider' not found"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_start_without_key_errors(void** state)
|
||||
{
|
||||
/* Self-set-up provider with no token. /ai start must refuse. */
|
||||
prof_input("/ai set provider testprov https://example.test/");
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Provider 'testprov' configured"));
|
||||
prof_timeout_reset();
|
||||
|
||||
prof_input("/ai start testprov model-x");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("No API key set for provider 'testprov'"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_start_with_key_opens_window(void** state)
|
||||
{
|
||||
/* Self-set-up provider with token; /ai start opens a WIN_AI window. */
|
||||
prof_input("/ai set provider testprov https://example.test/");
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Provider 'testprov' configured"));
|
||||
prof_timeout_reset();
|
||||
|
||||
prof_input("/ai set token testprov sk-fake-test-key");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("API token set for provider: testprov"));
|
||||
prof_timeout_reset();
|
||||
|
||||
prof_input("/ai start testprov model-x");
|
||||
|
||||
prof_timeout(5);
|
||||
/* /ai start switches focus to the new WIN_AI window; the window prints
|
||||
* "AI Chat: <provider>/<model>" as its first line. */
|
||||
assert_true(prof_output_regex("AI Chat: testprov/model-x"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_clear_without_window_errors(void** state)
|
||||
{
|
||||
/* /ai clear from the console (no active AI window) refuses with the
|
||||
* shared "must be in AI chat window" guard used by /ai switch as well. */
|
||||
prof_input("/ai clear");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Must be in an AI chat window"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_remove_provider_works(void** state)
|
||||
{
|
||||
/* Round-trip: add -> verify present -> remove -> verify gone. */
|
||||
prof_input("/ai set provider tmpprov http://127.0.0.1:2/");
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Provider 'tmpprov' configured"));
|
||||
prof_timeout_reset();
|
||||
|
||||
prof_input("/ai remove provider tmpprov");
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Provider 'tmpprov' removed"));
|
||||
prof_timeout_reset();
|
||||
|
||||
prof_input("/ai remove provider tmpprov");
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Provider 'tmpprov' not found"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_remove_provider_unknown_errors(void** state)
|
||||
{
|
||||
/* Removing a provider that was never added must report "not found". */
|
||||
prof_input("/ai remove provider nonexistent_xyz");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Provider 'nonexistent_xyz' not found"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_set_default_model_updates_provider(void** state)
|
||||
{
|
||||
/* Setting a default model is acknowledged on the console. */
|
||||
prof_input("/ai set provider testprov https://example.test/");
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Provider 'testprov' configured"));
|
||||
prof_timeout_reset();
|
||||
|
||||
prof_input("/ai set default-model testprov model-preview");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Default model for provider 'testprov' set to: model-preview"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_switch_without_window_errors(void** state)
|
||||
{
|
||||
/* /ai switch with no active AI window refuses with the shared
|
||||
* "must be in AI chat window" guard. */
|
||||
prof_input("/ai switch testprov model-x");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Must be in an AI chat window"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
void
|
||||
ai_bad_subcommand_shows_usage(void** state)
|
||||
{
|
||||
/* An unknown /ai subcommand should fall through to cons_bad_cmd_usage. */
|
||||
prof_input("/ai not_a_subcommand");
|
||||
|
||||
prof_timeout(5);
|
||||
assert_true(prof_output_regex("Invalid usage, see '/help ai'"));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
#ifndef __H_FUNC_TEST_AI
|
||||
#define __H_FUNC_TEST_AI
|
||||
|
||||
/*
|
||||
* AI tests don't exercise the XMPP path, but the test fixture's
|
||||
* stbbr_stop() hangs indefinitely when no client ever connected to
|
||||
* stabber. ai_init_test wraps init_prof_test with a prof_connect()
|
||||
* so stabber sees a graceful disconnect at teardown.
|
||||
*/
|
||||
int ai_init_test(void** state);
|
||||
|
||||
void ai_no_args_shows_help(void** state);
|
||||
void ai_providers_lists_defaults(void** state);
|
||||
void ai_providers_list_shows_key_status(void** state);
|
||||
void ai_set_provider_adds_custom(void** state);
|
||||
void ai_set_token_marks_key_set(void** state);
|
||||
void ai_start_unknown_provider_errors(void** state);
|
||||
void ai_start_without_key_errors(void** state);
|
||||
void ai_start_with_key_opens_window(void** state);
|
||||
void ai_clear_without_window_errors(void** state);
|
||||
void ai_remove_provider_works(void** state);
|
||||
void ai_remove_provider_unknown_errors(void** state);
|
||||
void ai_set_default_model_updates_provider(void** state);
|
||||
void ai_switch_without_window_errors(void** state);
|
||||
void ai_bad_subcommand_shows_usage(void** state);
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
* test_ai_client.h - AI client unit test declarations
|
||||
*/
|
||||
|
||||
int ai_client_setup(void** state);
|
||||
int ai_client_teardown(void** state);
|
||||
|
||||
void test_ai_client_init(void** state);
|
||||
void test_ai_add_provider(void** state);
|
||||
void test_ai_remove_provider(void** state);
|
||||
void test_ai_list_providers(void** state);
|
||||
void test_ai_set_provider_key(void** state);
|
||||
void test_ai_get_provider_key(void** state);
|
||||
void test_ai_session_create(void** state);
|
||||
void test_ai_session_ref_unref(void** state);
|
||||
void test_ai_session_add_message(void** state);
|
||||
void test_ai_session_clear_history(void** state);
|
||||
void test_ai_session_set_model(void** state);
|
||||
void test_ai_json_escape(void** state);
|
||||
void test_ai_json_escape_null(void** state);
|
||||
void test_ai_json_escape_empty(void** state);
|
||||
void test_ai_json_escape_special_chars(void** state);
|
||||
void test_ai_json_escape_percent_signs(void** state);
|
||||
void test_ai_json_escape_backslash_quote(void** state);
|
||||
void test_ai_session_api_key_is_copied(void** state);
|
||||
void test_ai_add_provider_update_existing(void** state);
|
||||
void test_ai_add_provider_null_name_returns_null(void** state);
|
||||
void test_ai_add_provider_null_url_returns_null(void** state);
|
||||
void test_ai_session_create_null_provider_returns_null(void** state);
|
||||
void test_ai_session_create_null_model_returns_null(void** state);
|
||||
void test_ai_session_api_key_null_when_no_key_set(void** state);
|
||||
/* Provider autocomplete tests */
|
||||
void test_ai_providers_find_forward(void** state);
|
||||
void test_ai_providers_find_forward_custom(void** state);
|
||||
void test_ai_providers_find_forward_no_match(void** state);
|
||||
void test_ai_providers_find_forward_partial_match(void** state);
|
||||
void test_ai_providers_find_next(void** state);
|
||||
void test_ai_providers_find_previous(void** state);
|
||||
void test_ai_providers_find_empty_search_str(void** state);
|
||||
void test_ai_providers_find_null_search_str(void** state);
|
||||
void test_ai_providers_find_case_insensitive(void** state);
|
||||
/* Provider default model and settings tests */
|
||||
void test_ai_set_provider_default_model(void** state);
|
||||
void test_ai_get_provider_default_model(void** state);
|
||||
void test_ai_set_provider_setting(void** state);
|
||||
void test_ai_get_provider_setting(void** state);
|
||||
/* Model caching tests */
|
||||
void test_ai_models_are_fresh_initial(void** state);
|
||||
/* Model parsing tests */
|
||||
void test_ai_parse_models_openai_format(void** state);
|
||||
void test_ai_parse_models_perplexity_format(void** state);
|
||||
void test_ai_parse_models_array_format(void** state);
|
||||
void test_ai_parse_models_empty_json(void** state);
|
||||
void test_ai_parse_models_null_handling(void** state);
|
||||
void test_ai_parse_models_escaped_quotes(void** state);
|
||||
void test_ai_parse_models_with_whitespace(void** state);
|
||||
/* AI autocomplete integration tests */
|
||||
void test_ai_start_provider_autocomplete_only_on_exact(void** state);
|
||||
void test_ai_providers_find_cycling(void** state);
|
||||
|
||||
/* Setup that also initializes prefs for round-trip persistence tests. */
|
||||
int ai_client_setup_with_prefs(void** state);
|
||||
int ai_client_teardown_with_prefs(void** state);
|
||||
|
||||
/* Chat response parser tests (ai_parse_response) */
|
||||
void test_ai_parse_response_openai_content(void** state);
|
||||
void test_ai_parse_response_perplexity_text(void** state);
|
||||
void test_ai_parse_response_text_preferred_over_content(void** state);
|
||||
void test_ai_parse_response_escaped_quote(void** state);
|
||||
void test_ai_parse_response_newline_escape(void** state);
|
||||
void test_ai_parse_response_empty_content(void** state);
|
||||
void test_ai_parse_response_missing_field(void** state);
|
||||
void test_ai_parse_response_null_input(void** state);
|
||||
void test_ai_parse_response_empty_input(void** state);
|
||||
void test_ai_parse_response_percent_signs_safe(void** state);
|
||||
void test_ai_parse_response_braces_in_content(void** state);
|
||||
void test_ai_parse_response_multiline_content(void** state);
|
||||
|
||||
/* Error response parser tests (ai_parse_error_message) */
|
||||
void test_ai_parse_error_standard_envelope(void** state);
|
||||
void test_ai_parse_error_with_escapes(void** state);
|
||||
void test_ai_parse_error_no_error_field(void** state);
|
||||
void test_ai_parse_error_no_message_field(void** state);
|
||||
void test_ai_parse_error_null_input(void** state);
|
||||
void test_ai_parse_error_empty_input(void** state);
|
||||
void test_ai_parse_error_tab_escape(void** state);
|
||||
void test_ai_parse_error_backslash_escape(void** state);
|
||||
|
||||
/* Extended JSON escape tests */
|
||||
void test_ai_json_escape_backspace(void** state);
|
||||
void test_ai_json_escape_formfeed(void** state);
|
||||
void test_ai_json_escape_carriage_return(void** state);
|
||||
void test_ai_json_escape_all_specials_combined(void** state);
|
||||
void test_ai_json_escape_passes_utf8_through(void** state);
|
||||
|
||||
/* Autocomplete cycling with many providers */
|
||||
void test_ai_providers_find_cycles_through_many(void** state);
|
||||
void test_ai_providers_find_previous_walks_backwards(void** state);
|
||||
void test_ai_providers_find_wraps_around(void** state);
|
||||
|
||||
/* Session edge cases */
|
||||
void test_ai_session_add_message_null_session_no_crash(void** state);
|
||||
void test_ai_session_add_message_null_role_no_crash(void** state);
|
||||
void test_ai_session_add_message_null_content_no_crash(void** state);
|
||||
void test_ai_session_history_preserves_large_order(void** state);
|
||||
void test_ai_session_clear_empty_history(void** state);
|
||||
void test_ai_session_set_model_null_keeps_old(void** state);
|
||||
void test_ai_session_unref_null_no_crash(void** state);
|
||||
void test_ai_session_ref_null_returns_null(void** state);
|
||||
|
||||
/* Provider edge cases */
|
||||
void test_ai_get_provider_null_returns_null(void** state);
|
||||
void test_ai_remove_provider_null_returns_false(void** state);
|
||||
void test_ai_remove_provider_twice_second_fails(void** state);
|
||||
void test_ai_provider_survives_via_session_after_removal(void** state);
|
||||
|
||||
/* Settings edge cases */
|
||||
void test_ai_settings_multiple_keys_independent(void** state);
|
||||
void test_ai_settings_get_missing_returns_null(void** state);
|
||||
void test_ai_settings_isolated_between_providers(void** state);
|
||||
|
||||
/* Model parsing edge cases */
|
||||
void test_ai_parse_models_data_not_array(void** state);
|
||||
void test_ai_parse_models_empty_data_array(void** state);
|
||||
void test_ai_parse_models_id_outside_data_ignored(void** state);
|
||||
void test_ai_parse_models_multiple_models(void** state);
|
||||
|
||||
/* Prefs round-trip tests */
|
||||
void test_ai_prefs_round_trip_api_key(void** state);
|
||||
void test_ai_prefs_round_trip_remove_key(void** state);
|
||||
void test_ai_prefs_multiple_providers_persist(void** state);
|
||||
|
||||
/* Autocomplete deeper coverage */
|
||||
void test_ai_providers_find_after_remove_skips_removed(void** state);
|
||||
|
||||
/* Reset hook + persistence */
|
||||
void test_ai_providers_reset_ac_restarts_cycle(void** state);
|
||||
void test_ai_add_provider_persisted_across_init(void** state);
|
||||
void test_ai_remove_provider_persisted_across_init(void** state);
|
||||
void test_ai_models_persisted_across_init(void** state);
|
||||
@@ -69,7 +69,7 @@ cmd_otr_log_on_shows_warning_when_chlog_disabled(void** state)
|
||||
prefs_set_boolean(PREF_CHLOG, FALSE);
|
||||
|
||||
expect_cons_show("OTR messages will be logged as plaintext.");
|
||||
expect_cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
expect_cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
|
||||
gboolean result = cmd_otr_log(NULL, CMD_OTR, args);
|
||||
assert_true(result);
|
||||
@@ -115,7 +115,7 @@ cmd_otr_log_redact_shows_warning_when_chlog_disabled(void** state)
|
||||
prefs_set_boolean(PREF_CHLOG, FALSE);
|
||||
|
||||
expect_cons_show("OTR messages will be logged as '[redacted]'.");
|
||||
expect_cons_show("Chat logging is currently disabled, use '/history on' to enable.");
|
||||
expect_cons_show("Chat logging is currently disabled, use '/logging chat on' to enable.");
|
||||
|
||||
gboolean result = cmd_otr_log(NULL, CMD_OTR, args);
|
||||
assert_true(result);
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* stub_ai.c - Stubs for AI window functions
|
||||
*/
|
||||
|
||||
#include "ui/window.h"
|
||||
#include "ai/ai_client.h"
|
||||
|
||||
ProfWin*
|
||||
win_create_ai(AISession* session)
|
||||
{
|
||||
/* Return NULL to simulate failure in unit tests */
|
||||
(void)session;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
aiwin_display_response(ProfAiWin* win, const char* response)
|
||||
{
|
||||
/* Stub: do nothing */
|
||||
(void)win;
|
||||
(void)response;
|
||||
}
|
||||
|
||||
void
|
||||
aiwin_display_error(ProfAiWin* win, const char* error_msg)
|
||||
{
|
||||
/* Stub: do nothing */
|
||||
(void)win;
|
||||
(void)error_msg;
|
||||
}
|
||||
|
||||
void
|
||||
win_print_outgoing(ProfWin* window, const char* show_char, const char* const id, const char* const replace_id, const char* const message)
|
||||
{
|
||||
/* Stub: do nothing */
|
||||
(void)window;
|
||||
(void)show_char;
|
||||
(void)id;
|
||||
(void)replace_id;
|
||||
(void)message;
|
||||
}
|
||||
@@ -36,7 +36,6 @@
|
||||
#include "test_form.h"
|
||||
#include "test_callbacks.h"
|
||||
#include "test_plugins_disco.h"
|
||||
#include "test_ai_client.h"
|
||||
#include "test_database_export.h"
|
||||
#include "test_database_stress.h"
|
||||
|
||||
@@ -660,128 +659,6 @@ main(int argc, char* argv[])
|
||||
cmocka_unit_test_setup_teardown(test_cmd_force_encryption_invalid_policy, load_preferences, close_preferences),
|
||||
cmocka_unit_test_setup_teardown(test_allow_unencrypted_message_invalid_mode, load_preferences, close_preferences),
|
||||
|
||||
// AI client tests
|
||||
cmocka_unit_test_setup_teardown(test_ai_client_init, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_add_provider, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_remove_provider, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_list_providers, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_set_provider_key, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_get_provider_key, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_create, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_ref_unref, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_add_message, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_clear_history, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_set_model, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_api_key_is_copied, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_add_provider_update_existing, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_add_provider_null_name_returns_null, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_add_provider_null_url_returns_null, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_create_null_provider_returns_null, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_create_null_model_returns_null, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_api_key_null_when_no_key_set, ai_client_setup, ai_client_teardown),
|
||||
/* Provider autocomplete tests */
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_forward, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_forward_custom, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_forward_no_match, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_forward_partial_match, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_next, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_previous, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_empty_search_str, ai_client_setup, ai_client_teardown),
|
||||
/* SIGSEGV on ai_providers_find(NULL, ...) — see test body and
|
||||
* AI_AUTOCOMPLETE_POSSIBLE_ISSUES.md, issue 1. Re-enable once the
|
||||
* NULL-search path is guarded. */
|
||||
/* cmocka_unit_test_setup_teardown(test_ai_providers_find_null_search_str, ai_client_setup, ai_client_teardown), */
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_case_insensitive, ai_client_setup, ai_client_teardown),
|
||||
/* Provider default model and settings tests */
|
||||
cmocka_unit_test_setup_teardown(test_ai_set_provider_default_model, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_get_provider_default_model, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_set_provider_setting, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_get_provider_setting, ai_client_setup, ai_client_teardown),
|
||||
/* Model caching tests */
|
||||
cmocka_unit_test_setup_teardown(test_ai_models_are_fresh_initial, ai_client_setup, ai_client_teardown),
|
||||
/* Model parsing tests */
|
||||
cmocka_unit_test(test_ai_parse_models_openai_format),
|
||||
cmocka_unit_test(test_ai_parse_models_perplexity_format),
|
||||
cmocka_unit_test(test_ai_parse_models_array_format),
|
||||
cmocka_unit_test(test_ai_parse_models_empty_json),
|
||||
cmocka_unit_test(test_ai_parse_models_null_handling),
|
||||
cmocka_unit_test(test_ai_parse_models_escaped_quotes),
|
||||
cmocka_unit_test(test_ai_parse_models_with_whitespace),
|
||||
/* AI autocomplete integration tests */
|
||||
cmocka_unit_test_setup_teardown(test_ai_start_provider_autocomplete_only_on_exact, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_cycling, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test(test_ai_json_escape),
|
||||
cmocka_unit_test(test_ai_json_escape_null),
|
||||
cmocka_unit_test(test_ai_json_escape_empty),
|
||||
cmocka_unit_test(test_ai_json_escape_special_chars),
|
||||
cmocka_unit_test(test_ai_json_escape_percent_signs),
|
||||
cmocka_unit_test(test_ai_json_escape_backslash_quote),
|
||||
/* Chat response parser */
|
||||
cmocka_unit_test(test_ai_parse_response_openai_content),
|
||||
cmocka_unit_test(test_ai_parse_response_perplexity_text),
|
||||
cmocka_unit_test(test_ai_parse_response_text_preferred_over_content),
|
||||
cmocka_unit_test(test_ai_parse_response_escaped_quote),
|
||||
cmocka_unit_test(test_ai_parse_response_newline_escape),
|
||||
cmocka_unit_test(test_ai_parse_response_empty_content),
|
||||
cmocka_unit_test(test_ai_parse_response_missing_field),
|
||||
cmocka_unit_test(test_ai_parse_response_null_input),
|
||||
cmocka_unit_test(test_ai_parse_response_empty_input),
|
||||
cmocka_unit_test(test_ai_parse_response_percent_signs_safe),
|
||||
cmocka_unit_test(test_ai_parse_response_braces_in_content),
|
||||
cmocka_unit_test(test_ai_parse_response_multiline_content),
|
||||
/* Error response parser */
|
||||
cmocka_unit_test(test_ai_parse_error_standard_envelope),
|
||||
cmocka_unit_test(test_ai_parse_error_with_escapes),
|
||||
cmocka_unit_test(test_ai_parse_error_no_error_field),
|
||||
cmocka_unit_test(test_ai_parse_error_no_message_field),
|
||||
cmocka_unit_test(test_ai_parse_error_null_input),
|
||||
cmocka_unit_test(test_ai_parse_error_empty_input),
|
||||
cmocka_unit_test(test_ai_parse_error_tab_escape),
|
||||
cmocka_unit_test(test_ai_parse_error_backslash_escape),
|
||||
/* Extended JSON escape coverage */
|
||||
cmocka_unit_test(test_ai_json_escape_backspace),
|
||||
cmocka_unit_test(test_ai_json_escape_formfeed),
|
||||
cmocka_unit_test(test_ai_json_escape_carriage_return),
|
||||
cmocka_unit_test(test_ai_json_escape_all_specials_combined),
|
||||
cmocka_unit_test(test_ai_json_escape_passes_utf8_through),
|
||||
/* Autocomplete cycling */
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_cycles_through_many, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_previous_walks_backwards, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_wraps_around, ai_client_setup, ai_client_teardown),
|
||||
/* Session edge cases */
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_add_message_null_session_no_crash, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_add_message_null_role_no_crash, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_add_message_null_content_no_crash, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_history_preserves_large_order, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_clear_empty_history, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_set_model_null_keeps_old, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_unref_null_no_crash, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_session_ref_null_returns_null, ai_client_setup, ai_client_teardown),
|
||||
/* Provider edge cases */
|
||||
cmocka_unit_test_setup_teardown(test_ai_get_provider_null_returns_null, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_remove_provider_null_returns_false, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_remove_provider_twice_second_fails, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_provider_survives_via_session_after_removal, ai_client_setup, ai_client_teardown),
|
||||
/* Settings */
|
||||
cmocka_unit_test_setup_teardown(test_ai_settings_multiple_keys_independent, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_settings_get_missing_returns_null, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_settings_isolated_between_providers, ai_client_setup, ai_client_teardown),
|
||||
/* Model parsing edge cases */
|
||||
cmocka_unit_test_setup_teardown(test_ai_parse_models_data_not_array, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_parse_models_empty_data_array, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_parse_models_id_outside_data_ignored, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_parse_models_multiple_models, ai_client_setup, ai_client_teardown),
|
||||
/* Prefs round-trip (uses prefs+ai setup) */
|
||||
cmocka_unit_test_setup_teardown(test_ai_prefs_round_trip_api_key, ai_client_setup_with_prefs, ai_client_teardown_with_prefs),
|
||||
cmocka_unit_test_setup_teardown(test_ai_prefs_round_trip_remove_key, ai_client_setup_with_prefs, ai_client_teardown_with_prefs),
|
||||
cmocka_unit_test_setup_teardown(test_ai_prefs_multiple_providers_persist, ai_client_setup_with_prefs, ai_client_teardown_with_prefs),
|
||||
/* Autocomplete deeper coverage */
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_find_after_remove_skips_removed, ai_client_setup, ai_client_teardown),
|
||||
/* Reset hook + persistence */
|
||||
cmocka_unit_test_setup_teardown(test_ai_providers_reset_ac_restarts_cycle, ai_client_setup, ai_client_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_ai_add_provider_persisted_across_init, ai_client_setup_with_prefs, ai_client_teardown_with_prefs),
|
||||
cmocka_unit_test_setup_teardown(test_ai_remove_provider_persisted_across_init, ai_client_setup_with_prefs, ai_client_teardown_with_prefs),
|
||||
cmocka_unit_test_setup_teardown(test_ai_models_persisted_across_init, ai_client_setup_with_prefs, ai_client_teardown_with_prefs),
|
||||
// Flatfile export/import round-trip
|
||||
cmocka_unit_test(test_ff_roundtrip_simple_chat),
|
||||
cmocka_unit_test(test_ff_roundtrip_with_all_metadata),
|
||||
|
||||
@@ -104,12 +104,6 @@ connection_create_uuid(void)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char*
|
||||
connection_create_stanza_id(void)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
connection_free_uuid(char* uuid)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user