/* Undo snapshots, persistent permission rules and session store helpers. */ #define _POSIX_C_SOURCE 200809L #include "cst_agent.h" #include "cst_perms.h" #include "cst_undo.h" #include #include #include #include #include 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[256]; static void wfile(const char *rel, const char *s) { char p[512]; snprintf(p, sizeof(p), "%s/%s", g_ws, rel); FILE *fp = fopen(p, "wb"); if (fp) { fputs(s, fp); fclose(fp); } } static int rfile_eq(const char *rel, const char *want) { char p[512], buf[256] = {0}; snprintf(p, sizeof(p), "%s/%s", g_ws, rel); FILE *fp = fopen(p, "rb"); if (!fp) return want == NULL; size_t n = fread(buf, 1, sizeof(buf) - 1, fp); fclose(fp); buf[n] = 0; return want && strcmp(buf, want) == 0; } static void test_undo(void) { const char *sid = "s1"; wfile("a.txt", "v1\n"); EXPECT(cst_undo_count(g_ws, sid) == 0, "no undo yet"); /* turn 1: edit a.txt twice, create new.txt */ EXPECT(cst_undo_snapshot(g_ws, sid, "t1", "a.txt") == 0, "snap a"); wfile("a.txt", "v2\n"); EXPECT(cst_undo_snapshot(g_ws, sid, "t1", "a.txt") == 0, "snap a again"); wfile("a.txt", "v3\n"); EXPECT(cst_undo_snapshot(g_ws, sid, "t1", "new.txt") == 0, "snap absent"); wfile("new.txt", "hello\n"); EXPECT(cst_undo_count(g_ws, sid) == 1, "one turn"); /* turn 2: edit a.txt again */ EXPECT(cst_undo_snapshot(g_ws, sid, "t2", "a.txt") == 0, "snap t2"); wfile("a.txt", "v4\n"); EXPECT(cst_undo_count(g_ws, sid) == 2, "two turns"); char rep[1024]; EXPECT(cst_undo_restore_last(g_ws, sid, rep, sizeof(rep)) == 0, "undo t2"); EXPECT(rfile_eq("a.txt", "v3\n"), "a back to v3"); EXPECT(rfile_eq("new.txt", "hello\n"), "new.txt kept by t2 undo"); EXPECT(strstr(rep, "a.txt") != NULL, "report names a.txt"); EXPECT(cst_undo_restore_last(g_ws, sid, rep, sizeof(rep)) == 0, "undo t1"); EXPECT(rfile_eq("a.txt", "v1\n"), "a back to v1 (first snapshot wins)"); EXPECT(rfile_eq("new.txt", NULL), "new.txt removed"); EXPECT(cst_undo_count(g_ws, sid) == 0, "nothing left"); EXPECT(cst_undo_restore_last(g_ws, sid, rep, sizeof(rep)) != 0, "undo with nothing fails"); /* subdirectory file that did not exist, parent dirs created later */ 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); snprintf(p, sizeof(p), "%s/d/e", g_ws); 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"); /* escaping paths are refused */ EXPECT(cst_undo_snapshot(g_ws, sid, "t4", "../evil") != 0, "reject .."); EXPECT(cst_undo_snapshot(g_ws, sid, "t4", "/etc/passwd") != 0, "reject absolute"); } static void test_perms(void) { cst_perms_t *p = cst_perms_open(g_ws); EXPECT(p != NULL, "open perms"); EXPECT(!cst_perms_tool_allowed(p, "write_file"), "empty rules"); EXPECT(cst_perms_allow_tool(p, "write_file") == 0, "allow tool"); EXPECT(cst_perms_allow_prefix(p, "git status") == 0, "allow prefix"); cst_perms_close(p); p = cst_perms_open(g_ws); EXPECT(cst_perms_tool_allowed(p, "write_file"), "tool rule persisted"); EXPECT(!cst_perms_tool_allowed(p, "delete_file"), "other tool not allowed"); EXPECT(cst_perms_shell_allowed(p, "git status"), "exact prefix"); EXPECT(cst_perms_shell_allowed(p, "git status -s"), "prefix + args"); EXPECT(!cst_perms_shell_allowed(p, "git statusx"), "word boundary"); EXPECT(!cst_perms_shell_allowed(p, "git status; rm -rf ~"), "meta ;"); EXPECT(!cst_perms_shell_allowed(p, "git status && make"), "meta &&"); EXPECT(!cst_perms_shell_allowed(p, "git status $(id)"), "meta $("); EXPECT(!cst_perms_shell_allowed(p, "git status > x"), "meta >"); EXPECT(!cst_perms_shell_allowed(p, "git status\nrm x"), "newline"); char *d = cst_perms_describe(p); EXPECT(d && strstr(d, "git status") && strstr(d, "write_file"), "describe"); free(d); EXPECT(cst_perms_reset(p) == 0, "reset"); EXPECT(!cst_perms_tool_allowed(p, "write_file"), "reset clears tools"); EXPECT(!cst_perms_shell_allowed(p, "git status"), "reset clears prefixes"); cst_perms_close(p); char out[128]; cst_perms_suggest_prefix("git commit -m 'x'", out, sizeof(out)); EXPECT(strcmp(out, "git commit") == 0, "git two words"); cst_perms_suggest_prefix(" ls -la src", out, sizeof(out)); EXPECT(strcmp(out, "ls") == 0, "ls one word"); cst_perms_suggest_prefix("make -j8", out, sizeof(out)); EXPECT(strcmp(out, "make") == 0, "make"); cst_perms_suggest_prefix("rm -rf build", out, sizeof(out)); EXPECT(out[0] == 0, "no rule for rm"); cst_perms_suggest_prefix("sudo make install", out, sizeof(out)); EXPECT(out[0] == 0, "no rule for sudo"); cst_perms_suggest_prefix("python3 -c 'print(1)'", out, sizeof(out)); EXPECT(out[0] == 0, "no rule for python3 -c"); cst_perms_suggest_prefix("python3 -m pytest", out, sizeof(out)); EXPECT(out[0] == 0, "no rule for python3 -m"); cst_perms_suggest_prefix("npm test", out, sizeof(out)); EXPECT(strcmp(out, "npm test") == 0, "npm test"); } static void test_store(void) { EXPECT(cst_agent_store_ensure(g_ws) == 0, "store ensure"); char path[1024]; EXPECT(cst_agent_store_path_for_id(g_ws, "20260101-000000-abcd", path, sizeof(path)) == 0, "path for id"); FILE *fp = fopen(path, "w"); EXPECT(fp != NULL, "create session file"); if (fp) { fputs("{\"role\":\"system\",\"content\":\"sys\"}\n", fp); fputs("{\"role\":\"user\",\"content\":\"fix the\\nbuild\"}\n", fp); fputs("{\"role\":\"assistant\",\"content\":\"ok\"}\n", fp); fclose(fp); } cst_agent_store_set_current(g_ws, "20260101-000000-abcd"); cst_session_info_t in; EXPECT(cst_agent_store_info(g_ws, "20260101-000000-abcd", &in) == 0, "info"); EXPECT(in.messages == 3, "message count"); EXPECT(strcmp(in.first_user, "fix the build") == 0, "first user line"); EXPECT(in.title[0] == 0, "no title yet"); EXPECT(cst_agent_store_title_set(g_ws, "20260101-000000-abcd", "Build fix") == 0, "set title"); cst_agent_store_info(g_ws, "20260101-000000-abcd", &in); EXPECT(strcmp(in.title, "Build fix") == 0, "title read back"); EXPECT(cst_agent_store_title_set(g_ws, "../x", "bad") != 0, "bad id"); EXPECT(cst_agent_store_delete(g_ws, "20260101-000000-abcd") == 0, "delete"); EXPECT(access(path, F_OK) != 0, "session file gone"); char cur[128]; EXPECT(cst_agent_store_get_current(g_ws, cur, sizeof(cur)) != 0, "CURRENT cleared"); } int main(void) { snprintf(g_ws, sizeof(g_ws), "/tmp/cst_test_undo_XXXXXX"); if (!mkdtemp(g_ws)) { 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) fprintf(stderr, "warning: cleanup failed\n"); if (nfail) { fprintf(stderr, "test_undo: %d failure(s)\n", nfail); return 1; } printf("test_undo: ok\n"); return 0; }