Some checks failed
CI Code / Check spelling (pull_request) Failing after 14s
CI Code / Check coding style (pull_request) Successful in 30s
CI Code / Linux (debian) (pull_request) Successful in 5m0s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m8s
CI Code / Linux (arch) (pull_request) Successful in 6m36s
CI Code / Code Coverage (pull_request) Successful in 3m36s
- bound _parse_responses scans to the output_text part's object and to
the content array, so a later sibling item's "text" (e.g. a reasoning
summary) can never be returned as the assistant reply
- do not retry the other flavour on a curl timeout: the request likely
reached the server and may still be generating, so a re-POST of the
conversation could trigger a second billed generation
- remember an unparseable 2xx from the first AUTO attempt and surface
stashed first-attempt errors in both error paths, instead of showing
only the final attempt's transport or HTTP error
- recognize Ollama's model-not-found wording in _names_model so a model
typo is not misread as a missing endpoint
- drop the dead, racy provider-lookup fallback in the generic request
thread: a missing provider ref is a caller bug and now fails loudly
- fix ai_providers_lists_defaults to expect the header the command
actually prints ("Configured providers:"); the test was broken since
its introduction but CI never ran it
- add functional test group 5 (AI command surface) to FUNC_TEST_GROUPS
so the CI parallel target runs it; proftest.c port ranges already
account for five groups
253 lines
7.5 KiB
C
253 lines
7.5 KiB
C
/*
|
|
* 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 configured list with URLs. */
|
|
prof_input("/ai providers");
|
|
|
|
prof_timeout(5);
|
|
assert_true(prof_output_exact("Configured 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();
|
|
}
|