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