#include "cJSON.h" #include "cst_agent.h" #include #include #include static int nfail; #define EXPECT(cond, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \ nfail++; \ } \ } while (0) static cst_message_t msg(cst_role_t role, const char *content) { cst_message_t m; memset(&m, 0, sizeof(m)); m.role = role; m.content = (char *)content; return m; } static cJSON *build(const cst_message_t *msgs, int n, int tools, size_t tool_cap) { cst_llm_request_t r; memset(&r, 0, sizeof(r)); r.model = "test-model"; r.msgs = msgs; r.nmsgs = n; r.enable_tools = tools; r.tool_out_cap = tool_cap; char *body = cst_llm_build_body(&r); EXPECT(body != NULL, "body built"); cJSON *j = body ? cJSON_Parse(body) : NULL; EXPECT(j != NULL, "body is valid JSON"); free(body); return j; } static const char *content_at(const cJSON *root, int i) { const cJSON *msgs = cJSON_GetObjectItemCaseSensitive(root, "messages"); const cJSON *m = cJSON_GetArrayItem(msgs, i); const cJSON *c = cJSON_GetObjectItemCaseSensitive(m, "content"); return cJSON_IsString(c) ? c->valuestring : NULL; } static const char *role_at(const cJSON *root, int i) { const cJSON *msgs = cJSON_GetObjectItemCaseSensitive(root, "messages"); const cJSON *m = cJSON_GetArrayItem(msgs, i); const cJSON *r = cJSON_GetObjectItemCaseSensitive(m, "role"); return cJSON_IsString(r) ? r->valuestring : ""; } static int nmsgs(const cJSON *root) { return cJSON_GetArraySize(cJSON_GetObjectItemCaseSensitive(root, "messages")); } /* TAB / quotes / backslash / control bytes / CJK must survive verbatim * (the old hand-rolled escaper turned a Makefile TAB into HTTP 400). */ static void test_escaping(void) { const char *text = "all:\n\tgcc -o \"a b\" x.c \\\n\x01\x1f end 中文"; cst_message_t m[2] = {msg(CST_ROLE_SYSTEM, "sys"), msg(CST_ROLE_USER, text)}; cJSON *j = build(m, 2, 0, 0); const char *c = content_at(j, 1); EXPECT(c && strcmp(c, text) == 0, "user content roundtrips exactly"); EXPECT(!cJSON_GetObjectItemCaseSensitive(j, "tools"), "no tools when off"); const cJSON *so = cJSON_GetObjectItemCaseSensitive(j, "stream_options"); EXPECT(cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(so, "include_usage")), "stream_options.include_usage"); cJSON_Delete(j); } static void test_invalid_utf8(void) { const char text[] = "ok \xff\xfe bad \xe4\xb8 trunc"; cst_message_t m[1] = {msg(CST_ROLE_USER, text)}; cJSON *j = build(m, 1, 0, 0); const char *c = content_at(j, 0); EXPECT(c != NULL, "content present"); EXPECT(c && strlen(c) == strlen(text), "sanitize keeps byte length"); EXPECT(c && strstr(c, "ok ") == c, "valid prefix kept"); cJSON_Delete(j); } static void test_tool_calls_and_null_content(void) { cst_message_t m[3]; m[0] = msg(CST_ROLE_USER, "go"); m[1] = msg(CST_ROLE_ASSISTANT, ""); m[1].tool_calls_json = "[{\"id\":\"c1\",\"type\":\"function\",\"function\":{\"name\":" "\"read_file\",\"arguments\":\"{\\\"path\\\":\\\"a\\tb.c\\\"}\"}}]"; m[2] = msg(CST_ROLE_TOOL, "[tool read_file id=c1 exit=0]\nhello"); snprintf(m[2].tool_call_id, sizeof(m[2].tool_call_id), "c1"); cJSON *j = build(m, 3, 1, 0); const cJSON *msgs = cJSON_GetObjectItemCaseSensitive(j, "messages"); const cJSON *a = cJSON_GetArrayItem(msgs, 1); EXPECT(cJSON_IsNull(cJSON_GetObjectItemCaseSensitive(a, "content")), "assistant content null with tool_calls"); const cJSON *tc = cJSON_GetObjectItemCaseSensitive(a, "tool_calls"); EXPECT(cJSON_IsArray(tc) && cJSON_GetArraySize(tc) == 1, "tool_calls kept"); const cJSON *t = cJSON_GetArrayItem(msgs, 2); EXPECT(strcmp(role_at(j, 2), "tool") == 0, "tool role"); const cJSON *tid = cJSON_GetObjectItemCaseSensitive(t, "tool_call_id"); EXPECT(cJSON_IsString(tid) && strcmp(tid->valuestring, "c1") == 0, "tool_call_id"); const cJSON *tools = cJSON_GetObjectItemCaseSensitive(j, "tools"); EXPECT(cJSON_IsArray(tools) && cJSON_GetArraySize(tools) >= 4, "tools[] schema present"); const cJSON *choice = cJSON_GetObjectItemCaseSensitive(j, "tool_choice"); EXPECT(cJSON_IsString(choice) && strcmp(choice->valuestring, "auto") == 0, "tool_choice auto"); cJSON_Delete(j); } static void test_merge_and_orphan_tool(void) { cst_message_t m[3]; m[0] = msg(CST_ROLE_USER, "first"); m[1] = msg(CST_ROLE_TOOL, "orphan output"); /* no id -> user text */ m[2] = msg(CST_ROLE_USER, "third"); cJSON *j = build(m, 3, 0, 0); EXPECT(nmsgs(j) == 1, "consecutive user messages merged"); const char *c = content_at(j, 0); EXPECT(c && strstr(c, "first") && strstr(c, "orphan output") && strstr(c, "third"), "merged content keeps all parts in order"); cJSON_Delete(j); } static void test_tool_cap_head_tail(void) { size_t n = 100000; char *big = malloc(n + 1); memset(big, 'x', n); memcpy(big, "HEAD", 4); memcpy(big + n - 4, "TAIL", 4); big[n] = 0; cst_message_t m[3]; m[0] = msg(CST_ROLE_USER, "go"); m[1] = msg(CST_ROLE_ASSISTANT, ""); m[1].tool_calls_json = "[{\"id\":\"c1\",\"type\":\"function\",\"function\":" "{\"name\":\"run_shell\",\"arguments\":\"{}\"}}]"; m[2] = msg(CST_ROLE_TOOL, big); snprintf(m[2].tool_call_id, sizeof(m[2].tool_call_id), "c1"); cJSON *j = build(m, 3, 1, 1000); const char *c = content_at(j, 2); EXPECT(c && strlen(c) < 1200, "tool output capped"); EXPECT(c && strncmp(c, "HEAD", 4) == 0, "head kept"); EXPECT(c && strcmp(c + strlen(c) - 4, "TAIL") == 0, "tail kept"); EXPECT(c && strstr(c, "omitted"), "omission note"); cJSON_Delete(j); /* user text (e.g. text-protocol result frames) is never capped */ cst_message_t u[1] = {msg(CST_ROLE_USER, big)}; j = build(u, 1, 0, 1000); c = content_at(j, 0); EXPECT(c && strlen(c) == n, "user text not capped"); cJSON_Delete(j); free(big); } int main(void) { test_escaping(); test_invalid_utf8(); test_tool_calls_and_null_content(); test_merge_and_orphan_tool(); test_tool_cap_head_tail(); if (nfail) { fprintf(stderr, "test_llm_body: %d failed\n", nfail); return 1; } printf("test_llm_body: OK\n"); return 0; }