Compare commits

..

2 Commits

Author SHA1 Message Date
ff0ec78e78 test(ai): align parse_response tests with chat completions API
Some checks failed
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Failing after 31s
CI Code / Linux (ubuntu) (pull_request) Successful in 7m40s
CI Code / Linux (debian) (pull_request) Successful in 7m54s
CI Code / Code Coverage (pull_request) Successful in 8m38s
CI Code / Linux (arch) (pull_request) Successful in 11m0s
The chat-completions refactor removed the legacy Perplexity "text"
extraction but left two tests asserting the old behavior, breaking
make check on every CI flavor:

- test_ai_parse_response_perplexity_text expected the legacy /v1/agent
  nested format to parse; it now yields NULL — renamed to
  test_ai_parse_response_legacy_text_format_unsupported.
- test_ai_parse_response_text_preferred_over_content expected "text"
  to win; "content" is authoritative now — renamed to
  test_ai_parse_response_content_preferred_over_text.

Add test_ai_set_provider_setting_reserved_key covering the new
reserved-key rejection.
2026-07-04 13:59:19 +03:00
1210daf357 fix(ai): harden custom settings payload against invalid JSON and races
Custom setting values were JSON-escaped but emitted unquoted, so any
non-numeric value (e.g. "high") produced an invalid JSON payload and
failed the whole request; quoting the value manually could not work
either, since the escaper turns quotes into \". Emit RFC 8259 scalars
(number/true/false/null) bare and everything else as a quoted JSON
string.

Reserved payload keys (model, messages, stream) would duplicate the
fixed fields with parser-dependent precedence; reject them in
ai_set_provider_setting (surfaced as an error by /ai set custom) and
skip them at payload-build time for settings loaded from hand-edited
prefs.

provider->settings was mutated on the main thread while the request
thread iterates it when building the payload; guard both sides with a
new per-provider settings_lock.

Also refresh stale docs: ai_parse_response no longer mentions the
dropped Perplexity "text" path, the payload docstring says "messages"
instead of "input", and the /ai help example uses a realistic scalar
setting.
2026-07-04 13:59:19 +03:00
2 changed files with 8 additions and 30 deletions

View File

@@ -17,15 +17,15 @@ typedef struct ai_message_t
*/
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 */
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 */
pthread_mutex_t settings_lock; /* Protects settings (iterated by the request thread) */
GHashTable* settings; /* Extensible per-provider settings (e.g., temperature=0.7) */
GList* models; /* Cached models (gchar*) */
gboolean models_fresh; /* Whether models cache is current */
gint32 ref_count; /* Reference count (atomic) */
GHashTable* settings; /* Extensible per-provider settings (e.g., temperature=0.7) */
GList* models; /* Cached models (gchar*) */
gboolean models_fresh; /* Whether models cache is current */
gint32 ref_count; /* Reference count (atomic) */
} AIProvider;
/**

View File

@@ -25,8 +25,6 @@
#include "ui/ui.h"
#include "xmpp/xmpp.h"
extern char** environ;
typedef struct EditorContext
{
gchar* filename;
@@ -142,22 +140,6 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
GSource* sigchld_warmup = g_child_watch_source_new(getpid());
g_source_unref(sigchld_warmup);
// Build the editor's env without LINES/COLUMNS pre-fork, so the child only
// reassigns environ instead of calling unsetenv() between fork and exec.
// The editor's (n)curses then reads the live window via ioctl(TIOCGWINSZ).
gsize env_len = 0;
while (environ[env_len]) {
env_len++;
}
gchar** editor_env = g_new0(gchar*, env_len + 1);
gsize env_kept = 0;
for (gsize i = 0; i < env_len; i++) {
if (g_str_has_prefix(environ[i], "LINES=") || g_str_has_prefix(environ[i], "COLUMNS=")) {
continue;
}
editor_env[env_kept++] = environ[i];
}
pid_t pid = fork();
if (pid == -1) {
log_error("[Editor] Failed to fork: %s", strerror(errno));
@@ -166,15 +148,12 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
ui_resize();
cons_show_error("Failed to start editor: %s", strerror(errno));
g_strfreev(editor_argv);
g_free(editor_env); // array only; strings are borrowed from environ
g_free(ctx->filename);
g_free(ctx);
return TRUE;
} else if (pid == 0) {
// Child process: Inherits TTY from parent
environ = editor_env; // live TIOCGWINSZ size, not the inherited LINES/COLUMNS
// SIGTSTP=SIG_DFL lets vim's :stop / Ctrl-Z work; profanity catches
// the STOPPED state via editor_check_stopped() and drops to the shell.
signal(SIGINT, SIG_DFL);
@@ -191,7 +170,6 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
editor_pid = pid;
g_child_watch_add((GPid)pid, _editor_exit_cb, ctx);
g_strfreev(editor_argv);
g_free(editor_env); // frees the parent's array only; the child keeps its own COW copy until execvp
return FALSE;
}