#define _POSIX_C_SOURCE 200809L #include "cJSON.h" #include "cst_agent.h" #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 const char *TRICKY = "tab\there \"q\" back\\slash\nnew line \x01 中文"; static const char *TC = "[{\"id\":\"c1\",\"type\":\"function\",\"function\":{" "\"name\":\"read_file\",\"arguments\":" "\"{\\\"path\\\":\\\"a.c\\\"}\"}}]"; /* compare JSON texts structurally */ static int json_eq(const char *a, const char *b) { cJSON *ja = cJSON_Parse(a), *jb = cJSON_Parse(b); int eq = ja && jb && cJSON_Compare(ja, jb, 1); cJSON_Delete(ja); cJSON_Delete(jb); return eq; } static void append(cst_session_t *s, cst_role_t role, const char *content, const char *tid, const char *tname, const char *tc, const char *meta) { cst_message_t m; memset(&m, 0, sizeof(m)); m.role = role; m.content = (char *)content; if (tid) snprintf(m.tool_call_id, sizeof(m.tool_call_id), "%s", tid); if (tname) snprintf(m.tool_name, sizeof(m.tool_name), "%s", tname); m.tool_calls_json = (char *)tc; m.meta_json = (char *)meta; EXPECT(cst_session_append(s, &m) == 0, "append"); } static void check_messages(cst_session_t *s) { EXPECT(cst_session_count(s) == 5, "5 messages"); const cst_message_t *m = cst_session_get(s, 0); EXPECT(m && m->role == CST_ROLE_SYSTEM, "system role"); m = cst_session_get(s, 1); EXPECT(m && m->role == CST_ROLE_USER && strcmp(m->content, TRICKY) == 0, "tricky user text exact"); m = cst_session_get(s, 2); EXPECT(m && m->role == CST_ROLE_ASSISTANT, "assistant role"); EXPECT(m && m->tool_calls_json && json_eq(m->tool_calls_json, TC), "tool_calls preserved"); EXPECT(m && m->meta_json && json_eq(m->meta_json, "{\"protocol\":\"native-eos\"}"), "assistant meta"); m = cst_session_get(s, 3); EXPECT(m && m->role == CST_ROLE_TOOL && strcmp(m->tool_call_id, "c1") == 0 && strcmp(m->tool_name, "read_file") == 0, "tool id/name"); EXPECT(m && strcmp(m->content, "int a;\n\tx\n") == 0, "tool content"); m = cst_session_get(s, 4); EXPECT(m && m->role == CST_ROLE_USER && strcmp(m->tool_call_id, "c2") == 0, "text-protocol result frame keeps tool_call_id"); EXPECT(m && m->meta_json && json_eq(m->meta_json, "{\"kind\":\"tool_result\",\"protocol\":\"ctl\"}"), "result frame meta"); } int main(void) { char path[256]; snprintf(path, sizeof(path), "/tmp/cst-session-test-%d.jsonl", (int)getpid()); remove(path); cst_session_t *s = cst_session_open(path); EXPECT(s != NULL, "open new"); if (!s) return 1; append(s, CST_ROLE_SYSTEM, "sys", NULL, NULL, NULL, NULL); append(s, CST_ROLE_USER, TRICKY, NULL, NULL, NULL, NULL); append(s, CST_ROLE_ASSISTANT, "", NULL, NULL, TC, "{\"protocol\":\"native-eos\"}"); append(s, CST_ROLE_TOOL, "int a;\n\tx\n", "c1", "read_file", NULL, NULL); append(s, CST_ROLE_USER, "result c2 exit=0 2\nok\nend", "c2", "run_shell", NULL, "{\"kind\":\"tool_result\",\"protocol\":\"ctl\"}"); check_messages(s); cst_session_close(s); /* reload from disk */ s = cst_session_open(path); EXPECT(s != NULL, "reopen"); if (!s) return 1; check_messages(s); /* every line on disk is standalone valid JSON with tool_calls as array */ FILE *fp = fopen(path, "r"); char line[8192]; int lines = 0, arrays = 0; while (fp && fgets(line, sizeof(line), fp)) { cJSON *j = cJSON_Parse(line); EXPECT(j != NULL, "jsonl line parses"); if (cJSON_IsArray(cJSON_GetObjectItemCaseSensitive(j, "tool_calls"))) arrays++; cJSON_Delete(j); lines++; } if (fp) fclose(fp); EXPECT(lines == 5 && arrays == 1, "5 lines, tool_calls stored as array"); /* rewrite + clear(keep_system) */ const cst_message_t *first = cst_session_get(s, 0); cst_message_t keep[2]; keep[0] = *first; keep[1] = *cst_session_get(s, 1); EXPECT(cst_session_rewrite(s, keep, 2) == 0, "rewrite"); EXPECT(cst_session_count(s) == 2, "rewrite count"); EXPECT(cst_session_clear(s, 1) == 0 && cst_session_count(s) == 1, "clear keeps system"); cst_session_close(s); /* legacy format: tool_calls_json stored as a string */ fp = fopen(path, "w"); fprintf(fp, "{\"role\":\"assistant\",\"content\":\"\",\"tool_calls_json\":" "\"[{\\\"id\\\":\\\"c9\\\",\\\"type\\\":\\\"function\\\"," "\\\"function\\\":{\\\"name\\\":\\\"run_shell\\\"," "\\\"arguments\\\":\\\"{}\\\"}}]\"}\n"); fprintf(fp, "not json at all\n"); fprintf(fp, "{\"role\":\"tool\",\"content\":\"out\",\"tool_call_id\":\"c9\"}\n"); fclose(fp); s = cst_session_open(path); EXPECT(s != NULL, "open legacy"); if (s) { EXPECT(cst_session_count(s) == 2, "legacy: bad line skipped"); const cst_message_t *m = cst_session_get(s, 0); EXPECT(m && m->tool_calls_json && strstr(m->tool_calls_json, "c9"), "legacy tool_calls_json read"); cst_session_close(s); } remove(path); if (nfail) { fprintf(stderr, "test_session_roundtrip: %d failed\n", nfail); return 1; } printf("test_session_roundtrip: OK\n"); return 0; }