/* The tools that answer "make me a document", "read this PDF", "get that URL" * and "where are we": md_to_html, read_pdf, download, session_search and * device_info. * * Nothing here touches the network: download is checked on the arguments and * the file-name rules it applies before any request is made, which is where * its mistakes would cost a disk. */ #define _POSIX_C_SOURCE 200809L #include "cst_agent.h" #include "cst_tools.h" #include #include #include #include #include #include #include "cst_test_util.h" static int nfail; #define EXPECT(cond, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \ nfail++; \ } \ } while (0) static char g_ws[256]; /* name, then key/value pairs, NULL-terminated. */ static ctl_tool_call_t mk(const char *name, ...) { ctl_tool_call_t c; memset(&c, 0, sizeof(c)); snprintf(c.id, sizeof(c.id), "t"); snprintf(c.name, sizeof(c.name), "%s", name); va_list ap; va_start(ap, name); const char *k; while ((k = va_arg(ap, const char *)) != NULL) { const char *v = va_arg(ap, const char *); ctl_arg_t *a = &c.args[c.nargs++]; snprintf(a->key, sizeof(a->key), "%s", k); a->value = (char *)v; a->value_len = strlen(v); } va_end(ap); return c; } static int run(cst_tool_ctx_t *ctx, ctl_tool_call_t c, cst_tool_result_t *r) { memset(r, 0, sizeof(*r)); cst_tool_run(ctx, &c, r); return r->exit_code; } static char *rfile(const char *rel) { char p[512]; snprintf(p, sizeof(p), "%s/%s", g_ws, rel); FILE *fp = fopen(p, "rb"); if (!fp) return NULL; static char buf[262144]; size_t n = fread(buf, 1, sizeof(buf) - 1, fp); buf[n] = 0; fclose(fp); return buf; } static void wfile(const char *rel, const char *s) { char p[512]; snprintf(p, sizeof(p), "%s/%s", g_ws, rel); FILE *fp = fopen(p, "wb"); if (fp) { fputs(s, fp); fclose(fp); } } static int exists(const char *rel) { char p[512]; struct stat st; snprintf(p, sizeof(p), "%s/%s", g_ws, rel); return stat(p, &st) == 0; } /* --- md_to_html: one file that stands on its own ------------------------- */ static void test_md_to_html(cst_tool_ctx_t *ctx) { cst_tool_result_t r; ctl_tool_call_t c = mk("md_to_html", "markdown", "# Report\n\n- one\n- two\n\n\n\n" "[x](javascript:alert(2))\n", "title", "Report", NULL); EXPECT(run(ctx, c, &r) == 0, "markdown renders to a document"); EXPECT(r.out && strstr(r.out, "Report.html") != NULL, "the answer names the file it wrote"); char *html = rfile("Report.html"); EXPECT(html != NULL, "and the file is there"); if (html) { EXPECT(strncmp(html, "", 15) == 0, "it is a whole page"); EXPECT(strstr(html, "charset=\"utf-8\"") != NULL, "with a charset"); EXPECT(strstr(html, "

Report

