Files
ligent/tests/test_meta.c
T
2026-10-04 13:50:33 +08:00

415 lines
15 KiB
C

/* test_meta.c - meta 历史编辑: 轮次切分、结构安全、各工具的行为 */
#include "agent.h"
#include "json.h"
#include "meta.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}