summaryrefslogtreecommitdiffstats
path: root/tests/test_agent.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_agent.cpp')
-rw-r--r--tests/test_agent.cpp260
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;
+}