/* test_agent.c - agent 上下文管理 (不涉及网络) */ #include "agent.h" #include "json.h" #include "util.h" #include #include #include static int failures = 0; static int checks = 0; #define CHECK(cond, ...) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ printf(" ✗ %s:%d: ", __FILE__, __LINE__); \ printf(__VA_ARGS__); \ printf("\n"); \ } \ } while (0) static size_t count_role(const Agent *a, const char *role) { size_t n = 0; for (size_t i = 0; i < json_array_size(a->messages); i++) { const char *r = json_get_string(json_array_get(a->messages, i), "role"); if (r && strcmp(r, role) == 0) n++; } return n; } /* ------------------------------------------------------------------ */ /* 上下文超限的自动恢复 */ /* ------------------------------------------------------------------ */ static void add_msg(Agent *a, const char *role, const char *content) { JsonValue *m = json_new_object(); json_object_set_string(m, "role", role); json_object_set_string(m, "content", content); json_array_push(a->messages, m); } /* 造一条 assistant 带 tool_calls 的消息 */ static void add_assistant_tool_call(Agent *a, const char *id, const char *name) { JsonValue *m = json_new_object(); json_object_set_string(m, "role", "assistant"); json_object_set_null(m, "content"); JsonValue *tcs = json_new_array(); JsonValue *tc = json_new_object(); json_object_set_string(tc, "id", id); json_object_set_string(tc, "type", "function"); JsonValue *fn = json_new_object(); json_object_set_string(fn, "name", name); json_object_set_string(fn, "arguments", "{}"); json_object_set(tc, "function", fn); json_array_push(tcs, tc); json_object_set(m, "tool_calls", tcs); json_array_push(a->messages, m); } static void add_tool_result(Agent *a, const char *id, const char *content) { JsonValue *m = json_new_object(); json_object_set_string(m, "role", "tool"); json_object_set_string(m, "tool_call_id", id); json_object_set_string(m, "content", content); json_array_push(a->messages, m); } /* 序列化历史, 便于断言 */ static char *dump_history(const Agent *a) { return json_dump(a->messages); } /* 配对不变量:任何 assistant(tool_calls) 后面必须**紧跟**同 id 的 tool 消息. * 服务端就是这么要求的, 违反了下一轮直接 400 —— 所以每次动过历史之后都要查. */ static void check_pairing(Agent *a) { size_t n = json_array_size(a->messages); for (size_t i = 0; i < n; i++) { JsonValue *m = json_array_get(a->messages, i); JsonValue *tcs = json_object_get(m, "tool_calls"); if (!tcs) continue; const char *id = json_get_string(json_array_get(tcs, 0), "id"); CHECK(i + 1 < n, "tool_calls 之后必须有消息"); if (i + 1 < n) { JsonValue *nxt = json_array_get(a->messages, i + 1); const char *nid = json_get_string(nxt, "tool_call_id"); CHECK(nid && id && strcmp(nid, id) == 0, "tool 结果必须紧跟其 tool_call (第 %zu 条)", i); } } } /* 一轮一轮摘下去 (/back 走的就是这条路), 历史在每一步都必须仍然自洽. * * 这里原来测的是"上下文超限自动裁剪" (agent_drop_oldest_round)—— 那套机制 * 已经**整个删掉**:它的判据只是错误文案里的关键词, 却会不可逆地删掉最老的 * 几轮对话. 现在超限就是如实报错、历史一个字节都不动, 怎么处理由用户决定 * (/back 摘一轮、/reset 重来). 但"摘掉一整轮之后 tool 配对不能散"这条 * 不变量与由谁触发无关, 所以留着继续测. */ static void test_take_rounds_keeps_pairing(void) { puts("逐轮摘掉之后历史仍然自洽"); Agent a; agent_init(&a); agent_add_system(&a, "SYS"); for (int i = 1; i <= 4; i++) { char buf[32]; snprintf(buf, sizeof buf, "U%d", i); add_msg(&a, "user", buf); add_assistant_tool_call(&a, "call_x", "read_file"); add_tool_result(&a, "call_x", "R"); add_msg(&a, "assistant", "A"); } int rounds = 0; for (;;) { JsonValue *r = agent_take_last_round(&a); if (!r) break; json_free(r); rounds++; check_pairing(&a); /* 每摘一轮都查一遍配对 */ } CHECK(rounds == 4, "共应摘掉 4 轮, 实际 %d", rounds); CHECK(agent_history_len(&a) == 1, "最后应只剩 system, 实际 %zu", agent_history_len(&a)); agent_free(&a); } static void test_take_last_round(void) { puts("摘掉最近一轮 (/back 用)"); Agent a; agent_init(&a); agent_add_system(&a, "SYS"); add_msg(&a, "user", "U1"); add_assistant_tool_call(&a, "call_1", "run_command"); add_tool_result(&a, "call_1", "R1"); add_msg(&a, "assistant", "A1"); add_msg(&a, "user", "U2"); add_msg(&a, "assistant", "A2"); CHECK(agent_history_len(&a) == 7, "初始 7 条, 实际 %zu", agent_history_len(&a)); JsonValue *r = agent_take_last_round(&a); CHECK(r != NULL, "应摘到一轮"); CHECK(r && json_array_size(r) == 2, "最后一轮应 2 条, 实际 %zu", r ? json_array_size(r) : 0); CHECK(agent_history_len(&a) == 5, "摘完应剩 5 条, 实际 %zu", agent_history_len(&a)); char *h = dump_history(&a); CHECK(strstr(h, "U2") == NULL, "U2 应被摘掉"); CHECK(strstr(h, "U1") != NULL, "U1 应保留"); free(h); json_free(r); /* 关键:工具调用与结果必须整组一起走 */ r = agent_take_last_round(&a); CHECK(r && json_array_size(r) == 4, "第一轮应 4 条, 实际 %zu", r ? json_array_size(r) : 0); h = dump_history(&a); CHECK(strstr(h, "call_1") == NULL && strstr(h, "R1") == NULL, "工具调用与结果应一并摘掉, 不能只删一半"); CHECK(strstr(h, "SYS") != NULL, "system 必须保留"); free(h); json_free(r); /* 只剩 system 时无可摘 */ r = agent_take_last_round(&a); CHECK(r == NULL, "没有用户轮次时应返回 NULL"); json_free(r); CHECK(agent_history_len(&a) == 1, "历史应只剩 system"); agent_free(&a); /* 没有 user 消息的历史 */ Agent b; agent_init(&b); add_msg(&b, "assistant", "只有助手"); CHECK(agent_take_last_round(&b) == NULL, "没有 user 时不应摘任何东西"); CHECK(agent_history_len(&b) == 1, "历史不应被改动"); agent_free(&b); } /* ------------------------------------------------------------------ */ /* 结构体检:assistant(tool_calls) 与 tool 消息的配对 */ /* ------------------------------------------------------------------ */ /* 造一条 assistant, 带 count 个工具调用 (id 依次是 c0, c1, ...) */ static JsonValue *mk_assistant(size_t count) { JsonValue *m = json_new_object(); json_object_set_string(m, "role", "assistant"); JsonValue *tcs = json_new_array(); for (size_t i = 0; i < count; i++) { char id[16]; snprintf(id, sizeof id, "c%zu", i); JsonValue *tc = json_new_object(); json_object_set_string(tc, "id", id); json_object_set_string(tc, "type", "function"); JsonValue *fn = json_new_object(); json_object_set_string(fn, "name", "run_command"); json_object_set_string(fn, "arguments", "{}"); json_object_set(tc, "function", fn); json_array_push(tcs, tc); } json_object_set(m, "tool_calls", tcs); return m; } static JsonValue *mk_tool(const char *id) { JsonValue *m = json_new_object(); json_object_set_string(m, "role", "tool"); json_object_set_string(m, "tool_call_id", id); json_object_set_string(m, "content", "ok"); return m; } static JsonValue *mk_plain(const char *role, const char *text) { JsonValue *m = json_new_object(); json_object_set_string(m, "role", role); json_object_set_string(m, "content", text); return m; } /* 数一数某个 assistant 之后紧跟的 tool 消息覆盖了几个 id */ static const char *nth_role(const JsonValue *msgs, size_t i) { return json_get_string(json_array_get((JsonValue *)msgs, i), "role"); } static int pairing_ok(const JsonValue *msgs) { size_t n = json_array_size((JsonValue *)msgs); for (size_t i = 0; i < n; i++) { JsonValue *m = json_array_get((JsonValue *)msgs, i); const char *r = json_get_string(m, "role"); if (!r || strcmp(r, "assistant") != 0) continue; JsonValue *tcs = json_object_get(m, "tool_calls"); if (!tcs || json_array_size(tcs) == 0) continue; for (size_t k = 0; k < json_array_size(tcs); k++) { const char *id = json_get_string(json_array_get(tcs, k), "id"); size_t j = i + 1; int found = 0; for (; j < n && strcmp(nth_role(msgs, j), "tool") == 0; j++) if (strcmp(json_get_string(json_array_get((JsonValue *)msgs, j), "tool_call_id"), id) == 0) found = 1; if (!found) return 0; } } return 1; } static void test_repair_pairing(void) { int fixed = 0; puts("结构体检:本来没问题就不该动它"); JsonValue *ok = json_new_array(); json_array_push(ok, mk_plain("user", "跑一下")); json_array_push(ok, mk_assistant(2)); json_array_push(ok, mk_tool("c0")); json_array_push(ok, mk_tool("c1")); json_array_push(ok, mk_plain("assistant", "做完了")); CHECK(pairing_ok(ok), "构造出来的历史本身应是合法的"); CHECK(agent_repair_tool_pairing(ok, &fixed) == NULL, "合法历史不该被重建 (NULL = 什么都没改)"); CHECK(fixed == 0, "合法历史的修复处数应为 0"); json_free(ok); puts("缺 tool 消息:补齐, 且补在原位"); /* 复现真实报错:assistant 带两个调用, 中间夹了一条 user (图省事直接 * 模仿当年那个 bug 的形状), 第二个调用没人回应 */ JsonValue *bad = json_new_array(); json_array_push(bad, mk_plain("user", "看看图")); json_array_push(bad, mk_assistant(2)); json_array_push(bad, mk_tool("c0")); json_array_push(bad, mk_plain("user", "图片附件")); json_array_push(bad, mk_tool("c1")); fixed = 0; JsonValue *r = agent_repair_tool_pairing(bad, &fixed); CHECK(r != NULL, "对不上时应该返回重建好的数组"); if (r) { CHECK(pairing_ok(r), "修完之后配对必须成立"); /* 两处:给 c1 补一条占位 (它没被回应), 再把错位的那条 c1 结果丢掉 * (它被 user 消息隔开, 已经不在 tool 段里了 —— 留着就是孤儿). * 丢掉真实结果听着可惜, 但结构坏了只能二选一:要么丢它, 要么留一段 * 服务端必然拒绝的历史. 模型会从占位消息里知道该重跑. */ CHECK(fixed == 2, "应补 1 处、丢 1 处孤儿, 实际 %d", fixed); CHECK(json_array_size(r) == 5, "补一条、丢一条之后仍是 5 条, 实际 %zu", json_array_size(r)); /* 占位消息必须紧跟在 assistant 的 tool 段里, 而不是被塞到最后 */ CHECK(strcmp(nth_role(r, 2), "tool") == 0 && strcmp(nth_role(r, 3), "tool") == 0, "两条 tool 消息应紧跟在 assistant 之后"); const char *c = json_get_string(json_array_get(r, 3), "content"); CHECK(c && strstr(c, "没有执行") == NULL && strstr(c, "没有得到结果") != NULL, "占位消息要说明它没拿到结果, 实际 %s", c ? c : "(null)"); json_free(r); } json_free(bad); puts("末尾悬空:assistant 之后再没有消息"); JsonValue *tail = json_new_array(); json_array_push(tail, mk_plain("user", "跑一下")); json_array_push(tail, mk_assistant(1)); fixed = 0; r = agent_repair_tool_pairing(tail, &fixed); CHECK(r && pairing_ok(r) && fixed == 1, "结尾缺的也要补上 (fixed=%d)", fixed); json_free(r); json_free(tail); puts("孤儿 tool 消息:没有对应的 tool_calls, 要丢掉"); JsonValue *orphan = json_new_array(); json_array_push(orphan, mk_plain("user", "你好")); json_array_push(orphan, mk_tool("谁也不是")); JsonValue *r2 = agent_repair_tool_pairing(orphan, &fixed); CHECK(fixed == 1, "孤儿 tool 消息应被计为 1 处, 实际 %d", fixed); CHECK(r2 != NULL && json_array_size(r2) == 1, "孤儿应被丢掉, 只剩 user"); json_free(r2); json_free(orphan); puts("空输入与边界"); fixed = 7; CHECK(agent_repair_tool_pairing(NULL, &fixed) == NULL && fixed == 0, "NULL 输入"); JsonValue *empty = json_new_array(); CHECK(agent_repair_tool_pairing(empty, &fixed) == NULL, "空历史没什么可修"); json_free(empty); /* out_fixed 可以为 NULL */ JsonValue *b2 = json_new_array(); json_array_push(b2, mk_assistant(1)); JsonValue *r3 = agent_repair_tool_pairing(b2, NULL); json_free(r3); json_free(b2); CHECK(1, "out_fixed 为 NULL 时不崩"); } /* ------------------------------------------------------------------ */ /* 面向用户的历史口径:不含 system */ /* ------------------------------------------------------------------ */ static void test_conversation_len(void) { puts("历史条数:system 是配置, 不该算进对话"); Agent a; agent_init(&a); CHECK(agent_history_len(&a) == 0, "空 agent 的数组长度是 0"); CHECK(agent_conversation_len(&a) == 0, "空 agent 的对话长度也是 0"); /* 只有 system:数组里 1 条, 但对话是空的 */ agent_add_system(&a, "你是助手"); CHECK(agent_history_len(&a) == 1, "只有 system 时数组长度 1"); CHECK(agent_conversation_len(&a) == 0, "只有 system 时**对话**长度必须是 0 (用户报的就是这里)"); add_msg(&a, "user", "hi"); add_msg(&a, "assistant", "你好"); CHECK(agent_history_len(&a) == 3, "system + 一问一答 = 3"); CHECK(agent_conversation_len(&a) == 2, "对话长度 2"); /* 没有 system 的历史 (比如裸构造的)不该被误减 */ Agent b; agent_init(&b); add_msg(&b, "user", "hi"); CHECK(agent_history_len(&b) == 1 && agent_conversation_len(&b) == 1, "没有 system 时两个口径一致"); /* reset 之后回到"只有 system" */ agent_reset(&a); CHECK(agent_history_len(&a) == 1 && agent_conversation_len(&a) == 0, "reset 之后数组剩 system、对话为空"); agent_free(&a); agent_free(&b); } int main(void) { Agent a; agent_init(&a); CHECK(a.messages && a.messages->type == JSON_ARRAY, "messages 应为数组"); agent_add_system(&a, "S1"); CHECK(agent_history_len(&a) == 1, "应只有 1 条 system"); CHECK(count_role(&a, "system") == 1, "system 计数"); agent_add_user(&a, "U1"); CHECK(agent_history_len(&a) == 2, "应 2 条"); /* 关键不变量:重复设置 system 应替换而不是追加 */ agent_add_system(&a, "S2"); CHECK(count_role(&a, "system") == 1, "重复设置 system 不应叠加, 实际 %zu 条", count_role(&a, "system")); CHECK(strcmp(json_get_string(json_array_get(a.messages, 0), "content"), "S2") == 0, "新 system 应在第 0 条"); CHECK(strcmp(json_get_string(json_array_get(a.messages, 1), "content"), "U1") == 0, "原有对话应保留"); agent_add_user(&a, "U2"); CHECK(agent_history_len(&a) == 3, "应 3 条"); agent_reset(&a); CHECK(agent_history_len(&a) == 1, "reset 后只保留 system, 实际 %zu", agent_history_len(&a)); CHECK(count_role(&a, "system") == 1, "reset 后 system 仍在"); CHECK(strcmp(json_get_string(json_array_get(a.messages, 0), "content"), "S2") == 0, "reset 保留的是最新 system"); /* 历史里没有 system 时 reset 应得到空历史 */ Agent b; agent_init(&b); agent_add_user(&b, "only user"); agent_reset(&b); CHECK(agent_history_len(&b) == 0, "无 system 时 reset 应为空, 实际 %zu", agent_history_len(&b)); agent_free(&a); agent_free(&b); test_take_rounds_keeps_pairing(); test_take_last_round(); test_repair_pairing(); test_conversation_len(); printf("\n%d 项检查, %d 项失败\n", checks, failures); return failures ? 1 : 0; }