/* test_meta.c - meta 历史编辑: 轮次切分、结构安全、各工具的行为 */ #include "meta.h" #include "json.h" #include "util.h" #include "agent.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 JsonValue *msg(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); return m; } /* assistant 带一个 tool_call */ static JsonValue *assistant_call(const char *id, const char *name, const char *args) { JsonValue *m = json_new_object(); json_object_set_string(m, "role", "assistant"); json_object_set_string(m, "content", ""); JsonValue *arr = 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", args); json_object_set(tc, "function", fn); json_array_push(arr, tc); json_object_set(m, "tool_calls", arr); return m; } /* 造一段典型历史: * #0 system * #1 user A ← 轮 1 * #2 assistant(tool_calls) * #3 tool * #4 assistant 回答 * #5 user B ← 轮 2 * #6 assistant 回答 * #7 user C ← 轮 3 * #8 assistant 回答 */ static JsonValue *sample(void) { JsonValue *a = json_new_array(); json_array_push(a, msg("system", "你是助手")); json_array_push(a, msg("user", "帮我用 Python 写")); json_array_push(a, assistant_call("call_1", "run_command", "{\"command\":\"ls\"}")); json_array_push(a, msg("tool", "file1\nfile2")); json_array_push(a, msg("assistant", "写好了")); json_array_push(a, msg("user", "改用 C")); json_array_push(a, msg("assistant", "好的, 改成 C")); json_array_push(a, msg("user", "加个注释")); json_array_push(a, msg("assistant", "加好了")); return a; } static void test_rounds(void) { puts("轮次切分"); JsonValue *m = sample(); MetaRounds r; CHECK(meta_rounds_build(m, &r) == 1, "应该能切出轮次"); CHECK(r.head == 1, "system 之后的 #1 才是第一条可编辑消息, 实际 %zu", r.head); CHECK(r.count == 3, "应切出 3 轮, 实际 %zu", r.count); CHECK(r.starts[0] == 1, "轮 1 从 #1 开始, 实际 %zu", r.starts[0]); CHECK(r.starts[1] == 5, "轮 2 从 #5 开始, 实际 %zu", r.starts[1]); CHECK(r.starts[2] == 7, "轮 3 从 #7 开始, 实际 %zu", r.starts[2]); meta_rounds_free(&r); /* 没有 system 也要能切 */ JsonValue *n = json_new_array(); json_array_push(n, msg("user", "x")); json_array_push(n, msg("assistant", "y")); CHECK(meta_rounds_build(n, &r) == 1, "没有 system 也能切"); CHECK(r.head == 0 && r.count == 1, "1 轮, 从头开始"); meta_rounds_free(&r); json_free(n); /* 只有 system: 没有可编辑的东西 */ JsonValue *only = json_new_array(); json_array_push(only, msg("system", "s")); CHECK(meta_rounds_build(only, &r) == 0, "只有 system 时不该切出轮次"); meta_rounds_free(&r); json_free(only); /* read_image 补的那条"只有图片附件、没有文字"的 user 消息**不算新的一轮** —— * 判据必须与主循环 (agent_opens_round)一致, 否则 /meta 报的轮号会和主循环的 * 口径对不上, 模型照轮号删下去, 删掉的不是它以为的那一轮. * 结构: system / user(text) / assistant(tool_calls) / tool / user(只有图) / assistant */ JsonValue *withimg = json_new_array(); json_array_push(withimg, msg("system", "s")); json_array_push(withimg, msg("user", "看图")); json_array_push(withimg, msg("assistant", "ok")); /* 占位, 模拟带 tool_calls 的轮内消息 */ json_array_push(withimg, msg("tool", "结果")); { JsonValue *att = json_new_object(); json_object_set_string(att, "role", "user"); JsonValue *c = json_new_array(); JsonValue *blk = json_new_object(); json_object_set_string(blk, "type", "image_url"); json_array_push(c, blk); json_object_set(att, "content", c); json_array_push(withimg, att); } json_array_push(withimg, msg("assistant", "看到图了")); CHECK(meta_rounds_build(withimg, &r) == 1, "带附件的历史也应该能切"); CHECK(r.count == 1, "只有附件的那条 user 不该算新的一轮, 实际 %zu 轮", r.count); meta_rounds_free(&r); json_free(withimg); json_free(m); } static void test_deletable(void) { puts("结构安全: 哪些消息能单独删"); JsonValue *m = sample(); CHECK(meta_deletable(m, 0) == 0, "system 不可删"); CHECK(meta_deletable(m, 1) == 1, "user 可删"); CHECK(meta_deletable(m, 2) == 0, "带 tool_calls 的 assistant 不可删"); CHECK(meta_deletable(m, 3) == 0, "tool 结果不可删"); CHECK(meta_deletable(m, 4) == 1, "普通 assistant 可删"); CHECK(meta_deletable(m, 99) == 0, "越界不可删"); json_free(m); } /* ---- 工具驱动 ---- */ static char *call(Agent *a, const char *name, const char *json) { char err[256]; JsonValue *args = json_parse(json, err, sizeof err); if (!args) { printf(" (测试自身出错: %s)\n", err); return xstrdup(""); } int is_err = 0; char *r = meta_tools_call(name, args, &is_err); json_free(args); return r; } /* 把一段历史挂上, 跑 body, 再摘下来 */ #define WITH_TOOLS(a) \ do { \ CHECK(meta_tools_attach(a) == 0, "attach 应成功"); \ CHECK(meta_tools_attach(a) != 0, "重复 attach 应被拒绝"); \ } while (0) static void test_replace(void) { puts("replace_message"); Agent a; agent_init(&a); json_free(a.messages); a.messages = sample(); meta_tools_attach(&a); int err = 0; char *r = call(&a, "replace_message", "{\"index\":1,\"content\":\"帮我用 C 写\"}"); CHECK(err == 0 && r && strstr(r, "已改写") != NULL, "应改写成功: %s", r ? r : ""); JsonValue *m1 = json_array_get(a.messages, 1); CHECK(strcmp(json_get_string(m1, "content"), "帮我用 C 写") == 0, "内容应被替换"); CHECK(strcmp(json_get_string(m1, "role"), "user") == 0, "角色不该变"); free(r); /* 改 assistant 说的话 —— 这是 meta 存在的理由 */ r = call(&a, "replace_message", "{\"index\":4,\"content\":\"改好了\"}"); CHECK(r && strstr(r, "已改写") != NULL, "应能改写 assistant 消息"); CHECK(strcmp(json_get_string(json_array_get(a.messages, 4), "content"), "改好了") == 0, "assistant 内容应被替换"); free(r); /* tool 结果和 system 都不该被动 */ r = call(&a, "replace_message", "{\"index\":3,\"content\":\"x\"}"); CHECK(r && strstr(r, "tool 结果") != NULL, "改 tool 结果应被拒绝: %s", r ? r : ""); free(r); r = call(&a, "replace_message", "{\"index\":0,\"content\":\"x\"}"); CHECK(r && strstr(r, "system") != NULL, "改 system 应被拒绝"); free(r); /* 越界 */ r = call(&a, "replace_message", "{\"index\":99,\"content\":\"x\"}"); CHECK(r && strstr(r, "越界") != NULL, "越界应报错"); free(r); meta_tools_detach(); agent_free(&a); } static void test_delete_safety(void) { puts("delete_messages 的结构安全"); Agent a; agent_init(&a); json_free(a.messages); a.messages = sample(); meta_tools_attach(&a); size_t before = json_array_size(a.messages); char *r = call(&a, "delete_messages", "{\"indices\":[2]}"); CHECK(r && strstr(r, "不可拆") != NULL, "带 tool_calls 的 assistant 应被拒绝"); CHECK(json_array_size(a.messages) == before, "被拒绝时不该改动任何东西"); free(r); r = call(&a, "delete_messages", "{\"indices\":[3]}"); CHECK(r && strstr(r, "不可拆") != NULL, "tool 结果应被拒绝"); CHECK(json_array_size(a.messages) == before, "仍不该改动"); free(r); /* 一条合法一条非法: 整批都不该动 (先校验后执行) */ r = call(&a, "delete_messages", "{\"indices\":[1,2]}"); CHECK(r && strstr(r, "不可拆") != NULL, "混了非法项应整体拒绝"); CHECK(json_array_size(a.messages) == before, "整体拒绝时不该改一半"); free(r); /* 合法删除: 删掉第 1 轮里那句 user, 并给上下文提示 */ r = call(&a, "delete_messages", "{\"indices\":[1]}"); CHECK(r && strstr(r, "已删除 1 条") != NULL, "合法删除应成功: %s", r ? r : ""); CHECK(strstr(r, "没有对应的提问") != NULL, "删掉提问后应提示后续回答失去上下文"); CHECK(json_array_size(a.messages) == before - 1, "条数应减一"); free(r); /* 多选 + 越界 */ r = call(&a, "delete_messages", "{\"indices\":[0]}"); CHECK(r && strstr(r, "不可拆") != NULL, "system 也在结构消息之列"); free(r); meta_tools_detach(); agent_free(&a); } static void test_delete_rounds(void) { puts("delete_rounds"); Agent a; agent_init(&a); json_free(a.messages); a.messages = sample(); meta_tools_attach(&a); size_t before = json_array_size(a.messages); char *r = call(&a, "delete_rounds", "{\"from\":2,\"to\":2}"); CHECK(r && strstr(r, "已删除轮 2..2") != NULL, "应删掉第 2 轮: %s", r ? r : ""); CHECK(json_array_size(a.messages) == before - 2, "第 2 轮是 2 条消息"); /* 剩下的结构必须还是自洽的: #2 的 tool_calls 后面仍跟着 tool */ CHECK(meta_deletable(a.messages, 2) == 0, "#2 仍应是结构消息"); CHECK(strcmp(json_get_string(json_array_get(a.messages, 3), "role"), "tool") == 0, "tool 结果应仍紧跟它的调用"); free(r); /* 整段删光要被拒 */ r = call(&a, "delete_rounds", "{\"from\":1,\"to\":2}"); CHECK(r && strstr(r, "/reset") != NULL, "删光整段应被拒绝并建议 /reset"); free(r); /* 越界 */ r = call(&a, "delete_rounds", "{\"from\":99,\"to\":99}"); CHECK(r && (strstr(r, "不合法") != NULL), "越界轮号应报错: %s", r ? r : ""); free(r); meta_tools_detach(); agent_free(&a); } static void test_insert_and_undo(void) { puts("insert_message 与 undo"); Agent a; agent_init(&a); json_free(a.messages); a.messages = sample(); meta_tools_attach(&a); size_t before = json_array_size(a.messages); char *r = call(&a, "insert_message", "{\"after\":8,\"role\":\"user\",\"content\":\"这个项目必须用 C99\"}"); CHECK(r && strstr(r, "插入一条 user") != NULL, "应插入成功: %s", r ? r : ""); CHECK(json_array_size(a.messages) == before + 1, "条数应加一"); CHECK(strcmp(json_get_string(json_array_get(a.messages, 9), "content"), "这个项目必须用 C99") == 0, "应插在 #8 之后"); free(r); /* tool 角色不能凭空插 —— 没有配对的 tool_calls */ r = call(&a, "insert_message", "{\"after\":0,\"role\":\"tool\",\"content\":\"x\"}"); CHECK(r && strstr(r, "role 只能是") != NULL, "插 tool 消息应被拒绝"); free(r); /* 撤销应回到插入前 */ CHECK(meta_tools_ops() == 1, "应有 1 次改动, 实际 %zu", meta_tools_ops()); r = call(&a, "undo_last_edit", "{}"); CHECK(r && strstr(r, "已撤销") != NULL, "撤销应成功"); CHECK(json_array_size(a.messages) == before, "撤销后条数应还原"); free(r); /* 没有更多可撤销的了 */ r = call(&a, "undo_last_edit", "{}"); CHECK(r && strstr(r, "没有可撤销") != NULL, "栈空时应明确说没有"); free(r); meta_tools_detach(); agent_free(&a); } static void test_read_tools(void) { puts("list_rounds / read_messages (只读)"); Agent a; agent_init(&a); json_free(a.messages); a.messages = sample(); meta_tools_attach(&a); char *r = call(&a, "list_rounds", "{}"); CHECK(r && strstr(r, "共 3 轮") != NULL, "应报告 3 轮: %s", r ? r : ""); CHECK(r && strstr(r, "轮 1") != NULL && strstr(r, "轮 3") != NULL, "应列出每轮"); CHECK(r && strstr(r, "Python") != NULL, "应带开头预览"); free(r); r = call(&a, "list_rounds", "{\"from\":2,\"to\":2}"); CHECK(r && strstr(r, "轮 2") != NULL && strstr(r, "轮 3") == NULL, "应只列第 2 轮"); free(r); r = call(&a, "read_messages", "{\"from\":2,\"to\":3}"); CHECK(r && strstr(r, "run_command") != NULL, "应能看到工具调用"); CHECK(r && strstr(r, "file1") != NULL, "应能看到工具结果"); CHECK(r && strstr(r, "结构消息, 不可单删") != NULL, "结构消息应有标注"); free(r); CHECK(meta_tools_mutating("list_rounds") == 0, "list_rounds 不是写操作"); CHECK(meta_tools_mutating("read_messages") == 0, "read_messages 不是写操作"); CHECK(meta_tools_mutating("delete_rounds") == 1, "delete_rounds 是写操作"); CHECK(meta_tools_mutating("replace_message") == 1, "replace_message 是写操作"); CHECK(meta_tools_mutating("不存在的工具") == 0, "未知工具不该被当成写操作"); r = call(&a, "delete_rounds_but_typo", "{}"); CHECK(r && strstr(r, "未知工具") != NULL, "未知工具名应被拒绝"); free(r); meta_tools_detach(); agent_free(&a); } static void test_reroot_flag(void) { puts("重新存根标记"); Agent a; agent_init(&a); json_free(a.messages); a.messages = sample(); meta_reroot_clear(); CHECK(meta_reroot_pending() == 0, "初始不该有标记"); meta_tools_attach(&a); char *r = call(&a, "list_rounds", "{}"); /* 只读, 不该打标记 */ free(r); CHECK(meta_reroot_pending() == 0, "只读操作不该打标记"); r = call(&a, "replace_message", "{\"index\":1,\"content\":\"x\"}"); free(r); CHECK(meta_reroot_pending() == 1, "改动之后应打上标记"); meta_tools_detach(); agent_free(&a); meta_reroot_clear(); } int main(void) { test_rounds(); test_deletable(); test_read_tools(); test_replace(); test_delete_safety(); test_delete_rounds(); test_insert_and_undo(); test_reroot_flag(); printf("\n%d 项检查, %d 项失败\n", checks, failures); return failures ? 1 : 0; }