") != NULL, "the heading rendered"); EXPECT(strstr(html, "
  • one
  • ") != NULL, "and the list"); EXPECT(strstr(html, ", and a title with a * path separator in it does not escape the directory */ c = mk("md_to_html", "markdown", "body\n", "title", "a & \"c\" ../d", "out", "hostile.html", NULL); EXPECT(run(ctx, c, &r) == 0 && rfile("hostile.html") != NULL, "a hostile title still writes where it was told"); cst_tool_result_free(&r); html = rfile("hostile.html"); EXPECT(html && strstr(html, "a <b> & "c" ../d" "") != NULL, "the title is escaped"); EXPECT(!exists("../d.html"), "and nothing lands outside the workspace"); /* a .md file, and an output directory */ char dir[512]; snprintf(dir, sizeof(dir), "%s/notes", g_ws); mkdir(dir, 0755); wfile("notes/source.md", "## From a file\n\nbody\n"); c = mk("md_to_html", "file", "notes/source.md", "out", "notes/", NULL); EXPECT(run(ctx, c, &r) == 0, "a workspace file renders"); EXPECT(r.out && strstr(r.out, "notes/source.html") != NULL, "the directory gets the source's name inside it"); EXPECT(rfile("notes/source.html") != NULL, "the html is there"); cst_tool_result_free(&r); /* the preview the window shows says what will be written */ char *prev = NULL; c = mk("md_to_html", "markdown", "hello", "out", "hello.html", NULL); EXPECT(cst_tool_preview(ctx, &c, &prev) == 0 && prev && strstr(prev, "hello.html") != NULL, "the preview names the file"); free(prev); c = mk("md_to_html", NULL); prev = NULL; EXPECT(cst_tool_preview(ctx, &c, &prev) != 0 && prev && strstr(prev, "markdown") != NULL, "an empty call is refused in the preview too"); free(prev); EXPECT(run(ctx, c, &r) == 1 && r.out && strstr(r.out, "error") != NULL, "and by the tool"); cst_tool_result_free(&r); } /* --- read_pdf: the text layer, and a plain refusal for anything else ---- */ static void test_read_pdf(cst_tool_ctx_t *ctx) { cst_tool_result_t r; wfile("notreally.pdf", "this is not a pdf at all\n"); ctl_tool_call_t c = mk("read_pdf", "path", "notreally.pdf", NULL); EXPECT(run(ctx, c, &r) == 1 && r.out && strstr(r.out, "PDF") != NULL, "a file that is not a PDF is refused"); cst_tool_result_free(&r); c = mk("read_pdf", "path", "missing.pdf", NULL); EXPECT(run(ctx, c, &r) == 1 && r.out && strstr(r.out, "error") != NULL, "and a missing one"); cst_tool_result_free(&r); c = mk("read_pdf", NULL); EXPECT(run(ctx, c, &r) == 1, "a call without a path fails"); cst_tool_result_free(&r); } /* --- download: the file name it picks, before it asks the network ------- */ static void test_download_args(cst_tool_ctx_t *ctx) { cst_tool_result_t r; ctl_tool_call_t c = mk("download", NULL); EXPECT(run(ctx, c, &r) == 1 && r.out && strstr(r.out, "url") != NULL, "download without a url fails"); cst_tool_result_free(&r); c = mk("download", "url", "ftp://example.com/x.bin", NULL); EXPECT(run(ctx, c, &r) == 1 && r.out && strstr(r.out, "http") != NULL, "only http(s) URLs are accepted"); cst_tool_result_free(&r); c = mk("download", "url", "https://example.invalid/x.bin", "path", "../escape.bin", NULL); EXPECT(run(ctx, c, &r) == 1 && r.out && strstr(r.out, "outside") != NULL, "a path that leaves the workspace is refused"); cst_tool_result_free(&r); EXPECT(!exists("escape.bin"), "and nothing was written"); } /* --- session_search: what was said before ------------------------------- */ static void test_session_search(cst_tool_ctx_t *ctx) { EXPECT(cst_agent_store_ensure(g_ws) == 0, "the store exists"); char path[512]; EXPECT(cst_agent_store_path_for_id(g_ws, "20260101-120000-fix", path, sizeof(path)) == 0, "a fixture session path"); cst_session_t *s = cst_session_open(path); EXPECT(s != NULL, "its transcript opens"); cst_message_t m; memset(&m, 0, sizeof(m)); m.role = CST_ROLE_USER; m.content = (char *)"how does the Session Store name a session?"; EXPECT(cst_session_append(s, &m) == 0, "a message goes in"); memset(&m, 0, sizeof(m)); m.role = CST_ROLE_ASSISTANT; m.content = (char *)"one directory per session, named by a uuid"; EXPECT(cst_session_append(s, &m) == 0, "and an answer"); cst_session_close(s); cst_tool_result_t r; ctl_tool_call_t c = mk("session_search", "query", "uuid", NULL); EXPECT(run(ctx, c, &r) == 0, "the search runs"); EXPECT(r.out && strstr(r.out, "20260101-120000-fix") != NULL && strstr(r.out, "uuid") != NULL, "the match names its session and shows the line"); EXPECT(r.out && strstr(r.out, "#2 assistant") != NULL, "with the message number and role"); cst_tool_result_free(&r); c = mk("session_search", "query", "UUID", "ignore_case", "true", NULL); EXPECT(run(ctx, c, &r) == 0 && r.out && strstr(r.out, "1 match") != NULL, "case-insensitive by default"); cst_tool_result_free(&r); c = mk("session_search", "query", "zzz-nothing-says-this", NULL); EXPECT(run(ctx, c, &r) == 0 && r.out && strstr(r.out, "no match") != NULL, "a query nothing matches says so (and is not an error)"); cst_tool_result_free(&r); c = mk("session_search", NULL); EXPECT(run(ctx, c, &r) == 1, "a query is required"); cst_tool_result_free(&r); /* A store is not read to the end on a whim: a byte budget stops the walk * and says so, and it never reports a match it did not get to. */ { char path2[512]; EXPECT(cst_agent_store_path_for_id(g_ws, "20260101-130000-big", path2, sizeof(path2)) == 0, "a second session path"); cst_session_t *s2 = cst_session_open(path2); EXPECT(s2 != NULL, "its transcript opens"); if (s2) { cst_message_t m2; memset(&m2, 0, sizeof(m2)); m2.role = CST_ROLE_USER; m2.content = (char *)"the start of a long one"; EXPECT(cst_session_append(s2, &m2) == 0, "a first line goes in"); for (int i = 0; i < 600; i++) { memset(&m2, 0, sizeof(m2)); m2.role = CST_ROLE_ASSISTANT; m2.content = (char *)"a line of tool output that says nothing special"; if (cst_session_append(s2, &m2) != 0) break; } memset(&m2, 0, sizeof(m2)); m2.role = CST_ROLE_ASSISTANT; m2.content = (char *)"the needle is only at the very end"; EXPECT(cst_session_append(s2, &m2) == 0, "the needle goes in last"); cst_session_close(s2); } /* Enough lines that a byte budget is spent before the end is reached. */ c = mk("session_search", "query", "needle", "max_bytes", "4096", NULL); EXPECT(run(ctx, c, &r) == 0 && r.out != NULL, "a bounded search runs"); EXPECT(r.out && strstr(r.out, "budget stopped the walk") != NULL, "the byte budget stops the walk and says so"); EXPECT(r.out == NULL || strstr(r.out, "the needle is only") == NULL, "and it does not report a match it never got to"); cst_tool_result_free(&r); /* The same query without a budget reaches the end and finds it. */ c = mk("session_search", "query", "needle", NULL); EXPECT(run(ctx, c, &r) == 0 && r.out && strstr(r.out, "the needle is only") != NULL, "the unbounded search finds it"); cst_tool_result_free(&r); } } /* --- device_info: the environment, without a shell ---------------------- */ static void test_device_info(cst_tool_ctx_t *ctx) { cst_tool_result_t r; ctl_tool_call_t c = mk("device_info", NULL); EXPECT(run(ctx, c, &r) == 0, "device_info answers"); EXPECT(r.out && strstr(r.out, "cst-agent") != NULL, "with the version"); EXPECT(r.out && strstr(r.out, "system: ") != NULL, "the system it runs on"); EXPECT(r.out && strstr(r.out, g_ws) != NULL, "the workspace in use"); cst_tool_result_free(&r); } int main(void) { if (test_tmpdir(g_ws, sizeof(g_ws), "cst-tools-2") != 0) { perror("tmpdir"); return 1; } cst_tool_ctx_t ctx; memset(&ctx, 0, sizeof(ctx)); ctx.workspace_root = g_ws; test_md_to_html(&ctx); test_read_pdf(&ctx); test_download_args(&ctx); test_session_search(&ctx); test_device_info(&ctx); test_rmrf(g_ws); if (nfail) { fprintf(stderr, "%d check(s) failed\n", nfail); return 1; } printf("test_tools_ext: OK\n"); return 0; }