// SPDX-License-Identifier: ISC/* * test_lua_agent -- exercise the agent.new()/:submit()/:free() subagent * prototype (lua_agent.c) against a canned HTTP server, on the same uv loop * as the parent agent (mirrors how a real tool invocation runs). */#include <stdio.h>#include <stdlib.h>#include <string.h> #include <cjson/cJSON.h>#include <uv.h> #include "clm/clm.h"#include "clm/host_uv.h"#include "clm/internal.h"#include "clm/lua_plugin.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) static const char *final_reply = "{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0," "\"message\":{\"role\":\"assistant\",\"content\":\"hi from child\"}}]}"; struct tstate { uv_loop_t *loop; struct clm_host *host; struct clm_agent *agent; struct clm_lua_env *env; struct canned_server *srv; int turn_done; int turn_status; char assistant[256];}; static voidon_assistant_text(const char *text, void *user){ struct tstate *st = user; (void)snprintf(st->assistant, sizeof(st->assistant), "%s", text ? text : "");} static voidon_turn_done(int status, void *user){ struct tstate *st = user; st->turn_done = 1; st->turn_status = status;} static const struct clm_callbacks parent_callbacks = { .on_assistant_text = on_assistant_text, .on_turn_done = on_turn_done,}; static inttest_spawn_and_submit(void){ struct tstate st = {0}; char base_url[256]; int r; st.loop = uv_default_loop(); r = clm_host_uv_new(st.loop, &st.host); CHECK(r == 0, "host creation"); st.srv = canned_start(st.loop); CHECK(st.srv != NULL, "canned server start"); if (st.srv == NULL) return 1; (void)snprintf(base_url, sizeof(base_url), "http://127.0.0.1:%d/v1/chat/completions", canned_port(st.srv)); struct clm_cfg cfg = { .api_key = "test", .base_url = base_url, .provider = CLM_PROVIDER_OPENAI, .model = "test", .max_iterations = 4, .stream = 0, }; r = clm_agent_new(&cfg, st.host, &parent_callbacks, &st, &st.agent); CHECK(r == 0, "parent agent creation"); if (r != 0) return 1; r = clm_lua_env_new(st.agent, &st.env); CHECK(r == 0, "lua env creation"); r = clm_lua_load_plugins(st.env, "test/plugins_agent"); CHECK(r == 0, "plugin dir loads"); /* Find and invoke spawn_test directly via the tool registry, exactly * as clm_tools_dispatch would, but without needing a canned parent * LLM response requesting it. */ struct clm_tool *t; struct clm_tool *found = NULL; TAILQ_FOREACH(t, &st.agent->tools, entries) { if (strcmp(t->name, "spawn_test") == 0) { found = t; break; } } CHECK(found != NULL, "spawn_test tool registered"); if (found == NULL) return 1; /* Drive it through a real tool-call turn so clm_tools_dispatch's * async machinery (and thus the coroutine yield/resume path in * lua_agent.c) is exercised faithfully. Queue a parent response that * calls spawn_test, then a second queued child reply already set * above, then a parent follow-up reply once the tool result comes * back. */ { 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("spawn_test")); cJSON_AddItemToObject(func, "arguments", cJSON_CreateString("{}")); 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(st.srv, printed != NULL ? printed : "{}"); free(printed); cJSON_Delete(root); } /* FIFO order: (1) parent's tool-call response above, (2) the child * subagent's own turn (final_reply), (3) parent's follow-up once the * tool result comes back. */ canned_reply(st.srv, final_reply); canned_reply(st.srv, "{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0," "\"message\":{\"role\":\"assistant\",\"content\":\"parent done\"}}]}"); r = clm_agent_submit(st.agent, "go"); CHECK(r == 0, "parent submit accepted"); for (int i = 0; i < 2000 && !st.turn_done; i++) uv_run(st.loop, UV_RUN_ONCE); CHECK(st.turn_done, "parent turn completed"); CHECK(st.turn_status == 0, "parent turn succeeded"); CHECK(strstr(st.assistant, "parent done") != NULL, "parent's final assistant text observed"); clm_lua_env_free(st.env); clm_agent_free(st.agent); canned_stop(st.srv); uv_run(st.loop, UV_RUN_ONCE); clm_host_uv_free(st.host); return 0;} intmain(void){ printf("test_lua_agent: running...\n"); test_spawn_and_submit(); if (failures > 0) { printf("test_lua_agent: %d failure(s)\n", failures); return 1; } printf("test_lua_agent: PASS\n"); return 0;}
// SPDX-License-Identifier: ISC
/*
* test_lua_agent -- exercise the agent.new()/:submit()/:free() subagent
* prototype (lua_agent.c) against a canned HTTP server, on the same uv loop
* as the parent agent (mirrors how a real tool invocation runs).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cjson/cJSON.h>
#include <uv.h>
#include "clm/clm.h"
#include "clm/host_uv.h"
#include "clm/internal.h"
#include "clm/lua_plugin.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)
static const char *final_reply =
"{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,"
"\"message\":{\"role\":\"assistant\",\"content\":\"hi from child\"}}]}";
struct tstate {
uv_loop_t *loop;
struct clm_host *host;
struct clm_agent *agent;
struct clm_lua_env *env;
struct canned_server *srv;
int turn_done;
int turn_status;
char assistant[256];
};
static void
on_assistant_text(const char *text, void *user)
{
struct tstate *st = user;
(void)snprintf(st->assistant, sizeof(st->assistant), "%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 const struct clm_callbacks parent_callbacks = {
.on_assistant_text = on_assistant_text,
.on_turn_done = on_turn_done,
};
static int
test_spawn_and_submit(void)
{
struct tstate st = {0};
char base_url[256];
int r;
st.loop = uv_default_loop();
r = clm_host_uv_new(st.loop, &st.host);
CHECK(r == 0, "host creation");
st.srv = canned_start(st.loop);
CHECK(st.srv != NULL, "canned server start");
if (st.srv == NULL)
return 1;
(void)snprintf(base_url, sizeof(base_url),
"http://127.0.0.1:%d/v1/chat/completions", canned_port(st.srv));
struct clm_cfg cfg = {
.api_key = "test",
.base_url = base_url,
.provider = CLM_PROVIDER_OPENAI,
.model = "test",
.max_iterations = 4,
.stream = 0,
};
r = clm_agent_new(&cfg, st.host, &parent_callbacks, &st, &st.agent);
CHECK(r == 0, "parent agent creation");
if (r != 0)
return 1;
r = clm_lua_env_new(st.agent, &st.env);
CHECK(r == 0, "lua env creation");
r = clm_lua_load_plugins(st.env, "test/plugins_agent");
CHECK(r == 0, "plugin dir loads");
/* Find and invoke spawn_test directly via the tool registry, exactly
* as clm_tools_dispatch would, but without needing a canned parent
* LLM response requesting it. */
struct clm_tool *t;
struct clm_tool *found = NULL;
TAILQ_FOREACH(t, &st.agent->tools, entries) {
if (strcmp(t->name, "spawn_test") == 0) {
found = t;
break;
}
}
CHECK(found != NULL, "spawn_test tool registered");
if (found == NULL)
return 1;
/* Drive it through a real tool-call turn so clm_tools_dispatch's
* async machinery (and thus the coroutine yield/resume path in
* lua_agent.c) is exercised faithfully. Queue a parent response that
* calls spawn_test, then a second queued child reply already set
* above, then a parent follow-up reply once the tool result comes
* back. */
{
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("spawn_test"));
cJSON_AddItemToObject(func, "arguments", cJSON_CreateString("{}"));
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(st.srv, printed != NULL ? printed : "{}");
free(printed);
cJSON_Delete(root);
}
/* FIFO order: (1) parent's tool-call response above, (2) the child
* subagent's own turn (final_reply), (3) parent's follow-up once the
* tool result comes back. */
canned_reply(st.srv, final_reply);
canned_reply(st.srv,
"{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,"
"\"message\":{\"role\":\"assistant\",\"content\":\"parent done\"}}]}");
r = clm_agent_submit(st.agent, "go");
CHECK(r == 0, "parent submit accepted");
for (int i = 0; i < 2000 && !st.turn_done; i++)
uv_run(st.loop, UV_RUN_ONCE);
CHECK(st.turn_done, "parent turn completed");
CHECK(st.turn_status == 0, "parent turn succeeded");
CHECK(strstr(st.assistant, "parent done") != NULL,
"parent's final assistant text observed");
clm_lua_env_free(st.env);
clm_agent_free(st.agent);
canned_stop(st.srv);
uv_run(st.loop, UV_RUN_ONCE);
clm_host_uv_free(st.host);
return 0;
}
int
main(void)
{
printf("test_lua_agent: running...\n");
test_spawn_and_submit();
if (failures > 0) {
printf("test_lua_agent: %d failure(s)\n", failures);
return 1;
}
printf("test_lua_agent: PASS\n");
return 0;
}