Files
CST-agent/tests/test_replay_real.c
T
WHC 2a43ae6622 Windows: official port -- UCRT static build, UTF-8 throughout, Low IL sandbox, ncurses TUI, installer; v0.6.0
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
2026-10-06 23:08:14 +08:00

297 lines
9.4 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "cJSON.h"
#include "ctl.h"
#include "cst_agent.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "cst_test_util.h"
/*
* Truncation experiment on recorded model streams (tests/fixtures/sse).
*
* For every recorded assistant message we rebuild the logical stream the
* runtime sees and cut it at every byte:
* - CTL traces: the content text; executable calls = CALL_READY events.
* - native traces: tool calls as a JSON array of {"name","arguments"};
* json-whole : execute only when the whole array parses (EOS baseline)
* json-object: execute each object once it parses (incremental JSON)
*
* Checked properties (the CST soundness claims):
* S1 no call is executable on a prefix unless its bytes are complete;
* S2 every call executed on a prefix is identical to the same call in the
* full stream (prefix consistency, i.e. execution is never revoked);
* S3 the number of executable calls is monotone in the prefix length.
* The printed table reports how early calls become executable (experiment C).
*/
static int nfail;
#define EXPECT(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \
nfail++; \
} \
} while (0)
typedef struct {
char *buf;
size_t len, cap;
} sbuf_t;
static void sb_add(sbuf_t *b, const char *s, size_t n) {
if (b->len + n + 1 > b->cap) {
size_t nc = b->cap ? b->cap * 2 : 1024;
while (nc < b->len + n + 1)
nc *= 2;
b->buf = realloc(b->buf, nc);
b->cap = nc;
}
memcpy(b->buf + b->len, s, n);
b->len += n;
b->buf[b->len] = 0;
}
#define MAXC 32
typedef struct {
sbuf_t content;
int ncalls;
char names[MAXC][64];
sbuf_t args[MAXC];
} trace_t;
static void trace_free(trace_t *t) {
free(t->content.buf);
for (int i = 0; i < MAXC; i++)
free(t->args[i].buf);
memset(t, 0, sizeof(*t));
}
static int load_trace(const char *path, trace_t *t) {
memset(t, 0, sizeof(*t));
FILE *fp = fopen(path, "rb");
if (!fp)
return -1;
char *line = NULL;
size_t cap = 0;
ssize_t n;
while ((n = test_getline(&line, &cap, fp)) > 0) {
if (strncmp(line, "data:", 5) != 0)
continue;
cJSON *j = cJSON_Parse(line + 5);
if (!j)
continue;
const cJSON *ch = cJSON_GetArrayItem(cJSON_GetObjectItem(j, "choices"), 0);
const cJSON *d = cJSON_GetObjectItem(ch, "delta");
const cJSON *c = cJSON_GetObjectItem(d, "content");
if (cJSON_IsString(c))
sb_add(&t->content, c->valuestring, strlen(c->valuestring));
const cJSON *tc = NULL;
cJSON_ArrayForEach(tc, cJSON_GetObjectItem(d, "tool_calls")) {
const cJSON *ji = cJSON_GetObjectItem(tc, "index");
int idx = cJSON_IsNumber(ji) ? ji->valueint : 0;
if (idx < 0 || idx >= MAXC)
continue;
if (idx >= t->ncalls)
t->ncalls = idx + 1;
const cJSON *fn = cJSON_GetObjectItem(tc, "function");
const cJSON *nm = cJSON_GetObjectItem(fn, "name");
const cJSON *ar = cJSON_GetObjectItem(fn, "arguments");
if (cJSON_IsString(nm) && nm->valuestring[0])
snprintf(t->names[idx], sizeof(t->names[idx]), "%s", nm->valuestring);
if (cJSON_IsString(ar))
sb_add(&t->args[idx], ar->valuestring, strlen(ar->valuestring));
}
cJSON_Delete(j);
}
free(line);
fclose(fp);
return 0;
}
/* --- CTL side ------------------------------------------------------------- */
typedef struct {
int n;
char sig[MAXC][512]; /* id|name|k=v... (truncated) as identity */
} ready_t;
static void on_ctl(const ctl_event_t *ev, void *ud) {
ready_t *r = ud;
if (ev->type != CTL_EV_CALL_READY || r->n >= MAXC)
return;
char *s = r->sig[r->n++];
int o = snprintf(s, 512, "%s|%s", ev->call->id, ev->call->name);
for (int i = 0; i < ev->call->nargs && o < 500; i++)
o += snprintf(s + o, (size_t)(512 - o), "|%s=%.*s", ev->call->args[i].key,
(int)(ev->call->args[i].value_len > 200
? 200
: ev->call->args[i].value_len),
ev->call->args[i].value);
}
static void ctl_ready(const char *s, size_t n, int finish, ready_t *r) {
memset(r, 0, sizeof(*r));
ctl_parser_t *p = ctl_parser_create(on_ctl, r);
ctl_parser_feed(p, s, n);
if (finish)
ctl_parser_finish(p);
ctl_parser_destroy(p);
}
/* --- JSON side ------------------------------------------------------------ */
static void build_json_stream(const trace_t *t, sbuf_t *out) {
sb_add(out, "[", 1);
for (int i = 0; i < t->ncalls; i++) {
char head[160];
int hn = snprintf(head, sizeof(head), "%s{\"name\":\"%s\",\"arguments\":",
i ? "," : "", t->names[i]);
sb_add(out, head, (size_t)hn);
if (t->args[i].len)
sb_add(out, t->args[i].buf, t->args[i].len);
else
sb_add(out, "{}", 2);
sb_add(out, "}", 1);
}
sb_add(out, "]", 1);
}
static int json_whole(const char *s, size_t n) {
cJSON *j = cJSON_ParseWithLength(s, n);
int k = cJSON_IsArray(j) ? cJSON_GetArraySize(j) : 0;
cJSON_Delete(j);
return k;
}
static int json_objects(const char *s, size_t n) {
int k = 0;
size_t off = 0;
while (off < n) {
cst_json_tool_t t;
const char *p = memchr(s + off, '{', n - off);
if (!p)
break;
if (!cst_json_try_parse_tool(p, n - (size_t)(p - s), &t))
break;
k++;
/* skip past this object */
const char *end = NULL;
cJSON *j = cJSON_ParseWithLengthOpts(p, n - (size_t)(p - s), &end, 0);
cJSON_Delete(j);
if (!end || end <= p)
break;
off = (size_t)(end - s);
}
return k;
}
/* --- experiment ----------------------------------------------------------- */
static void report_row(const char *label, size_t n, const int *at_pct,
int total) {
printf(" %-26s bytes=%-6zu calls=%d executable@25/50/75/90/100%%: "
"%d/%d/%d/%d/%d\n",
label, n, total, at_pct[0], at_pct[1], at_pct[2], at_pct[3],
at_pct[4]);
}
static const int PCTS[5] = {25, 50, 75, 90, 100};
static void run_ctl(const char *label, const sbuf_t *c) {
ready_t full;
ctl_ready(c->buf, c->len, 1, &full);
int prev = 0, at[5] = {0};
size_t first_ready_at = 0;
for (size_t cut = 0; cut <= c->len; cut++) {
ready_t r;
ctl_ready(c->buf, cut, cut == c->len, &r);
EXPECT(r.n >= prev, "S3 monotone");
EXPECT(r.n <= full.n, "S1 never more than the full stream");
for (int i = 0; i < r.n && i < full.n; i++)
EXPECT(strcmp(r.sig[i], full.sig[i]) == 0, "S2 prefix-consistent call");
if (r.n > 0 && prev == 0)
first_ready_at = cut;
prev = r.n;
for (int k = 0; k < 5; k++)
if (cut == c->len * (size_t)PCTS[k] / 100)
at[k] = r.n;
}
report_row(label, c->len, at, full.n);
if (full.n > 0)
printf(" %-26s first call executable at byte %zu (%.0f%% of message)\n",
"", first_ready_at, 100.0 * (double)first_ready_at / (double)c->len);
}
static void run_native(const char *label, const trace_t *t) {
sbuf_t js = {0};
build_json_stream(t, &js);
int whole_full = json_whole(js.buf, js.len);
int obj_full = json_objects(js.buf, js.len);
EXPECT(whole_full == t->ncalls, "json-whole parses at full length");
EXPECT(obj_full == t->ncalls, "json-object parses all at full length");
int at_w[5] = {0}, at_o[5] = {0}, prev_o = 0;
for (size_t cut = 0; cut <= js.len; cut++) {
int w = json_whole(js.buf, cut);
int o = json_objects(js.buf, cut);
if (cut < js.len)
EXPECT(w == 0, "json-whole executes nothing before the end");
EXPECT(o >= prev_o, "S3 monotone (json-object)");
prev_o = o;
for (int k = 0; k < 5; k++) {
if (cut == js.len * (size_t)PCTS[k] / 100) {
at_w[k] = w;
at_o[k] = o;
}
}
}
char l[80];
snprintf(l, sizeof(l), "%s json-whole", label);
report_row(l, js.len, at_w, whole_full);
snprintf(l, sizeof(l), "%s json-object", label);
report_row(l, js.len, at_o, obj_full);
free(js.buf);
}
int main(void) {
static const char *dirs[] = {"tests/fixtures/sse/deepseek-ctl-heredoc",
"tests/fixtures/sse/deepseek-ctl",
"tests/fixtures/sse/deepseek-native-eos"};
int traces = 0;
for (size_t d = 0; d < sizeof(dirs) / sizeof(dirs[0]); d++) {
printf("%s\n", dirs[d]);
for (int seq = 0; seq < 64; seq++) {
char path[512];
snprintf(path, sizeof(path), "%s/%04d.sse", dirs[d], seq);
if (access(path, R_OK) != 0)
break;
trace_t t;
if (load_trace(path, &t) != 0)
continue;
traces++;
char label[64];
snprintf(label, sizeof(label), "#%04d", seq);
if (t.ncalls > 0)
run_native(label, &t);
else if (t.content.len > 0 && strstr(t.content.buf, "call "))
run_ctl(label, &t.content);
else
printf(" %-26s (final answer, no calls)\n", label);
trace_free(&t);
}
}
if (traces == 0)
printf("(no recorded traces found; run from the repo root)\n");
if (nfail) {
fprintf(stderr, "test_replay_real: %d failed\n", nfail);
return 1;
}
printf("test_replay_real: OK\n");
return 0;
}