Files
Hermit/tests/test_runtime.c
T
WHCandCursor f00aad9e3e feat: solo default, job completion reports, tools wave-1 (0.9.32)
Password Realm no longer auto-enables parliament (keeps task/worker tools);
finished jobs fan out to agent notes + TUI; raise PAR_MAX/grep caps and tag truncations.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 19:48:02 +08:00

154 lines
8.4 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "cst_runtime.h"
#include "cst_worktree.h"
#include "cst_test_util.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
static pthread_mutex_t probe_mu = PTHREAD_MUTEX_INITIALIZER;
static int active, peak;
static int work(void *p, volatile int *cancel, cst_tool_result_t *out) {
(void)p;
os_observe_output("live output", 11);
struct timespec delay = {0, 1000000};
for (int i = 0; i < 30 && !*cancel; i++) nanosleep(&delay, NULL);
out->out = strdup("verified job output"); out->out_len = strlen(out->out);
out->exit_code = *cancel ? 130 : 0; return 0;
}
static int locked_work(void *p, volatile int *cancel, cst_tool_result_t *out) {
pthread_mutex_lock(&probe_mu); active++; if (active > peak) peak = active; pthread_mutex_unlock(&probe_mu);
int rc = work(p, cancel, out);
pthread_mutex_lock(&probe_mu); active--; pthread_mutex_unlock(&probe_mu); return rc;
}
static int instant(void *p, volatile int *cancel, cst_tool_result_t *out) {
(void)p; (void)cancel; out->out = strdup("archived output"); out->out_len = strlen(out->out); return 0;
}
/* Runs to a failed exit even if cancel raced after the side effect. */
static int fail_then_note_cancel(void *p, volatile int *cancel, cst_tool_result_t *out) {
(void)p;
out->out = strdup("command failed"); out->out_len = strlen(out->out);
out->exit_code = 7;
if (cancel) *cancel = 1; /* late cancel must not rewrite exit 7 → 130 */
return 0;
}
static void write_text(const char *dir, const char *name, const char *text) {
char p[8192]; assert(!os_path_join(p, sizeof(p), dir, name));
FILE *f = fopen(p, "wb"); assert(f); assert(fputs(text, f) >= 0); assert(!fclose(f));
}
static char *shell(const char *dir, const char *cmd) {
os_run_result_t out = {0}; assert(!os_run_shell_c(dir, cmd, 65536, &out));
assert(!out.exit_code); char *s = out.out; out.out = NULL; os_run_result_free(&out); return s;
}
int main(void) {
char dir[2048], path[4096], error[256], id[64];
assert(!test_tmpdir(dir, sizeof(dir), "runtime"));
snprintf(path, sizeof(path), "%s/session", dir);
cst_runtime_t *r = cst_runtime_open(path); assert(r);
unsigned long rev = cst_runtime_begin(r, "test execution");
assert(cst_runtime_check(r, rev, 1, 1));
assert(cst_runtime_finish(r, "completed", "unsupported claim", 1, error, sizeof(error)));
char *ref = cst_runtime_evidence(r, "read_file", 0, "actual evidence", 0);
assert(ref); free(ref);
assert(!cst_runtime_finish(r, "completed", "verified", 1, error, sizeof(error)));
assert(!cst_runtime_steer(r, "only inspect, do not delegate", 1, 0));
assert(!cst_runtime_check(r, rev, 0, 0));
assert(!cst_runtime_check(r, 0, 1, 0));
assert(!cst_runtime_check(r, 0, 0, 1));
char *s = cst_runtime_take_steering(r); assert(s && strstr(s, "only inspect")); free(s);
assert(!cst_runtime_take_steering(r));
assert(!cst_runtime_controls(r, 0, 1, 0));
cst_runtime_begin(r, "asynchronous work");
assert(!cst_job_start(r, "read", "", 1, work, NULL, NULL, id, sizeof(id)));
assert(cst_runtime_finish(r, "completed", "premature", 0, error, sizeof(error)));
s = cst_job_read(r, id, 2000); assert(s && strstr(s, "completed") && strstr(s, "verified job output")); free(s);
/* Dual completion rings: agent and UI drain independently. */
{
cst_job_completion_t comps[4], ui[4];
int n = cst_runtime_take_completions(r, comps, 4);
assert(n >= 1);
assert(comps[0].id[0] && !strcmp(comps[0].status, "completed"));
assert(strstr(comps[0].summary, "exit=") || comps[0].exit_code == 0);
assert(cst_runtime_take_completions(r, comps, 4) == 0);
int nu = cst_runtime_take_ui_completions(r, ui, 4);
assert(nu >= 1 && !strcmp(ui[0].status, "completed"));
assert(cst_runtime_take_ui_completions(r, ui, 4) == 0);
}
assert(!cst_job_start(r, "external", "device:test", 0, work, NULL, NULL, id, sizeof(id)));
/* Let the scheduler start the operation before requesting a safe stop. */
for (int i = 0; i < 100; i++) {
s = cst_job_read(r, id, 1); int running = s && strstr(s, "\"status\":\"running\""); free(s);
if (running) break;
}
assert(!cst_job_cancel(r, id, 0, "user"));
s = cst_job_read(r, id, 0); assert(s && strstr(s, "running_protected") && strstr(s, "user")); free(s);
assert(!cst_runtime_steer(r, "inspect something else", -1, -1));
assert(cst_runtime_jobs_pending(r) && !cst_runtime_current_jobs_pending(r));
s = cst_runtime_take_steering(r); free(s);
assert(!cst_job_cancel(r, id, 1, "user_force"));
s = cst_job_read(r, id, 2000); assert(s && strstr(s, "cancelled") && strstr(s, "user_force")); free(s);
assert(!cst_runtime_finish(r, "partial", "explicit partial", 0, error, sizeof(error)));
assert(!cst_runtime_finish(r, "completed", "must not overwrite partial", 0, error, sizeof(error)));
s = cst_runtime_json(r); assert(s && strstr(s, "explicit partial")); free(s);
/* Pause sticks across reopen; begin() clears it so a new turn can run. */
assert(!cst_runtime_controls(r, 0, 1, 1));
cst_runtime_close(r);
r = cst_runtime_open(path); assert(r);
assert(!cst_runtime_check(r, 0, 1, 1));
s = cst_job_read(r, id, 0); assert(s && strstr(s, "cancelled")); free(s);
unsigned long next = cst_runtime_begin(r, "after pause");
assert(cst_runtime_check(r, next, 1, 1));
assert(!cst_job_start(r, "shell", "", 1, fail_then_note_cancel, NULL, NULL, id,
sizeof(id)));
s = cst_job_read(r, id, 2000);
assert(s && strstr(s, "\"status\":\"failed\"") && strstr(s, "\"exit_code\":7"));
free(s);
cst_runtime_close(r);
write_text(dir, "session.task-v2.json", "{\"revision\":7,\"sequence\":1,\"status\":\"running\",\"jobs\":[{\"job_id\":\"job-7-1\",\"status\":\"running\",\"kind\":\"external\"}]}");
r = cst_runtime_open(path); assert(r && !cst_runtime_jobs_pending(r));
s = cst_job_read(r, "job-7-1", 0); assert(s && strstr(s, "interrupted")); free(s);
cst_runtime_begin(r, "resource locks");
pthread_mutex_lock(&probe_mu); active = peak = 0; pthread_mutex_unlock(&probe_mu);
char second[64];
assert(!cst_job_start(r, "shell", "same-resource", 1, locked_work, NULL, NULL, id, sizeof(id)));
assert(!cst_job_start(r, "shell", "same-resource", 1, locked_work, NULL, NULL, second, sizeof(second)));
s = cst_job_read(r, id, 2000); free(s); s = cst_job_read(r, second, 2000); free(s);
assert(peak == 1);
char first[64] = "";
for (int i = 0; i < 66; i++) {
assert(!cst_job_start(r, "read", "", 1, instant, NULL, NULL, id, sizeof(id)));
if (!i) snprintf(first, sizeof(first), "%s", id);
s = cst_job_read(r, id, 2000); assert(s && strstr(s, "completed")); free(s);
}
s = cst_job_read(r, first, 0); assert(s && strstr(s, "archived output")); free(s);
cst_runtime_close(r);
assert(cst_worktree_path_allowed("[\"src\"]", "src/core.c"));
assert(!cst_worktree_path_allowed("[\"src\"]", "other.c"));
assert(!cst_worktree_path_allowed("[\".\"]", "../escape"));
s = shell(dir, "git init -q && git config user.name Test && git config user.email test@localhost"); free(s);
write_text(dir, "file.txt", "first\nmiddle\nlast\n");
s = shell(dir, "git add file.txt && git commit -qm initial"); free(s);
write_text(dir, "file.txt", "user staged\nmiddle\nlast\n");
s = shell(dir, "git add file.txt"); free(s);
char *index_before = shell(dir, "git write-tree");
char wt[4096];
assert(!cst_worktree_create(dir, "[\"file.txt\"]", wt, sizeof(wt), error, sizeof(error)));
write_text(wt, "file.txt", "user staged\nmiddle\nworker edit\n");
write_text(dir, "file.txt", "user newer\nmiddle\nlast\n");
int code;
s = cst_worktree_diff(dir, wt, 0, &code); assert(!code && strstr(s, "worker edit")); free(s);
s = cst_worktree_diff(dir, wt, 1, &code); assert(!code); free(s);
s = shell(dir, "git write-tree"); assert(!strcmp(s, index_before)); free(s); free(index_before);
s = shell(dir, "git diff -- file.txt"); assert(strstr(s, "user newer") && strstr(s, "worker edit")); free(s);
/* Conflicting integration must preserve the user's bytes. */
assert(!cst_worktree_create(dir, "[\"file.txt\"]", wt, sizeof(wt), error, sizeof(error)));
write_text(wt, "file.txt", "worker conflict\nmiddle\nworker edit\n");
write_text(dir, "file.txt", "user conflict\nmiddle\nworker edit\n");
s = cst_worktree_diff(dir, wt, 1, &code); assert(code && strstr(s, "conflict")); free(s);
s = shell(dir, "git diff -- file.txt"); assert(strstr(s, "user conflict") && !strstr(s, "<<<<<<<")); free(s);
test_rmrf(dir);
puts("test_runtime: OK"); return 0;
}