mirror of
https://git.jabber.space/devs/cproof.git
synced 2026-07-19 05:46:20 +00:00
Unit tests (tests/unittests/test_ai_client.c): +50 cases on top of the
existing 50 — total 100. Covers:
- chat response parser (ai_parse_response): OpenAI content + Perplexity
text formats, escape decoding, empty/null/missing inputs, format-string
safety, multiline content
- error envelope parser (ai_parse_error_message): standard envelope,
nested escapes, missing fields, null/empty
- extended JSON escape: \b, \f, \r, all-specials, UTF-8 pass-through
- provider autocomplete cycling with >=2 matches and wrap-around
- session edge cases: NULL args, 100-message order preservation,
set_model(NULL), ref/unref(NULL)
- provider edge cases: get/remove with NULL, double-remove, survival
via session ref after ai_remove_provider
- settings: multi-key independence, missing key, cross-provider
isolation
- model parsing edges: data not array, empty data, "id" outside data,
multiple models
- prefs round-trip: set token -> shutdown -> init -> token reloaded
from disk (uses load_preferences fixture)
Functional tests:
- Tier A (test_ai.c): 15 cases for the /ai command surface that don't
need HTTP. Covers /ai help, providers list, set provider/token,
start with/without key/unknown provider, clear, remove, default
provider/model, switch without window, bad subcommand.
- Tier B (test_ai_http.c + ai_http_stub.{c,h,py}): 5 cases that
exercise the libcurl path against a local Python HTTP stub. The
stub serves canned bodies in five modes (ok, openai, 401, 500,
models) so each chat/models/error path is hit end-to-end without
network.
Infrastructure:
- TEST_GROUPS bumped to 5 in proftest.c; AI tests live in their own
Group 5 because mixing them with stabber-driven tests in Group 4
poisoned stbbr_stop() teardown.
- PROF_FUNC_TEST_AI macro in functionaltests.c registers AI tests
with ai_init_test() which wraps init_prof_test with a prof_connect()
so stabber sees a graceful disconnect at teardown.
- dist_check_DATA ships ai_http_stub.py with the source tarball.
Source-level changes to enable testing:
- src/ai/ai_client.{c,h}: dropped 'static' from _parse_ai_response
(renamed ai_parse_response) and _parse_error_response (renamed
ai_parse_error_message); declared under a "Parsing helpers (exposed
for testing)" section. Same approach as ai_parse_models_from_json.
Results in Docker (cproof-debian image):
- 595/595 unit tests pass
- 20/20 AI functional tests pass (Group 5)
101 lines
2.3 KiB
C
101 lines
2.3 KiB
C
/*
|
|
* ai_http_stub.c
|
|
*
|
|
* Spawns tests/functionaltests/ai_http_stub.py as a child process for
|
|
* functional tests that exercise the AI client's HTTP code path. The
|
|
* stub listens on 127.0.0.1 on an OS-chosen port and prints "PORT=<n>"
|
|
* as its first line; we read it back and hand the port to the caller.
|
|
*/
|
|
|
|
#include "ai_http_stub.h"
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#define STUB_SCRIPT "../tests/functionaltests/ai_http_stub.py"
|
|
#define READLINE_MAX 64
|
|
|
|
static pid_t stub_pid = 0;
|
|
|
|
int
|
|
ai_http_stub_start(const char* mode, const char* reply)
|
|
{
|
|
int pipefd[2];
|
|
if (pipe(pipefd) != 0) {
|
|
return 0;
|
|
}
|
|
|
|
pid_t pid = fork();
|
|
if (pid < 0) {
|
|
close(pipefd[0]);
|
|
close(pipefd[1]);
|
|
return 0;
|
|
}
|
|
|
|
if (pid == 0) {
|
|
/* Child: redirect stdout to the pipe write end, then exec the stub. */
|
|
close(pipefd[0]);
|
|
dup2(pipefd[1], STDOUT_FILENO);
|
|
close(pipefd[1]);
|
|
|
|
if (mode) {
|
|
setenv("AI_STUB_MODE", mode, 1);
|
|
}
|
|
if (reply) {
|
|
setenv("AI_STUB_REPLY", reply, 1);
|
|
}
|
|
/* Argument "0" -> let the OS pick a free port. */
|
|
execlp("python3", "python3", STUB_SCRIPT, "0", (char*)NULL);
|
|
/* Falls through on exec failure. */
|
|
_exit(127);
|
|
}
|
|
|
|
/* Parent: read "PORT=<n>\n" from the child's stdout. */
|
|
close(pipefd[1]);
|
|
|
|
char buf[READLINE_MAX] = { 0 };
|
|
ssize_t total = 0;
|
|
while (total < (ssize_t)sizeof(buf) - 1) {
|
|
ssize_t n = read(pipefd[0], buf + total, sizeof(buf) - 1 - total);
|
|
if (n <= 0) {
|
|
break;
|
|
}
|
|
total += n;
|
|
if (memchr(buf, '\n', total)) {
|
|
break;
|
|
}
|
|
}
|
|
close(pipefd[0]);
|
|
|
|
int port = 0;
|
|
if (sscanf(buf, "PORT=%d", &port) != 1 || port <= 0) {
|
|
kill(pid, SIGTERM);
|
|
waitpid(pid, NULL, 0);
|
|
return 0;
|
|
}
|
|
|
|
stub_pid = pid;
|
|
return port;
|
|
}
|
|
|
|
void
|
|
ai_http_stub_stop(void)
|
|
{
|
|
if (stub_pid <= 0) {
|
|
return;
|
|
}
|
|
kill(stub_pid, SIGTERM);
|
|
/* Give the stub a moment to exit cleanly, then reap. */
|
|
int status = 0;
|
|
pid_t waited = waitpid(stub_pid, &status, 0);
|
|
(void)waited;
|
|
(void)status;
|
|
stub_pid = 0;
|
|
}
|