/* * test_ai_http.c * * Functional tests for the /ai command surface that exercise the libcurl * HTTP code path. A small Python HTTP stub (ai_http_stub.py) is spawned * per-test on a free local port; profanity is configured to point at it * via `/ai set provider`. The stub serves canned JSON whose shape is * controlled via AI_STUB_MODE. * * These tests intentionally do not race the worker thread directly — * they rely on prof_timeout() retries to catch the response once it * propagates back through the AI window. */ #include #include "prof_cmocka.h" #include #include #include "proftest.h" #include "ai_http_stub.h" #define WAIT_AI_RESPONSE_SEC 10 /* * Configure profanity to use the spawned stub as a provider named "mock" * with a dummy bearer token, then issue /ai start to open the AI window. * Returns the stub port on success, fails the test on stub-spawn failure. */ static int _setup_mock_provider(const char* mode, const char* reply) { int port = ai_http_stub_start(mode, reply); assert_int_not_equal(0, port); char cmd[256]; g_snprintf(cmd, sizeof(cmd), "/ai set provider mock http://127.0.0.1:%d/", port); prof_input(cmd); prof_timeout(5); assert_true(prof_output_regex("Provider 'mock' configured")); prof_timeout_reset(); prof_input("/ai set token mock fake-test-token"); prof_timeout(5); assert_true(prof_output_regex("API token set for provider: mock")); prof_timeout_reset(); return port; } void ai_http_prompt_returns_ok(void** state) { /* Happy path: stub returns Responses API "text" body; reply appears in window. */ _setup_mock_provider("ok", "STUB-HELLO"); prof_input("/ai start mock testmodel"); prof_timeout(5); assert_true(prof_output_regex("AI Chat: mock/testmodel")); prof_timeout_reset(); /* Plain text in AI window goes through cl_ev_send_ai_msg -> ai_send_prompt. */ prof_input("hello stub"); prof_timeout(WAIT_AI_RESPONSE_SEC); assert_true(prof_output_regex("STUB-HELLO")); prof_timeout_reset(); ai_http_stub_stop(); } void ai_http_prompt_openai_format(void** state) { /* Legacy OpenAI chat completions shape: "choices[].message.content". */ _setup_mock_provider("openai", "FROM-CHOICES"); prof_input("/ai start mock m"); prof_timeout(5); assert_true(prof_output_regex("AI Chat: mock/m")); prof_timeout_reset(); prof_input("anything"); prof_timeout(WAIT_AI_RESPONSE_SEC); assert_true(prof_output_regex("FROM-CHOICES")); prof_timeout_reset(); ai_http_stub_stop(); } void ai_http_prompt_401_shows_error(void** state) { /* 401 with an OpenAI-style error envelope. * ai_parse_error_message() should extract the inner message; the AI * window then displays "HTTP 401: ". */ _setup_mock_provider("401", "Bad token, try another"); prof_input("/ai start mock m"); prof_timeout(5); assert_true(prof_output_regex("AI Chat: mock/m")); prof_timeout_reset(); prof_input("prompt"); prof_timeout(WAIT_AI_RESPONSE_SEC); /* Either the parsed message surfaces, or the raw HTTP 401 envelope. */ assert_true(prof_output_regex("HTTP 401") && prof_output_regex("Bad token, try another")); prof_timeout_reset(); ai_http_stub_stop(); } void ai_http_prompt_500_surfaced(void** state) { /* 500 with a non-JSON HTML body. Parser falls back to raw body in HTTP error. */ _setup_mock_provider("500", NULL); prof_input("/ai start mock m"); prof_timeout(5); assert_true(prof_output_regex("AI Chat: mock/m")); prof_timeout_reset(); prof_input("trigger"); prof_timeout(WAIT_AI_RESPONSE_SEC); assert_true(prof_output_regex("HTTP 500")); prof_timeout_reset(); ai_http_stub_stop(); } void ai_http_models_fetch_lists_models(void** state) { /* /v1/models endpoint returns a list of three models. /ai models

* caches them silently; /ai models

--cached then displays the names. */ _setup_mock_provider("models", NULL); prof_input("/ai models mock"); prof_timeout(WAIT_AI_RESPONSE_SEC); assert_true(prof_output_regex("Cached 3 models for provider 'mock'")); prof_timeout_reset(); prof_input("/ai models mock --cached"); prof_timeout(5); assert_true(prof_output_regex("model-a")); assert_true(prof_output_regex("model-b")); assert_true(prof_output_regex("model-c")); prof_timeout_reset(); ai_http_stub_stop(); }