Files
Hermit/tests/test_attach.c
T
WHC 895b47274c
ci / linux (clang) (push) Successful in 4m41s
ci / linux (gcc) (push) Successful in 5m35s
ci / musl (push) Failing after 30s
ci / windows-cross (push) Successful in 9m59s
v0.7.0: image input, model registry, prefix-cache savings
Images
- vision gate: text-only models get a note instead of image parts and no
  view_image tool; unknown models retry once without images on HTTP 400
- paste: Ctrl+V and /attach paste read the clipboard (wl-paste, xclip,
  pngpaste/osascript, Win32 PNG or CF_DIB converted to PNG); pasting image
  file paths attaches them
- MCP image content is stored in the session and sent with the tool result
- attach: rel path validation, base64 decode, /retry keeps images

Model registry
- built-in src/models_builtin.json embedded at build time, merged with
  <config>/models.json and cached GET /models metadata (context, vision,
  pricing); most specific rule wins per field
- /model labels, /status source, --list-models [provider]

Cache and cost
- stable request prefix (frozen tools, notes as messages), DeepSeek prefix
  completion, OpenAI/Anthropic cache hints, /cost and hit rate in the status
2026-10-08 20:34:09 +08:00

544 lines
19 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "cJSON.h"
#include "cst_attach.h"
#include "cst_agent.h"
#include "cst_clipboard.h"
#include "cst_test_util.h"
#include "os_compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.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 void put_be32(unsigned char *p, unsigned v) {
p[0] = (unsigned char)(v >> 24);
p[1] = (unsigned char)(v >> 16);
p[2] = (unsigned char)(v >> 8);
p[3] = (unsigned char)v;
}
static void put_le16(unsigned char *p, unsigned v) {
p[0] = (unsigned char)(v & 0xFF);
p[1] = (unsigned char)((v >> 8) & 0xFF);
}
/* 8 signature + 4 length + "IHDR" + w + h (what cst_attach_dimensions reads). */
static size_t png_hdr(unsigned char *b, unsigned w, unsigned h) {
static const unsigned char sig[8] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'};
memcpy(b, sig, 8);
b[8] = b[9] = b[10] = 0;
b[11] = 13;
memcpy(b + 12, "IHDR", 4);
put_be32(b + 16, w);
put_be32(b + 20, h);
return 32;
}
static size_t gif_hdr(unsigned char *b, unsigned w, unsigned h) {
memcpy(b, "GIF89a", 6);
put_le16(b + 6, w);
put_le16(b + 8, h);
return 16;
}
static void put_le32(unsigned char *p, unsigned v) {
p[0] = (unsigned char)(v & 0xFF);
p[1] = (unsigned char)((v >> 8) & 0xFF);
p[2] = (unsigned char)((v >> 16) & 0xFF);
p[3] = (unsigned char)((v >> 24) & 0xFF);
}
static size_t bmp_hdr(unsigned char *b, int w, int h) {
memset(b, 0, 64);
b[0] = 'B';
b[1] = 'M';
put_le32(b + 18, (unsigned)w);
put_le32(b + 22, (unsigned)h);
return 64;
}
/* FFD8 + a filler APP0 segment + SOF0 carrying the frame size. */
static size_t jpeg_hdr(unsigned char *b, unsigned w, unsigned h) {
size_t i = 0;
b[i++] = 0xFF;
b[i++] = 0xD8;
b[i++] = 0xFF;
b[i++] = 0xE0;
b[i++] = 0x00;
b[i++] = 0x04;
b[i++] = 0x00;
b[i++] = 0x00;
b[i++] = 0xFF;
b[i++] = 0xC0;
b[i++] = 0x00;
b[i++] = 0x0B;
b[i++] = 0x08;
b[i++] = (unsigned char)(h >> 8);
b[i++] = (unsigned char)h;
b[i++] = (unsigned char)(w >> 8);
b[i++] = (unsigned char)w;
b[i++] = 0x01; /* one component */
b[i++] = 0x01; /* component id */
b[i++] = 0x11; /* sampling */
b[i++] = 0x00; /* quant table */
return i;
}
static void test_sniff(void) {
unsigned char b[64];
char mime[32];
size_t n;
n = png_hdr(b, 32, 24);
EXPECT(cst_attach_sniff(b, n, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/png") == 0, "PNG sniffed");
n = jpeg_hdr(b, 20, 10);
EXPECT(cst_attach_sniff(b, n, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/jpeg") == 0, "JPEG sniffed");
n = gif_hdr(b, 3, 5);
EXPECT(cst_attach_sniff(b, n, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/gif") == 0, "GIF sniffed");
memcpy(b, "RIFF\0\0\0\0WEBPVP8 ", 16);
EXPECT(cst_attach_sniff(b, 30, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/webp") == 0, "WebP sniffed");
n = bmp_hdr(b, 7, 9);
EXPECT(cst_attach_sniff(b, n, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/bmp") == 0, "BMP sniffed");
/* an extension never decides: text and a PDF are not images */
EXPECT(cst_attach_sniff("hello, world", 12, mime, sizeof(mime)) != 0,
"text rejected");
EXPECT(cst_attach_sniff("%PDF-1.7\n", 9, mime, sizeof(mime)) != 0,
"PDF rejected");
EXPECT(cst_attach_sniff("\x89PNG", 4, mime, sizeof(mime)) != 0,
"truncated PNG rejected");
EXPECT(cst_attach_sniff("", 0, mime, sizeof(mime)) != 0, "empty rejected");
}
static void test_dimensions(void) {
unsigned char b[64];
int w = 0, h = 0;
size_t n;
n = png_hdr(b, 1280, 720);
EXPECT(cst_attach_dimensions(b, n, &w, &h) == 0 && w == 1280 && h == 720,
"PNG dimensions");
n = gif_hdr(b, 16, 40);
EXPECT(cst_attach_dimensions(b, n, &w, &h) == 0 && w == 16 && h == 40,
"GIF dimensions");
n = bmp_hdr(b, 7, 9);
EXPECT(cst_attach_dimensions(b, n, &w, &h) == 0 && w == 7 && h == 9,
"BMP dimensions");
n = jpeg_hdr(b, 640, 480);
EXPECT(cst_attach_dimensions(b, n, &w, &h) == 0 && w == 640 && h == 480,
"JPEG dimensions");
w = h = -1;
EXPECT(cst_attach_dimensions("nope", 4, &w, &h) != 0 && w == 0 && h == 0,
"unknown format gives 0x0");
}
static void test_base64(void) {
char *s = cst_base64_encode("Man", 3);
EXPECT(s && strcmp(s, "TWFu") == 0, "base64 <Man>");
free(s);
s = cst_base64_encode("Ma", 2);
EXPECT(s && strcmp(s, "TWE=") == 0, "base64 padding 1");
free(s);
s = cst_base64_encode("M", 1);
EXPECT(s && strcmp(s, "TQ==") == 0, "base64 padding 2");
free(s);
s = cst_base64_encode("\x00\xff\xfe", 3);
EXPECT(s && strcmp(s, "AP/+") == 0, "base64 binary");
free(s);
size_t n = 0;
char *d = cst_base64_decode("TWFu", 4, &n);
EXPECT(d && n == 3 && memcmp(d, "Man", 3) == 0, "decode <Man>");
free(d);
d = cst_base64_decode("TQ==", 4, &n);
EXPECT(d && n == 1 && d[0] == 'M', "decode with padding");
free(d);
d = cst_base64_decode("AP/+\n", 5, &n);
EXPECT(d && n == 3 && memcmp(d, "\x00\xff\xfe", 3) == 0,
"decode binary, newline ignored");
free(d);
d = cst_base64_decode("AP_-", 4, &n);
EXPECT(d && n == 3 && memcmp(d, "\x00\xff\xfe", 3) == 0, "URL-safe alphabet");
free(d);
EXPECT(cst_base64_decode("A*B=", 4, &n) == NULL, "bad character refused");
unsigned char raw[200];
for (int i = 0; i < 200; i++)
raw[i] = (unsigned char)(i * 37);
for (size_t len = 0; len < 7; len++) {
char *e = cst_base64_encode(raw, 190 + len);
d = e ? cst_base64_decode(e, strlen(e), &n) : NULL;
EXPECT(d && n == 190 + len && memcmp(d, raw, n) == 0, "round trip");
free(e);
free(d);
}
}
static void test_rel_ok(const char *root) {
EXPECT(cst_attach_rel_ok("img-0a1b2c3d.png"), "normal name accepted");
EXPECT(!cst_attach_rel_ok("../x.png"), "parent dir refused");
EXPECT(!cst_attach_rel_ok("a/../../b.png"), "slash refused");
EXPECT(!cst_attach_rel_ok("/etc/passwd"), "absolute refused");
EXPECT(!cst_attach_rel_ok("a\\b.png"), "backslash refused");
EXPECT(!cst_attach_rel_ok(".hidden"), "dot file refused");
EXPECT(!cst_attach_rel_ok(""), "empty refused");
cJSON *arr = cJSON_Parse("[{\"id\":\"x\",\"rel\":\"../../secret.png\"},"
"{\"id\":\"y\",\"rel\":\"img-1.png\"}]");
int n = 0;
cst_attachment_t *back = cst_attachments_from_json(arr, &n);
EXPECT(n == 1 && back && strcmp(back[0].id, "y") == 0,
"escaping rel dropped on load");
cst_attachments_free(back, n);
cJSON_Delete(arr);
/* a stored file swapped for non-image bytes is not sent */
char session[1024], full[1024], err[256];
snprintf(session, sizeof(session), "%.900s/sessions/r.jsonl", root);
unsigned char b[64];
size_t bn = png_hdr(b, 4, 4);
cst_attachment_t *a =
cst_attach_import_bytes(session, "x.png", (const char *)b, bn, err,
sizeof(err));
EXPECT(a != NULL, "stored");
if (!a)
return;
EXPECT(cst_attach_full_path(session, a, full, sizeof(full)) == 0, "path");
FILE *f = fopen(full, "wb");
if (f) {
fputs("plain text now", f);
fclose(f);
}
size_t rn = 0;
EXPECT(cst_attach_read(session, a, &rn, err, sizeof(err)) == NULL &&
strstr(err, "no longer an image"),
"read re-checks the bytes");
EXPECT(cst_attach_data_url(session, a) == NULL, "no data url for it");
snprintf(a->rel, sizeof(a->rel), "../r.jsonl");
EXPECT(cst_attach_full_path(session, a, full, sizeof(full)) != 0,
"full path refuses an escaping rel");
cst_attach_free(a);
}
static void test_store(const char *root) {
char session[1024], dir[1024];
snprintf(session, sizeof(session), "%.900s/sessions/a.jsonl", root);
unsigned char b[64];
size_t n = png_hdr(b, 32, 24);
char err[256];
cst_attachment_t *a = cst_attach_import_bytes(session, "shot.png",
(const char *)b, n, err,
sizeof(err));
EXPECT(a != NULL, "import stored");
if (!a) {
fprintf(stderr, " err: %s\n", err);
return;
}
EXPECT(strcmp(a->mime, "image/png") == 0, "stored mime");
EXPECT(a->width == 32 && a->height == 24, "stored dimensions");
EXPECT(a->bytes == (long long)n, "stored size");
EXPECT(strcmp(a->name, "shot.png") == 0, "stored name");
EXPECT(strcmp(a->origin, "user") == 0, "origin=user");
EXPECT(strcmp(a->rel, "img-") != 0 && strstr(a->rel, ".png") != NULL,
"rel has a .png extension");
EXPECT(cst_attach_dir(session, dir, sizeof(dir)) == 0, "attach dir");
char full[1024];
EXPECT(cst_attach_full_path(session, a, full, sizeof(full)) == 0 &&
strstr(full, "attachments/") != NULL,
"full path inside attachments/");
FILE *f = fopen(full, "rb");
EXPECT(f != NULL, "stored file exists");
if (f)
fclose(f);
char desc[128];
cst_attach_describe(a, desc, sizeof(desc));
EXPECT(strstr(desc, "32x24") && strstr(desc, "png"), "describe has size");
size_t rn = 0;
char *data = cst_attach_read(session, a, &rn, err, sizeof(err));
EXPECT(data && rn == n && memcmp(data, b, n) == 0, "read back exact bytes");
free(data);
char *url = cst_attach_data_url(session, a);
EXPECT(url && strncmp(url, "data:image/png;base64,", 22) == 0,
"data url prefix");
if (url)
EXPECT(strlen(url) == 22 + ((n + 2) / 3) * 4, "data url length");
free(url);
cst_attach_free(a);
/* a text file named .png is still refused */
EXPECT(cst_attach_import_bytes(session, "fake.png", "not an image", 12, err,
sizeof(err)) == NULL,
"content decides, not the name");
EXPECT(strstr(err, "not a supported image") != NULL, "err explains why");
EXPECT(cst_attach_import_bytes(session, "empty.png", "", 0, err,
sizeof(err)) == NULL,
"empty data refused");
}
static void test_import_file(const char *root) {
char session[1024], src[1024];
snprintf(session, sizeof(session), "%.900s/sessions/b.jsonl", root);
snprintf(src, sizeof(src), "%.900s/logo.gif", root);
unsigned char b[64];
size_t n = gif_hdr(b, 8, 6);
FILE *f = fopen(src, "wb");
EXPECT(f != NULL, "write source image");
if (f) {
fwrite(b, 1, n, f);
fclose(f);
}
char err[256];
cst_attachment_t *a =
cst_attach_import_file(session, src, err, sizeof(err));
EXPECT(a != NULL, "file imported");
if (a) {
EXPECT(strcmp(a->mime, "image/gif") == 0, "imported mime");
EXPECT(strcmp(a->name, "logo.gif") == 0, "imported keeps the leaf name");
EXPECT(a->width == 8 && a->height == 6, "imported dimensions");
cst_attach_free(a);
}
EXPECT(cst_attach_import_file(session, "/nonexistent/x.png", err,
sizeof(err)) == NULL,
"missing file refused");
}
static void test_json_roundtrip(void) {
cst_attachment_t list[3];
memset(list, 0, sizeof(list));
snprintf(list[0].id, sizeof(list[0].id), "img-1");
snprintf(list[0].name, sizeof(list[0].name), "a.png");
snprintf(list[0].mime, sizeof(list[0].mime), "image/png");
snprintf(list[0].rel, sizeof(list[0].rel), "img-1.png");
list[0].bytes = 12345;
list[0].width = 640;
list[0].height = 480;
snprintf(list[0].origin, sizeof(list[0].origin), "user");
snprintf(list[1].id, sizeof(list[1].id), "img-2");
snprintf(list[1].name, sizeof(list[1].name), "b.jpg");
snprintf(list[1].mime, sizeof(list[1].mime), "image/jpeg");
snprintf(list[1].rel, sizeof(list[1].rel), "img-2.jpg");
list[1].bytes = 4096;
snprintf(list[1].detail, sizeof(list[1].detail), "high");
snprintf(list[1].origin, sizeof(list[1].origin), "view_image");
snprintf(list[2].id, sizeof(list[2].id), "img-3");
snprintf(list[2].name, sizeof(list[2].name), "c.webp");
snprintf(list[2].mime, sizeof(list[2].mime), "image/webp");
snprintf(list[2].rel, sizeof(list[2].rel), "img-3.webp");
list[2].bytes = 7;
char *json = cst_attachments_to_json(list, 3);
EXPECT(json != NULL, "to_json");
int n = 0;
cJSON *parsed = cJSON_Parse(json);
cst_attachment_t *back = cst_attachments_from_json(parsed, &n);
cJSON_Delete(parsed);
EXPECT(back != NULL && n == 3, "from_json count");
if (back && n == 3) {
EXPECT(strcmp(back[0].id, "img-1") == 0 && back[0].bytes == 12345 &&
back[0].width == 640 && back[0].height == 480,
"first entry survives");
EXPECT(strcmp(back[1].detail, "high") == 0 &&
strcmp(back[1].origin, "view_image") == 0,
"detail and origin survive");
EXPECT(strcmp(back[2].mime, "image/webp") == 0 && back[2].width == 0,
"entry without dimensions");
EXPECT(strcmp(back[0].detail, "auto") == 0, "missing detail defaults");
cst_attachments_free(back, n);
}
free(json);
/* entries without rel/id are dropped; a non-array is NULL */
cJSON *arr = cJSON_Parse("[{\"id\":\"x\"},{\"id\":\"y\",\"rel\":\"y.png\"}]");
back = cst_attachments_from_json(arr, &n);
EXPECT(back != NULL && n == 1 && strcmp(back[0].id, "y") == 0,
"incomplete entries skipped");
cst_attachments_free(back, n);
cJSON_Delete(arr);
cJSON *obj = cJSON_Parse("{}");
n = 7;
EXPECT(cst_attachments_from_json(obj, &n) == NULL && n == 0,
"object is not an attachment array");
cJSON_Delete(obj);
EXPECT(cst_attachments_to_json(NULL, 0) == NULL, "empty list is NULL");
}
static void test_lists(void) {
cst_attachment_t src;
memset(&src, 0, sizeof(src));
snprintf(src.id, sizeof(src.id), "img-x");
snprintf(src.mime, sizeof(src.mime), "image/png");
cst_attachment_t *list = NULL;
int n = 0, cap = 0;
for (int i = 0; i < 10; i++)
EXPECT(cst_attachments_push(&list, &n, &cap, &src) == 0, "push");
EXPECT(n == 10 && cap >= 10, "list grew");
EXPECT(strcmp(list[9].id, "img-x") == 0, "last entry copied");
cst_attachment_t *dup = cst_attachments_dup(list, n);
EXPECT(dup && strcmp(dup[3].mime, "image/png") == 0, "dup copies fields");
dup[3].id[0] = 'Z';
EXPECT(list[3].id[0] == 'i', "dup is a deep copy");
cst_attachments_free(dup, n);
EXPECT(cst_attachments_dup(NULL, 0) == NULL, "dup of nothing is NULL");
cst_attachments_free(list, n);
}
static void test_agent_pending(const char *root) {
cst_agent_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", root);
snprintf(cfg.session_path, sizeof(cfg.session_path), "%s/sessions/c.jsonl",
root);
cst_agent_t *a = cst_agent_create(&cfg);
EXPECT(a != NULL, "agent created");
if (!a)
return;
char err[256], line[128];
EXPECT(cst_agent_pending_count(a) == 0, "no pending at start");
EXPECT(cst_agent_pending_describe(a, 0, line, sizeof(line)) == -1,
"describe out of range");
unsigned char b[32];
size_t n = png_hdr(b, 4, 2);
char src[1024];
snprintf(src, sizeof(src), "%.900s/pic.png", root);
FILE *f = fopen(src, "wb");
EXPECT(f != NULL, "write the source png");
if (f) {
fwrite(b, 1, n, f);
fclose(f);
}
EXPECT(cst_agent_attach_file(a, src, err, sizeof(err)) == 0,
"attach through the agent");
EXPECT(cst_agent_pending_count(a) == 1, "one pending");
EXPECT(cst_agent_pending_describe(a, 0, line, sizeof(line)) == 0 &&
strstr(line, "4x2") != NULL,
"pending describe shows the size");
EXPECT(cst_agent_attach_file(a, "/nonexistent/x.png", err, sizeof(err)) == -1,
"missing file refused");
EXPECT(cst_agent_pending_count(a) == 1, "a failure keeps the queue");
for (int i = 1; i < CST_ATTACH_MAX_COUNT; i++)
EXPECT(cst_agent_attach_file(a, src, err, sizeof(err)) == 0, "attach more");
EXPECT(cst_agent_pending_count(a) == CST_ATTACH_MAX_COUNT, "queue full");
EXPECT(cst_agent_attach_file(a, src, err, sizeof(err)) == -1 &&
strstr(err, "at most") != NULL,
"one over the limit is refused");
cst_agent_pending_clear(a);
EXPECT(cst_agent_pending_count(a) == 0, "cleared");
cst_agent_destroy(a);
}
static void test_paste_paths(void) {
static char p[CST_PASTE_MAX_PATHS][CST_PASTE_PATH_MAX];
const char *s = "/tmp/a.png";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == 1 &&
strcmp(p[0], "/tmp/a.png") == 0,
"single path");
s = " '/home/u/my shot.png' \"/x/y z.jpg\"\n/plain.gif\r\n";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == 3, "quoted paths count");
EXPECT(strcmp(p[0], "/home/u/my shot.png") == 0, "single quotes");
EXPECT(strcmp(p[1], "/x/y z.jpg") == 0, "double quotes");
EXPECT(strcmp(p[2], "/plain.gif") == 0, "newline separated");
s = "file:///home/u/a%20b.png file://localhost/c.png";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == 2, "file uris");
EXPECT(strcmp(p[0], "/home/u/a b.png") == 0, "percent decoded");
EXPECT(strcmp(p[1], "/c.png") == 0, "localhost host");
s = "file://evil.example/c.png";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == -1, "remote host refused");
#ifndef _WIN32
s = "/home/u/my\\ shot.png";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == 1 &&
strcmp(p[0], "/home/u/my shot.png") == 0,
"backslash escape");
#endif
s = "'/unterminated.png";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == -1, "unbalanced quote");
s = " \n ";
EXPECT(cst_paste_paths(s, strlen(s), p, 8) == -1, "blank");
s = "a b c";
EXPECT(cst_paste_paths(s, strlen(s), p, 2) == -1, "too many");
}
static void test_dib(void) {
/* 3x2, 24-bit, bottom-up: stride 12 (9 bytes + pad). */
unsigned char dib[40 + 24];
memset(dib, 0, sizeof(dib));
put_le32(dib, 40);
put_le32(dib + 4, 3);
put_le32(dib + 8, 2);
put_le16(dib + 12, 1);
put_le16(dib + 14, 24);
dib[40] = 0xff; /* bottom-left pixel blue (BGR) */
size_t n = 0;
char *img = cst_dib_to_image(dib, sizeof(dib), &n);
char mime[32];
int w = 0, h = 0;
EXPECT(img && cst_attach_sniff(img, n, mime, sizeof(mime)) == 0 &&
strcmp(mime, "image/png") == 0,
"24-bit DIB becomes a PNG");
EXPECT(img && cst_attach_dimensions(img, n, &w, &h) == 0 && w == 3 && h == 2,
"PNG keeps the size");
free(img);
put_le16(dib + 14, 8); /* palette needs 256 entries: past the end */
EXPECT(cst_dib_to_image(dib, sizeof(dib), &n) == NULL, "short DIB refused");
put_le32(dib, 4);
EXPECT(cst_dib_to_image(dib, sizeof(dib), &n) == NULL, "bad header size");
}
int main(void) {
char root[1024];
if (test_tmpdir(root, sizeof(root), "cst-attach") != 0) {
fprintf(stderr, "cannot create a scratch dir\n");
return 1;
}
test_sniff();
test_dimensions();
test_base64();
test_rel_ok(root);
test_paste_paths();
test_dib();
test_store(root);
test_import_file(root);
test_json_roundtrip();
test_lists();
test_agent_pending(root);
test_rmrf(root);
if (nfail) {
fprintf(stderr, "test_attach: %d failed\n", nfail);
return 1;
}
printf("test_attach: OK\n");
return 0;
}