/* * 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 , HTTP error envelopes) are out of scope here and * belong in a separate suite backed by a local HTTP stub. */ #include #include "prof_cmocka.h" #include #include #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: /" 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(); }