Files
CST-agent/tests/test_tools_ext.c
T
WHC dae51e06c7 fix the session-list scan's line count, and pin both new paths with tests
The rewritten cst_agent_store_info() counted a transcript line that ran past a
whole 64 KiB read block a second time, when the newline ending its tail
arrived: a session with one very long line (a tool that read a file back)
reported one message too many -- exactly what the sidebar shows the person.

- src/agent_store.c: the tail's newline ends a line that was already counted.
- tests/test_store.c: test_info_scan() holds the scan down -- a 70 KiB line is
  one line, the first user message comes from the head of the file, and a user
  line past the window is not called the first one.
- tests/test_tools_ext.c: a byte budget stops the session_search walk, says so
  in the result, and never reports a match it did not reach; the same query
  without a budget finds it.

make check and make asan are green.
2026-10-11 20:03:39 +08:00

319 lines
12 KiB
C

/* 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 <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#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<script>alert(1)</script>\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, "<!doctype html>", 15) == 0, "it is a whole page");
EXPECT(strstr(html, "charset=\"utf-8\"") != NULL, "with a charset");
EXPECT(strstr(html, "<h1>Report</h1>") != NULL, "the heading rendered");
EXPECT(strstr(html, "<li>one</li>") != NULL, "and the list");
EXPECT(strstr(html, "<script") == NULL, "no script tag survives");
EXPECT(strstr(html, "javascript:") == NULL, "no javascript: link survives");
}
cst_tool_result_free(&r);
/* a title that is markup stays text in the <title>, and a title with a
* path separator in it does not escape the directory */
c = mk("md_to_html", "markdown", "body\n", "title",
"a <b> & \"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, "<title>a &lt;b&gt; &amp; &quot;c&quot; ../d"
"</title>") != 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;
}