#include "tools.h" #include #include #include #include #include #include #include #include #include #include #include #include "limits.h" static int failures = 0; static int checks = 0; #define CHECK(cond, msg) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ } \ } while (0) #define CHECK_CONT(hay, needle, msg) \ do { \ checks++; \ if (!hay || !strstr(hay, needle)) { \ failures++; \ fprintf(stderr, "FAIL %s:%d: %s (got `%s`, want substring `%s`)\n", \ __FILE__, __LINE__, (msg), hay ? hay : "(null)", \ (needle)); \ } \ } while (0) #define CHECK_NOT_CONT(hay, needle, msg) \ do { \ checks++; \ if (hay && strstr(hay, needle)) { \ failures++; \ fprintf(stderr, "FAIL %s:%d: %s (got `%s`, must not contain `%s`)\n", \ __FILE__, __LINE__, (msg), hay, (needle)); \ } \ } while (0) static char *tmpdir(const char *tag) { char path[256]; snprintf(path, sizeof path, "/tmp/llshc-tools-%s-%d", tag, (int)getpid()); char cmd[320]; snprintf(cmd, sizeof cmd, "rm -rf '%s'", path); if (system(cmd) != 0) {} snprintf(cmd, sizeof cmd, "mkdir -p '%s'", path); if (system(cmd) != 0) exit(1); return xstrdup(path); } static void wfile(const char *dir, const char *rel, const char *content) { char path[512]; snprintf(path, sizeof path, "%s/%s", dir, rel); char *slash = strrchr(path, '/'); if (slash) { char dirpart[512]; size_t n = (size_t)(slash - path); memcpy(dirpart, path, n); dirpart[n] = '\0'; char cmd[600]; snprintf(cmd, sizeof cmd, "mkdir -p '%s'", dirpart); if (system(cmd) != 0) exit(1); } FILE *f = fopen(path, "w"); if (!f) exit(1); fputs(content, f); fclose(f); } static tool_limits mklimits2(const char **list, size_t n, bool to_deny) { tool_limits l; tool_limits_defaults(&l); for (size_t i = 0; i < n; i++) sl_push_copy(to_deny ? &l.shell_denylist : &l.shell_allowlist, list[i]); return l; } static void test_read_write_roundtrip(void) { char *dir = tmpdir("rw"); char *p = xstrfmt("%s/nested/roundtrip.txt", dir); char *err = NULL; char *r = tool_write_file(p, "hi there", &err); CHECK(r != NULL, "write_file"); if (!r && err) fprintf(stderr, " write_file err: %s\n", err); free(r); char *content = tool_read_file(p, 0, 0, &err); CHECK(content && strcmp(content, "hi there") == 0, "read_file content"); if (!content && err) fprintf(stderr, " read_file err: %s\n", err); free(content); char *rm = xstrfmt("rm -rf '%s'", dir); system(rm); free(rm); free(p); free(dir); } static void test_read_file_missing_errors(void) { char *dir = tmpdir("missing"); char *p = xstrfmt("%s/nope.txt", dir); char *err = NULL; char *r = tool_read_file(p, 0, 0, &err); CHECK(r == NULL, "missing file should fail"); CHECK(err && strstr(err, "nope.txt") != NULL, "error names the path"); free(err); free(p); free(dir); } static void test_read_file_empty_path_errors(void) { char *err = NULL; CHECK(tool_read_file("", 0, 0, &err) == NULL, "empty path should fail"); CHECK(err && strstr(err, "path") != NULL, "error names the missing path"); free(err); err = NULL; CHECK(tool_read_file(NULL, 0, 0, &err) == NULL, "NULL path should fail"); CHECK(err && strstr(err, "path") != NULL, "NULL path error names it"); free(err); } static void test_read_file_too_large(void) { char *dir = tmpdir("large"); char *p = xstrfmt("%s/big.bin", dir); /* 3 MiB of zeros: above the 2 MiB default max_file_bytes */ FILE *f = fopen(p, "wb"); if (f) { char chunk[4096]; memset(chunk, 0, sizeof chunk); for (int i = 0; i < 768; i++) fwrite(chunk, 1, sizeof chunk, f); fclose(f); } char *err = NULL; char *r = tool_read_file(p, 0, 0, &err); CHECK(r == NULL, "oversized file should fail"); CHECK(err && strstr(err, "too large to read") != NULL, "error says too large"); free(err); free(p); char *rm = xstrfmt("rm -rf '%s'", dir); system(rm); free(rm); free(dir); } static void test_read_file_binary_rejected(void) { char *dir = tmpdir("bin"); char *p = xstrfmt("%s/blob.bin", dir); FILE *f = fopen(p, "wb"); if (f) { unsigned char b[] = {0xFF, 0xFE, 0x01}; fwrite(b, 1, sizeof b, f); fclose(f); } char *err = NULL; char *r = tool_read_file(p, 0, 0, &err); CHECK(r == NULL, "binary file should fail"); CHECK(err && strstr(err, "valid UTF-8") != NULL, "error mentions UTF-8"); free(err); free(p); char *rm = xstrfmt("rm -rf '%s'", dir); system(rm); free(rm); free(dir); } static void test_read_file_offset_limit(void) { char *dir = tmpdir("offlimit"); char *p = xstrfmt("%s/lines.txt", dir); wfile(dir, "lines.txt", "l1\nl2\nl3\nl4\nl5\n"); char *err = NULL; /* offset=1, limit=2 -> first two lines + a truncation note */ char *r = tool_read_file(p, 1, 2, &err); CHECK(r != NULL, "read with offset/limit succeeds"); CHECK_CONT(r, "l1\nl2", "first two lines present"); CHECK_CONT(r, "3 more lines", "notes how many lines remain"); CHECK_CONT(r, "offset=3", "notes the offset to continue from"); free(r); /* offset=3, limit=2 -> lines 3-4, exactly one line remaining */ r = tool_read_file(p, 3, 2, &err); CHECK(r != NULL && strcmp(r, "l3\nl4\n\n[... 1 more line; use offset=5 to continue " "...]") == 0, "middle slice + singular note, exact"); free(r); /* limit beyond the end -> the rest of the file, no note */ r = tool_read_file(p, 2, 100, &err); CHECK(r != NULL && strcmp(r, "l2\nl3\nl4\nl5\n") == 0, "limit beyond end returns the rest with no note"); free(r); /* the last single line, no note */ r = tool_read_file(p, 5, 1, &err); CHECK(r != NULL && strcmp(r, "l5\n") == 0, "last line only, no note"); free(r); /* offset past the end -> empty string */ r = tool_read_file(p, 6, 1, &err); CHECK(r != NULL && strcmp(r, "") == 0, "offset past the end is empty"); free(r); free(p); char *rm = xstrfmt("rm -rf '%s'", dir); system(rm); free(rm); free(dir); } static void test_read_file_offset_after_unterminated_last_line(void) { char *dir = tmpdir("unterminated"); char *path = xstrfmt("%s/one.txt", dir); wfile(dir, "one.txt", "only line"); char *err = NULL; char *out = tool_read_file(path, 2, 1, &err); CHECK(out && strcmp(out, "") == 0, "offset beyond unterminated final line returns empty"); free(out); free(err); free(path); char *rm = xstrfmt("rm -rf '%s'", dir); system(rm); free(rm); free(dir); } static void test_read_file_rejects_embedded_nul(void) { char *dir = tmpdir("nul"); char *path = xstrfmt("%s/nul.txt", dir); FILE *f = fopen(path, "wb"); const char bytes[] = {'a', '\0', 'b'}; fwrite(bytes, 1, sizeof bytes, f); fclose(f); char *err = NULL; char *out = tool_read_file(path, 0, 0, &err); CHECK(out == NULL, "embedded NUL is rejected as non-text"); free(out); free(err); free(path); char *rm = xstrfmt("rm -rf '%s'", dir); system(rm); free(rm); free(dir); } static void test_write_file_rejects_missing_content(void) { char *err = NULL; CHECK(tool_write_file("/tmp/llsh-must-not-be-created", NULL, &err) == NULL, "missing content rejected"); CHECK(err && strstr(err, "content"), "missing content is explained"); free(err); } static void test_run_shell_success_output(void) { char *err = NULL; char *out = tool_run_shell("echo hello-llsh", &err); CHECK(out != NULL, "run_shell"); CHECK_CONT(out, "hello-llsh", "stdout captured"); CHECK_NOT_CONT(out, "exit code 0", "no status note on success"); free(out); } static void test_run_shell_success_no_output(void) { char *err = NULL; char *out = tool_run_shell("true", &err); CHECK(out && strcmp(out, "(no output)") == 0, "no-output marker"); if (!out && err) fprintf(stderr, " err: %s\n", err); free(out); } static void test_run_shell_failure_status(void) { char *err = NULL; char *out = tool_run_shell("exit 3", &err); CHECK(out != NULL, "failing command still returns output"); CHECK_CONT(out, "exit code 3", "exit code reported"); free(out); } static void test_run_shell_stderr_section(void) { char *err = NULL; char *out = tool_run_shell("echo out-line; echo err-line 1>&2", &err); CHECK(out != NULL, "run_shell with stderr"); CHECK_CONT(out, "out-line", "stdout part"); CHECK_CONT(out, "[stderr]\nerr-line", "stderr section"); free(out); } static void test_run_shell_signal(void) { char *err = NULL; char *out = tool_run_shell("kill -9 $$", &err); CHECK(out != NULL, "signaled command returns output"); CHECK_CONT(out, "terminated by signal", "signal note"); free(out); } static void test_run_shell_denylist_blocks(void) { tool_limits saved; tool_limits_get(&saved); const char *deny[] = {"curl"}; tool_limits l = mklimits2(deny, 1, true); tool_limits_set(&l); char *err = NULL; char *out = tool_run_shell("curl example.com", &err); CHECK(out == NULL, "denylisted command blocked"); CHECK(err && strstr(err, "denylist") != NULL, "denylist mentioned"); free(err); tool_limits_free(&l); tool_limits_set(&saved); tool_limits_free(&saved); } static void test_run_shell_allowlist_requires_match(void) { tool_limits saved; tool_limits_get(&saved); const char *allow[] = {"git"}; tool_limits l = mklimits2(allow, 1, false); tool_limits_set(&l); char *err = NULL; char *out = tool_run_shell("git status", &err); CHECK(out != NULL, "allowlisted command runs"); free(out); err = NULL; out = tool_run_shell("ls -la", &err); CHECK(out == NULL, "non-allowlisted command blocked"); CHECK(err && strstr(err, "allowlist") != NULL, "allowlist mentioned"); free(err); free(out); tool_limits_free(&l); tool_limits_set(&saved); tool_limits_free(&saved); } static bool deny_hook(const char *cmd, void *ud) { (void)cmd; (void)ud; return false; } static void test_run_shell_confirm_hook_aborts(void) { tool_limits saved; tool_limits_get(&saved); tool_limits l; tool_limits_defaults(&l); l.confirm_destructive = true; tool_limits_set(&l); set_confirm_hook(deny_hook, NULL); char *err = NULL; char *out = tool_run_shell("rm -rf /tmp/llsh-test", &err); CHECK(out == NULL, "declined destructive command aborts"); CHECK(err && strstr(err, "declined") != NULL, "declined mentioned"); free(err); set_confirm_hook(NULL, NULL); tool_limits_free(&l); tool_limits_set(&saved); tool_limits_free(&saved); } /* Number of live /proc entries whose cmdline contains `needle`. */ static int leftover_cmdlines(const char *needle) { DIR *d = opendir("/proc"); if (!d) return 0; int found = 0; struct dirent *e; while ((e = readdir(d)) != NULL) { bool digits = e->d_name[0] != '\0'; for (const char *c = e->d_name; *c; c++) if (!isdigit((unsigned char)*c)) { digits = false; break; } if (!digits) continue; char path[300]; snprintf(path, sizeof path, "/proc/%s/cmdline", e->d_name); FILE *f = fopen(path, "rb"); if (!f) continue; char buf[8192]; size_t n = fread(buf, 1, sizeof buf - 1, f); fclose(f); buf[n] = '\0'; for (size_t i = 0; i <= n && !found; i++) { if (i < n && buf[i] != '\0') continue; if (strstr(buf + i, needle)) found = 1; i++; } } closedir(d); return found; } static void test_run_shell_timeout_kills_process_group(void) { char *err = NULL; char *out = tool_run_shell_timed("sleep 60 & echo spawned; wait", 1, &err); CHECK(out == NULL, "long command times out"); CHECK(err && strstr(err, "timed out") != NULL, "timeout message"); free(err); /* Give the kernel a moment to reap, then check nothing survived. */ struct timespec ts = {0, 300 * 1000 * 1000}; nanosleep(&ts, NULL); CHECK(leftover_cmdlines("sleep 60") == 0, "backgrounded sleep must die with its process group"); } static void test_run_shell_large_output_does_not_hang(void) { /* A child writing more than MAX_SHELL_OUTPUT_BYTES (1MB) used to block on * a full pipe because draining stopped at the cap, forcing a full timeout. * It must now complete promptly with its output capped. */ char *err = NULL; char *out = tool_run_shell_timed("head -c 2000000 /dev/zero | tr '\\0' 'x'", 5, &err); CHECK(out != NULL, "large output completes (no spurious timeout)"); if (out) { CHECK(strlen(out) <= 16000 + 256, "output is capped/truncated"); free(out); } free(err); } static void test_truncate_output(void) { xbuf long_b; xbuf_init(&long_b); for (int i = 0; i < 20000; i++) xbuf_push(&long_b, 'x'); char *long_s = xbuf_steal(&long_b); char *t = tool_truncate_output(long_s); CHECK_CONT(t, "truncated", "truncation note"); CHECK(strlen(t) < 20000, "output shortened"); free(t); char *short_s = xstrdup("small"); char *t2 = tool_truncate_output(short_s); CHECK(t2 && strcmp(t2, "small") == 0, "short text unchanged"); free(t2); free(long_s); free(short_s); } static void test_tool_list(void) { const tool_entry *entries; size_t n = tool_list_entries(&entries); CHECK(n == 8, "eight tools registered"); const char *want[8] = {"read_file", "write_file", "run_shell", "use_skill", "search_memories", "embed", "llm", "compact"}; bool ok = true; for (size_t i = 0; i < n; i++) if (strcmp(entries[i].name, want[i]) != 0) ok = false; CHECK(ok, "tool names in order"); } static void test_tool_params_schemas(void) { for (size_t i = 0; i < tool_builtin_count(); i++) { const char *name = tool_builtin_name(i); cJSON *schema = tool_params_json(name); CHECK(schema != NULL, "schema non-NULL"); const cJSON *stype = cJSON_GetObjectItem(schema, "type"); CHECK(cJSON_IsString(stype) && strcmp(stype->valuestring, "object") == 0, "schema type is object"); const cJSON *props = cJSON_GetObjectItem(schema, "properties"); CHECK(cJSON_IsObject(props), "properties is an object"); if (!cJSON_IsObject(props)) { cJSON_Delete(schema); continue; } for (const cJSON *p = props->child; p; p = p->next) { int duplicates = 0; for (const cJSON *q = props->child; q; q = q->next) if (p->string && q->string && strcmp(p->string, q->string) == 0) duplicates++; CHECK(duplicates == 1, "property key appears exactly once"); CHECK(cJSON_HasObjectItem(p, "type"), "property carries a type"); } cJSON_Delete(schema); } cJSON *rf = tool_params_json("read_file"); const cJSON *path = cJSON_GetObjectItem( cJSON_GetObjectItem(rf, "properties"), "path"); CHECK(cJSON_IsObject(path), "read_file has a path property"); const cJSON *ptype = path ? cJSON_GetObjectItem(path, "type") : NULL; CHECK(cJSON_IsString(ptype) && strcmp(ptype->valuestring, "string") == 0, "read_file path type is string"); const cJSON *pd = path ? cJSON_GetObjectItem(path, "description") : NULL; CHECK(cJSON_IsString(pd) && pd->valuestring[0] != 0, "read_file path keeps its description"); char *rfstr = cJSON_PrintUnformatted(rf); CHECK_CONT(rfstr, "\"path\":{\"type\":\"string\",\"description\":", "read_file schema serializes path property"); free(rfstr); cJSON_Delete(rf); } static void test_tokenize(void) { strlist t; sl_init(&t); tool_tokenize("Billing Notes (DB)", &t); CHECK(t.len == 3, "three tokens"); CHECK(t.len == 3 && strcmp(t.items[0], "billing") == 0, "token 1"); CHECK(t.len == 3 && strcmp(t.items[1], "notes") == 0, "token 2"); CHECK(t.len == 3 && strcmp(t.items[2], "db") == 0, "token 3"); sl_free(&t); strlist empty; sl_init(&empty); tool_tokenize(" ", &empty); CHECK(empty.len == 0, "whitespace-only query has no tokens"); sl_free(&empty); } static void test_is_subsequence(void) { CHECK(tool_is_subsequence("abc", "a1b2c3"), "scattered chars match"); CHECK(!tool_is_subsequence("abc", "acb"), "out-of-order rejected"); CHECK(tool_is_subsequence("bill", "billed"), "prefix subsequence"); CHECK(!tool_is_subsequence("billing", "billed"), "longer needle rejected"); } static void test_field_match_strength(void) { const char *field = "billing is monthly"; double word = tool_field_match_strength(field, "billing"); double sub = tool_field_match_strength(field, "billin"); double fuzzy = tool_field_match_strength(field, "billng"); double none = tool_field_match_strength(field, "zzz"); CHECK(word > sub, "word beats substring"); CHECK(sub > fuzzy, "substring beats fuzzy"); CHECK(fuzzy > none, "fuzzy beats no match"); CHECK(none == 0.0, "no match is zero"); CHECK(tool_field_match_strength("abc", "ac") == 0.0, "2-char terms get no fuzzy credit"); CHECK(tool_field_match_strength("billing", "billing") == 1.0, "exact match is 1.0"); CHECK(tool_field_match_strength("", "x") == 0.0, "empty field is zero"); } static memory mk_mem(const char *id, const char *importance, const char *title, const char **tags, size_t ntags, const char *summary, const char *body) { memory m; m.id = xstrdup(id); m.title = xstrdup(title); m.importance = xstrdup(importance); m.tags = NULL; m.tag_count = 0; for (size_t i = 0; i < ntags; i++) m.tags = (char* * ) realloc(m.tags, (m.tag_count + 1) * sizeof(char *)), m.tags[m.tag_count++] = xstrdup(tags[i]); m.summary = xstrdup(summary); m.body = xstrdup(body); m.layer = LAYER_WORKSPACE; return m; } static void free_mem(memory *m) { free(m->id); free(m->title); free(m->importance); for (size_t i = 0; i < m->tag_count; i++) free(m->tags[i]); free(m->tags); free(m->summary); free(m->body); } static void test_memory_relevance(void) { strlist q_billing; sl_init(&q_billing); tool_tokenize("billing", &q_billing); memory in_title = mk_mem("a", "low", "Billing", NULL, 0, "", ""); memory in_body = mk_mem("b", "low", "Other", NULL, 0, "", "billing monthly"); CHECK(tool_memory_relevance(&in_title, &q_billing) > tool_memory_relevance(&in_body, &q_billing), "title hit outranks body-only hit"); strlist q_two; sl_init(&q_two); tool_tokenize("billing note", &q_two); memory covers_both = mk_mem("c", "low", "Billing Note", NULL, 0, "", ""); memory covers_one = mk_mem("d", "low", "Billing", NULL, 0, "", ""); CHECK(tool_memory_relevance(&covers_both, &q_two) > tool_memory_relevance(&covers_one, &q_two), "covering both terms outranks covering one"); memory unrelated = mk_mem("e", "low", "Unrelated", NULL, 0, "", ""); CHECK(tool_memory_relevance(&unrelated, &q_billing) == 0.0, "no match scores zero"); free_mem(&in_title); free_mem(&in_body); free_mem(&covers_both); free_mem(&covers_one); free_mem(&unrelated); sl_free(&q_billing); sl_free(&q_two); } static void test_search_memories_ranks_relevance(void) { char *home = getcwd(NULL, 0); char *dir = tmpdir("memrank"); wfile(dir, ".agents/memories/important.md", "---\nid: important\ntitle: Random\nimportance: high\n---\n" "We mention billing here."); wfile(dir, ".agents/memories/relevant.md", "---\nid: relevant\ntitle: Billing\nimportance: low\n---\n" "Details."); if (chdir(dir) != 0) exit(1); dotagents dot; dotagents_load(&dot); char *err = NULL; char *out = tool_search_memories(&dot, "billing", &err); CHECK(out != NULL, "search_memories"); if (!out && err) fprintf(stderr, " err: %s\n", err); char *p_relevant = out ? strstr(out, "## Billing") : NULL; char *p_important = out ? strstr(out, "## Random") : NULL; CHECK(p_relevant && p_important && p_relevant < p_important, "relevance outranks importance"); free(out); dotagents_free(&dot); if (chdir(home) != 0) exit(1); free(home); char *rm = xstrfmt("rm -rf '%s'", dir); system(rm); free(rm); free(dir); } static void test_search_memories_empty_query(void) { char *err = NULL; char *out = tool_search_memories(NULL, " ", &err); CHECK(out == NULL, "blank query fails"); CHECK(err && strstr(err, "query must not be empty") != NULL, "error message"); free(err); } static void test_use_skill_and_search_memories_workspace(void) { char *home = getcwd(NULL, 0); char *dir = tmpdir("ws"); wfile(dir, ".agents/skills/demo/skill.md", "---\nid: demo\nname: Demo Skill\ndescription: a demo\n---\n" "Step one. Step two."); wfile(dir, ".agents/memories/note.md", "---\nid: note\ntitle: Billing Note\nimportance: high\n" "tags: billing, db\n---\nWe bill monthly."); if (chdir(dir) != 0) exit(1); dotagents dot; dotagents_load(&dot); char *err = NULL; char *out = tool_use_skill(&dot, "demo", &err); CHECK(out != NULL, "use_skill"); if (!out && err) fprintf(stderr, " use_skill err: %s\n", err); CHECK_CONT(out, "Demo Skill", "skill name"); CHECK_CONT(out, "Step one. Step two.", "skill body"); free(out); err = NULL; out = tool_use_skill(&dot, "nope", &err); CHECK(out == NULL, "unknown skill fails"); CHECK(err && strstr(err, "no skill") != NULL, "error names available"); free(err); err = NULL; out = tool_search_memories(&dot, "billing", &err); CHECK(out != NULL, "search_memories hit"); CHECK_CONT(out, "Billing Note", "memory title"); CHECK_CONT(out, "importance: high", "importance line"); CHECK_CONT(out, "tags: billing, db", "tags line"); free(out); err = NULL; out = tool_search_memories(&dot, "zzz-qqq-xxx", &err); CHECK(out != NULL, "no-match search returns a message"); CHECK_CONT(out, "no memories matching", "no-match message"); free(out); dotagents_free(&dot); if (chdir(home) != 0) exit(1); free(home); char *rm = xstrfmt("rm -rf '%s'", dir); system(rm); free(rm); free(dir); } /* Tools must serve the session's loaded `.agents` snapshot, not re-read * the disk (AUDIT: no live mid-session reload). */ static void test_tools_read_snapshot_not_disk(void) { char *home = getcwd(NULL, 0); char *dir = tmpdir("snap"); wfile(dir, ".agents/skills/demo/skill.md", "---\nid: demo\nname: Demo Skill\ndescription: a demo\n---\n" "ORIGINAL BODY."); wfile(dir, ".agents/memories/note.md", "---\nid: note\ntitle: Billing Note\nimportance: high\n" "tags: billing\n---\nORIGINAL MEMORY."); if (chdir(dir) != 0) exit(1); dotagents dot; dotagents_load(&dot); /* Mutate the on-disk state after the snapshot was taken. */ wfile(dir, ".agents/skills/demo/skill.md", "---\nid: demo\nname: Demo Skill\ndescription: a demo\n---\n" "REPLACED BODY."); wfile(dir, ".agents/memories/note.md", "entirely new memory"); wfile(dir, ".agents/skills/fresh/skill.md", "---\nid: fresh\nname: Fresh\ndescription: added later\n---\n" "brand new skill."); char *err = NULL; char *out = tool_use_skill(&dot, "demo", &err); CHECK(out != NULL, "skill served from snapshot"); CHECK_CONT(out, "ORIGINAL BODY.", "pre-mutation body intact"); CHECK_NOT_CONT(out, "REPLACED BODY", "disk change not visible"); free(out); err = NULL; out = tool_search_memories(&dot, "billing", &err); CHECK(out != NULL, "memory served from snapshot"); CHECK_CONT(out, "ORIGINAL MEMORY.", "pre-mutation memory body"); CHECK_NOT_CONT(out, "entirely new memory", "new disk file not visible"); free(out); err = NULL; out = tool_use_skill(&dot, "fresh", &err); CHECK(out == NULL, "skill added after snapshot is unknown"); CHECK(err != NULL, "unknown-skill error set"); free(out); free(err); dotagents_free(&dot); if (chdir(home) != 0) exit(1); free(home); char *rm = xstrfmt("rm -rf '%s'", dir); system(rm); free(rm); free(dir); } static void test_write_file_creates_parents(void) { char *dir = tmpdir("wpar"); char *p = xstrfmt("%s/deep/deeper/file.txt", dir); char *err = NULL; char *out = tool_write_file(p, "nested", &err); CHECK(out != NULL, "write_file with new parents"); CHECK_CONT(out, "wrote 6 bytes to", "byte count reported"); free(out); char *back = tool_read_file(p, 0, 0, &err); CHECK(back && strcmp(back, "nested") == 0, "content roundtrip"); free(back); free(p); char *rm = xstrfmt("rm -rf '%s'", dir); system(rm); free(rm); free(dir); } static void test_display_vtable(void) { const tool_display *op; op = tool_display_for("run_shell"); CHECK(op != NULL, "run_shell has a vtable entry"); CHECK(op->render_call != NULL, "run_shell has a custom renderer"); CHECK(op->dim_result, "run_shell results render muted"); char *line = op->render_call("{\"command\":\"ls -la src\"}"); CHECK(line != NULL, "renderer returns a line"); CHECK_CONT(line, "$ ", "renders a shell prompt line"); CHECK_CONT(line, "ls -la src", "shell line carries the command"); CHECK_NOT_CONT(line, "command", "raw JSON key does not leak"); CHECK(line && line[0] == '\n' && line[strlen(line) - 1] == '\n', "line framed with newlines"); free(line); CHECK(tool_display_for("run_shell")->render_call(NULL) == NULL, "NULL args -> generic fallback"); CHECK(tool_display_for("run_shell")->render_call("{}") == NULL, "missing command -> generic fallback"); CHECK(tool_display_for("run_shell")->render_call("nope") == NULL, "bad json -> generic fallback"); op = tool_display_for("read_file"); CHECK(op && op->render_call == NULL && op->dim_result, "read_file: generic display, muted result"); op = tool_display_for("write_file"); CHECK(op && op->render_call == NULL && !op->dim_result, "write_file: generic display, normal result"); op = tool_display_for("use_skill"); CHECK(op && op->render_call == NULL && !op->dim_result, "use_skill: generic display, normal result"); op = tool_display_for("search_memories"); CHECK(op && op->render_call == NULL && !op->dim_result, "search_memories: generic display, normal result"); CHECK(tool_display_for("frobnicate") == NULL, "unknown tool -> NULL"); CHECK(tool_display_for("mcp__srv__tool") == NULL, "mcp tool -> NULL"); CHECK(tool_display_for(NULL) == NULL, "NULL name -> NULL"); } /* ------------------------------------------------------------------ */ /* embed tool */ /* ------------------------------------------------------------------ */ static dotagents embed_dot(const char *model, const char *key, const char *url) { dotagents d; memset(&d, 0, sizeof d); fm_sl_init(&d.warnings); d.models.default_name = xstrdup("emb"); d.models.presets = (model_preset *) xmalloc(sizeof *d.models.presets); memset(d.models.presets, 0, sizeof *d.models.presets); d.models.presets[0].name = xstrdup("emb"); d.models.presets[0].model = model ? xstrdup(model) : NULL; d.models.presets[0].api_key = key ? xstrdup(key) : NULL; d.models.presets[0].base_url = url ? xstrdup(url) : NULL; d.models.preset_count = 1; d.has_models = true; return d; } static config embed_cfg(const char *model, const char *key, const char *url) { config c; memset(&c, 0, sizeof c); sl_init(&c.shell_allowlist); sl_init(&c.shell_denylist); if (model) c.model = xstrdup(model); if (key) c.api_key = xstrdup(key); if (url) c.base_url = xstrdup(url); return c; } static void free_embed_cfg(config *c) { free(c->model); free(c->api_key); free(c->base_url); sl_free(&c->shell_allowlist); sl_free(&c->shell_denylist); } static void test_embed_resolve_profile_values(void) { config c = embed_cfg(NULL, NULL, NULL); dotagents d = embed_dot("emb-model", "emb-key", "https://emb.example/v1"); embed_target t; char *err = NULL; CHECK(tool_embed_resolve(&c, &d, &t, &err) == 0, "resolve ok"); CHECK(t.model && strcmp(t.model, "emb-model") == 0, "preset model"); CHECK(t.api_key && strcmp(t.api_key, "emb-key") == 0, "preset api_key"); CHECK(t.base_url && strcmp(t.base_url, "https://emb.example/v1") == 0, "preset base_url"); embed_target_free(&t); dotagents_free(&d); free_embed_cfg(&c); } static void test_embed_resolve_falls_back_to_top_level(void) { config c = embed_cfg("top-model", "top-key", "https://top.example/v1"); dotagents d = embed_dot(NULL, NULL, NULL); /* preset with empty fields */ embed_target t; char *err = NULL; CHECK(tool_embed_resolve(&c, &d, &t, &err) == 0, "resolve ok"); CHECK(t.model && strcmp(t.model, "top-model") == 0, "inherits model"); CHECK(t.api_key && strcmp(t.api_key, "top-key") == 0, "inherits api_key"); CHECK(t.base_url && strcmp(t.base_url, "https://top.example/v1") == 0, "inherits base_url"); embed_target_free(&t); dotagents_free(&d); free_embed_cfg(&c); } static void test_embed_resolve_errors(void) { config c; memset(&c, 0, sizeof c); sl_init(&c.shell_allowlist); sl_init(&c.shell_denylist); embed_target t; char *err = NULL; CHECK(tool_embed_resolve(&c, NULL, &t, &err) == -1 && err, "no models.json -> error"); CHECK(err && strstr(err, "not configured"), "not-configured message"); free(err); err = NULL; dotagents d = embed_dot("emb-model", "emb-key", "https://emb.example/v1"); free(d.models.default_name); d.models.default_name = xstrdup("ghost"); CHECK(tool_embed_resolve(&c, &d, &t, &err) == -1 && err, "unknown preset -> error"); CHECK(err && strstr(err, "ghost"), "unknown-preset message"); free(err); err = NULL; dotagents_free(&d); config c2 = embed_cfg(NULL, NULL, NULL); dotagents d2 = embed_dot("emb-model", NULL, "https://emb.example/v1"); err = NULL; CHECK(tool_embed_resolve(&c2, &d2, &t, &err) == -1 && err, "preset missing api_key -> error"); CHECK(err && strstr(err, "api_key"), "missing-key message"); free(err); dotagents_free(&d2); free_embed_cfg(&c2); sl_free(&c.shell_allowlist); sl_free(&c.shell_denylist); } static void test_embed_request_body(void) { char *err = NULL; char *req = tool_embed_request("emb-model", "hello \"world\"\n", &err); CHECK(req && !err, "request built"); CHECK_CONT(req, "\"model\":\"emb-model\"", "model in body"); CHECK_CONT(req, "\"input\":\"hello \\\"world\\\"\\n\"", "input escaped"); free(req); free(err); err = NULL; CHECK(tool_embed_request("", "x", &err) == NULL, "empty model rejected"); free(err); err = NULL; CHECK(tool_embed_request("m", "", &err) == NULL, "empty text rejected"); free(err); } static void test_embed_url(void) { char *u = tool_embed_url("https://avois.org/v1/"); CHECK(u && strcmp(u, "https://avois.org/v1/embeddings") == 0, "trailing slash trimmed"); free(u); u = tool_embed_url("http://localhost:11434"); CHECK(u && strcmp(u, "http://localhost:11434/embeddings") == 0, "plain base"); free(u); u = tool_embed_url(""); CHECK(u && strcmp(u, "https://api.openai.com/v1/embeddings") == 0, "default base"); free(u); } static void test_embed_parse(void) { char *vec = NULL; char *err = NULL; const char *body = "{\"object\":\"list\",\"data\":[{\"object\":\"embedding\"," "\"embedding\":[0.1,-0.2,0.3],\"index\":0}],\"model\":\"emb\"}"; CHECK(tool_embed_parse(body, &vec, &err) == 0, "parse ok"); CHECK(vec, "vector non-NULL"); /* Compare values, not serialization: float formatting is not stable * across FP environments (valgrind shifts last ulps). */ cJSON *v = vec ? cJSON_Parse(vec) : NULL; CHECK(v && cJSON_IsArray(v) && cJSON_GetArraySize(v) == 3, "vector has 3 elements"); if (v && cJSON_IsArray(v) && cJSON_GetArraySize(v) == 3) { CHECK(fabs(cJSON_GetArrayItem(v, 0)->valuedouble - 0.1) < 1e-9, "v[0] ~= 0.1"); CHECK(fabs(cJSON_GetArrayItem(v, 1)->valuedouble - (-0.2)) < 1e-9, "v[1] ~= -0.2"); CHECK(fabs(cJSON_GetArrayItem(v, 2)->valuedouble - 0.3) < 1e-9, "v[2] ~= 0.3"); cJSON_Delete(v); } CHECK(!err, "no error"); free(vec); vec = NULL; CHECK(tool_embed_parse("{\"data\":[]}", &vec, &err) == -1, "empty data -> error"); CHECK(err && strstr(err, "data[0].embedding"), "missing-array message"); free(err); err = NULL; CHECK(tool_embed_parse("{\"data\":[{\"embedding\":[1,\"x\"]}]}", &vec, &err) == -1, "non-numeric element -> error"); free(err); err = NULL; CHECK(tool_embed_parse("not json", &vec, &err) == -1, "bad JSON -> error"); free(err); err = NULL; CHECK(tool_embed_parse("", &vec, &err) == -1, "empty body -> error"); free(err); } static void test_embed_tool_unset(void) { config c; memset(&c, 0, sizeof c); sl_init(&c.shell_allowlist); sl_init(&c.shell_denylist); char *err = NULL; CHECK(tool_embed("text", &c, NULL, &err) == NULL, "unset -> NULL"); CHECK(err && strstr(err, "not configured"), "unset message"); free(err); sl_free(&c.shell_allowlist); sl_free(&c.shell_denylist); } static config mkllmcfg(void) { config c; memset(&c, 0, sizeof c); sl_init(&c.shell_allowlist); sl_init(&c.shell_denylist); c.provider = PROV_OPENAI; c.model = xstrdup("test-model"); c.base_url = xstrdup("http://127.0.0.1:9/v1"); c.api_key = xstrdup("k"); return c; } static void freellmcfg(config *c) { sl_free(&c->shell_allowlist); sl_free(&c->shell_denylist); free((void *)c->model); free((void *)c->base_url); free((void *)c->api_key); } /* ------------------------------------------------------------------ */ /* llm tool */ /* ------------------------------------------------------------------ */ static void test_llm_tool_schemas(void) { const char *names[] = {"llm", "compact"}; for (size_t i = 0; i < sizeof names / sizeof names[0]; i++) { const char *name = names[i]; cJSON *schema = tool_params_json(name); CHECK(schema != NULL, "schema non-NULL"); if (!schema) continue; const cJSON *stype = cJSON_GetObjectItem(schema, "type"); CHECK(cJSON_IsString(stype) && strcmp(stype->valuestring, "object") == 0, "schema type is object"); CHECK(cJSON_IsObject(cJSON_GetObjectItem(schema, "properties")), "properties is an object"); CHECK(cJSON_IsArray(cJSON_GetObjectItem(schema, "required")), "required is an array"); cJSON_Delete(schema); } /* llm: required `prompt`, and every documented parameter present. */ cJSON *llm = tool_params_json("llm"); const cJSON *req = cJSON_GetObjectItem(llm, "required"); int req_has_prompt = 0; if (cJSON_IsArray(req)) { for (const cJSON *r = req->child; r; r = r->next) if (cJSON_IsString(r) && strcmp(r->valuestring, "prompt") == 0) req_has_prompt = 1; } CHECK(req_has_prompt, "llm requires prompt"); const char *props[] = {"prompt", "model", "temperature", "max_tokens", "reasoning_effort", "thinking_budget", "response_schema", "embed"}; const cJSON *llmprops = cJSON_GetObjectItem(llm, "properties"); for (size_t i = 0; i < sizeof props / sizeof props[0]; i++) CHECK(cJSON_HasObjectItem(llmprops, props[i]), props[i]); /* reasoning_effort is an enum of the three allowed values. */ const cJSON *re = cJSON_GetObjectItem(llmprops, "reasoning_effort"); const cJSON *reenum = cJSON_GetObjectItem(re, "enum"); CHECK(cJSON_IsArray(reenum) && cJSON_GetArraySize(reenum) == 3, "reasoning_effort enum has 3 values"); cJSON_Delete(llm); /* compact: takes no parameters. */ cJSON *cmp = tool_params_json("compact"); const cJSON *cmpreq = cJSON_GetObjectItem(cmp, "required"); CHECK(cJSON_IsArray(cmpreq) && cJSON_GetArraySize(cmpreq) == 0, "compact required is empty"); const cJSON *cmpprops = cJSON_GetObjectItem(cmp, "properties"); CHECK(cJSON_IsObject(cmpprops) && cmpprops->child == NULL, "compact properties is empty"); cJSON_Delete(cmp); } static void test_llm_tool_requires_prompt(void) { config c = mkllmcfg(); llm_tool_call call; memset(&call, 0, sizeof call); char *err = NULL; CHECK(tool_llm(&call, &c, NULL, &err) == NULL, "no prompt -> NULL"); CHECK(err && strstr(err, "prompt"), "names the prompt"); free(err); freellmcfg(&c); } static void test_llm_tool_bad_schema(void) { config c = mkllmcfg(); llm_tool_call call; memset(&call, 0, sizeof call); call.prompt = "hi"; call.response_schema = "not-json"; char *err = NULL; CHECK(tool_llm(&call, &c, NULL, &err) == NULL, "bad schema -> NULL"); CHECK(err && strstr(err, "response_schema"), "names the schema"); free(err); freellmcfg(&c); } static void test_llm_tool_null_call(void) { config c = mkllmcfg(); char *err = NULL; CHECK(tool_llm(NULL, &c, NULL, &err) == NULL, "null call -> NULL"); CHECK(err && strstr(err, "prompt"), "names the prompt"); free(err); freellmcfg(&c); } int main(void) { test_read_write_roundtrip(); test_read_file_missing_errors(); test_read_file_empty_path_errors(); test_read_file_too_large(); test_read_file_binary_rejected(); test_read_file_offset_limit(); test_read_file_offset_after_unterminated_last_line(); test_read_file_rejects_embedded_nul(); test_write_file_rejects_missing_content(); test_run_shell_success_output(); test_run_shell_success_no_output(); test_run_shell_failure_status(); test_run_shell_stderr_section(); test_run_shell_signal(); test_run_shell_denylist_blocks(); test_run_shell_allowlist_requires_match(); test_run_shell_confirm_hook_aborts(); test_run_shell_timeout_kills_process_group(); test_run_shell_large_output_does_not_hang(); test_truncate_output(); test_tool_list(); test_display_vtable(); test_tool_params_schemas(); test_tokenize(); test_is_subsequence(); test_field_match_strength(); test_memory_relevance(); test_search_memories_ranks_relevance(); test_search_memories_empty_query(); test_use_skill_and_search_memories_workspace(); test_tools_read_snapshot_not_disk(); test_write_file_creates_parents(); test_embed_resolve_profile_values(); test_embed_resolve_falls_back_to_top_level(); test_embed_resolve_errors(); test_embed_request_body(); test_embed_url(); test_embed_parse(); test_embed_tool_unset(); test_llm_tool_schemas(); test_llm_tool_requires_prompt(); test_llm_tool_bad_schema(); test_llm_tool_null_call(); printf("%d checks, %d failures\n", checks, failures); return failures ? 1 : 0; }