/* * 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. */ 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("openai")); assert_true(prof_output_regex("perplexity")); 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:")); assert_true(prof_output_regex("openai")); assert_true(prof_output_regex("perplexity")); 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) { /* Setting a token must flip the provider's key status to "configured". */ prof_input("/ai set token openai sk-fake-test-key"); prof_timeout(5); assert_true(prof_output_regex("API token set for provider: openai")); prof_timeout_reset(); prof_input("/ai providers list"); prof_timeout(5); /* openai now shows "Key: configured" while perplexity stays unconfigured. */ 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) { /* Known provider without an API key should refuse to start. */ prof_input("/ai start openai gpt-4"); prof_timeout(5); assert_true(prof_output_regex("No API key set for provider 'openai'")); prof_timeout_reset(); } void ai_start_with_key_opens_window(void** state) { /* With a token set, /ai start should create a WIN_AI window. */ prof_input("/ai set token openai sk-fake-test-key"); prof_timeout(5); assert_true(prof_output_regex("API token set for provider: openai")); prof_timeout_reset(); prof_input("/ai start openai gpt-4"); prof_timeout(5); /* /ai start switches focus to the new WIN_AI window; cons_show output * to the console is therefore not the right place to look. The window * itself prints "AI Chat: /" as its first line. */ assert_true(prof_output_regex("AI Chat: openai/gpt-4")); prof_timeout_reset(); } void ai_clear_without_window_errors(void** state) { /* /ai clear from the console (no active AI window) should report nicely. */ prof_input("/ai clear"); prof_timeout(5); assert_true(prof_output_regex("No active AI chat window to clear")); 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_provider_unknown_errors(void** state) { /* Setting an unknown default provider should error, not silently accept. */ prof_input("/ai set default-provider does_not_exist"); prof_timeout(5); assert_true(prof_output_regex("Provider 'does_not_exist' 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 default-model openai gpt-5-preview"); prof_timeout(5); assert_true(prof_output_regex("Default model for provider 'openai' set to: gpt-5-preview")); prof_timeout_reset(); } void ai_switch_without_window_errors(void** state) { /* /ai switch with no active AI window should produce an actionable error. */ prof_input("/ai switch openai gpt-4"); prof_timeout(5); assert_true(prof_output_regex("No active 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(); }