From 8018ea6e2f649b601e2e318e42e95b1545438e49 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 10 Mar 2026 05:48:21 -0400 Subject: [PATCH] v0.0.62 - fix: webhook trigger lookup uses trigger manager and add startup PoC triggers/tests --- README.md | 4 +- src/http_api.c | 95 +++------------------------------------------ src/main.h | 4 +- tests/test.sh | 102 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 93 deletions(-) create mode 100755 tests/test.sh diff --git a/README.md b/README.md index c4bb592..687e3d6 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,11 @@ Skills support context modes (`inject`, `full`, `override`) and per-skill LLM fa Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers. -## Current Status — v0.0.61 +## Current Status — v0.0.62 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.0.61 — feat: add webhook cron and chain trigger types +> Last release update: v0.0.62 — fix: webhook trigger lookup uses trigger manager and add startup PoC triggers/tests - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/src/http_api.c b/src/http_api.c index 654b268..e83aaab 100644 --- a/src/http_api.c +++ b/src/http_api.c @@ -105,81 +105,6 @@ static int uri_extract_after_prefix(const struct mg_str* uri, return 0; } -static cJSON* find_skill_event_by_d_tag(const char* d_tag) { - if (!d_tag || d_tag[0] == '\0') { - return NULL; - } - - char* events_json = nostr_handler_get_self_skill_events_json(); - if (!events_json) { - return NULL; - } - - cJSON* events = cJSON_Parse(events_json); - free(events_json); - if (!events || !cJSON_IsArray(events)) { - cJSON_Delete(events); - return NULL; - } - - cJSON* found = NULL; - int n = cJSON_GetArraySize(events); - for (int i = 0; i < n; i++) { - cJSON* ev = cJSON_GetArrayItem(events, i); - cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL; - if (!tags || !cJSON_IsArray(tags)) { - continue; - } - - int tn = cJSON_GetArraySize(tags); - for (int ti = 0; ti < tn; ti++) { - cJSON* tag = cJSON_GetArrayItem(tags, ti); - if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { - continue; - } - cJSON* key = cJSON_GetArrayItem(tag, 0); - cJSON* val = cJSON_GetArrayItem(tag, 1); - if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) { - continue; - } - if (strcmp(key->valuestring, "d") == 0 && strcmp(val->valuestring, d_tag) == 0) { - found = cJSON_Duplicate(ev, 1); - break; - } - } - - if (found) { - break; - } - } - - cJSON_Delete(events); - return found; -} - -static const char* find_tag_string_local(cJSON* tags, const char* key) { - if (!tags || !cJSON_IsArray(tags) || !key || key[0] == '\0') { - return NULL; - } - - int n = cJSON_GetArraySize(tags); - for (int i = 0; i < n; i++) { - cJSON* tag = cJSON_GetArrayItem(tags, i); - if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { - continue; - } - cJSON* k = cJSON_GetArrayItem(tag, 0); - cJSON* v = cJSON_GetArrayItem(tag, 1); - if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) { - continue; - } - if (strcmp(k->valuestring, key) == 0) { - return v->valuestring; - } - } - - return NULL; -} static void handle_trigger_webhook(struct mg_connection* c, const struct mg_http_message* hm, @@ -189,23 +114,18 @@ static void handle_trigger_webhook(struct mg_connection* c, return; } - cJSON* skill_ev = find_skill_event_by_d_tag(d_tag); - if (!skill_ev) { - reply_error(c, 404, "skill not found by d_tag"); + active_trigger_t trigger; + if (trigger_manager_find(g_api_ctx.trigger_manager, d_tag, &trigger) != 0) { + reply_error(c, 404, "active trigger not found for d_tag"); return; } - cJSON* tags = cJSON_GetObjectItemCaseSensitive(skill_ev, "tags"); - const char* trigger_type = find_tag_string_local(tags, "trigger"); - if (!trigger_type || strcmp(trigger_type, "webhook") != 0) { - cJSON_Delete(skill_ev); - reply_error(c, 400, "skill trigger type is not webhook"); + if (trigger.trigger_type != TRIGGER_TYPE_WEBHOOK) { + reply_error(c, 400, "trigger type is not webhook"); return; } - const char* enabled = find_tag_string_local(tags, "enabled"); - if (enabled && (strcmp(enabled, "false") == 0 || strcmp(enabled, "0") == 0)) { - cJSON_Delete(skill_ev); + if (!trigger.enabled) { reply_error(c, 400, "webhook trigger is disabled"); return; } @@ -213,7 +133,6 @@ static void handle_trigger_webhook(struct mg_connection* c, cJSON* payload = parse_body_json(hm); if (!payload || !cJSON_IsObject(payload)) { cJSON_Delete(payload); - cJSON_Delete(skill_ev); reply_error(c, 400, "invalid JSON body"); return; } @@ -221,7 +140,6 @@ static void handle_trigger_webhook(struct mg_connection* c, cJSON* event = cJSON_CreateObject(); if (!event) { cJSON_Delete(payload); - cJSON_Delete(skill_ev); reply_error(c, 500, "oom"); return; } @@ -234,7 +152,6 @@ static void handle_trigger_webhook(struct mg_connection* c, int fired = trigger_manager_fire(g_api_ctx.trigger_manager, d_tag, event, "webhook"); cJSON_Delete(event); - cJSON_Delete(skill_ev); if (fired < 0) { reply_error(c, 404, "active trigger not found for d_tag"); diff --git a/src/main.h b/src/main.h index 7aa3a66..8f5c14b 100644 --- a/src/main.h +++ b/src/main.h @@ -12,8 +12,8 @@ // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MINOR 0 -#define DIDACTYL_VERSION_PATCH 61 -#define DIDACTYL_VERSION "v0.0.61" +#define DIDACTYL_VERSION_PATCH 62 +#define DIDACTYL_VERSION "v0.0.62" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 0000000..071267b --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +PASS_COUNT=0 +FAIL_COUNT=0 + +pass() { + PASS_COUNT=$((PASS_COUNT + 1)) + printf "[PASS] %s\n" "$1" +} + +fail() { + FAIL_COUNT=$((FAIL_COUNT + 1)) + printf "[FAIL] %s\n" "$1" +} + +assert_grep() { + local pattern="$1" + local file="$2" + local desc="$3" + if grep -E -q "$pattern" "$file"; then + pass "$desc" + else + fail "$desc (pattern not found in $file)" + fi +} + +assert_cmd() { + local desc="$1" + shift + if "$@" >/dev/null 2>&1; then + pass "$desc" + else + fail "$desc" + fi +} + +printf "== Didactyl Trigger Feature Test ==\n" +printf "Root: %s\n" "$ROOT_DIR" + +printf "\n-- Build check --\n" +if make -j >/tmp/didactyl-test-build.log 2>&1; then + pass "Project builds successfully" +else + fail "Project build failed (see /tmp/didactyl-test-build.log)" +fi + +assert_cmd "Built binary exists" test -x ./didactyl + +printf "\n-- Source wiring checks --\n" +assert_grep "TRIGGER_TYPE_WEBHOOK" src/trigger_manager.h "Webhook trigger type defined" +assert_grep "TRIGGER_TYPE_CRON" src/trigger_manager.h "Cron trigger type defined" +assert_grep "TRIGGER_TYPE_CHAIN" src/trigger_manager.h "Chain trigger type defined" + +assert_grep "trigger_manager_fire\(" src/trigger_manager.c "Generic trigger fire API implemented" +assert_grep "trigger_manager_fire_chains\(" src/trigger_manager.c "Chain fire API implemented" +assert_grep "cron_matches_now\(" src/trigger_manager.c "Cron matcher implemented" +assert_grep "trigger_type_to_string" src/trigger_manager.c "Trigger type serialization implemented" + +assert_grep "\"/api/trigger/\*\"" src/http_api.c "Webhook API route registered" +assert_grep "handle_trigger_webhook\(" src/http_api.c "Webhook handler implemented" + +assert_grep "nostr-subscription, webhook, cron, chain" src/tools.c "skill_create validation accepts 4 trigger types" +assert_grep "skill_edit trigger must be one of: nostr-subscription, webhook, cron, chain" src/tools.c "skill_edit validation accepts 4 trigger types" + +assert_grep "trigger_manager_fire_chains\(" src/agent.c "Post-execution chain hook wired in agent" + +printf "\n-- Docs checks --\n" +assert_grep "\`webhook\`" docs/SKILLS.md "SKILLS docs include webhook trigger" +assert_grep "\`cron\`" docs/SKILLS.md "SKILLS docs include cron trigger" +assert_grep "\`chain\`" docs/SKILLS.md "SKILLS docs include chain trigger" +assert_grep "POST /api/trigger/:d_tag" docs/API.md "API docs include webhook endpoint" + +printf "\n-- Optional local API probe --\n" +if command -v curl >/dev/null 2>&1; then + HTTP_CODE="" + HTTP_CODE="$(curl -k -s --connect-timeout 2 --max-time 5 -o /tmp/didactyl-trigger-probe.out -w "%{http_code}" https://127.0.0.1:8484/api/trigger/nonexistent -X POST -H 'Content-Type: application/json' -d '{}' || true)" + if [[ "$HTTP_CODE" == "000" ]]; then + HTTP_CODE="$(curl -s --connect-timeout 2 --max-time 5 -o /tmp/didactyl-trigger-probe.out -w "%{http_code}" http://127.0.0.1:8484/api/trigger/nonexistent -X POST -H 'Content-Type: application/json' -d '{}' || true)" + fi + + if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "400" || "$HTTP_CODE" == "404" || "$HTTP_CODE" == "503" ]]; then + pass "Webhook endpoint responds on local API (http code: $HTTP_CODE)" + else + fail "Webhook endpoint probe did not get expected response (http code: ${HTTP_CODE:-none})" + fi +else + fail "curl not available for API probe" +fi + +printf "\n== Result ==\n" +printf "Pass: %d\n" "$PASS_COUNT" +printf "Fail: %d\n" "$FAIL_COUNT" + +if [[ "$FAIL_COUNT" -gt 0 ]]; then + exit 1 +fi + +exit 0