feat: add v2 task control and asynchronous isolated workers
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
#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;
|
||||
}
|
||||
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);
|
||||
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);
|
||||
assert(!cst_runtime_controls(r, 1, 0, 1));
|
||||
cst_runtime_close(r);
|
||||
r = cst_runtime_open(path); assert(r);
|
||||
assert(!cst_runtime_check(r, 0, 0, 0));
|
||||
s = cst_job_read(r, id, 0); assert(s && strstr(s, "cancelled")); 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;
|
||||
}
|
||||
+19
-3
@@ -388,6 +388,22 @@ int main(void) {
|
||||
"read. Custom.\n");
|
||||
put(root, "ws/.agent/sessions/.keep", "");
|
||||
|
||||
/* Machine defaults apply outside configured workspaces; local config wins. */
|
||||
put(root, "cst-agent/workers.json", "{\"max_steps\":13}");
|
||||
char *worker_defaults = cst_workers_config_get(ws);
|
||||
CHECK(worker_defaults && strstr(worker_defaults, "13"), "user worker defaults");
|
||||
free(worker_defaults);
|
||||
put(root, "ws/.agent/workers.json", "{\"max_steps\":17}");
|
||||
worker_defaults = cst_workers_config_get(ws);
|
||||
CHECK(worker_defaults && strstr(worker_defaults, "17") && !strstr(worker_defaults, "13"),
|
||||
"workspace configuration replaces user defaults");
|
||||
free(worker_defaults);
|
||||
char defaults_path[1024];
|
||||
snprintf(defaults_path, sizeof(defaults_path), "%s/ws/.agent/workers.json", root);
|
||||
CHECK(unlink(defaults_path) == 0, "remove temporary workspace defaults");
|
||||
snprintf(defaults_path, sizeof(defaults_path), "%s/cst-agent/workers.json", root);
|
||||
CHECK(unlink(defaults_path) == 0, "remove temporary user defaults");
|
||||
|
||||
/* roles */
|
||||
cst_agent_role_t roles[CST_ROLES_MAX];
|
||||
int nr = cst_roles_list(ws, roles, CST_ROLES_MAX);
|
||||
@@ -602,9 +618,9 @@ int main(void) {
|
||||
"delegation cap rejects without HTTP request");
|
||||
cst_tool_result_free(&r);
|
||||
cst_agent_config_mut(a)->orchestrate = 1;
|
||||
CHECK(!cst_tool_active(cst_agent_config(a), cst_tool_find("write_file")) &&
|
||||
!cst_tool_active(cst_agent_config(a), cst_tool_find("run_shell")) &&
|
||||
cst_tool_active(cst_agent_config(a), td), "coordinator delegates mutations");
|
||||
CHECK(cst_tool_active(cst_agent_config(a), cst_tool_find("write_file")) &&
|
||||
cst_tool_active(cst_agent_config(a), cst_tool_find("run_shell")) &&
|
||||
cst_tool_active(cst_agent_config(a), td), "coordinator retains authorized direct tools");
|
||||
CHECK(cst_workers_config_set(ws,
|
||||
"{\"max_steps\":20,\"max_tasks\":8,\"max_requests\":3}", err, sizeof(err)) == 0,
|
||||
"configure small concurrent budget");
|
||||
|
||||
Reference in New Issue
Block a user