// SPDX-License-Identifier: ISC#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h> #include <cjson/cJSON.h>#include <uv.h> #include "clm/clm.h"#include "clm/host_uv.h"#include "clm/internal.h"#include "clm/history.h"#include "canned.h" static int failures; #define CHECK(cond, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "FAIL: %s (%s:%d)\n", (msg), __FILE__, \ __LINE__); \ failures++; \ } \ } while (0) struct tstate { uv_loop_t *loop; struct clm_host *host; struct clm_agent *agent; int stream; enum clm_provider provider; const char *system_prompt; int turn_done; int turn_status; size_t asst_len; char assistant[256]; int tool_results; enum clm_tool_outcome last_outcome; char tool_content[256]; size_t reason_len; char reasoning[128]; int got_finish; enum clm_finish_reason finish; int got_usage; struct clm_usage usage; /* Permission-gate testing. */ int perm_prompts; /* times on_permission fired */ enum clm_permission_decision perm_decision; /* what to answer */ int notices; char notice[256];}; static voidon_assistant_text(const char *text, void *user){ struct tstate *st = user; int n = snprintf(st->assistant + st->asst_len, sizeof(st->assistant) - st->asst_len, "%s", text ? text : ""); if (n > 0) st->asst_len += (size_t)n;} static voidon_tool_result(const char *name, const char *content, enum clm_tool_outcome outcome, void *user){ struct tstate *st = user; (void)name; st->tool_results++; st->last_outcome = outcome; (void)snprintf(st->tool_content, sizeof(st->tool_content), "%s", content ? content : "");} /* * Queue a tool-call assistant reply, built with cJSON so the nested * "arguments" string (JSON encoded inside JSON) is escaped correctly. * args_json is the tool's raw arguments object, e.g. {"path":"/x"}. */static voidcanned_tool_call(struct canned_server *srv, const char *name, const char *args_json){ cJSON *root = cJSON_CreateObject(); cJSON *choices = cJSON_CreateArray(); cJSON *choice = cJSON_CreateObject(); cJSON *message = cJSON_CreateObject(); cJSON *calls = cJSON_CreateArray(); cJSON *call = cJSON_CreateObject(); cJSON *func = cJSON_CreateObject(); char *printed; cJSON_AddItemToObject(func, "name", cJSON_CreateString(name)); cJSON_AddItemToObject(func, "arguments", cJSON_CreateString(args_json)); cJSON_AddItemToObject(call, "id", cJSON_CreateString("c1")); cJSON_AddItemToObject(call, "type", cJSON_CreateString("function")); cJSON_AddItemToObject(call, "function", func); cJSON_AddItemToArray(calls, call); cJSON_AddItemToObject(message, "role", cJSON_CreateString("assistant")); cJSON_AddItemToObject(message, "content", cJSON_CreateString("")); cJSON_AddItemToObject(message, "tool_calls", calls); cJSON_AddItemToObject(choice, "finish_reason", cJSON_CreateString("tool_calls")); cJSON_AddItemToObject(choice, "message", message); cJSON_AddItemToArray(choices, choice); cJSON_AddItemToObject(root, "choices", choices); printed = cJSON_PrintUnformatted(root); canned_reply(srv, printed != NULL ? printed : "{}"); free(printed); cJSON_Delete(root);} static const char *final_reply = "{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0," "\"message\":{\"role\":\"assistant\",\"content\":\"done\"}}]}"; static voidon_reasoning(const char *text, void *user){ struct tstate *st = user; int n = snprintf(st->reasoning + st->reason_len, sizeof(st->reasoning) - st->reason_len, "%s", text ? text : ""); if (n > 0) st->reason_len += (size_t)n;} static voidon_finish_reason(enum clm_finish_reason reason, void *user){ struct tstate *st = user; st->got_finish = 1; st->finish = reason;} static voidon_usage(const struct clm_usage *usage, void *user){ struct tstate *st = user; st->got_usage = 1; st->usage = *usage;} static voidon_notice(const char *text, void *user){ struct tstate *st = user; st->notices++; (void)snprintf(st->notice, sizeof(st->notice), "%s", text ? text : "");} static voidon_turn_done(int status, void *user){ struct tstate *st = user; st->turn_done = 1; st->turn_status = status;} static voidon_permission(const struct clm_permission_req *req, void *user){ struct tstate *st = user; st->perm_prompts++; clm_tool_permission_respond(st->agent, req, st->perm_decision);} static const struct clm_callbacks callbacks = { .on_assistant_text = on_assistant_text, .on_reasoning = on_reasoning, .on_permission = on_permission, .on_tool_result = on_tool_result, .on_finish_reason = on_finish_reason, .on_usage = on_usage, .on_turn_done = on_turn_done, .on_notice = on_notice,}; /* A mock tool that completes synchronously with a fixed string. */static voidecho_hello(struct clm_tool_invocation *inv, void *user){ (void)user; clm_tool_complete(inv, "hello");} /* A mock tool that returns raw binary output (embedded NUL, non-UTF-8 bytes) * via clm_tool_complete_buf, to exercise inv_finalize's binary-output * detector (see find_binary_offset in tools.c). */static voidemit_binary(struct clm_tool_invocation *inv, void *user){ static const uint8_t data[] = { 'h', 'i', 0x00, 0x89, 'P', 'N', 'G' }; struct clm_buffer buf; (void)user; buf.data = data; buf.len = sizeof(data); clm_tool_complete_buf(inv, buf);} static struct clm_agent *make_agent(struct tstate *st, int port){ char url[128]; struct clm_agent *agent = NULL; struct clm_cfg cfg = {0}; int r; (void)snprintf(url, sizeof(url), "http://127.0.0.1:%d/v1/chat/completions", port); cfg.api_key = "test"; cfg.base_url = url; cfg.provider = st->provider; cfg.model = "test-model"; cfg.stream = st->stream; cfg.system_prompt = st->system_prompt; r = clm_host_uv_new(st->loop, &st->host); CHECK(r == 0, "clm_host_uv_new"); r = clm_agent_new(&cfg, st->host, &callbacks, st, &agent); CHECK(r == 0, "clm_agent_new"); CHECK(clm_tools_register_shell(agent) == 0, "register shell"); CHECK(clm_tools_register_bg(agent) == 0, "register bg"); return agent;} static voidrun_until_done(struct tstate *st){ while (!st->turn_done) uv_run(st->loop, UV_RUN_ONCE);} static voidteardown(struct tstate *st, struct canned_server *srv){ clm_agent_free(st->agent); clm_host_uv_free(st->host); canned_stop(srv); uv_run(st->loop, UV_RUN_DEFAULT); /* drain pending closes */} /* (a) A plain text prompt and a canned assistant reply. */static voidtest_text_reply(uv_loop_t *loop){ struct tstate st = {0}; struct canned_server *srv; st.loop = loop; st.system_prompt = "SENTINEL_SYS_PROMPT"; srv = canned_start(loop); CHECK(srv != NULL, "canned_start"); canned_reply(srv, "{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0," "\"message\":{\"role\":\"assistant\",\"content\":\"hi there\"}}]}"); st.agent = make_agent(&st, canned_port(srv)); CHECK(clm_agent_submit(st.agent, "hello") == 0, "submit"); run_until_done(&st); CHECK(st.turn_status == 0, "text turn ok"); CHECK(strstr(st.assistant, "hi there") != NULL, "assistant text delivered"); CHECK(canned_request_count(srv) == 1, "one request"); CHECK(canned_last_request(srv) != NULL && strstr(canned_last_request(srv), "SENTINEL_SYS_PROMPT") != NULL, "custom system prompt sent"); teardown(&st, srv);} /* (b) A prompt whose reply is a tool call, then a final answer. */static voidtest_tool_call(uv_loop_t *loop){ struct tstate st = {0}; struct canned_server *srv; struct clm_tool_def def = {0}; st.loop = loop; srv = canned_start(loop); CHECK(srv != NULL, "canned_start"); /* Turn 1: the model requests the echo tool. Turn 2: final answer. */ canned_tool_call(srv, "echo_hello", "{}"); canned_reply(srv, final_reply); st.agent = make_agent(&st, canned_port(srv)); def.name = "echo_hello"; def.description = "echo hello"; def.params_schema = "{\"type\":\"object\",\"properties\":{}}"; def.invoke = echo_hello; CHECK(clm_tool_add(st.agent, &def) == 0, "clm_tool_add"); CHECK(clm_agent_submit(st.agent, "use the tool") == 0, "submit"); run_until_done(&st); CHECK(st.turn_status == 0, "tool turn ok"); CHECK(st.tool_results == 1, "one tool result"); CHECK(st.last_outcome == CLM_TOOL_OK, "tool outcome ok"); CHECK(strstr(st.assistant, "done") != NULL, "final answer delivered"); CHECK(canned_request_count(srv) == 2, "two requests (call + result)"); /* The tool result must be fed back to the model on the second request. */ CHECK(canned_last_request(srv) != NULL && strstr(canned_last_request(srv), "hello") != NULL, "tool result echoed back to model"); teardown(&st, srv);} /* * (b2) A tool that reports raw binary output via clm_tool_complete_buf: the * history/request-building path must never emit invalid-UTF-8 bytes into the * JSON request (cJSON_CreateString on non-UTF-8 data is a landmine some * cJSON builds silently mishandle), so inv_finalize replaces it with a * human-readable warning before it ever reaches history or the wire. */static voidtest_binary_tool_output(uv_loop_t *loop){ struct tstate st = {0}; struct canned_server *srv; struct clm_tool_def def = {0}; st.loop = loop; srv = canned_start(loop); CHECK(srv != NULL, "canned_start"); canned_tool_call(srv, "emit_binary", "{}"); canned_reply(srv, final_reply); st.agent = make_agent(&st, canned_port(srv)); def.name = "emit_binary"; def.description = "emit binary output"; def.params_schema = "{\"type\":\"object\",\"properties\":{}}"; def.invoke = emit_binary; CHECK(clm_tool_add(st.agent, &def) == 0, "clm_tool_add"); CHECK(clm_agent_submit(st.agent, "use the tool") == 0, "submit"); run_until_done(&st); CHECK(st.turn_status == 0, "binary tool turn ok"); CHECK(st.tool_results == 1, "one tool result"); CHECK(st.last_outcome == CLM_TOOL_OK, "tool outcome ok"); CHECK(strstr(st.tool_content, "binary data at byte offset 2 of 7") != NULL, "on_tool_result sees the offset warning, not raw bytes"); CHECK(canned_request_count(srv) == 2, "two requests (call + result)"); CHECK(canned_last_request(srv) != NULL && strstr(canned_last_request(srv), "binary data at byte offset 2 of 7") != NULL, "warning (not raw binary) is what actually gets sent to the API"); teardown(&st, srv);} /* * (b3) bg_exec: the model starts a real background job via bg_exec, gets * back an immediate "started" result (turn 1 ends normally, no waiting on * the child process), and once the process actually exits, its output must * arrive as an AUTOMATIC follow-up turn via clm_agent_notify -- a third * canned request the test never explicitly triggers, proving the * job-exit -> notify -> clm_agent_submit chain in agent.c/tool_bg.c actually * fires end to end (not just that bg_exec's own invocation completes). */static voidtest_bg_exec(uv_loop_t *loop){ struct tstate st = {0}; struct canned_server *srv; st.loop = loop; srv = canned_start(loop); CHECK(srv != NULL, "canned_start"); /* Turn 1: the model starts a background job. Turn 2 (automatic, once * the job exits): the model acknowledges the job's result. */ canned_tool_call(srv, "bg_exec", "{\"command\":\"printf bgoutput123\",\"label\":\"probe\"}"); canned_reply(srv, "{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0," "\"message\":{\"role\":\"assistant\",\"content\":\"started it\"}}]}"); canned_reply(srv, "{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0," "\"message\":{\"role\":\"assistant\",\"content\":\"job acknowledged\"}}]}"); st.agent = make_agent(&st, canned_port(srv)); CHECK(clm_agent_submit(st.agent, "run a background job") == 0, "submit"); run_until_done(&st); CHECK(st.turn_status == 0, "turn 1 ok"); CHECK(st.tool_results == 1, "bg_exec's own invocation completed once"); CHECK(strstr(st.tool_content, "started background job") != NULL, "bg_exec's immediate result is a start acknowledgement, not the job's output"); CHECK(strstr(st.assistant, "started it") != NULL, "turn 1 final answer delivered"); CHECK(canned_request_count(srv) == 2, "two requests for turn 1 (call + start ack)"); /* Turn 1 is done, but the background job (a real subprocess) may not * have exited yet. Wait for the automatic follow-up turn its exit * triggers via clm_agent_notify -- run_until_done again, watching for * a second turn_done. */ st.turn_done = 0; run_until_done(&st); CHECK(st.turn_status == 0, "turn 2 (automatic) ok"); CHECK(strstr(st.assistant, "job acknowledged") != NULL, "turn 2's final answer delivered"); CHECK(canned_request_count(srv) == 3, "job exit triggered a third request automatically"); CHECK(canned_last_request(srv) != NULL && strstr(canned_last_request(srv), "bgoutput123") != NULL, "the job's real output reached the model in the auto-triggered turn"); CHECK(canned_last_request(srv) != NULL && strstr(canned_last_request(srv), "probe") != NULL, "the job's label reached the model in the auto-triggered turn"); teardown(&st, srv);} /* * a bg_exec job outlives the invocation that started it, so freeing the agent * after the immediate result must detach the job before its child exits. the * marker keeps the child alive until after clm_agent_free() has returned. */static voidtest_agent_free_during_bg_exec(uv_loop_t *loop){ struct tstate st = {0}; struct canned_server *srv; char dir[] = "/tmp/clm-bg-free.XXXXXX"; char marker[128]; char args[512]; FILE *f; st.loop = loop; CHECK(mkdtemp(dir) != NULL, "mkdtemp"); (void)snprintf(marker, sizeof(marker), "%s/go", dir); (void)snprintf(args, sizeof(args), "{\"command\":\"while [ ! -f %s ]; do sleep 0.01; done; " "printf bgdone\",\"label\":\"detach probe\"}", marker); srv = canned_start(loop); CHECK(srv != NULL, "canned_start"); canned_tool_call(srv, "bg_exec", args); canned_reply(srv, final_reply); st.agent = make_agent(&st, canned_port(srv)); CHECK(clm_agent_submit(st.agent, "start a background job") == 0, "submit"); run_until_done(&st); CHECK(st.turn_status == 0, "background job start turn ok"); CHECK(st.tool_results == 1, "bg_exec completed immediately"); CHECK(canned_request_count(srv) == 2, "background child is still blocked"); clm_agent_free(st.agent); st.agent = NULL; canned_stop(srv); f = fopen(marker, "w"); CHECK(f != NULL, "create background child marker"); if (f != NULL) fclose(f); uv_run(loop, UV_RUN_DEFAULT); clm_host_uv_free(st.host); uv_run(loop, UV_RUN_DEFAULT); (void)unlink(marker); (void)rmdir(dir);} /* * (b2) Mid-chain autocompact: if usage crosses the threshold on a response * that itself requests another tool call (i.e. the turn is NOT done yet), * clm_agent_tools_done() must trigger clm_agent_compact() itself right then * -- not wait for the whole turn to finish first, which is what tui.c's own * end-of-turn check does and why this needed a second, earlier check inside * agent.c (see clm_agent_tools_done). ctx_max is never probed against the * canned server (no /props), so this exercises the absolute-token fallback * path, not the percentage one -- both share the same * clm_agent_over_autocompact_threshold(), so covering one covers the calc * for both. * * Reply sequence: (1) tool call, with usage already over * CLM_AUTOCOMPACT_FALLBACK_TOKENS -- clm_agent_tools_done sees this once the * tool finishes and, instead of proceeding straight to the next LLM call, * fires off (2) a compaction request. (3) is the resumed turn's real next * LLM call, which must still happen automatically -- if the mid-chain * compact swallowed the turn instead of resuming it, turn_done would never * fire and run_until_done would hang instead of completing. */static voidtest_autocompact_mid_chain(uv_loop_t *loop){ struct tstate st = {0}; struct canned_server *srv; struct clm_tool_def def = {0}; st.loop = loop;Lines 1–500 of 1742. Load more
// SPDX-License-Identifier: ISC #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <cjson/cJSON.h> #include <uv.h> #include "clm/clm.h" #include "clm/host_uv.h" #include "clm/internal.h" #include "clm/history.h" #include "canned.h" static int failures; #define CHECK(cond, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "FAIL: %s (%s:%d)\n", (msg), __FILE__, \ __LINE__); \ failures++; \ } \ } while (0) struct tstate { uv_loop_t *loop; struct clm_host *host; struct clm_agent *agent; int stream; enum clm_provider provider; const char *system_prompt; int turn_done; int turn_status; size_t asst_len; char assistant[256]; int tool_results; enum clm_tool_outcome last_outcome; char tool_content[256]; size_t reason_len; char reasoning[128]; int got_finish; enum clm_finish_reason finish; int got_usage; struct clm_usage usage; /* Permission-gate testing. */ int perm_prompts; /* times on_permission fired */ enum clm_permission_decision perm_decision; /* what to answer */ int notices; char notice[256]; }; static void on_assistant_text(const char *text, void *user) { struct tstate *st = user; int n = snprintf(st->assistant + st->asst_len, sizeof(st->assistant) - st->asst_len, "%s", text ? text : ""); if (n > 0) st->asst_len += (size_t)n; } static void on_tool_result(const char *name, const char *content, enum clm_tool_outcome outcome, void *user) { struct tstate *st = user; (void)name; st->tool_results++; st->last_outcome = outcome; (void)snprintf(st->tool_content, sizeof(st->tool_content), "%s", content ? content : ""); } /* * Queue a tool-call assistant reply, built with cJSON so the nested * "arguments" string (JSON encoded inside JSON) is escaped correctly. * args_json is the tool's raw arguments object, e.g. {"path":"/x"}. */ static void canned_tool_call(struct canned_server *srv, const char *name, const char *args_json) { cJSON *root = cJSON_CreateObject(); cJSON *choices = cJSON_CreateArray(); cJSON *choice = cJSON_CreateObject(); cJSON *message = cJSON_CreateObject(); cJSON *calls = cJSON_CreateArray(); cJSON *call = cJSON_CreateObject(); cJSON *func = cJSON_CreateObject(); char *printed; cJSON_AddItemToObject(func, "name", cJSON_CreateString(name)); cJSON_AddItemToObject(func, "arguments", cJSON_CreateString(args_json)); cJSON_AddItemToObject(call, "id", cJSON_CreateString("c1")); cJSON_AddItemToObject(call, "type", cJSON_CreateString("function")); cJSON_AddItemToObject(call, "function", func); cJSON_AddItemToArray(calls, call); cJSON_AddItemToObject(message, "role", cJSON_CreateString("assistant")); cJSON_AddItemToObject(message, "content", cJSON_CreateString("")); cJSON_AddItemToObject(message, "tool_calls", calls); cJSON_AddItemToObject(choice, "finish_reason", cJSON_CreateString("tool_calls")); cJSON_AddItemToObject(choice, "message", message); cJSON_AddItemToArray(choices, choice); cJSON_AddItemToObject(root, "choices", choices); printed = cJSON_PrintUnformatted(root); canned_reply(srv, printed != NULL ? printed : "{}"); free(printed); cJSON_Delete(root); } static const char *final_reply = "{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0," "\"message\":{\"role\":\"assistant\",\"content\":\"done\"}}]}"; static void on_reasoning(const char *text, void *user) { struct tstate *st = user; int n = snprintf(st->reasoning + st->reason_len, sizeof(st->reasoning) - st->reason_len, "%s", text ? text : ""); if (n > 0) st->reason_len += (size_t)n; } static void on_finish_reason(enum clm_finish_reason reason, void *user) { struct tstate *st = user; st->got_finish = 1; st->finish = reason; } static void on_usage(const struct clm_usage *usage, void *user) { struct tstate *st = user; st->got_usage = 1; st->usage = *usage; } static void on_notice(const char *text, void *user) { struct tstate *st = user; st->notices++; (void)snprintf(st->notice, sizeof(st->notice), "%s", text ? text : ""); } static void on_turn_done(int status, void *user) { struct tstate *st = user; st->turn_done = 1; st->turn_status = status; } static void on_permission(const struct clm_permission_req *req, void *user) { struct tstate *st = user; st->perm_prompts++; clm_tool_permission_respond(st->agent, req, st->perm_decision); } static const struct clm_callbacks callbacks = { .on_assistant_text = on_assistant_text, .on_reasoning = on_reasoning, .on_permission = on_permission, .on_tool_result = on_tool_result, .on_finish_reason = on_finish_reason, .on_usage = on_usage, .on_turn_done = on_turn_done, .on_notice = on_notice, }; /* A mock tool that completes synchronously with a fixed string. */ static void echo_hello(struct clm_tool_invocation *inv, void *user) { (void)user; clm_tool_complete(inv, "hello"); } /* A mock tool that returns raw binary output (embedded NUL, non-UTF-8 bytes) * via clm_tool_complete_buf, to exercise inv_finalize's binary-output * detector (see find_binary_offset in tools.c). */ static void emit_binary(struct clm_tool_invocation *inv, void *user) { static const uint8_t data[] = { 'h', 'i', 0x00, 0x89, 'P', 'N', 'G' }; struct clm_buffer buf; (void)user; buf.data = data; buf.len = sizeof(data); clm_tool_complete_buf(inv, buf); } static struct clm_agent * make_agent(struct tstate *st, int port) { char url[128]; struct clm_agent *agent = NULL; struct clm_cfg cfg = {0}; int r; (void)snprintf(url, sizeof(url), "http://127.0.0.1:%d/v1/chat/completions", port); cfg.api_key = "test"; cfg.base_url = url; cfg.provider = st->provider; cfg.model = "test-model"; cfg.stream = st->stream; cfg.system_prompt = st->system_prompt; r = clm_host_uv_new(st->loop, &st->host); CHECK(r == 0, "clm_host_uv_new"); r = clm_agent_new(&cfg, st->host, &callbacks, st, &agent); CHECK(r == 0, "clm_agent_new"); CHECK(clm_tools_register_shell(agent) == 0, "register shell"); CHECK(clm_tools_register_bg(agent) == 0, "register bg"); return agent; } static void run_until_done(struct tstate *st) { while (!st->turn_done) uv_run(st->loop, UV_RUN_ONCE); } static void teardown(struct tstate *st, struct canned_server *srv) { clm_agent_free(st->agent); clm_host_uv_free(st->host); canned_stop(srv); uv_run(st->loop, UV_RUN_DEFAULT); /* drain pending closes */ } /* (a) A plain text prompt and a canned assistant reply. */ static void test_text_reply(uv_loop_t *loop) { struct tstate st = {0}; struct canned_server *srv; st.loop = loop; st.system_prompt = "SENTINEL_SYS_PROMPT"; srv = canned_start(loop); CHECK(srv != NULL, "canned_start"); canned_reply(srv, "{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0," "\"message\":{\"role\":\"assistant\",\"content\":\"hi there\"}}]}"); st.agent = make_agent(&st, canned_port(srv)); CHECK(clm_agent_submit(st.agent, "hello") == 0, "submit"); run_until_done(&st); CHECK(st.turn_status == 0, "text turn ok"); CHECK(strstr(st.assistant, "hi there") != NULL, "assistant text delivered"); CHECK(canned_request_count(srv) == 1, "one request"); CHECK(canned_last_request(srv) != NULL && strstr(canned_last_request(srv), "SENTINEL_SYS_PROMPT") != NULL, "custom system prompt sent"); teardown(&st, srv); } /* (b) A prompt whose reply is a tool call, then a final answer. */ static void test_tool_call(uv_loop_t *loop) { struct tstate st = {0}; struct canned_server *srv; struct clm_tool_def def = {0}; st.loop = loop; srv = canned_start(loop); CHECK(srv != NULL, "canned_start"); /* Turn 1: the model requests the echo tool. Turn 2: final answer. */ canned_tool_call(srv, "echo_hello", "{}"); canned_reply(srv, final_reply); st.agent = make_agent(&st, canned_port(srv)); def.name = "echo_hello"; def.description = "echo hello"; def.params_schema = "{\"type\":\"object\",\"properties\":{}}"; def.invoke = echo_hello; CHECK(clm_tool_add(st.agent, &def) == 0, "clm_tool_add"); CHECK(clm_agent_submit(st.agent, "use the tool") == 0, "submit"); run_until_done(&st); CHECK(st.turn_status == 0, "tool turn ok"); CHECK(st.tool_results == 1, "one tool result"); CHECK(st.last_outcome == CLM_TOOL_OK, "tool outcome ok"); CHECK(strstr(st.assistant, "done") != NULL, "final answer delivered"); CHECK(canned_request_count(srv) == 2, "two requests (call + result)"); /* The tool result must be fed back to the model on the second request. */ CHECK(canned_last_request(srv) != NULL && strstr(canned_last_request(srv), "hello") != NULL, "tool result echoed back to model"); teardown(&st, srv); } /* * (b2) A tool that reports raw binary output via clm_tool_complete_buf: the * history/request-building path must never emit invalid-UTF-8 bytes into the * JSON request (cJSON_CreateString on non-UTF-8 data is a landmine some * cJSON builds silently mishandle), so inv_finalize replaces it with a * human-readable warning before it ever reaches history or the wire. */ static void test_binary_tool_output(uv_loop_t *loop) { struct tstate st = {0}; struct canned_server *srv; struct clm_tool_def def = {0}; st.loop = loop; srv = canned_start(loop); CHECK(srv != NULL, "canned_start"); canned_tool_call(srv, "emit_binary", "{}"); canned_reply(srv, final_reply); st.agent = make_agent(&st, canned_port(srv)); def.name = "emit_binary"; def.description = "emit binary output"; def.params_schema = "{\"type\":\"object\",\"properties\":{}}"; def.invoke = emit_binary; CHECK(clm_tool_add(st.agent, &def) == 0, "clm_tool_add"); CHECK(clm_agent_submit(st.agent, "use the tool") == 0, "submit"); run_until_done(&st); CHECK(st.turn_status == 0, "binary tool turn ok"); CHECK(st.tool_results == 1, "one tool result"); CHECK(st.last_outcome == CLM_TOOL_OK, "tool outcome ok"); CHECK(strstr(st.tool_content, "binary data at byte offset 2 of 7") != NULL, "on_tool_result sees the offset warning, not raw bytes"); CHECK(canned_request_count(srv) == 2, "two requests (call + result)"); CHECK(canned_last_request(srv) != NULL && strstr(canned_last_request(srv), "binary data at byte offset 2 of 7") != NULL, "warning (not raw binary) is what actually gets sent to the API"); teardown(&st, srv); } /* * (b3) bg_exec: the model starts a real background job via bg_exec, gets * back an immediate "started" result (turn 1 ends normally, no waiting on * the child process), and once the process actually exits, its output must * arrive as an AUTOMATIC follow-up turn via clm_agent_notify -- a third * canned request the test never explicitly triggers, proving the * job-exit -> notify -> clm_agent_submit chain in agent.c/tool_bg.c actually * fires end to end (not just that bg_exec's own invocation completes). */ static void test_bg_exec(uv_loop_t *loop) { struct tstate st = {0}; struct canned_server *srv; st.loop = loop; srv = canned_start(loop); CHECK(srv != NULL, "canned_start"); /* Turn 1: the model starts a background job. Turn 2 (automatic, once * the job exits): the model acknowledges the job's result. */ canned_tool_call(srv, "bg_exec", "{\"command\":\"printf bgoutput123\",\"label\":\"probe\"}"); canned_reply(srv, "{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0," "\"message\":{\"role\":\"assistant\",\"content\":\"started it\"}}]}"); canned_reply(srv, "{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0," "\"message\":{\"role\":\"assistant\",\"content\":\"job acknowledged\"}}]}"); st.agent = make_agent(&st, canned_port(srv)); CHECK(clm_agent_submit(st.agent, "run a background job") == 0, "submit"); run_until_done(&st); CHECK(st.turn_status == 0, "turn 1 ok"); CHECK(st.tool_results == 1, "bg_exec's own invocation completed once"); CHECK(strstr(st.tool_content, "started background job") != NULL, "bg_exec's immediate result is a start acknowledgement, not the job's output"); CHECK(strstr(st.assistant, "started it") != NULL, "turn 1 final answer delivered"); CHECK(canned_request_count(srv) == 2, "two requests for turn 1 (call + start ack)"); /* Turn 1 is done, but the background job (a real subprocess) may not * have exited yet. Wait for the automatic follow-up turn its exit * triggers via clm_agent_notify -- run_until_done again, watching for * a second turn_done. */ st.turn_done = 0; run_until_done(&st); CHECK(st.turn_status == 0, "turn 2 (automatic) ok"); CHECK(strstr(st.assistant, "job acknowledged") != NULL, "turn 2's final answer delivered"); CHECK(canned_request_count(srv) == 3, "job exit triggered a third request automatically"); CHECK(canned_last_request(srv) != NULL && strstr(canned_last_request(srv), "bgoutput123") != NULL, "the job's real output reached the model in the auto-triggered turn"); CHECK(canned_last_request(srv) != NULL && strstr(canned_last_request(srv), "probe") != NULL, "the job's label reached the model in the auto-triggered turn"); teardown(&st, srv); } /* * a bg_exec job outlives the invocation that started it, so freeing the agent * after the immediate result must detach the job before its child exits. the * marker keeps the child alive until after clm_agent_free() has returned. */ static void test_agent_free_during_bg_exec(uv_loop_t *loop) { struct tstate st = {0}; struct canned_server *srv; char dir[] = "/tmp/clm-bg-free.XXXXXX"; char marker[128]; char args[512]; FILE *f; st.loop = loop; CHECK(mkdtemp(dir) != NULL, "mkdtemp"); (void)snprintf(marker, sizeof(marker), "%s/go", dir); (void)snprintf(args, sizeof(args), "{\"command\":\"while [ ! -f %s ]; do sleep 0.01; done; " "printf bgdone\",\"label\":\"detach probe\"}", marker); srv = canned_start(loop); CHECK(srv != NULL, "canned_start"); canned_tool_call(srv, "bg_exec", args); canned_reply(srv, final_reply); st.agent = make_agent(&st, canned_port(srv)); CHECK(clm_agent_submit(st.agent, "start a background job") == 0, "submit"); run_until_done(&st); CHECK(st.turn_status == 0, "background job start turn ok"); CHECK(st.tool_results == 1, "bg_exec completed immediately"); CHECK(canned_request_count(srv) == 2, "background child is still blocked"); clm_agent_free(st.agent); st.agent = NULL; canned_stop(srv); f = fopen(marker, "w"); CHECK(f != NULL, "create background child marker"); if (f != NULL) fclose(f); uv_run(loop, UV_RUN_DEFAULT); clm_host_uv_free(st.host); uv_run(loop, UV_RUN_DEFAULT); (void)unlink(marker); (void)rmdir(dir); } /* * (b2) Mid-chain autocompact: if usage crosses the threshold on a response * that itself requests another tool call (i.e. the turn is NOT done yet), * clm_agent_tools_done() must trigger clm_agent_compact() itself right then * -- not wait for the whole turn to finish first, which is what tui.c's own * end-of-turn check does and why this needed a second, earlier check inside * agent.c (see clm_agent_tools_done). ctx_max is never probed against the * canned server (no /props), so this exercises the absolute-token fallback * path, not the percentage one -- both share the same * clm_agent_over_autocompact_threshold(), so covering one covers the calc * for both. * * Reply sequence: (1) tool call, with usage already over * CLM_AUTOCOMPACT_FALLBACK_TOKENS -- clm_agent_tools_done sees this once the * tool finishes and, instead of proceeding straight to the next LLM call, * fires off (2) a compaction request. (3) is the resumed turn's real next * LLM call, which must still happen automatically -- if the mid-chain * compact swallowed the turn instead of resuming it, turn_done would never * fire and run_until_done would hang instead of completing. */ static void test_autocompact_mid_chain(uv_loop_t *loop) { struct tstate st = {0}; struct canned_server *srv; struct clm_tool_def def = {0}; st.loop = loop;