diff options
| author | Ethan Werner <mail@ethanwerner.com> | 2026-10-10 20:55:33 +0000 |
|---|---|---|
| committer | Ethan Werner <mail@ethanwerner.com> | 2026-10-10 20:56:50 +0000 |
| commit | f8423ebea413605b42de23f5dd06c5479d3fa1dd (patch) | |
| tree | 2612a5eb026c59e7aae01f10a1cf62f2ee43042c /tests/test_agent.cpp | |
Diffstat (limited to 'tests/test_agent.cpp')
| -rw-r--r-- | tests/test_agent.cpp | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/tests/test_agent.cpp b/tests/test_agent.cpp new file mode 100644 index 0000000..c5212ae --- /dev/null +++ b/tests/test_agent.cpp @@ -0,0 +1,260 @@ +#include "agent.h" + +#include <cjson/cJSON.h> +#include <curl/curl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static int failures = 0; +static int checks = 0; + +#define CHECK(cond, msg) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ + } \ + } while (0) + +#define CHECK_EQ_STR(a, b, msg) \ + do { \ + checks++; \ + const char *_a = (a), *_b = (b); \ + if (!_a || (_b && strcmp(_a, _b) != 0)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s (got `%s`, want `%s`)\n", \ + __FILE__, __LINE__, (msg), _a ? _a : "(null)", \ + _b ? _b : "(null)"); \ + } \ + } while (0) + +static config mkcfg(provider_kind p, const char *model, const char *base, + const char *key) { + config c; + memset(&c, 0, sizeof c); + c.provider = p; + c.model = xstrdup(model); + c.base_url = base ? xstrdup(base) : NULL; + c.api_key = key ? xstrdup(key) : NULL; + return c; +} + +static void freecfg(config *c) { + free(c->model); + free(c->base_url); + free(c->api_key); +} + +static void test_extract_openai_context_length(void) { + config c = mkcfg(PROV_OPENAI, "llm", NULL, NULL); + cJSON *j = cJSON_Parse("{\"id\":\"llm\",\"object\":\"model\"," + "\"context_length\":262144}"); + CHECK(extract_context_limit(j, &c) == 262144, "openai top-level limit"); + cJSON_Delete(j); + j = cJSON_Parse("{\"id\":\"llm\",\"object\":\"model\"}"); + CHECK(extract_context_limit(j, &c) == 0, "openai missing limit"); + cJSON_Delete(j); + j = cJSON_Parse("{\"object\":\"list\",\"data\":[" + "{\"id\":\"llm\",\"aliases\":[\"llm\"]," + "\"owned_by\":\"llamacpp\",\"meta\":{\"n_ctx\":131072}}," + "{\"id\":\"other\",\"meta\":{\"n_ctx\":8192}}]}"); + CHECK(extract_context_limit(j, &c) == 131072, + "llama.cpp meta.n_ctx for matching model"); + cJSON_Delete(j); + freecfg(&c); +} + +static void test_openai_model_present(void) { + CHECK(openai_model_present("{\"data\":[{\"id\":\"llm\"},{\"id\":\"other\"}]}", + "llm"), + "id match in data[]"); + CHECK(!openai_model_present("{\"data\":[{\"id\":\"llm\"},{\"id\":\"other\"}]}", + "nope"), + "id miss"); + CHECK(openai_model_present( + "{\"models\":[{\"name\":\"llm\"}]," + "\"data\":[{\"id\":\"x\",\"aliases\":[\"llm\"]}]}", + "llm"), + "models[].name and data[].aliases variants"); + CHECK(!openai_model_present("{\"data\":[]}", "llm"), "empty data[]"); + CHECK(!openai_model_present("not json", "llm"), "malformed body"); + /* parity: a non-array `data` blocks the `models` fallback (Rust + * get("data").or_else(|| get("models"))) */ + CHECK(!openai_model_present( + "{\"data\":{\"id\":\"x\"},\"models\":[{\"id\":\"llm\"}]}", "llm"), + "non-array data does not fall back to models"); +} + +static void test_extract_ollama_context_length(void) { + config c = mkcfg(PROV_OLLAMA, "llama3.2", NULL, NULL); + cJSON *j = cJSON_Parse("{\"name\":\"llama3.2\"," + "\"model_details\":{\"family\":\"llama\"," + "\"context_length\":8192}," + "\"modelfiles\":[{\"parameters\":" + "{\"context_length\":4096}}]}"); + CHECK(extract_context_limit(j, &c) == 8192, "ollama model_details"); + cJSON_Delete(j); + j = cJSON_Parse("{\"modelfiles\":[{\"parameters\":" + "{\"context_length\":4096}}]}"); + CHECK(extract_context_limit(j, &c) == 4096, "ollama modelfiles fallback"); + cJSON_Delete(j); + j = cJSON_Parse("{\"name\":\"llama3.2\",\"model_details\":{}}"); + CHECK(extract_context_limit(j, &c) == 0, "ollama absent"); + cJSON_Delete(j); + freecfg(&c); +} + +static void test_extract_anthropic_max_tokens(void) { + cJSON *j = cJSON_Parse("{\"data\":[" + "{\"id\":\"claude-haiku\",\"max_tokens\":8192}," + "{\"id\":\"claude-sonnet-4-6\",\"max_tokens\":64000}]}"); + config c = mkcfg(PROV_ANTHROPIC, "claude-sonnet-4-6", NULL, NULL); + CHECK(extract_context_limit(j, &c) == 64000, "matching model wins"); + freecfg(&c); + c = mkcfg(PROV_ANTHROPIC, "unknown-model", NULL, NULL); + CHECK(extract_context_limit(j, &c) == 8192, + "unknown model falls back to the first entry"); + freecfg(&c); + cJSON_Delete(j); + j = cJSON_Parse("{\"data\":[]}"); + c = mkcfg(PROV_ANTHROPIC, "unknown-model", NULL, NULL); + CHECK(extract_context_limit(j, &c) == 0, "empty data[] -> none"); + cJSON_Delete(j); + freecfg(&c); +} + +static void test_anthropic_model_present(void) { + const char *body = + "{\"data\":[{\"id\":\"claude-haiku\"},{\"id\":\"claude-sonnet-4-6\"}]}"; + CHECK(anthropic_model_present(body, "claude-sonnet-4-6"), "id match"); + CHECK(!anthropic_model_present(body, "claude-unknown"), "id miss"); + CHECK(!anthropic_model_present("not json", "claude-haiku"), + "malformed body never present"); + CHECK(!anthropic_model_present("{\"data\":[]}", "claude-haiku"), + "empty list"); + CHECK(!anthropic_model_present("{\"data\":{\"id\":\"x\"}}", "x"), + "data not an array"); +} + +static void test_probe_helpers(void) { + provider_probe ok = {true, xstrdup("model `llm` reachable at http://x")}; + CHECK(ok.ok, "ok flag"); + CHECK_EQ_STR(ok.detail, "model `llm` reachable at http://x", "ok detail"); + provider_probe_free(&ok); + CHECK(ok.detail == NULL, "probe_free releases detail"); + provider_probe bad = {false, xstrdup("could not reach http://x " + "(connection: timeout)")}; + CHECK(!bad.ok, "fail flag"); + CHECK(strstr(bad.detail, "timeout") != NULL, "fail detail content"); + provider_probe_free(&bad); + CHECK(bad.detail == NULL, "probe_free releases failed detail"); +} + +static void test_default_base_urls(void) { + CHECK_EQ_STR(agent_default_base_url(PROV_OPENAI), "https://api.openai.com/v1", + "openai default base"); + CHECK_EQ_STR(agent_default_base_url(PROV_ANTHROPIC), + "https://api.anthropic.com", "anthropic default base"); + CHECK_EQ_STR(agent_default_base_url(PROV_OLLAMA), "http://localhost:11434", + "ollama default base"); +} + +static void test_models_url(void) { + config c = mkcfg(PROV_OPENAI, "llm", NULL, NULL); + char *u = agent_models_url(&c); + CHECK_EQ_STR(u, "https://api.openai.com/v1/models", "openai default url"); + free(u); + freecfg(&c); + + c = mkcfg(PROV_OPENAI, "llm", "http://localhost:8080/v1/", "k"); + u = agent_models_url(&c); + CHECK_EQ_STR(u, "http://localhost:8080/v1/models", + "custom base, trailing slash trimmed"); + free(u); + freecfg(&c); + + c = mkcfg(PROV_ANTHROPIC, "claude-sonnet-4-6", NULL, "k"); + u = agent_models_url(&c); + CHECK_EQ_STR(u, "https://api.anthropic.com/v1/models?limit=100", + "anthropic url"); + free(u); + freecfg(&c); + + c = mkcfg(PROV_OLLAMA, "llama3.2", NULL, NULL); + u = agent_models_url(&c); + CHECK_EQ_STR(u, "http://localhost:11434/api/show?name=llama3.2", + "ollama url"); + free(u); + freecfg(&c); + + c = mkcfg(PROV_OLLAMA, "llama 3.2", "http://host:1/x/", NULL); + u = agent_models_url(&c); + CHECK(strstr(u, "name=llama%203.2") != NULL, "ollama name is url-encoded"); + free(u); + freecfg(&c); +} + +static void test_auth(void) { + config c = mkcfg(PROV_OPENAI, "llm", NULL, "sk-1"); + char *h = NULL, *v = NULL; + agent_auth(&c, &h, &v); + CHECK_EQ_STR(h, "Authorization", "openai auth header"); + CHECK_EQ_STR(v, "Bearer sk-1", "openai bearer value"); + free(h); + free(v); + freecfg(&c); + + c = mkcfg(PROV_OPENAI, "llm", "http://x", NULL); + agent_auth(&c, &h, &v); + CHECK(h == NULL && v == NULL, "openai without key: no auth"); + free(h); + free(v); + freecfg(&c); + + c = mkcfg(PROV_ANTHROPIC, "m", NULL, "ak"); + agent_auth(&c, &h, &v); + CHECK_EQ_STR(h, "x-api-key", "anthropic auth header"); + CHECK_EQ_STR(v, "ak", "anthropic key passthrough"); + free(h); + free(v); + freecfg(&c); + + c = mkcfg(PROV_OLLAMA, "m", NULL, "ignored"); + agent_auth(&c, &h, &v); + CHECK(h == NULL && v == NULL, "ollama sends no auth"); + free(h); + free(v); + freecfg(&c); +} + +static void test_http_get_error_path(void) { + long status = 0; + char *body = NULL, *err = NULL; + /* port 1 on localhost: connection refused, no server expected */ + int rc = agent_http_get("http://127.0.0.1:1/models", NULL, NULL, &status, + &body, &err); + CHECK(rc == -1, "unreachable endpoint -> transport failure"); + CHECK(err != NULL, "transport error message present"); + CHECK(body == NULL, "no body on transport failure"); + free(err); + free(body); +} + +int main(void) { + curl_global_init(CURL_GLOBAL_DEFAULT); + test_extract_openai_context_length(); + test_openai_model_present(); + test_extract_ollama_context_length(); + test_extract_anthropic_max_tokens(); + test_anthropic_model_present(); + test_probe_helpers(); + test_default_base_urls(); + test_models_url(); + test_auth(); + test_http_get_error_path(); + curl_global_cleanup(); + printf("test_agent: %d checks, %d failures\n", checks, failures); + return failures ? 1 : 0; +} |