From 9582ba8617afbb9be9a775c0bae2823fbc523be9 Mon Sep 17 00:00:00 2001 From: Adrianno Esnarriaga Sereno Date: Wed, 17 Jun 2026 12:14:14 +0000 Subject: [PATCH 1/3] test(tools,providers): cover get_context buffer guard and tool-call JSON Assert undersized caller buffers get a clear error after snapshot priming, and openai_compat serializes assistant tool_calls plus tool results. --- tests/test_context.c | 6 +++++ tests/test_openai.c | 56 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/tests/test_context.c b/tests/test_context.c index 5406dda..23b7464 100644 --- a/tests/test_context.c +++ b/tests/test_context.c @@ -152,6 +152,12 @@ int main(void) tool_context_set_config(NULL); tool_context_test_set_http_bodies(GEO_OK, WX_A, HY_OK); CHECK(tool_context_get()->execute("{}", out1, sizeof(out1)) == 0, "snapshot priming exec"); + { + char tiny[80]; + CHECK(tool_context_get()->execute("{}", tiny, sizeof(tiny)) == -1, + "tiny buffer must fail"); + CHECK(strstr(tiny, "too large") != NULL, "expects buffer size error"); + } { char *snap = tool_context_snapshot_json(); CHECK(snap != NULL, "snapshot_json alloc"); diff --git a/tests/test_openai.c b/tests/test_openai.c index ac2ceb6..3214712 100644 --- a/tests/test_openai.c +++ b/tests/test_openai.c @@ -5,7 +5,9 @@ #define _POSIX_C_SOURCE 200809L #include "core/config.h" +#include "providers/openai_compat.h" #include "providers/provider.h" +#include "cJSON.h" #include #include #include @@ -155,6 +157,59 @@ static int test_parse_null_json_returns_error(void) provider_response_clear(&response); return 0; } + +static int test_build_chat_body_serializes_tool_calls(void) +{ + provider_tool_call_t tool_call = { + .id = "call_1", + .name = "get_weather", + .arguments = "{\"city\":\"Berlin\"}", + }; + provider_message_t assistant = { + .role = "assistant", + .content = "", + .tool_calls = &tool_call, + .tool_calls_count = 1, + .tool_use_id = NULL, + }; + provider_message_t tool_result = { + .role = "tool", + .content = "sunny", + .tool_calls = NULL, + .tool_calls_count = 0, + .tool_use_id = "call_1", + }; + provider_message_t messages[] = {assistant, tool_result}; + provider_tool_def_t tools[] = { + { + .name = "get_weather", + .description = "Weather lookup", + .parameters_json = "{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}", + }, + }; + provider_response_t response = {0}; + char *body; + cJSON *parsed; + cJSON *msg_arr; + cJSON *first; + cJSON *tool_calls; + cJSON *tools_arr; + + body = openai_compat_build_chat_body("gpt-4o-mini", 128, 0.2, messages, 2, tools, 1, &response); + ASSERT(body != NULL); + parsed = cJSON_Parse(body); + ASSERT(parsed != NULL); + msg_arr = cJSON_GetObjectItem(parsed, "messages"); + ASSERT(msg_arr != NULL && cJSON_GetArraySize(msg_arr) == 2); + first = cJSON_GetArrayItem(msg_arr, 0); + tool_calls = cJSON_GetObjectItem(first, "tool_calls"); + ASSERT(tool_calls != NULL && cJSON_GetArraySize(tool_calls) == 1); + tools_arr = cJSON_GetObjectItem(parsed, "tools"); + ASSERT(tools_arr != NULL && cJSON_GetArraySize(tools_arr) == 1); + cJSON_Delete(parsed); + cJSON_free(body); + return 0; +} #endif int main(void) @@ -170,6 +225,7 @@ int main(void) RUN(test_parse_empty_choices_returns_zero()); RUN(test_parse_valid_message_returns_content()); RUN(test_parse_null_json_returns_error()); + RUN(test_build_chat_body_serializes_tool_calls()); #endif printf("test_openai: all tests passed\n"); return 0; From a57625577c7cd57f752f1434fd4e44b7516bd534 Mon Sep 17 00:00:00 2001 From: Adrianno Esnarriaga Sereno Date: Thu, 18 Jun 2026 12:09:05 +0000 Subject: [PATCH 2/3] test(core): add reload stale-queue and SIGHUP unit tests Cover on_hup, stale_enqueue, and stale_free_all in reload.c which participates in the 80% coverage gate but lacked dedicated tests. --- Makefile | 16 ++++++++-- tests/test_reload.c | 77 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 tests/test_reload.c diff --git a/Makefile b/Makefile index 095e7f1..2a2d541 100644 --- a/Makefile +++ b/Makefile @@ -547,6 +547,17 @@ test_rate_limit: tests/test_rate_limit.c src/gateway/rate_limit.c src/gateway/ra $(CC) $(CFLAGS) $(LDFLAGS) $(INC) -pthread -o $(BINDIR)/$@ tests/test_rate_limit.c src/gateway/rate_limit.c -pthread $(DSYM_SCRIPT) +RELOAD_TEST_O := $(BINDIR)/reload_test.o + +$(RELOAD_TEST_O): src/core/reload.c src/core/reload.h src/core/bootstrap.h src/core/config.h src/channels/channel.h src/channels/heartbeat.h src/providers/provider.h src/tools/tool.h + @mkdir -p $(BINDIR) + $(CC) $(CFLAGS) -ffunction-sections -fdata-sections $(INC) -c -o $@ src/core/reload.c + +test_reload: tests/test_reload.c $(RELOAD_TEST_O) $(CONFIG_O) $(TOML_O) + @mkdir -p $(BINDIR) + $(CC) $(CFLAGS) -ffunction-sections $(LDFLAGS) -Wl,--gc-sections $(INC) -o $(BINDIR)/$@ tests/test_reload.c $(RELOAD_TEST_O) $(CONFIG_O) $(TOML_O) $(LDLIBS) + $(DSYM_SCRIPT) + test_daemon_smoke: shellclaw @chmod +x tests/test_daemon_smoke.sh scripts/install.sh scripts/update.sh SHELLCLAW_TEST_BIN="$(BINDIR)/shellclaw" ./tests/test_daemon_smoke.sh @@ -577,7 +588,7 @@ static: --suppress=constParameterCallback \ -q src/ -test: test_config test_memory test_skill test_provider test_anthropic test_openai test_local_provider test_router test_heartbeat test_agent test_channel test_cli test_shell test_file test_telegram test_discord_helpers test_web_search test_cron test_context test_crypto test_hardware_stub test_ws test_manifest $(ASAP_UNIT_TESTS) test_sandbox test_allowlist test_rate_limit test_daemon_smoke test_update_script test_install_script test_web_dashboard +test: test_config test_memory test_skill test_provider test_anthropic test_openai test_local_provider test_router test_heartbeat test_agent test_channel test_cli test_shell test_file test_telegram test_discord_helpers test_web_search test_cron test_context test_crypto test_hardware_stub test_ws test_manifest $(ASAP_UNIT_TESTS) test_sandbox test_allowlist test_rate_limit test_reload test_daemon_smoke test_update_script test_install_script test_web_dashboard $(BINDIR)/test_config $(BINDIR)/test_memory $(BINDIR)/test_skill @@ -605,6 +616,7 @@ test: test_config test_memory test_skill test_provider test_anthropic test_opena $(BINDIR)/test_sandbox $(BINDIR)/test_allowlist $(BINDIR)/test_rate_limit + $(BINDIR)/test_reload $(MAKE) test_install_script $(MAKE) test_web_dashboard $(MAKE) test_auth && $(BINDIR)/test_auth @@ -616,7 +628,7 @@ COVERAGE_DIR := build/coverage COVERAGE_MIN := 80 coverage: clean - $(MAKE) BUILD=coverage GATEWAY=0 test_config test_memory test_skill test_provider test_anthropic test_openai test_local_provider test_router test_heartbeat test_agent test_channel test_cli test_shell test_file test_telegram test_discord_helpers test_web_search test_cron test_context test_crypto test_hardware_stub test_ws test_manifest $(ASAP_UNIT_TESTS) test_sandbox test_allowlist test_rate_limit test_auth + $(MAKE) BUILD=coverage GATEWAY=0 test_config test_memory test_skill test_provider test_anthropic test_openai test_local_provider test_router test_heartbeat test_agent test_channel test_cli test_shell test_file test_telegram test_discord_helpers test_web_search test_cron test_context test_crypto test_hardware_stub test_ws test_manifest $(ASAP_UNIT_TESTS) test_sandbox test_allowlist test_rate_limit test_reload test_auth @if [ "$(GATEWAY)" = "1" ]; then $(MAKE) BUILD=coverage GATEWAY=1 shellclaw test_gateway_http test_static; fi @chmod +x scripts/coverage.sh @BINDIR=$(BINDIR) COVERAGE_DIR=$(COVERAGE_DIR) COVERAGE_MIN=$(COVERAGE_MIN) GATEWAY=$(GATEWAY) ./scripts/coverage.sh diff --git a/tests/test_reload.c b/tests/test_reload.c new file mode 100644 index 0000000..eb50118 --- /dev/null +++ b/tests/test_reload.c @@ -0,0 +1,77 @@ +/** + * @file test_reload.c + * @brief Unit tests for SIGHUP reload queue and config swap. + */ +#define _POSIX_C_SOURCE 200809L + +#include "core/reload.h" +#include "tests/test_runner.h" +#include +#include + +static int test_on_hup_sets_reload_flag(void) +{ + g_reload_requested = 0; + on_hup(SIGHUP); + ASSERT(g_reload_requested == 1); + g_reload_requested = 0; + return 0; +} + +static int test_stale_enqueue_null_is_noop(void) +{ + stale_free_all(); + ASSERT(stale_enqueue(NULL) == 0); + stale_free_all(); + return 0; +} + +static int test_stale_enqueue_preserves_independent_configs(void) +{ + char path[128]; + FILE *f; + config_t *stale_a = NULL; + config_t *stale_b = NULL; + config_t *live = NULL; + char errbuf[256]; + + ASSERT(test_runner_mkstemp_path("shellclaw_test_reload", path, sizeof(path)) == 0); + f = fopen(path, "w"); + ASSERT(f); + fprintf(f, "[agent]\nmodel = \"stale-a\"\nmax_tokens = 100\n"); + fclose(f); + ASSERT(config_load(path, &stale_a, errbuf, sizeof(errbuf)) == 0); + + f = fopen(path, "w"); + ASSERT(f); + fprintf(f, "[agent]\nmodel = \"stale-b\"\nmax_tokens = 200\n"); + fclose(f); + ASSERT(config_load(path, &stale_b, errbuf, sizeof(errbuf)) == 0); + + f = fopen(path, "w"); + ASSERT(f); + fprintf(f, "[agent]\nmodel = \"live-model\"\nmax_tokens = 300\n"); + fclose(f); + ASSERT(config_load(path, &live, errbuf, sizeof(errbuf)) == 0); + + stale_free_all(); + ASSERT(stale_enqueue(stale_a) == 0); + ASSERT(stale_enqueue(stale_b) == 0); + ASSERT(strcmp(config_agent_model(stale_a), "stale-a") == 0); + ASSERT(strcmp(config_agent_model(stale_b), "stale-b") == 0); + ASSERT(strcmp(config_agent_model(live), "live-model") == 0); + + stale_free_all(); + config_free(live); + remove(path); + return 0; +} + +int main(void) +{ + RUN(test_on_hup_sets_reload_flag()); + RUN(test_stale_enqueue_null_is_noop()); + RUN(test_stale_enqueue_preserves_independent_configs()); + printf("test_reload: all tests passed\n"); + return 0; +} From 009dacdf4f95ec71478e9256c8fe0ac326f60c18 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 14 Jul 2026 12:06:45 +0000 Subject: [PATCH 3/3] test(core): cover handle_message slash commands and agent dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add unit tests for /reset, /status, provider forwarding, null text, and agent-failure fallback messaging — the central channel entry point previously had zero coverage on main. --- Makefile | 16 +- tests/stubs/bootstrap_dispatch_stub.c | 75 ++++++++ tests/test_dispatch.c | 267 ++++++++++++++++++++++++++ 3 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 tests/stubs/bootstrap_dispatch_stub.c create mode 100644 tests/test_dispatch.c diff --git a/Makefile b/Makefile index 2a2d541..b205c82 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,7 @@ DAEMON_O := src/core/daemon.o RELOAD_O := src/core/reload.o BOOTSTRAP_O := src/core/bootstrap.o DISPATCH_O := src/core/dispatch.o +BOOTSTRAP_DISPATCH_STUB_O := tests/stubs/bootstrap_dispatch_stub.o # Vendor TOML_O := vendor/tomlc99/toml.o SQLITE3_O := vendor/sqlite3/sqlite3.o @@ -162,6 +163,9 @@ $(BOOTSTRAP_O): src/core/bootstrap.c src/core/bootstrap.h src/core/config.h src/ $(DISPATCH_O): src/core/dispatch.c src/core/dispatch.h src/core/agent.h src/core/bootstrap.h src/core/memory.h src/channels/channel.h $(CC) $(CFLAGS) $(INC) -c -o $@ src/core/dispatch.c +$(BOOTSTRAP_DISPATCH_STUB_O): tests/stubs/bootstrap_dispatch_stub.c src/core/bootstrap.h src/core/config.h src/providers/provider.h src/tools/tool.h + $(CC) $(CFLAGS) $(INC) -c -o $@ tests/stubs/bootstrap_dispatch_stub.c + $(TOML_O): vendor/tomlc99/toml.c vendor/tomlc99/toml.h $(CC) $(VENDOR_CFLAGS) $(INC) -c -o $@ $< @@ -558,6 +562,11 @@ test_reload: tests/test_reload.c $(RELOAD_TEST_O) $(CONFIG_O) $(TOML_O) $(CC) $(CFLAGS) -ffunction-sections $(LDFLAGS) -Wl,--gc-sections $(INC) -o $(BINDIR)/$@ tests/test_reload.c $(RELOAD_TEST_O) $(CONFIG_O) $(TOML_O) $(LDLIBS) $(DSYM_SCRIPT) +test_dispatch: tests/test_dispatch.c $(DISPATCH_O) $(BOOTSTRAP_DISPATCH_STUB_O) $(AGENT_O) $(ROUTER_O) $(STUB_O) $(ANTHROPIC_O) $(OPENAI_COMPAT_O) $(OPENAI_O) $(LOCAL_O) $(PROVIDER_COMMON_O) $(MEMORY_O) $(SQLITE3_O) $(SKILL_O) $(CONFIG_O) $(TOML_O) $(CJSON_O) + @mkdir -p $(BINDIR) + $(CC) $(CFLAGS) $(LDFLAGS) $(INC) -o $(BINDIR)/$@ tests/test_dispatch.c $(DISPATCH_O) $(BOOTSTRAP_DISPATCH_STUB_O) $(AGENT_O) $(ROUTER_O) $(STUB_O) $(ANTHROPIC_O) $(OPENAI_COMPAT_O) $(OPENAI_O) $(LOCAL_O) $(PROVIDER_COMMON_O) $(MEMORY_O) $(SQLITE3_O) $(SKILL_O) $(CONFIG_O) $(TOML_O) $(CJSON_O) $(LDLIBS) -pthread + $(DSYM_SCRIPT) + test_daemon_smoke: shellclaw @chmod +x tests/test_daemon_smoke.sh scripts/install.sh scripts/update.sh SHELLCLAW_TEST_BIN="$(BINDIR)/shellclaw" ./tests/test_daemon_smoke.sh @@ -588,7 +597,7 @@ static: --suppress=constParameterCallback \ -q src/ -test: test_config test_memory test_skill test_provider test_anthropic test_openai test_local_provider test_router test_heartbeat test_agent test_channel test_cli test_shell test_file test_telegram test_discord_helpers test_web_search test_cron test_context test_crypto test_hardware_stub test_ws test_manifest $(ASAP_UNIT_TESTS) test_sandbox test_allowlist test_rate_limit test_reload test_daemon_smoke test_update_script test_install_script test_web_dashboard +test: test_config test_memory test_skill test_provider test_anthropic test_openai test_local_provider test_router test_heartbeat test_agent test_channel test_cli test_shell test_file test_telegram test_discord_helpers test_web_search test_cron test_context test_crypto test_hardware_stub test_ws test_manifest $(ASAP_UNIT_TESTS) test_sandbox test_allowlist test_rate_limit test_reload test_dispatch test_daemon_smoke test_update_script test_install_script test_web_dashboard $(BINDIR)/test_config $(BINDIR)/test_memory $(BINDIR)/test_skill @@ -617,6 +626,7 @@ test: test_config test_memory test_skill test_provider test_anthropic test_opena $(BINDIR)/test_allowlist $(BINDIR)/test_rate_limit $(BINDIR)/test_reload + $(BINDIR)/test_dispatch $(MAKE) test_install_script $(MAKE) test_web_dashboard $(MAKE) test_auth && $(BINDIR)/test_auth @@ -628,7 +638,7 @@ COVERAGE_DIR := build/coverage COVERAGE_MIN := 80 coverage: clean - $(MAKE) BUILD=coverage GATEWAY=0 test_config test_memory test_skill test_provider test_anthropic test_openai test_local_provider test_router test_heartbeat test_agent test_channel test_cli test_shell test_file test_telegram test_discord_helpers test_web_search test_cron test_context test_crypto test_hardware_stub test_ws test_manifest $(ASAP_UNIT_TESTS) test_sandbox test_allowlist test_rate_limit test_reload test_auth + $(MAKE) BUILD=coverage GATEWAY=0 test_config test_memory test_skill test_provider test_anthropic test_openai test_local_provider test_router test_heartbeat test_agent test_channel test_cli test_shell test_file test_telegram test_discord_helpers test_web_search test_cron test_context test_crypto test_hardware_stub test_ws test_manifest $(ASAP_UNIT_TESTS) test_sandbox test_allowlist test_rate_limit test_reload test_dispatch test_auth @if [ "$(GATEWAY)" = "1" ]; then $(MAKE) BUILD=coverage GATEWAY=1 shellclaw test_gateway_http test_static; fi @chmod +x scripts/coverage.sh @BINDIR=$(BINDIR) COVERAGE_DIR=$(COVERAGE_DIR) COVERAGE_MIN=$(COVERAGE_MIN) GATEWAY=$(GATEWAY) ./scripts/coverage.sh @@ -642,5 +652,5 @@ clean: clean-root-dsym rm -f $(OBJS) $(PROVIDER_COMMON_O) $(STUB_O) $(ANTHROPIC_O) $(OPENAI_COMPAT_O) $(OPENAI_O) $(LOCAL_O) $(ROUTER_O) $(CJSON_O) $(ANTHROPIC_TEST_O) $(OPENAI_TEST_O) $(LOCAL_TEST_O) $(CONTEXT_TEST_OBJS) $(HEARTBEAT_TEST_O) $(CHANNEL_TG_TEST_O) $(CHANNEL_COMMON_O) $(CHANNEL_STUB_O) $(CHANNEL_CLI_O) $(CHANNEL_TG_O) $(CHANNEL_DISCORD_O) $(DISCORD_HELPERS_O) $(CHANNEL_HEARTBEAT_O) $(CHANNEL_WEBCHAT_O) $(AUTH_O) $(STATIC_O) $(HTTP_O) $(HTTP_LWS_O) $(ROUTES_O) $(WS_O) $(MANIFEST_O) $(ENVELOPE_O) $(ULID_O) $(CLIENT_O) $(ASAP_REGISTRY_O) $(SERVER_O) $(ASAP_LOG_O) $(RATE_LIMIT_O) $(SHELL_O) $(WEBSEARCH_O) $(FILE_O) $(REGISTRY_O) $(CONTEXT_O) $(CONTEXT_CACHE_O) $(CONTEXT_HTTP_O) $(CONTEXT_GEO_O) $(CRYPTO_O) $(HARDWARE_STUB_O) $(CRON_O) $(ASAP_INVOKE_O) $(SANDBOX_O) $(ALLOWLIST_O) rm -f src/gateway/ui_assets.h find . -name '*.gcno' -o -name '*.gcda' -o -name '*.gcov' | xargs rm -f 2>/dev/null || true - rm -f $(WS_TEST_O) $(BINDIR)/asap_registry_test.o $(BINDIR)/asap_invoke_test.o $(CONTEXT_TEST_OBJS) $(HEARTBEAT_TEST_O) $(BINDIR)/shellclaw $(BINDIR)/test_config $(BINDIR)/test_memory $(BINDIR)/test_skill $(BINDIR)/test_provider $(BINDIR)/test_anthropic $(BINDIR)/test_openai $(BINDIR)/test_local_provider $(BINDIR)/test_router $(BINDIR)/test_heartbeat $(BINDIR)/test_crypto $(BINDIR)/test_hardware_stub $(BINDIR)/test_ws $(BINDIR)/test_agent $(BINDIR)/test_channel $(BINDIR)/test_cli $(BINDIR)/test_shell $(BINDIR)/test_file $(BINDIR)/test_telegram $(BINDIR)/test_discord_helpers $(BINDIR)/test_web_search $(BINDIR)/test_cron $(BINDIR)/test_context $(BINDIR)/test_manifest $(BINDIR)/test_asap_envelope $(BINDIR)/test_asap_ulid $(BINDIR)/test_asap_client $(BINDIR)/test_asap_registry $(BINDIR)/test_asap_server $(BINDIR)/test_asap_invoke $(BINDIR)/test_asap_log $(BINDIR)/test_auth $(BINDIR)/test_gateway_http $(BINDIR)/test_static $(BINDIR)/test_sandbox $(BINDIR)/test_allowlist $(BINDIR)/test_rate_limit + rm -f $(WS_TEST_O) $(BINDIR)/asap_registry_test.o $(BINDIR)/asap_invoke_test.o $(CONTEXT_TEST_OBJS) $(HEARTBEAT_TEST_O) $(BINDIR)/shellclaw $(BINDIR)/test_config $(BINDIR)/test_memory $(BINDIR)/test_skill $(BINDIR)/test_provider $(BINDIR)/test_anthropic $(BINDIR)/test_openai $(BINDIR)/test_local_provider $(BINDIR)/test_router $(BINDIR)/test_heartbeat $(BINDIR)/test_crypto $(BINDIR)/test_hardware_stub $(BINDIR)/test_ws $(BINDIR)/test_agent $(BINDIR)/test_channel $(BINDIR)/test_cli $(BINDIR)/test_shell $(BINDIR)/test_file $(BINDIR)/test_telegram $(BINDIR)/test_discord_helpers $(BINDIR)/test_web_search $(BINDIR)/test_cron $(BINDIR)/test_context $(BINDIR)/test_manifest $(BINDIR)/test_asap_envelope $(BINDIR)/test_asap_ulid $(BINDIR)/test_asap_client $(BINDIR)/test_asap_registry $(BINDIR)/test_asap_server $(BINDIR)/test_asap_invoke $(BINDIR)/test_asap_log $(BINDIR)/test_auth $(BINDIR)/test_gateway_http $(BINDIR)/test_static $(BINDIR)/test_sandbox $(BINDIR)/test_allowlist $(BINDIR)/test_rate_limit $(BINDIR)/test_dispatch $(BOOTSTRAP_DISPATCH_STUB_O) rm -rf $(BINDIR)/*.dSYM $(DSYMDIR) diff --git a/tests/stubs/bootstrap_dispatch_stub.c b/tests/stubs/bootstrap_dispatch_stub.c new file mode 100644 index 0000000..9be3afa --- /dev/null +++ b/tests/stubs/bootstrap_dispatch_stub.c @@ -0,0 +1,75 @@ +/** + * @file bootstrap_dispatch_stub.c + * @brief Minimal bootstrap surface for dispatch unit tests (avoids full subsystem init). + */ +#define _POSIX_C_SOURCE 200809L + +#include "core/bootstrap.h" +#include "providers/provider.h" +#include "tools/tool.h" +#include +#include + +#define MAX_TOOLS 8 + +static config_t *g_cfg; +static const provider_t *g_provider; +static const tool_t *g_tools[MAX_TOOLS]; +static size_t g_tool_count; +static char g_config_path[512]; + +config_t *bootstrap_get_cfg(void) +{ + return g_cfg; +} + +void bootstrap_set_cfg(config_t *cfg) +{ + g_cfg = cfg; +} + +void bootstrap_set_config_path(const char *path) +{ + if (path != NULL) + snprintf(g_config_path, sizeof(g_config_path), "%s", path); + else + g_config_path[0] = '\0'; +} + +const char *bootstrap_get_config_path(void) +{ + return (g_config_path[0] != '\0') ? g_config_path : NULL; +} + +const provider_t *bootstrap_get_provider(void) +{ + return g_provider; +} + +void bootstrap_set_provider_for_test(const provider_t *provider) +{ + g_provider = provider; +} + +size_t bootstrap_tool_count(void) +{ + return g_tool_count; +} + +const tool_t *bootstrap_tool_at(size_t index) +{ + if (index >= g_tool_count) + return NULL; + return g_tools[index]; +} + +void bootstrap_reset_tools_for_test(void) +{ + g_tool_count = 0; +} + +void bootstrap_add_tool_for_test(const tool_t *tool) +{ + if (tool != NULL && g_tool_count < MAX_TOOLS) + g_tools[g_tool_count++] = tool; +} diff --git a/tests/test_dispatch.c b/tests/test_dispatch.c new file mode 100644 index 0000000..fcba09d --- /dev/null +++ b/tests/test_dispatch.c @@ -0,0 +1,267 @@ +/** + * @file test_dispatch.c + * @brief Unit tests for handle_message slash commands and agent dispatch. + */ +#define _POSIX_C_SOURCE 200809L + +#include "channels/channel.h" +#include "core/bootstrap.h" +#include "core/config.h" +#include "core/dispatch.h" +#include "core/memory.h" +#include "providers/provider.h" +#include "tests/test_runner.h" +#include +#include +#include + +void bootstrap_set_provider_for_test(const provider_t *provider); +void bootstrap_reset_tools_for_test(void); + +#define SEND_BUF_SIZE 4096 +static char g_last_session[SEND_BUF_SIZE]; +static char g_last_text[SEND_BUF_SIZE]; +static int g_send_calls; + +static int mock_send(const char *session_id, const char *text, + const channel_attachment_t *attachments, size_t attachments_count) +{ + (void)attachments; + (void)attachments_count; + g_send_calls++; + strncpy(g_last_session, session_id ? session_id : "", sizeof(g_last_session) - 1); + g_last_session[sizeof(g_last_session) - 1] = '\0'; + strncpy(g_last_text, text ? text : "", sizeof(g_last_text) - 1); + g_last_text[sizeof(g_last_text) - 1] = '\0'; + return 0; +} + +static const channel_t mock_channel = { + .name = "mock", + .init = NULL, + .poll = NULL, + .send = mock_send, + .cleanup = NULL, +}; + +static int spy_init(const config_t *cfg) +{ + (void)cfg; + return 0; +} + +static int spy_chat(const provider_message_t *messages, size_t message_count, + const provider_tool_def_t *tools, size_t tool_count, + provider_response_t *response) +{ + (void)messages; + (void)message_count; + (void)tools; + (void)tool_count; + response->error = 0; + response->content = strdup("agent-ok"); + response->tool_calls = NULL; + response->tool_calls_count = 0; + return 0; +} + +static void spy_cleanup(void) {} + +static const provider_t spy_provider = { + .name = "spy", + .init = spy_init, + .chat = spy_chat, + .cleanup = spy_cleanup, +}; + +static int fail_chat(const provider_message_t *messages, size_t message_count, + const provider_tool_def_t *tools, size_t tool_count, + provider_response_t *response) +{ + (void)messages; + (void)message_count; + (void)tools; + (void)tool_count; + response->error = 1; + response->content = NULL; + response->tool_calls = NULL; + response->tool_calls_count = 0; + return -3; +} + +static const provider_t fail_provider = { + .name = "fail", + .init = spy_init, + .chat = fail_chat, + .cleanup = spy_cleanup, +}; + +static config_t *load_minimal_cfg(const char *path) +{ + config_t *cfg = NULL; + char errbuf[256]; + + if (config_load(path, &cfg, errbuf, sizeof(errbuf)) != 0) + return NULL; + return cfg; +} + +static int write_minimal_toml(const char *path) +{ + FILE *fp = fopen(path, "w"); + + if (!fp) + return -1; + fprintf(fp, + "[channels.discord]\n" + "enabled = false\n" + "[agent]\n" + "model = \"stub\"\n" + "[providers]\n" + "fallback_chain = [\"stub\"]\n"); + fclose(fp); + return 0; +} + +static void reset_send_spy(void) +{ + g_send_calls = 0; + g_last_session[0] = '\0'; + g_last_text[0] = '\0'; +} + +static int test_reset_clears_session(void) +{ + const char *db_path = "build/test_dispatch_reset.db"; + char path[128]; + channel_incoming_msg_t msg = {0}; + config_t *cfg = NULL; + char history[512]; + + reset_send_spy(); + memory_cleanup(); + ASSERT(memory_init(db_path) == 0); + ASSERT(session_save("ws:test", "[{\"role\":\"user\",\"content\":\"hi\"}]") == 0); + + ASSERT(test_runner_mkstemp_path("shellclaw_test_dispatch", path, sizeof(path)) == 0); + ASSERT(write_minimal_toml(path) == 0); + cfg = load_minimal_cfg(path); + ASSERT(cfg != NULL); + bootstrap_set_cfg(cfg); + bootstrap_reset_tools_for_test(); + + msg.session_id = "ws:test"; + msg.text = "/reset"; + ASSERT(handle_message(&mock_channel, &msg) == 0); + ASSERT(g_send_calls == 1); + ASSERT(strstr(g_last_text, "Session cleared") != NULL); + ASSERT(session_load("ws:test", history, sizeof(history)) != 0); + + config_free(cfg); + unlink(path); + memory_cleanup(); + return 0; +} + +static int test_status_returns_version(void) +{ + channel_incoming_msg_t msg = {0}; + + reset_send_spy(); + msg.session_id = "cli:test"; + msg.text = "/status"; + ASSERT(handle_message(&mock_channel, &msg) == 0); + ASSERT(g_send_calls == 1); + ASSERT(strstr(g_last_text, "ShellClaw 0.2.0") != NULL); + ASSERT(strstr(g_last_text, "agent ready") != NULL); + return 0; +} + +static int test_agent_failure_fallback_message(void) +{ + channel_incoming_msg_t msg = {0}; + char path[128]; + config_t *cfg = NULL; + + reset_send_spy(); + ASSERT(test_runner_mkstemp_path("shellclaw_test_dispatch_fail", path, sizeof(path)) == 0); + ASSERT(write_minimal_toml(path) == 0); + cfg = load_minimal_cfg(path); + ASSERT(cfg != NULL); + bootstrap_set_cfg(cfg); + bootstrap_set_provider_for_test(&fail_provider); + bootstrap_reset_tools_for_test(); + + msg.session_id = "cli:fail"; + msg.text = "hello"; + ASSERT(handle_message(&mock_channel, &msg) == 0); + ASSERT(g_send_calls == 1); + ASSERT(strstr(g_last_text, "Error: agent failed (code -1)") != NULL); + + config_free(cfg); + unlink(path); + return 0; +} + +static int test_normal_message_uses_provider(void) +{ + channel_incoming_msg_t msg = {0}; + char path[128]; + config_t *cfg = NULL; + + reset_send_spy(); + ASSERT(test_runner_mkstemp_path("shellclaw_test_dispatch_spy", path, sizeof(path)) == 0); + ASSERT(write_minimal_toml(path) == 0); + cfg = load_minimal_cfg(path); + ASSERT(cfg != NULL); + bootstrap_set_cfg(cfg); + bootstrap_set_provider_for_test(&spy_provider); + bootstrap_reset_tools_for_test(); + + msg.session_id = "cli:spy"; + msg.text = "ping"; + ASSERT(handle_message(&mock_channel, &msg) == 0); + ASSERT(g_send_calls == 1); + ASSERT(strstr(g_last_text, "agent-ok") != NULL); + + config_free(cfg); + unlink(path); + return 0; +} + +static int test_null_text_forwards_empty_to_agent(void) +{ + channel_incoming_msg_t msg = {0}; + char path[128]; + config_t *cfg = NULL; + + reset_send_spy(); + ASSERT(test_runner_mkstemp_path("shellclaw_test_dispatch_null", path, sizeof(path)) == 0); + ASSERT(write_minimal_toml(path) == 0); + cfg = load_minimal_cfg(path); + ASSERT(cfg != NULL); + bootstrap_set_cfg(cfg); + bootstrap_set_provider_for_test(&spy_provider); + bootstrap_reset_tools_for_test(); + + msg.session_id = "cli:null"; + msg.text = NULL; + ASSERT(handle_message(&mock_channel, &msg) == 0); + ASSERT(g_send_calls == 1); + ASSERT(strstr(g_last_text, "agent-ok") != NULL); + + config_free(cfg); + unlink(path); + return 0; +} + +int main(void) +{ + RUN(test_reset_clears_session()); + RUN(test_status_returns_version()); + RUN(test_agent_failure_fallback_message()); + RUN(test_normal_message_uses_provider()); + RUN(test_null_text_forwards_empty_to_agent()); + printf("test_dispatch: all tests passed\n"); + return 0; +}