#define _POSIX_C_SOURCE 200809L #include "cst_runtime.h" #include "cst_worktree.h" #include "cst_test_util.h" #include #include #include #include #include #include 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; }