1742 lines · 58.2 KB · C
RawDownload
// 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.