Windows: official port -- UCRT static build, UTF-8 throughout, Low IL sandbox, ncurses TUI, installer; v0.6.0
ci / linux (clang) (push) Canceled after 0s
ci / linux (gcc) (push) Canceled after 0s
ci / sanitizers (push) Canceled after 0s
ci / musl (push) Canceled after 0s
ci / macos (push) Canceled after 0s

Build
  * cross build switches to pkgsCross.ucrt64 (shell-win.nix); curl (Schannel),
    winpthreads and ncurses 6.6 are linked statically, so the release is
    cst-agent.exe + busybox.exe (busybox-w32 Unicode) with no extra DLLs
  * windres embeds a manifest with activeCodePage=UTF-8 and a version resource
  * ncurses 6.6 Windows console driver patches (mouse button state, A_DIM,
    wide-char declarations): scripts/patches/
  * scripts/bootstrap_msys2.sh builds the same release natively in MSYS2
    UCRT64; PR #1's MINGW64 notes now point there
  * vendor/tre-regex (musl's TRE) provides <regex.h> on MinGW

UTF-8
  * paths cross the system call boundary as UTF-16 (*W APIs); console code
    pages set to 65001 and restored on exit; files are read and written in
    binary mode
  * child output that is not valid UTF-8 is decoded from the console, OEM or
    ANSI system code page
  * read_file hides CR; edit_file, apply_patch and write_file keep CRLF line
    endings and a UTF-8 BOM

OS layer and processes
  * os_dir_* and cst_fnmatch replace the POSIX-only directory walks in
    subagent, plugins, skills, undo and fs_tools; realpath / path_under and
    symlink rejection work on Windows
  * every child goes through CreateProcessW with its own environment block
    inside a Job Object, so timeouts and Esc end the whole process tree

Sandbox (auto mode, "auto · lowil")
  * run_shell runs with a restricted Low integrity token; the workspace gets
    an inheritable Low label once, the sandbox TEMP is labeled Low, and the
    credentials file gets a no-read-up label
  * drive roots, the profile and system folders are never labeled
  * network commands still ask, since Low IL does not block the network;
    Access denied and MSYS2's 0xc0000142 offer a rerun outside the sandbox
  * /sandbox lists labeled folders, /sandbox reset-labels restores them

TUI
  * raster console fonts are switched to Consolas / NSimSun while running
  * the home logo falls back to text when the console font lacks U+2580
  * mouse drag copy goes to the Windows clipboard

Distribution
  * scripts/install_windows.ps1 installs a Gitea release into
    %LOCALAPPDATA%\Programs\cst-agent and adds it to the user PATH;
    cst-agent-update.cmd reruns it; scripts/release_windows.sh uploads the
    zip with the token taken from the environment
  * scripts/run_windows.bat is gone; docs/README-windows.md is rewritten

Tests
  * new test_lowil, test_win_utf8 and test_os_dir; tests made portable
    (cst_test_util.h)
  * scripts/win_test.sh and scripts/winvm/ run the suite on English and
    Chinese Windows Server VMs as Administrator, a standard user and a
    Chinese user name: 29/29 on all six
This commit is contained in:
WHC
2026-10-06 23:08:14 +08:00
parent 9862c29f58
commit 2a43ae6622
84 changed files with 9784 additions and 1083 deletions
+208
View File
@@ -0,0 +1,208 @@
/* Portable helpers for the test programs: scratch dirs, env, recursive
* delete, symlinks and (with CST_TEST_SOCKETS) a loopback socket layer. */
#ifndef CST_TEST_UTIL_H
#define CST_TEST_UTIL_H
#include "os_compat.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#ifdef CST_TEST_SOCKETS
#include <winsock2.h> /* before windows.h */
#include <ws2tcpip.h>
#endif
#include <direct.h>
#include <io.h>
#include <windows.h>
#else
#include <sys/stat.h>
#include <unistd.h>
#endif
#if defined(__GNUC__)
#define CST_TEST_UNUSED __attribute__((unused))
#else
#define CST_TEST_UNUSED
#endif
/* mkdir(path, mode) that also builds on Windows. */
#define test_mkdir(p, mode) os_mkdir((p), (mode))
/* Fresh empty directory "<tmp>/<tag>-<pid>-<n>"; 0 on success. */
static CST_TEST_UNUSED int test_tmpdir(char *out, size_t n, const char *tag) {
static int seq;
char base[1024];
if (os_temp_dir(base, sizeof(base)) != 0)
snprintf(base, sizeof(base), ".");
for (int i = 0; i < 100; i++) {
int k = snprintf(out, n, "%s/%s-%d-%d", base, tag, os_getpid(), seq++);
if (k < 0 || (size_t)k >= n)
return -1;
#ifdef _WIN32
int rc = _mkdir(out);
#else
int rc = mkdir(out, 0755);
#endif
if (rc == 0)
return 0;
if (errno != EEXIST)
return -1;
}
return -1;
}
/* rm -rf without a shell; links are removed, never followed. */
static CST_TEST_UNUSED int test_rmrf(const char *path) {
os_stat_t st;
if (os_lstat(path, &st) != 0)
return 0;
if (st.is_link) {
#ifdef _WIN32
if (RemoveDirectoryA(path) || DeleteFileA(path))
return 0;
return -1;
#else
return unlink(path);
#endif
}
if (!st.is_dir) {
#ifdef _WIN32
SetFileAttributesA(path, FILE_ATTRIBUTE_NORMAL);
#endif
return remove(path);
}
os_dir_t *d = os_dir_open(path);
if (d) {
const os_dirent_t *e;
while ((e = os_dir_next(d))) {
if (!strcmp(e->name, ".") || !strcmp(e->name, ".."))
continue;
size_t L = strlen(path) + strlen(e->name) + 2;
char *c = malloc(L);
if (!c)
break;
snprintf(c, L, "%s/%s", path, e->name);
test_rmrf(c);
free(c);
}
os_dir_close(d);
}
#ifdef _WIN32
return _rmdir(path);
#else
return rmdir(path);
#endif
}
/* 0 on success; -1 when the platform or account cannot make links (Windows
* without Developer Mode or admin rights): callers skip those checks. */
static CST_TEST_UNUSED int test_symlink(const char *target, const char *link) {
#ifdef _WIN32
wchar_t *wt = os_utf8_to_wide(target), *wl = os_utf8_to_wide(link);
int ok = 0;
if (wt && wl) {
for (wchar_t *p = wt; *p; p++)
if (*p == L'/')
*p = L'\\';
DWORD attr = GetFileAttributesW(wt);
if (attr == INVALID_FILE_ATTRIBUTES) {
/* relative targets resolve against the link's directory */
wchar_t *slash = wcsrchr(wl, L'/');
if (!slash)
slash = wcsrchr(wl, L'\\');
if (slash && wt[0] != L'\\' && !(wt[0] && wt[1] == L':')) {
size_t dl = (size_t)(slash - wl) + 1, tl = wcslen(wt);
wchar_t *full = malloc((dl + tl + 1) * sizeof(wchar_t));
if (full) {
memcpy(full, wl, dl * sizeof(wchar_t));
memcpy(full + dl, wt, (tl + 1) * sizeof(wchar_t));
attr = GetFileAttributesW(full);
free(full);
}
}
}
DWORD flags = 0x2; /* SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE */
if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY))
flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;
ok = CreateSymbolicLinkW(wl, wt, flags) != 0;
}
free(wt);
free(wl);
return ok ? 0 : -1;
#else
return symlink(target, link);
#endif
}
/* rand_r replacement (same sequence everywhere). */
static CST_TEST_UNUSED int test_rand(unsigned *seed) {
*seed = *seed * 1103515245u + 12345u;
return (int)((*seed >> 16) & 0x7fff);
}
/* getline replacement: -1 at EOF. */
static CST_TEST_UNUSED long test_getline(char **line, size_t *cap, FILE *fp) {
if (!*line || *cap == 0) {
*cap = 256;
*line = malloc(*cap);
if (!*line)
return -1;
}
size_t n = 0;
int c;
while ((c = fgetc(fp)) != EOF) {
if (n + 2 > *cap) {
size_t nc = *cap * 2;
char *nl = realloc(*line, nc);
if (!nl)
return -1;
*line = nl;
*cap = nc;
}
(*line)[n++] = (char)c;
if (c == '\n')
break;
}
if (n == 0)
return -1;
(*line)[n] = 0;
return (long)n;
}
static CST_TEST_UNUSED void test_sleep_ms(int ms) {
#ifdef _WIN32
Sleep((DWORD)ms);
#else
usleep((useconds_t)ms * 1000);
#endif
}
#ifdef CST_TEST_SOCKETS
#ifdef _WIN32
typedef SOCKET test_sock_t;
#define TEST_BAD_SOCK INVALID_SOCKET
#define test_sock_close closesocket
#ifndef MSG_NOSIGNAL
#define MSG_NOSIGNAL 0
#endif
static CST_TEST_UNUSED void test_sock_init(void) {
WSADATA w;
WSAStartup(MAKEWORD(2, 2), &w);
}
#else
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <sys/socket.h>
typedef int test_sock_t;
#define TEST_BAD_SOCK (-1)
#define test_sock_close close
static CST_TEST_UNUSED void test_sock_init(void) { signal(SIGPIPE, SIG_IGN); }
#endif
#endif /* CST_TEST_SOCKETS */
#endif
+6 -5
View File
@@ -7,6 +7,7 @@
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "cst_test_util.h"
static int nfail;
@@ -180,12 +181,12 @@ static void test_tool_multi_file(void) {
int main(void) {
test_buffer_basic();
test_buffer_edge();
snprintf(g_ws, sizeof(g_ws), "/tmp/cst-patch-test-%d", (int)getpid());
mkdir(g_ws, 0755);
if (test_tmpdir(g_ws, sizeof(g_ws), "cst-patch-test") != 0) {
perror("tmpdir");
return 1;
}
test_tool_multi_file();
char cmd[300];
snprintf(cmd, sizeof(cmd), "rm -rf %s /tmp/escape.c", g_ws);
if (system(cmd) != 0)
if (test_rmrf(g_ws) != 0)
fprintf(stderr, "warning: cleanup failed\n");
if (nfail) {
fprintf(stderr, "test_apply_patch: %d failed\n", nfail);
+26 -7
View File
@@ -9,6 +9,11 @@
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "cst_test_util.h"
#ifdef _WIN32
#include <aclapi.h>
#include <sddl.h>
#endif
#define CHECK(c) \
do { \
@@ -23,7 +28,7 @@ static char g_dir[256];
static void write_file(const char *name, const char *text) {
char path[512];
snprintf(path, sizeof(path), "%s/cst-agent/%s", g_dir, name);
FILE *fp = fopen(path, "w");
FILE *fp = fopen(path, "wb");
CHECK(fp);
fputs(text, fp);
fclose(fp);
@@ -57,9 +62,26 @@ static void test_creds(void) {
char path[512];
snprintf(path, sizeof(path), "%s/cst-agent/credentials", g_dir);
#ifdef _WIN32
{
wchar_t *wp = os_utf8_to_wide(path);
PSECURITY_DESCRIPTOR sd = NULL;
LPWSTR s = NULL;
CHECK(wp && GetNamedSecurityInfoW(wp, SE_FILE_OBJECT,
LABEL_SECURITY_INFORMATION, NULL, NULL,
NULL, NULL, &sd) == ERROR_SUCCESS);
CHECK(ConvertSecurityDescriptorToStringSecurityDescriptorW(
sd, SDDL_REVISION_1, LABEL_SECURITY_INFORMATION, &s, NULL));
CHECK(wcsstr(s, L"NR") && wcsstr(s, L";ME)")); /* Low-IL cannot read */
LocalFree(s);
LocalFree(sd);
free(wp);
}
#else
struct stat st;
CHECK(stat(path, &st) == 0);
CHECK((st.st_mode & 0077) == 0);
#endif
CHECK(cst_creds_resolve("deepseek", key, sizeof(key), &from) == 0);
CHECK(strcmp(from, "saved") == 0);
@@ -124,23 +146,20 @@ static void test_effort(void) {
}
int main(void) {
snprintf(g_dir, sizeof(g_dir), "/tmp/cst-config-XXXXXX");
CHECK(mkdtemp(g_dir));
CHECK(test_tmpdir(g_dir, sizeof(g_dir), "cst-config") == 0);
setenv("XDG_CONFIG_HOME", g_dir, 1);
unsetenv("DEEPSEEK_API_KEY");
unsetenv("MINIMAX_API_KEY");
unsetenv("OPENAI_API_KEY");
char sub[300];
snprintf(sub, sizeof(sub), "%s/cst-agent", g_dir);
CHECK(mkdir(sub, 0700) == 0);
CHECK(test_mkdir(sub, 0755) == 0);
test_creds();
test_settings();
test_effort();
char cmd[400];
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", g_dir);
CHECK(system(cmd) == 0);
CHECK(test_rmrf(g_dir) == 0);
printf("test_config: OK\n");
return 0;
}
+14 -7
View File
@@ -9,6 +9,7 @@
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "cst_test_util.h"
static int nfail;
@@ -30,6 +31,11 @@ static void put(const char *p, const char *s) {
/* GNU diff output, or NULL when diff is not installed. */
static char *gnu_diff(const char *a, const char *b) {
#ifdef _WIN32
(void)a;
(void)b;
return NULL; /* no GNU diff to compare against */
#else
char pa[] = "/tmp/cstdiffaXXXXXX", pb[] = "/tmp/cstdiffbXXXXXX";
int fa = mkstemp(pa), fb = mkstemp(pb);
if (fa < 0 || fb < 0)
@@ -67,6 +73,7 @@ static char *gnu_diff(const char *a, const char *b) {
unlink(pa);
unlink(pb);
return out;
#endif
}
static int have_gnu_diff(void) {
@@ -130,11 +137,11 @@ static char *random_text(unsigned *seed, int lines) {
size_t cap = (size_t)lines * 16 + 2, n = 0;
char *s = malloc(cap);
for (int i = 0; i < lines; i++) {
const char *w = words[rand_r(seed) % 8];
n += (size_t)snprintf(s + n, cap - n, "%s%d\n", w, rand_r(seed) % 3);
const char *w = words[test_rand(seed) % 8];
n += (size_t)snprintf(s + n, cap - n, "%s%d\n", w, test_rand(seed) % 3);
}
s[n] = 0;
if (n && rand_r(seed) % 5 == 0)
if (n && test_rand(seed) % 5 == 0)
s[n - 1] = 0; /* no newline at end of file */
return s;
}
@@ -146,15 +153,15 @@ static char *mutate(unsigned *seed, const char *a) {
while (*p) {
const char *e = strchr(p, '\n');
size_t len = e ? (size_t)(e - p) + 1 : strlen(p);
int r = rand_r(seed) % 10;
int r = test_rand(seed) % 10;
if (r == 0) {
/* drop the line */
} else if (r == 1) {
o += (size_t)snprintf(b + o, cap - o, "inserted%d\n", rand_r(seed) % 9);
o += (size_t)snprintf(b + o, cap - o, "inserted%d\n", test_rand(seed) % 9);
memcpy(b + o, p, len);
o += len;
} else if (r == 2) {
o += (size_t)snprintf(b + o, cap - o, "changed%d\n", rand_r(seed) % 9);
o += (size_t)snprintf(b + o, cap - o, "changed%d\n", test_rand(seed) % 9);
} else {
memcpy(b + o, p, len);
o += len;
@@ -198,7 +205,7 @@ int main(void) {
unsigned seed = 12345;
int ok = 0, total = 400;
for (int i = 0; i < total; i++) {
char *a = random_text(&seed, 1 + (int)(rand_r(&seed) % 60));
char *a = random_text(&seed, 1 + (int)(test_rand(&seed) % 60));
char *b = mutate(&seed, a);
ok += round_trip(a, b);
free(a);
+184
View File
@@ -0,0 +1,184 @@
/* Windows Low-IL shell sandbox: writes only in the workspace and the
* sandbox TEMP, the credentials file is unreadable, timeouts end the whole
* process tree, and /sandbox reset-labels restores the workspace label.
* Elsewhere it only checks that the kind is never reported. */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "cst_sandbox.h"
#include "os_compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.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)
#ifdef _WIN32
static int run(const char *ws, const char *cmd, os_run_result_t *r) {
memset(r, 0, sizeof(*r));
if (cst_sandbox_run_shell(ws, cmd, 0, r) != 0)
return -1;
return r->exit_code;
}
static int has(const os_run_result_t *r, const char *s) {
return r->out && strstr(r->out, s);
}
static void write_file(const char *path, const char *text) {
FILE *f = fopen(path, "wb");
if (f) {
fputs(text, f);
fclose(f);
}
}
static void confined(void) {
char root[1024], cfg[1100], ws[1100], out[1100], cred[1300], cmd[4096];
char wsr[1100];
if (test_tmpdir(root, sizeof(root), "cst-lowil") != 0) {
EXPECT(0, "tmpdir");
return;
}
snprintf(cfg, sizeof(cfg), "%s/cfg", root);
snprintf(ws, sizeof(ws), "%s/项目 ws", root);
snprintf(out, sizeof(out), "%s/outside", root);
test_mkdir(cfg, 0700);
test_mkdir(ws, 0755);
test_mkdir(out, 0755);
setenv("XDG_CONFIG_HOME", cfg, 1);
char cdir[1200];
EXPECT(os_config_dir(cdir, sizeof(cdir)) == 0, "config dir");
os_mkdir_p(cdir, 0700);
snprintf(cred, sizeof(cred), "%s/credentials", cdir);
write_file(cred, "deepseek=sk-test-secret\n");
EXPECT(os_chmod_private(cred) == 0, "label credentials");
EXPECT(os_realpath(ws, wsr, sizeof(wsr)) == 0, "realpath ws");
os_path_norm_slashes(wsr);
os_run_result_t r;
EXPECT(run(wsr, "echo hi > in.txt && cat in.txt", &r) == 0,
"write inside the workspace");
EXPECT(has(&r, "hi"), "output captured");
EXPECT(has(&r, "labeled"), "first run says the workspace was labeled");
os_run_result_free(&r);
EXPECT(run(wsr, "echo again > in.txt && cat in.txt", &r) == 0,
"second run");
EXPECT(!has(&r, "labeled"), "labeled only once");
EXPECT(!has(&r, "background"), "conhost is not a background process");
os_run_result_free(&r);
char *labels = cst_sandbox_labels();
EXPECT(labels && strstr(labels, wsr), "workspace recorded");
free(labels);
snprintf(cmd, sizeof(cmd), "echo x > '%s/o.txt'", out);
int rc = run(wsr, cmd, &r);
EXPECT(rc != 0, "write outside the workspace fails");
EXPECT(cst_sandbox_blocked(r.out, r.out_len, rc), "outside write blocked");
os_run_result_free(&r);
snprintf(cmd, sizeof(cmd), "%s/o.txt", out);
os_stat_t st;
EXPECT(os_lstat(cmd, &st) != 0, "nothing written outside");
snprintf(cmd, sizeof(cmd), "cat '%s'", cred);
rc = run(wsr, cmd, &r);
EXPECT(rc != 0 && !has(&r, "sk-test-secret"), "credentials unreadable");
EXPECT(cst_sandbox_blocked(r.out, r.out_len, rc), "credential read blocked");
os_run_result_free(&r);
EXPECT(run(wsr, "echo t > \"$TEMP/t.txt\" && cat \"$TEMP/t.txt\"", &r) == 0,
"TEMP is writable");
EXPECT(has(&r, "t"), "TEMP read back");
os_run_result_free(&r);
/* System32's whoami: busybox has an applet and MSYS2's cannot start at
* Low IL */
char sys[MAX_PATH];
UINT sl = GetSystemDirectoryA(sys, sizeof(sys));
for (UINT i = 0; i < sl; i++)
if (sys[i] == '\\')
sys[i] = '/';
snprintf(cmd, sizeof(cmd), "'%s/whoami.exe' /groups | grep S-1-16-4096",
sys);
rc = run(wsr, cmd, &r);
EXPECT(rc == 0, "child runs at Low integrity");
if (rc != 0)
fprintf(stderr, "whoami: exit %d: %.*s\n", rc, (int)r.out_len,
r.out ? r.out : "");
os_run_result_free(&r);
/* timeout ends background children too */
os_set_run_timeout(2);
double t0 = os_now_ms();
rc = run(wsr, "sleep 30 & sleep 30; echo late", &r);
double ms = os_now_ms() - t0;
EXPECT(rc == 124, "timeout exit code");
EXPECT(ms < 10000, "process tree killed promptly");
EXPECT(!has(&r, "late"), "command did not finish");
os_run_result_free(&r);
os_set_run_timeout(0);
const char *prof = getenv("USERPROFILE");
char profr[1100];
if (prof && os_realpath(prof, profr, sizeof(profr)) == 0) {
os_path_norm_slashes(profr);
rc = run(profr, "echo no", &r);
EXPECT(rc == 126 && has(&r, "will not label"), "profile is not labeled");
EXPECT(cst_sandbox_blocked(r.out, r.out_len, rc), "offers a rerun");
os_run_result_free(&r);
}
rc = run("C:/", "echo no", &r);
EXPECT(rc == 126 && has(&r, "will not label"), "drive root is not labeled");
os_run_result_free(&r);
char *rep = NULL;
EXPECT(cst_sandbox_reset_labels(&rep) >= 1, "reset-labels restored");
EXPECT(rep && strstr(rep, "restored"), "reset report");
free(rep);
labels = cst_sandbox_labels();
EXPECT(!labels, "record cleared");
free(labels);
char sddl[256];
EXPECT(os_win_get_label(wsr, sddl, sizeof(sddl)) == 0 &&
strcmp(sddl, "S:") == 0,
"workspace has no explicit label again");
unsetenv("XDG_CONFIG_HOME");
test_rmrf(root);
}
#endif
int main(void) {
int kind = cst_sandbox_probe();
printf("test_lowil: sandbox %s\n", cst_sandbox_name(kind));
EXPECT(strcmp(cst_sandbox_name(CST_SANDBOX_LOWIL), "lowil") == 0, "name");
#ifdef _WIN32
EXPECT(kind == CST_SANDBOX_LOWIL, "lowil available");
if (kind == CST_SANDBOX_LOWIL)
confined();
#else
EXPECT(kind != CST_SANDBOX_LOWIL, "lowil is Windows only");
char *rep = NULL;
EXPECT(cst_sandbox_reset_labels(&rep) == 0 && !rep, "nothing to reset");
EXPECT(!cst_sandbox_labels(), "no labels");
#endif
if (nfail) {
fprintf(stderr, "test_lowil: %d failure(s)\n", nfail);
return 1;
}
puts("test_lowil: OK");
return 0;
}
+8 -3
View File
@@ -14,7 +14,10 @@
#include <string.h>
#include <unistd.h>
#include "cst_test_util.h"
static int nfail;
static char g_tmp[OS_PATH_MAX];
#define CHECK(cond, msg) \
do { \
@@ -159,7 +162,7 @@ static int call(const char *tool, const char *k, const char *v,
}
cst_tool_ctx_t ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.workspace_root = "/tmp";
ctx.workspace_root = g_tmp;
memset(r, 0, sizeof(*r));
return cst_tool_run(&ctx, &c, r);
}
@@ -181,6 +184,8 @@ int main(int argc, char **argv) {
fprintf(stderr, "cannot resolve %s\n", argv[0]);
return 1;
}
if (os_temp_dir(g_tmp, sizeof(g_tmp)) != 0)
snprintf(g_tmp, sizeof(g_tmp), ".");
setenv("CST_TEST_SECRET", "s3cret", 1);
char cfg[OS_PATH_MAX + 128];
snprintf(cfg, sizeof(cfg),
@@ -206,7 +211,7 @@ int main(int argc, char **argv) {
cst_mcp_counts(&ready, &total);
CHECK(ready == 0 && total == 3, "counts before start (disabled excluded)");
cst_mcp_start_all("/tmp");
cst_mcp_start_all(g_tmp);
cst_mcp_wait_ready(10000);
cst_mcp_info_t mi[CST_MCP_MAX];
@@ -281,7 +286,7 @@ int main(int argc, char **argv) {
/* a live server stops cleanly too */
CHECK(cst_mcp_add_config_json("fake2", cfg, NULL, "user") == 0, "re-add");
cst_mcp_start_all("/tmp");
cst_mcp_start_all(g_tmp);
cst_mcp_wait_ready(10000);
CHECK(cst_tool_find("mcp__fake2__echo"), "second server ready");
t0 = os_now_ms();
+14 -20
View File
@@ -8,18 +8,10 @@
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
int main(void) {
puts("test_models: skipped on Windows");
return 0;
}
#else
#define CST_TEST_SOCKETS
#include "cst_test_util.h"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/socket.h>
#include <unistd.h>
#define CHECK(c) \
do { \
@@ -32,35 +24,38 @@ int main(void) {
/* One canned HTTP response per accepted connection. */
static const char *g_responses[8];
static int g_nresp;
static int g_listen = -1;
static test_sock_t g_listen = TEST_BAD_SOCK;
static char g_request[4096];
static void *serve(void *arg) {
(void)arg;
for (int i = 0; i < g_nresp; i++) {
int c = accept(g_listen, NULL, NULL);
if (c < 0)
test_sock_t c = accept(g_listen, NULL, NULL);
if (c == TEST_BAD_SOCK)
return NULL;
ssize_t n = recv(c, g_request, sizeof(g_request) - 1, 0);
long n = recv(c, g_request, sizeof(g_request) - 1, 0);
g_request[n > 0 ? n : 0] = 0;
const char *r = g_responses[i];
ssize_t off = 0, len = (ssize_t)strlen(r);
long off = 0, len = (long)strlen(r);
while (off < len) {
ssize_t w = send(c, r + off, (size_t)(len - off), 0);
long w = send(c, r + off, (int)(len - off), 0);
if (w <= 0)
break;
off += w;
}
close(c);
test_sock_close(c);
}
return NULL;
}
static int start_server(void) {
test_sock_init();
g_listen = socket(AF_INET, SOCK_STREAM, 0);
CHECK(g_listen >= 0);
CHECK(g_listen != TEST_BAD_SOCK);
#ifndef _WIN32
int one = 1;
setsockopt(g_listen, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
#endif
struct sockaddr_in a;
memset(&a, 0, sizeof(a));
a.sin_family = AF_INET;
@@ -118,7 +113,7 @@ int main(void) {
CHECK(n == -1 && strstr(err, "500") != NULL);
pthread_join(th, NULL);
close(g_listen);
test_sock_close(g_listen);
/* nothing listening: network error, not an auth failure */
n = cst_llm_list_models(base, "sk-test", ids, 8, err, sizeof(err));
@@ -127,4 +122,3 @@ int main(void) {
puts("test_models: ok");
return 0;
}
#endif
+147
View File
@@ -0,0 +1,147 @@
/* cst_fnmatch and the os_dir / os_lstat layer that replaced the POSIX-only
* directory code; runs on every platform. */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "cst_util.h"
#include "os_compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.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 void fnm(const char *pat, const char *s, int flags, int want) {
int got = cst_fnmatch(pat, s, flags) == 0;
if (got != want) {
fprintf(stderr, "FAIL fnmatch(\"%s\", \"%s\", %d) = %d\n", pat, s, flags,
got);
nfail++;
}
}
static void test_fnmatch(void) {
fnm("*.c", "main.c", 0, 1);
fnm("*.c", "main.h", 0, 0);
fnm("*.c", "src/main.c", 0, 1); /* '*' crosses '/' */
fnm("?.txt", "a.txt", 0, 1);
fnm("?.txt", "ab.txt", 0, 0);
fnm("?.txt", "中.txt", 0, 1); /* one code point */
fnm("[abc]x", "bx", 0, 1);
fnm("[!abc]x", "bx", 0, 0);
fnm("[a-c]x", "cx", 0, 1);
fnm("[[:digit:]]*", "7up", 0, 1);
fnm("[[:digit:]]*", "up", 0, 0);
fnm("\\*", "*", 0, 1);
fnm("\\*", "a", 0, 0);
fnm("*", ".hidden", CST_FNM_PERIOD, 0);
fnm(".*", ".hidden", CST_FNM_PERIOD, 1);
fnm("*", ".hidden", 0, 1);
fnm("*文件*", "我的文件.txt", 0, 1);
fnm("README*", "readme.md", CST_FNM_CASEFOLD, 1);
#ifdef _WIN32
fnm("README*", "readme.md", 0, 1); /* Windows names are case-insensitive */
#else
fnm("README*", "readme.md", 0, 0);
#endif
fnm("", "", 0, 1);
fnm("a", "", 0, 0);
}
static void touch(const char *dir, const char *name) {
char p[1200];
snprintf(p, sizeof(p), "%s/%s", dir, name);
FILE *f = fopen(p, "wb");
if (f) {
fputs("x", f);
fclose(f);
}
}
static void test_dir(void) {
char root[1024], sub[1100], p[1200];
if (test_tmpdir(root, sizeof(root), "cst-osdir") != 0) {
EXPECT(0, "tmpdir");
return;
}
snprintf(sub, sizeof(sub), "%s/子目录", root);
EXPECT(test_mkdir(sub, 0755) == 0, "mkdir with a Chinese name");
touch(root, "a.txt");
touch(root, "文件 一.md");
touch(root, ".hidden");
int files = 0, dirs = 0, seen_cn = 0, seen_hidden = 0, dots = 0;
os_dir_t *d = os_dir_open(root);
EXPECT(d != NULL, "os_dir_open");
const os_dirent_t *e;
while (d && (e = os_dir_next(d)) != NULL) {
if (!strcmp(e->name, ".") || !strcmp(e->name, ".."))
dots++;
if (e->is_dir)
dirs++;
else
files++;
seen_cn |= strcmp(e->name, "文件 一.md") == 0 && !e->is_dir;
seen_hidden |= strcmp(e->name, ".hidden") == 0;
if (!strcmp(e->name, "子目录"))
EXPECT(e->is_dir && !e->is_link, "subdir flags");
}
os_dir_close(d);
EXPECT(dots == 0, "no . or .. entries");
EXPECT(files == 3 && dirs == 1, "entry counts");
EXPECT(seen_cn, "UTF-8 file name listed");
EXPECT(seen_hidden, "dot files listed");
d = os_dir_open(sub);
EXPECT(d != NULL && os_dir_next(d) == NULL, "empty Chinese subdir");
if (d)
os_dir_close(d);
os_stat_t st;
snprintf(p, sizeof(p), "%s/文件 一.md", root);
EXPECT(os_lstat(p, &st) == 0 && st.is_reg && !st.is_dir && st.size == 1,
"os_lstat on a UTF-8 file");
EXPECT(os_lstat(sub, &st) == 0 && st.is_dir, "os_lstat on a dir");
snprintf(p, sizeof(p), "%s/missing", root);
EXPECT(os_lstat(p, &st) != 0, "os_lstat on a missing path");
EXPECT(os_dir_open(p) == NULL, "os_dir_open on a missing path");
char real[1200], rr[1200];
EXPECT(os_realpath(sub, real, sizeof(real)) == 0 &&
os_realpath(root, rr, sizeof(rr)) == 0,
"os_realpath");
EXPECT(os_path_under(rr, real), "realpath of subdir is under root");
EXPECT(!os_path_under(real, rr), "root is not under subdir");
snprintf(p, sizeof(p), "%s/link", root);
int lk = test_symlink(sub, p);
if (lk == 0) {
EXPECT(os_lstat(p, &st) == 0 && st.is_link, "symlink is reported as link");
} else {
printf("test_os_dir: symlinks not permitted, link checks skipped\n");
}
test_rmrf(root);
EXPECT(os_lstat(root, &st) != 0, "test_rmrf removed everything");
}
int main(void) {
os_console_init();
test_fnmatch();
test_dir();
if (nfail) {
fprintf(stderr, "test_os_dir: %d failure(s)\n", nfail);
return 1;
}
puts("test_os_dir: OK");
return 0;
}
+8 -7
View File
@@ -14,6 +14,7 @@
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "cst_test_util.h"
static int nfail;
@@ -33,11 +34,11 @@ static void put(const char *rel, const char *text) {
for (char *q = p + strlen(g_root) + 1; *q; q++) {
if (*q == '/') {
*q = 0;
mkdir(p, 0755);
test_mkdir(p, 0755);
*q = '/';
}
}
FILE *fp = fopen(p, "w");
FILE *fp = fopen(p, "wb");
if (fp) {
fputs(text, fp);
fclose(fp);
@@ -70,8 +71,10 @@ static void run(const char *ws, const char *tool, const char *k1,
}
int main(void) {
snprintf(g_root, sizeof(g_root), "/tmp/cst-plugins-%d", (int)getpid());
mkdir(g_root, 0755);
if (test_tmpdir(g_root, sizeof(g_root), "cst-plugins") != 0) {
perror("tmpdir");
return 1;
}
char ws[300], cfgdir[300];
snprintf(ws, sizeof(ws), "%s/ws", g_root);
snprintf(cfgdir, sizeof(cfgdir), "%s/cfg", g_root);
@@ -206,9 +209,7 @@ int main(void) {
CHECK(!cst_tool_find("skill") && !cst_tool_find("echo_args"),
"extensions unload");
char cmd[400];
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_root);
if (system(cmd) != 0)
if (test_rmrf(g_root) != 0)
fprintf(stderr, "note: cleanup failed\n");
if (nfail) {
fprintf(stderr, "test_plugins: %d failure(s)\n", nfail);
+2 -1
View File
@@ -8,6 +8,7 @@
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "cst_test_util.h"
/*
* Truncation experiment on recorded model streams (tests/fixtures/sse).
@@ -79,7 +80,7 @@ static int load_trace(const char *path, trace_t *t) {
char *line = NULL;
size_t cap = 0;
ssize_t n;
while ((n = getline(&line, &cap, fp)) > 0) {
while ((n = test_getline(&line, &cap, fp)) > 0) {
if (strncmp(line, "data:", 5) != 0)
continue;
cJSON *j = cJSON_Parse(line + 5);
+38
View File
@@ -73,6 +73,43 @@ static void test_blocked(void) {
const char *d = "curl: (7) Failed to connect to example.com port 443 after 9 "
"ms: Could not connect to server\n";
CHECK(cst_sandbox_blocked(d, strlen(d), 7));
const char *e = "Access is denied.\r\n";
CHECK(cst_sandbox_blocked(e, strlen(e), 1));
const char *f = "拒绝访问。\r\n";
CHECK(cst_sandbox_blocked(f, strlen(f), 1));
const char *g = "sh: C:/msys64/usr/bin/ls.exe: A dynamic link library (DLL) "
"initialization routine failed. Error 0xc0000142\n";
CHECK(cst_sandbox_blocked(g, strlen(g), 1));
}
static void test_network_rules(void) {
static const char *const NET[] = {
"curl -fsSL https://example.com",
"cd x && wget http://a/b",
"/usr/bin/ssh host ls",
"C:\\tools\\curl.exe -O x",
"git push origin main",
"git --no-pager fetch",
"npm install",
"python -m pip install requests",
"go get example.com/x",
"cargo install ripgrep",
"Invoke-WebRequest -Uri x",
};
static const char *const LOCAL[] = {
"ls -la", "git status", "git commit -m push", "npm run build",
"make -j8", "pip list", "echo curly", "cat curl.txt",
};
for (size_t i = 0; i < sizeof(NET) / sizeof(NET[0]); i++) {
if (!cst_shell_uses_network(NET[i]))
fprintf(stderr, "expected network: %s\n", NET[i]);
CHECK(cst_shell_uses_network(NET[i]));
}
for (size_t i = 0; i < sizeof(LOCAL) / sizeof(LOCAL[0]); i++) {
if (cst_shell_uses_network(LOCAL[i]))
fprintf(stderr, "expected local: %s\n", LOCAL[i]);
CHECK(!cst_shell_uses_network(LOCAL[i]));
}
}
#if !defined(_WIN32)
@@ -314,6 +351,7 @@ static void test_confined(int kind) {
int main(void) {
test_rules();
test_blocked();
test_network_rules();
int kind = cst_sandbox_probe();
printf("test_sandbox: sandbox %s\n", cst_sandbox_name(kind));
#if !defined(_WIN32)
+7 -3
View File
@@ -2,6 +2,7 @@
#include "cJSON.h"
#include "cst_agent.h"
#include "os_compat.h"
#include <stdio.h>
#include <stdlib.h>
@@ -76,8 +77,11 @@ static void check_messages(cst_session_t *s) {
}
int main(void) {
char path[256];
snprintf(path, sizeof(path), "/tmp/cst-session-test-%d.jsonl", (int)getpid());
char path[1024], tmp[900];
if (os_temp_dir(tmp, sizeof(tmp)) != 0)
snprintf(tmp, sizeof(tmp), ".");
snprintf(path, sizeof(path), "%s/cst-session-test-%d.jsonl", tmp,
os_getpid());
remove(path);
cst_session_t *s = cst_session_open(path);
@@ -129,7 +133,7 @@ int main(void) {
cst_session_close(s);
/* legacy format: tool_calls_json stored as a string */
fp = fopen(path, "w");
fp = fopen(path, "wb");
fprintf(fp, "{\"role\":\"assistant\",\"content\":\"\",\"tool_calls_json\":"
"\"[{\\\"id\\\":\\\"c9\\\",\\\"type\\\":\\\"function\\\","
"\\\"function\\\":{\\\"name\\\":\\\"run_shell\\\","
+6 -5
View File
@@ -8,6 +8,7 @@
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "cst_test_util.h"
/* run_shell must never hang the agent: timeout kills the whole process
* group, stdin is empty, huge output is capped. */
@@ -41,8 +42,10 @@ static int sh(const char *cmd, cst_tool_result_t *r, double *ms) {
}
int main(void) {
snprintf(g_ws, sizeof(g_ws), "/tmp/cst-shell-test-%d", (int)getpid());
mkdir(g_ws, 0755);
if (test_tmpdir(g_ws, sizeof(g_ws), "cst-shell-test") != 0) {
perror("tmpdir");
return 1;
}
cst_tool_result_t r;
double ms;
@@ -81,9 +84,7 @@ int main(void) {
EXPECT(ms < 20000, "large output does not hang");
cst_tool_result_free(&r);
char cmd[300];
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_ws);
if (system(cmd) != 0)
if (test_rmrf(g_ws) != 0)
fprintf(stderr, "warning: cleanup failed\n");
if (nfail) {
fprintf(stderr, "test_shell: %d failed\n", nfail);
+10 -9
View File
@@ -11,6 +11,7 @@
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "cst_test_util.h"
static int nfail;
@@ -30,11 +31,11 @@ static void put(const char *rel, const char *text) {
for (char *q = p + strlen(g_root) + 1; *q; q++) {
if (*q == '/') {
*q = 0;
mkdir(p, 0755);
test_mkdir(p, 0755);
*q = '/';
}
}
FILE *fp = fopen(p, "w");
FILE *fp = fopen(p, "wb");
if (fp) {
fputs(text, fp);
fclose(fp);
@@ -69,8 +70,10 @@ static void frontmatter(void) {
int main(void) {
frontmatter();
snprintf(g_root, sizeof(g_root), "/tmp/cst-skills-%d", (int)getpid());
mkdir(g_root, 0755);
if (test_tmpdir(g_root, sizeof(g_root), "cst-skills") != 0) {
perror("tmpdir");
return 1;
}
char ws[300], cfgdir[300];
snprintf(ws, sizeof(ws), "%s/ws", g_root);
snprintf(cfgdir, sizeof(cfgdir), "%s/cfg", g_root);
@@ -94,8 +97,8 @@ int main(void) {
char link[512], target[512];
snprintf(link, sizeof(link), "%s/ws/.agent/skills/forms/leak", g_root);
snprintf(target, sizeof(target), "%s/ws/secret.txt", g_root);
if (symlink(target, link) != 0)
perror("symlink");
if (test_symlink(target, link) != 0)
fprintf(stderr, "note: no symlinks here, escape check is partial\n");
}
cst_skill_t v[CST_SKILLS_MAX];
@@ -180,9 +183,7 @@ int main(void) {
CHECK(sp && !strstr(sp, "Skills ("), "minimal prompt unchanged");
free(sp);
char cmd[400];
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_root);
if (system(cmd) != 0)
if (test_rmrf(g_root) != 0)
fprintf(stderr, "note: cleanup failed\n");
if (nfail) {
fprintf(stderr, "test_skills: %d failure(s)\n", nfail);
+3 -6
View File
@@ -7,6 +7,7 @@
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "cst_test_util.h"
/* Drives cst_llm_chat_stream through CST_REPLAY_SSE with synthetic and
* recorded (tests/fixtures/sse) streams, at several network chunk sizes. */
@@ -279,18 +280,14 @@ static void test_recorded(void) {
}
int main(void) {
snprintf(g_dir, sizeof(g_dir), "/tmp/cst-sse-test-%d", (int)getpid());
char cmd[300];
snprintf(cmd, sizeof(cmd), "mkdir -p %s", g_dir);
if (system(cmd) != 0)
if (test_tmpdir(g_dir, sizeof(g_dir), "cst-sse-test") != 0)
return 1;
test_text_reasoning_usage();
test_parallel_calls();
test_same_index_new_id();
test_invalid_args_and_error();
test_recorded();
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_dir);
if (system(cmd) != 0)
if (test_rmrf(g_dir) != 0)
fprintf(stderr, "warning: cleanup failed\n");
if (nfail) {
fprintf(stderr, "test_sse_decode: %d failed\n", nfail);
+27 -32
View File
@@ -15,19 +15,16 @@
#include "cst_util.h"
#include "os_compat.h"
#include <arpa/inet.h>
#define CST_TEST_SOCKETS
#include "cst_test_util.h"
#include <curl/curl.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
static int nfail;
@@ -45,9 +42,9 @@ static atomic_int g_child_active, g_child_max, g_child_reqs;
static atomic_int g_child_saw_task, g_child_saw_write, g_child_role_prompt;
static atomic_int g_parent_saw_task;
static void send_all(int fd, const char *s, size_t n) {
static void send_all(test_sock_t fd, const char *s, size_t n) {
while (n > 0) {
ssize_t w = send(fd, s, n, MSG_NOSIGNAL);
long w = send(fd, s, (int)n, MSG_NOSIGNAL);
if (w <= 0)
return;
s += w;
@@ -55,7 +52,7 @@ static void send_all(int fd, const char *s, size_t n) {
}
}
static void sse(int fd, cJSON *delta, const char *finish) {
static void sse(test_sock_t fd, cJSON *delta, const char *finish) {
cJSON *o = cJSON_CreateObject();
cJSON *ch = cJSON_AddArrayToObject(o, "choices");
cJSON *c0 = cJSON_CreateObject();
@@ -80,7 +77,7 @@ static void sse(int fd, cJSON *delta, const char *finish) {
cJSON_Delete(o);
}
static void reply_text(int fd, const char *text) {
static void reply_text(test_sock_t fd, const char *text) {
cJSON *d = cJSON_CreateObject();
cJSON_AddStringToObject(d, "role", "assistant");
cJSON_AddStringToObject(d, "content", text);
@@ -90,7 +87,7 @@ static void reply_text(int fd, const char *text) {
}
/* calls: {name, args_json} pairs */
static void reply_calls(int fd, const char *const *calls, int n) {
static void reply_calls(test_sock_t fd, const char *const *calls, int n) {
for (int i = 0; i < n; i++) {
cJSON *d = cJSON_CreateObject();
cJSON *tc = cJSON_AddArrayToObject(d, "tool_calls");
@@ -121,7 +118,7 @@ static int has_tool(const cJSON *body, const char *name) {
return 0;
}
static void handle(int fd, const char *body_txt) {
static void handle(test_sock_t fd, const char *body_txt) {
cJSON *body = cJSON_Parse(body_txt);
const cJSON *msgs = cJSON_GetObjectItemCaseSensitive(body, "messages");
int nm = cJSON_GetArraySize(msgs);
@@ -155,9 +152,9 @@ static void handle(int fd, const char *body_txt) {
if (strstr(sys->valuestring, "Role instructions:\nYou only read."))
atomic_store(&g_child_role_prompt, 1);
if (strstr(user, "Slow")) {
usleep(4000 * 1000); /* cancelled before this ends */
test_sleep_ms(4000); /* cancelled before this ends */
} else {
usleep(300 * 1000);
test_sleep_ms(300);
}
if (strcmp(last_role, "user") == 0) {
const char *calls[] = {"read_file", "{\"path\":\"a.txt\"}"};
@@ -206,13 +203,13 @@ static void handle(int fd, const char *body_txt) {
}
static void *conn_main(void *p) {
int fd = (int)(intptr_t)p;
test_sock_t fd = (test_sock_t)(intptr_t)p;
cst_buf_t req = {0};
char tmp[8192];
long need = -1;
size_t hdr_end = 0;
for (;;) {
ssize_t n = recv(fd, tmp, sizeof(tmp), 0);
long n = recv(fd, tmp, sizeof(tmp), 0);
if (n <= 0)
break;
cst_buf_append(&req, tmp, (size_t)n);
@@ -230,23 +227,23 @@ static void *conn_main(void *p) {
if (hdr_end)
handle(fd, req.buf + hdr_end);
cst_buf_free(&req);
close(fd);
test_sock_close(fd);
return NULL;
}
static int g_lfd;
static test_sock_t g_lfd;
static void *server_main(void *p) {
(void)p;
for (;;) {
int fd = accept(g_lfd, NULL, NULL);
if (fd < 0)
test_sock_t fd = accept(g_lfd, NULL, NULL);
if (fd == TEST_BAD_SOCK)
return NULL;
pthread_t th;
if (pthread_create(&th, NULL, conn_main, (void *)(intptr_t)fd) == 0)
pthread_detach(th);
else
close(fd);
test_sock_close(fd);
}
}
@@ -282,10 +279,10 @@ static void put(const char *root, const char *rel, const char *text) {
for (char *q = p + strlen(root) + 1; *q; q++)
if (*q == '/') {
*q = 0;
mkdir(p, 0755);
test_mkdir(p, 0755);
*q = '/';
}
FILE *fp = fopen(p, "w");
FILE *fp = fopen(p, "wb");
if (fp) {
fputs(text, fp);
fclose(fp);
@@ -293,13 +290,13 @@ static void put(const char *root, const char *rel, const char *text) {
}
static void *cancel_later(void *p) {
usleep(800 * 1000);
test_sleep_ms(800);
cst_agent_request_cancel(p);
return NULL;
}
int main(void) {
signal(SIGPIPE, SIG_IGN);
test_sock_init();
curl_global_init(CURL_GLOBAL_DEFAULT);
g_lfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
@@ -316,10 +313,10 @@ int main(void) {
pthread_t srv;
pthread_create(&srv, NULL, server_main, NULL);
char root[256], ws[300];
snprintf(root, sizeof(root), "/tmp/cst-sub-%d", (int)getpid());
char root[600], ws[700];
if (test_tmpdir(root, sizeof(root), "cst-sub") != 0)
return 1;
snprintf(ws, sizeof(ws), "%s/ws", root);
mkdir(root, 0755);
setenv("XDG_CONFIG_HOME", root, 1);
put(root, "ws/a.txt", "alpha\n");
put(root, "ws/.agent/agents/reviewer.md",
@@ -470,11 +467,9 @@ int main(void) {
CHECK(cst_subagent_list(ws, "parent1", recs, 8) == 0,
"delete removes sub-agent transcripts");
char cmd[400];
snprintf(cmd, sizeof(cmd), "rm -rf %s", root);
if (system(cmd) != 0)
if (test_rmrf(root) != 0)
fprintf(stderr, "note: cleanup failed\n");
close(g_lfd);
test_sock_close(g_lfd);
if (nfail) {
fprintf(stderr, "test_subagent: %d failure(s)\n", nfail);
return 1;
+12 -10
View File
@@ -12,6 +12,8 @@
#include <sys/stat.h>
#include <unistd.h>
#include "cst_test_util.h"
static int nfail;
#define EXPECT(cond, msg) \
@@ -98,17 +100,19 @@ static int run(cst_tool_ctx_t *ctx, ctl_tool_call_t c, cst_tool_result_t *r) {
static void reset(cst_tool_result_t *r) { cst_tool_result_free(r); }
int main(void) {
snprintf(g_ws, sizeof(g_ws), "/tmp/cst-tools-test-%d", (int)getpid());
mkdir(g_ws, 0755);
if (test_tmpdir(g_ws, sizeof(g_ws), "cst-tools-test") != 0) {
perror("tmpdir");
return 1;
}
char p[512];
snprintf(p, sizeof(p), "%s/src", g_ws);
mkdir(p, 0755);
test_mkdir(p, 0755);
snprintf(p, sizeof(p), "%s/src/deep", g_ws);
mkdir(p, 0755);
test_mkdir(p, 0755);
snprintf(p, sizeof(p), "%s/node_modules", g_ws);
mkdir(p, 0755);
test_mkdir(p, 0755);
snprintf(p, sizeof(p), "%s/build", g_ws);
mkdir(p, 0755);
test_mkdir(p, 0755);
wfile("src/main.c", "#include <stdio.h>\nint main(void) {\n int x = 42;\n"
" printf(\"%d\\n\", x);\n return 0;\n}\n");
wfile("src/deep/util.c", "int add(int a, int b) { return a + b; }\n"
@@ -119,7 +123,7 @@ int main(void) {
wfile(".gitignore", "build/\n*.log\n");
wfile("run.log", "int add in log\n");
{
FILE *fp = fopen(strcat(strcpy(p, g_ws), "/lines.txt"), "w");
FILE *fp = fopen(strcat(strcpy(p, g_ws), "/lines.txt"), "wb");
for (int i = 1; i <= 100; i++)
fprintf(fp, "line %d\n", i);
fclose(fp);
@@ -410,9 +414,7 @@ int main(void) {
"patch targets");
}
char cmd[400];
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_ws);
if (system(cmd) != 0)
if (test_rmrf(g_ws) != 0)
fprintf(stderr, "note: cleanup failed\n");
if (nfail) {
fprintf(stderr, "test_tools_fs: %d failure(s)\n", nfail);
+6 -8
View File
@@ -10,6 +10,7 @@
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "cst_test_util.h"
static int nfail;
@@ -81,9 +82,9 @@ static void test_undo(void) {
EXPECT(cst_undo_snapshot(g_ws, sid, "t3", "d/e/f.txt") == 0, "snap nested");
char p[512];
snprintf(p, sizeof(p), "%s/d", g_ws);
mkdir(p, 0755);
test_mkdir(p, 0755);
snprintf(p, sizeof(p), "%s/d/e", g_ws);
mkdir(p, 0755);
test_mkdir(p, 0755);
wfile("d/e/f.txt", "x");
EXPECT(cst_undo_restore_last(g_ws, sid, rep, sizeof(rep)) == 0, "undo t3");
EXPECT(rfile_eq("d/e/f.txt", NULL), "nested file removed");
@@ -146,7 +147,7 @@ static void test_store(void) {
EXPECT(cst_agent_store_path_for_id(g_ws, "20260101-000000-abcd", path,
sizeof(path)) == 0,
"path for id");
FILE *fp = fopen(path, "w");
FILE *fp = fopen(path, "wb");
EXPECT(fp != NULL, "create session file");
if (fp) {
fputs("{\"role\":\"system\",\"content\":\"sys\"}\n", fp);
@@ -174,17 +175,14 @@ static void test_store(void) {
}
int main(void) {
snprintf(g_ws, sizeof(g_ws), "/tmp/cst_test_undo_XXXXXX");
if (!mkdtemp(g_ws)) {
if (test_tmpdir(g_ws, sizeof(g_ws), "cst_test_undo") != 0) {
perror("mkdtemp");
return 1;
}
test_undo();
test_perms();
test_store();
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", g_ws);
if (system(cmd) != 0)
if (test_rmrf(g_ws) != 0)
fprintf(stderr, "warning: cleanup failed\n");
if (nfail) {
fprintf(stderr, "test_undo: %d failure(s)\n", nfail);
+238
View File
@@ -0,0 +1,238 @@
/* UTF-8 everywhere: Chinese workspace and file names through the file
* tools and the shell, CRLF and BOM files edited by apply_patch, write_file
* and edit_file without losing their style, and (Windows) child output in
* the OEM code page turned into UTF-8. */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "cst_agent.h"
#include "cst_util.h"
#include "os_compat.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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[1100];
/* tool(name, key, value, key, value, ..., NULL) */
static int tool(cst_tool_result_t *r, 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 *);
snprintf(c.args[c.nargs].key, sizeof(c.args[c.nargs].key), "%s", k);
c.args[c.nargs].value = (char *)v;
c.args[c.nargs].value_len = strlen(v);
c.nargs++;
}
va_end(ap);
memset(r, 0, sizeof(*r));
cst_tool_dispatch(g_ws, &c, r);
return r->exit_code;
}
static char *slurp(const char *rel, size_t *n) {
char p[1200];
snprintf(p, sizeof(p), "%s/%s", g_ws, rel);
FILE *f = fopen(p, "rb");
if (!f)
return NULL;
char *b = malloc(1 << 16);
*n = b ? fread(b, 1, (1 << 16) - 1, f) : 0;
if (b)
b[*n] = 0;
fclose(f);
return b;
}
static void spit(const char *rel, const char *s) {
char p[1200];
snprintf(p, sizeof(p), "%s/%s", g_ws, rel);
FILE *f = fopen(p, "wb");
if (f) {
fputs(s, f);
fclose(f);
}
}
static int file_is(const char *rel, const char *want) {
size_t n = 0;
char *b = slurp(rel, &n);
int ok = b && n == strlen(want) && memcmp(b, want, n) == 0;
if (!ok)
fprintf(stderr, " %s is \"%s\"\n", rel, b ? b : "(missing)");
free(b);
return ok;
}
static void test_patch_buffer(void) {
char *out = NULL, err[256];
size_t n = 0;
int nh = 0;
const char *crlf = "a\r\nb\r\nc\r\n";
const char *hunk = "@@ -1,3 +1,3 @@\n a\n-b\n+B\n c\n";
EXPECT(cst_patch_apply_buffer(crlf, strlen(crlf), hunk, strlen(hunk), &out,
&n, &nh, err, sizeof(err)) == 0,
"LF hunk applies to a CRLF file");
EXPECT(out && strcmp(out, "a\r\nB\r\nc\r\n") == 0, "CRLF kept");
free(out);
const char *bom = "\xEF\xBB\xBF" "first\nsecond\n";
const char *h2 = "@@ -1,2 +1,2 @@\n-first\n+FIRST\n second\n";
EXPECT(cst_patch_apply_buffer(bom, strlen(bom), h2, strlen(h2), &out, &n,
&nh, err, sizeof(err)) == 0,
"first line matches past the BOM");
EXPECT(out && strcmp(out, "\xEF\xBB\xBF" "FIRST\nsecond\n") == 0,
"BOM kept");
free(out);
const char *lf = "x\ny\n";
const char *h3 = "@@ -1,2 +1,3 @@\n x\n+中文\n y\n";
EXPECT(cst_patch_apply_buffer(lf, strlen(lf), h3, strlen(h3), &out, &n, &nh,
err, sizeof(err)) == 0 &&
out && strcmp(out, "x\n中文\ny\n") == 0,
"LF file stays LF");
free(out);
EXPECT(cst_text_is_crlf("a\r\nb\r\n", 6), "is_crlf");
EXPECT(!cst_text_is_crlf("a\r\nb\n", 5), "mixed is not crlf");
EXPECT(!cst_text_is_crlf("ab", 2), "no newline is not crlf");
}
static void test_tools(void) {
cst_tool_result_t r;
/* Chinese names through write/read/list/glob/grep */
EXPECT(tool(&r, "write_file", "path", "目录/文件.txt", "content",
"你好,世界\n第二行\n", NULL) == 0,
"write_file with a Chinese path");
cst_tool_result_free(&r);
EXPECT(tool(&r, "read_file", "path", "目录/文件.txt", NULL) == 0 && r.out &&
strstr(r.out, "你好,世界"),
"read_file with a Chinese path");
cst_tool_result_free(&r);
EXPECT(tool(&r, "list_dir", "path", "目录", NULL) == 0 && r.out &&
strstr(r.out, "文件.txt"),
"list_dir shows the Chinese name");
cst_tool_result_free(&r);
EXPECT(tool(&r, "glob", "pattern", "**/*.txt", NULL) == 0 && r.out &&
strstr(r.out, "目录/文件.txt"),
"glob finds the Chinese name");
cst_tool_result_free(&r);
EXPECT(tool(&r, "grep", "pattern", "世界", NULL) == 0 && r.out &&
strstr(r.out, "文件.txt"),
"grep matches Chinese text");
cst_tool_result_free(&r);
/* CRLF + BOM file */
spit("win.txt", "\xEF\xBB\xBFone\r\ntwo\r\nthree\r\n");
EXPECT(tool(&r, "read_file", "path", "win.txt", NULL) == 0 && r.out &&
strcmp(r.out, "one\ntwo\nthree\n") == 0,
"read_file hides BOM and CR");
cst_tool_result_free(&r);
EXPECT(tool(&r, "edit_file", "path", "win.txt", "old_string", "two",
"new_string", "二\n2", NULL) == 0,
"edit_file single-line match");
cst_tool_result_free(&r);
EXPECT(file_is("win.txt", "\xEF\xBB\xBFone\r\n二\r\n2\r\nthree\r\n"),
"edit_file inserts CRLF");
EXPECT(tool(&r, "edit_file", "path", "win.txt", "old_string", "one\n二",
"new_string", "壹\n贰", NULL) == 0,
"edit_file multi-line LF match");
cst_tool_result_free(&r);
EXPECT(file_is("win.txt", "\xEF\xBB\xBF壹\r\n贰\r\n2\r\nthree\r\n"),
"edit_file keeps CRLF and BOM");
EXPECT(tool(&r, "apply_patch", "patch",
"--- a/win.txt\n+++ b/win.txt\n@@ -3,2 +3,2 @@\n 2\n-three\n"
"+叁\n",
NULL) == 0,
"apply_patch on a CRLF file");
if (r.exit_code)
fprintf(stderr, " apply_patch: %s\n", r.out ? r.out : "");
cst_tool_result_free(&r);
EXPECT(file_is("win.txt", "\xEF\xBB\xBF壹\r\n贰\r\n2\r\n叁\r\n"),
"apply_patch keeps CRLF and BOM");
EXPECT(tool(&r, "write_file", "path", "win.txt", "content", "new\ntext\n",
NULL) == 0,
"write_file over a CRLF file");
cst_tool_result_free(&r);
EXPECT(file_is("win.txt", "\xEF\xBB\xBFnew\r\ntext\r\n"),
"write_file keeps CRLF and BOM");
EXPECT(tool(&r, "write_file", "path", "new.txt", "content", "lf\n", NULL) ==
0 &&
file_is("new.txt", "lf\n"),
"new files are LF without BOM");
cst_tool_result_free(&r);
/* the shell sees and prints UTF-8 */
EXPECT(tool(&r, "run_shell", "cmd", "cat 目录/文件.txt && echo 完成", NULL) ==
0 &&
r.out && strstr(r.out, "你好,世界") && strstr(r.out, "完成"),
"run_shell with Chinese arguments and output");
if (!r.out || !strstr(r.out, "完成"))
fprintf(stderr, " run_shell: %s\n", r.out ? r.out : "");
cst_tool_result_free(&r);
}
#ifdef _WIN32
static void test_oem_output(void) {
/* "中文" in GBK */
const char gbk[] = "\xD6\xD0\xCE\xC4\r\n";
size_t n = 0;
char *u = os_output_to_utf8(gbk, sizeof(gbk) - 1, &n);
wchar_t b[8] = L"0";
GetLocaleInfoW(LOCALE_SYSTEM_DEFAULT, LOCALE_IDEFAULTCODEPAGE, b, 8);
unsigned oem = (unsigned)wcstoul(b, NULL, 10);
if (oem == 936)
EXPECT(u && strstr(u, "中文"), "GBK output decoded on a 936 system");
else
EXPECT(u != NULL, "non-UTF-8 output is decoded");
free(u);
EXPECT(os_output_to_utf8("中文", strlen("中文"), &n) == NULL,
"UTF-8 output left alone");
printf("test_win_utf8: ACP %u, OEM %u, system OEM %u, console %u\n",
GetACP(), GetOEMCP(), oem, GetConsoleOutputCP());
EXPECT(GetACP() == CP_UTF8, "manifest makes UTF-8 the process code page");
}
#endif
int main(void) {
os_console_init();
char base[1024];
if (test_tmpdir(base, sizeof(base), "cst-utf8") != 0) {
perror("tmpdir");
return 1;
}
snprintf(g_ws, sizeof(g_ws), "%s/项目 空格", base);
test_mkdir(g_ws, 0755);
test_patch_buffer();
test_tools();
#ifdef _WIN32
test_oem_output();
#endif
test_rmrf(base);
if (nfail) {
fprintf(stderr, "test_win_utf8: %d failure(s)\n", nfail);
return 1;
}
puts("test_win_utf8: OK");
return 0;
}