init
This commit is contained in:
commit
e54390a9f8
85 files changed
+29923
No files matched your search
@@ -0,0 +1,597 @@
|
||||
/* test_tools.c - 工具层测试 */
|
||||
#include "tools.h"
|
||||
#include "image.h"
|
||||
#include "json.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.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 char *call(const char *name, const char *args_json, int *is_error)
|
||||
{
|
||||
char err[256];
|
||||
JsonValue *args = json_parse(args_json, err, sizeof err);
|
||||
if (!args) {
|
||||
printf(" 测试用例 args JSON 非法: %s\n", err);
|
||||
exit(2);
|
||||
}
|
||||
char *r = tool_execute(name, args, is_error);
|
||||
json_free(args);
|
||||
return r;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char tmpl[] = "/tmp/ligent_test_XXXXXX";
|
||||
char *dir = mkdtemp(tmpl);
|
||||
if (!dir) { perror("mkdtemp"); return 2; }
|
||||
if (chdir(dir) != 0) { perror("chdir"); return 2; }
|
||||
|
||||
/* ---- 工具 schema 合法性 ---- */
|
||||
puts("tools_schema");
|
||||
JsonValue *schema = tools_schema();
|
||||
CHECK(json_array_size(schema) == 6, "应有 6 个工具,实际 %zu",
|
||||
json_array_size(schema));
|
||||
for (size_t i = 0; i < json_array_size(schema); i++) {
|
||||
JsonValue *t = json_array_get(schema, i);
|
||||
CHECK(strcmp(json_get_string(t, "type"), "function") == 0, "type");
|
||||
JsonValue *fn = json_object_get(t, "function");
|
||||
CHECK(json_get_string(fn, "name") != NULL, "缺 name");
|
||||
JsonValue *params = json_object_get(fn, "parameters");
|
||||
CHECK(json_get_string(params, "type") != NULL, "缺 parameters.type");
|
||||
CHECK(json_object_get(params, "properties") != NULL, "缺 properties");
|
||||
CHECK(json_object_get(params, "required") != NULL, "缺 required");
|
||||
}
|
||||
{
|
||||
int has_image = 0;
|
||||
for (size_t i = 0; i < json_array_size(schema); i++) {
|
||||
JsonValue *fn = json_object_get(json_array_get(schema, i), "function");
|
||||
const char *nm = json_get_string(fn, "name");
|
||||
if (nm && strcmp(nm, "read_image") == 0) has_image = 1;
|
||||
}
|
||||
CHECK(has_image, "schema 里应有 read_image");
|
||||
}
|
||||
{
|
||||
/* line 是集合型参数:schema 里必须是 array(元素 integer),
|
||||
* 否则模型根本不会想到可以给数组 —— 参数表的 "integer[]" 写法
|
||||
* 只有 schema 展开对了才作数 */
|
||||
JsonValue *props = NULL, *reqd = NULL;
|
||||
for (size_t i = 0; i < json_array_size(schema); i++) {
|
||||
JsonValue *fn = json_object_get(json_array_get(schema, i), "function");
|
||||
const char *nm = json_get_string(fn, "name");
|
||||
if (nm && strcmp(nm, "edit_file") == 0) {
|
||||
JsonValue *par = json_object_get(fn, "parameters");
|
||||
props = json_object_get(par, "properties");
|
||||
reqd = json_object_get(par, "required");
|
||||
}
|
||||
}
|
||||
/* line 是可选的(省略 = 全文件替换),不该出现在 required 里 */
|
||||
int line_required = 0;
|
||||
for (size_t i = 0; i < json_array_size(reqd); i++) {
|
||||
const char *nm = json_string_value(json_array_get(reqd, i));
|
||||
if (nm && strcmp(nm, "line") == 0) line_required = 1;
|
||||
}
|
||||
CHECK(!line_required, "line 可以省略(= 全文件替换),不该是 required");
|
||||
|
||||
JsonValue *line = props ? json_object_get(props, "line") : NULL;
|
||||
const char *lt = line ? json_get_string(line, "type") : NULL;
|
||||
CHECK(lt && strcmp(lt, "array") == 0, "edit_file 的 line 应是 array,实际 %s",
|
||||
lt ? lt : "(缺)");
|
||||
JsonValue *items = line ? json_object_get(line, "items") : NULL;
|
||||
const char *it = items ? json_get_string(items, "type") : NULL;
|
||||
CHECK(it && strcmp(it, "integer") == 0, "line 的元素应是 integer,实际 %s",
|
||||
it ? it : "(缺)");
|
||||
}
|
||||
|
||||
/* 序列化一遍确保可发给服务端 */
|
||||
char *schema_json = json_dump(schema);
|
||||
JsonValue *re = json_parse(schema_json, NULL, 0);
|
||||
CHECK(re != NULL, "schema 无法被自身解析器往返");
|
||||
free(schema_json);
|
||||
json_free(re);
|
||||
json_free(schema);
|
||||
|
||||
/* ---- run_command ---- */
|
||||
puts("run_command");
|
||||
int err = 0;
|
||||
char *r = call("run_command", "{\"command\":\"echo hello\"}", &err);
|
||||
CHECK(err == 0, "exit 0 不应算错误: %s", r);
|
||||
CHECK(strstr(r, "hello") != NULL, "应含 hello: %s", r);
|
||||
CHECK(strstr(r, "[exit code: 0]") != NULL, "应含退出码: %s", r);
|
||||
free(r);
|
||||
|
||||
r = call("run_command", "{\"command\":\"exit 3\"}", &err);
|
||||
CHECK(err == 1, "非零退出应标记为错误");
|
||||
CHECK(strstr(r, "[exit code: 3]") != NULL, "应报告退出码 3: %s", r);
|
||||
free(r);
|
||||
|
||||
r = call("run_command", "{\"command\":\"echo err >&2; exit 1\"}", &err);
|
||||
CHECK(strstr(r, "err") != NULL, "stderr 应被合并捕获: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 超时 */
|
||||
r = call("run_command", "{\"command\":\"sleep 5\",\"timeout\":1}", &err);
|
||||
CHECK(err == 1 && strstr(r, "超时") != NULL, "应超时: %s", r);
|
||||
free(r);
|
||||
|
||||
r = call("run_command", "{}", &err);
|
||||
CHECK(err == 1 && strstr(r, "command") != NULL, "缺参数应报错: %s", r);
|
||||
free(r);
|
||||
|
||||
/* ---- write_file / read_file ---- */
|
||||
puts("write_file / read_file");
|
||||
r = call("write_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"content\":\"line1\\nline2\\nline3\\n\"}", &err);
|
||||
CHECK(err == 0, "写入应成功: %s", r);
|
||||
CHECK(access("sub/dir/a.txt", R_OK) == 0, "父目录应被自动创建");
|
||||
free(r);
|
||||
|
||||
r = call("read_file", "{\"path\":\"sub/dir/a.txt\"}", &err);
|
||||
CHECK(err == 0, "读取应成功: %s", r);
|
||||
CHECK(strstr(r, "line1") && strstr(r, "line3"), "内容应完整: %s", r);
|
||||
CHECK(strstr(r, "1\t") != NULL, "应带行号: %s", r);
|
||||
free(r);
|
||||
|
||||
r = call("read_file", "{\"path\":\"sub/dir/a.txt\",\"offset\":2,\"limit\":1}", &err);
|
||||
CHECK(strstr(r, "line2") && !strstr(r, "line1") && !strstr(r, "line3"),
|
||||
"offset/limit 应生效: %s", r);
|
||||
free(r);
|
||||
|
||||
r = call("read_file", "{\"path\":\"does_not_exist\"}", &err);
|
||||
CHECK(err == 1, "读不存在的文件应报错: %s", r);
|
||||
free(r);
|
||||
|
||||
/* ---- edit_file:替换由行号钉住 ---- */
|
||||
puts("edit_file");
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"line\":2,\"old_string\":\"line2\","
|
||||
"\"new_string\":\"LINE-TWO\"}", &err);
|
||||
CHECK(err == 0, "按行号替换应成功: %s", r);
|
||||
CHECK(strstr(r, "第 2 行") != NULL, "回执里应带上行号: %s", r);
|
||||
free(r);
|
||||
r = call("read_file", "{\"path\":\"sub/dir/a.txt\"}", &err);
|
||||
CHECK(strstr(r, "LINE-TWO") != NULL, "替换结果应落盘: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 省略 line 不再是错误:那是"全文件替换"这条逃生门(见下面那组用例) */
|
||||
|
||||
/* 行号对不上:必须报出它**真正在哪一行**,否则模型只能反复猜 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"line\":3,\"old_string\":\"LINE-TWO\","
|
||||
"\"new_string\":\"x\"}", &err);
|
||||
CHECK(err == 1, "行号不对应报错: %s", r);
|
||||
CHECK(strstr(r, "第 2 行") != NULL, "应指出它真正的行号: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 越界行号:要连文件总行数一起说清楚 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"line\":99,\"old_string\":\"x\","
|
||||
"\"new_string\":\"y\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "只有 3 行") != NULL,
|
||||
"越界行号应报错并给出总行数: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 唯一性只要求**在这一行里**唯一:同一个词出现在别的行不该拦住替换
|
||||
* (这正是"全文唯一"最烦人的地方——文件一长,随便一个短串都不唯一) */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"sub/dir/a.txt\",\"line\":1,\"old_string\":\"line\","
|
||||
"\"new_string\":\"LINE\"}", &err);
|
||||
CHECK(err == 0, "同一行里唯一即可,别的行的重复不该拦: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 同一行里出现两处 → 仍然拒绝(行内也要定得下来) */
|
||||
r = call("write_file", "{\"path\":\"dup.txt\",\"content\":\"foo foo\\n\"}", &err);
|
||||
CHECK(err == 0, "写入 dup.txt: %s", r);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"dup.txt\",\"line\":1,\"old_string\":\"foo\",\"new_string\":\"bar\"}",
|
||||
&err);
|
||||
CHECK(err == 1 && strstr(r, "第 1 行里有 2 处") != NULL,
|
||||
"同一行里多匹配应拒绝: %s", r);
|
||||
free(r);
|
||||
|
||||
/* replace_all 仍然换全文件,但 line 必须指向其中真实的一处 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"dup.txt\",\"line\":1,\"old_string\":\"foo\",\"new_string\":\"bar\","
|
||||
"\"replace_all\":true}", &err);
|
||||
CHECK(err == 0, "replace_all 应成功: %s", r);
|
||||
free(r);
|
||||
r = call("read_file", "{\"path\":\"dup.txt\"}", &err);
|
||||
CHECK(strstr(r, "bar bar") != NULL && !strstr(r, "foo"),
|
||||
"replace_all 应换掉文件里全部匹配: %s", r);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"dup.txt\",\"line\":7,\"old_string\":\"bar\",\"new_string\":\"x\","
|
||||
"\"replace_all\":true}", &err);
|
||||
CHECK(err == 1, "replace_all 也必须锚定在真实的一行上: %s", r);
|
||||
free(r);
|
||||
|
||||
/* ---- edit_file:line 是集合型 ----
|
||||
* 集合是**搜索范围**(old_string 的起点落在其中任一行即可),不是
|
||||
* "每行都改一次":真改动的仍是唯一那一处。 */
|
||||
puts("edit_file:line 集合");
|
||||
r = call("write_file",
|
||||
"{\"path\":\"set.txt\",\"content\":\"aaa\\nbbb\\nccc\\nddd\\n\"}", &err);
|
||||
CHECK(err == 0, "写入 set.txt: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 数组写法:起点落在集合里的哪一行都算命中;回执要带上整个集合 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"set.txt\",\"line\":[1,2],\"old_string\":\"aaa\","
|
||||
"\"new_string\":\"AAA\"}", &err);
|
||||
CHECK(err == 0, "集合圈住的替换应成功: %s", r);
|
||||
CHECK(strstr(r, "第 1、2 行") != NULL, "回执里应带上整个集合: %s", r);
|
||||
free(r);
|
||||
r = call("read_file", "{\"path\":\"set.txt\"}", &err);
|
||||
CHECK(strstr(r, "AAA") != NULL && !strstr(r, "aaa"), "替换应落盘: %s", r);
|
||||
free(r);
|
||||
|
||||
/* old_string 跨行时,把覆盖到的几行都填进集合——不必赌起点是第几行 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"set.txt\",\"line\":[2,3],\"old_string\":\"bbb\\nccc\","
|
||||
"\"new_string\":\"B\\nC\"}", &err);
|
||||
CHECK(err == 0, "跨行替换(集合给两行)应成功: %s", r);
|
||||
free(r);
|
||||
r = call("read_file", "{\"path\":\"set.txt\"}", &err);
|
||||
CHECK(strstr(r, "B") && strstr(r, "C") && !strstr(r, "bbb"), "跨行替换应落盘: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 集合里有多处匹配 → 仍然拒绝(集合不是"每行改一次"的授权) */
|
||||
r = call("write_file", "{\"path\":\"two.txt\",\"content\":\"foo\\nx\\nfoo\\n\"}", &err);
|
||||
CHECK(err == 0, "写入 two.txt: %s", r);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"two.txt\",\"line\":[1,3],\"old_string\":\"foo\","
|
||||
"\"new_string\":\"bar\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "第 1、3 行里有 2 处") != NULL,
|
||||
"集合里多处匹配应拒绝: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 重复的行号要去重:不然同一处匹配会被数成两处 */
|
||||
r = call("write_file", "{\"path\":\"dup3.txt\",\"content\":\"foo\\n\"}", &err);
|
||||
CHECK(err == 0, "写入 dup3.txt: %s", r);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"dup3.txt\",\"line\":[1,1],\"old_string\":\"foo\","
|
||||
"\"new_string\":\"bar\"}", &err);
|
||||
CHECK(err == 0, "重复行号应去重,而不是当成两处: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 集合里有一行越界 → 和单个行号时一样,报出文件总行数 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"set.txt\",\"line\":[1,99],\"old_string\":\"x\","
|
||||
"\"new_string\":\"y\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "只有 4 行") != NULL,
|
||||
"集合里的越界行号应报错并给出总行数: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 写法本身不对的三种:空数组、非整数元素、一次给太多 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"set.txt\",\"line\":[],\"old_string\":\"x\","
|
||||
"\"new_string\":\"y\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "空数组") != NULL, "空数组应报错: %s", r);
|
||||
CHECK(strstr(r, "省略") != NULL, "空数组的报错要提示可以整个省略 line: %s", r);
|
||||
free(r);
|
||||
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"set.txt\",\"line\":[1,1.5],\"old_string\":\"x\","
|
||||
"\"new_string\":\"y\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "第 2 个") != NULL,
|
||||
"非整数元素应报错并指出是第几个: %s", r);
|
||||
free(r);
|
||||
|
||||
/* ---- 省略 line = 全文件替换所有匹配 ----
|
||||
* 这是 range 那样的集合参数该有的默认值:不给范围就是不设范围。 */
|
||||
r = call("write_file", "{\"path\":\"all.txt\",\"content\":\"k\\nx\\nk\\n\"}", &err);
|
||||
CHECK(err == 0, "写入 all.txt: %s", r);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"all.txt\",\"old_string\":\"k\",\"new_string\":\"K\"}", &err);
|
||||
CHECK(err == 0, "省略 line 应做全文件替换: %s", r);
|
||||
CHECK(strstr(r, "全文件替换") != NULL && strstr(r, "2 处") != NULL,
|
||||
"回执要说清是全文件替换、换了几处: %s", r);
|
||||
free(r);
|
||||
r = call("read_file", "{\"path\":\"all.txt\"}", &err);
|
||||
CHECK(strstr(r, "K") != NULL && !strstr(r, "k"), "两处都该被换掉: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 省略 line 时文件里没有 old_string:只说整个文件里没有(没有"哪几行"可说) */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"all.txt\",\"old_string\":\"zzz\",\"new_string\":\"y\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "整个文件里没有") != NULL, "该报整个文件里没有: %s", r);
|
||||
free(r);
|
||||
|
||||
/* line 给错了类型(不是"想全局"):仍然当场报错,并告诉它可以省略 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"all.txt\",\"line\":\"1\",\"old_string\":\"K\","
|
||||
"\"new_string\":\"z\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "整数或整数数组") != NULL, "line 类型错应报错: %s", r);
|
||||
CHECK(strstr(r, "整个省略") != NULL, "报错要指出可以省略 line: %s", r);
|
||||
free(r);
|
||||
|
||||
{
|
||||
/* 65 个行号:超过一次能圈住的范围,要明说 */
|
||||
char big[600];
|
||||
size_t o = (size_t)snprintf(big, sizeof big, "{\"path\":\"set.txt\",\"line\":[");
|
||||
for (int i = 1; i <= 65; i++)
|
||||
o += (size_t)snprintf(big + o, sizeof big - o, "%s%d", i > 1 ? "," : "", i);
|
||||
snprintf(big + o, sizeof big - o, "],\"old_string\":\"x\",\"new_string\":\"y\"}");
|
||||
r = call("edit_file", big, &err);
|
||||
CHECK(err == 1 && strstr(r, "一次最多") != NULL, "行号过多应报错: %s", r);
|
||||
free(r);
|
||||
}
|
||||
|
||||
/* ---- list_dir ---- */
|
||||
puts("list_dir");
|
||||
r = call("list_dir", "{\"path\":\".\"}", &err);
|
||||
CHECK(err == 0 && strstr(r, "sub/") != NULL, "应列出子目录: %s", r);
|
||||
free(r);
|
||||
|
||||
r = call("list_dir", "{\"path\":\"nope\"}", &err);
|
||||
CHECK(err == 1, "不存在的目录应报错: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 这两个工具都已经删掉:搜索交给模型自己用 run_command 里的真 grep,
|
||||
* 子代理(spawn_subagent)整个功能也去掉了。留着断言是为了钉住
|
||||
* "不要再冒出一个半吊子的替代品"。 */
|
||||
r = call("grep", "{\"pattern\":\"x\",\"path\":\".\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "未知工具") != NULL, "不该再有 grep 工具: %s", r);
|
||||
free(r);
|
||||
r = call("spawn_subagent", "{}", &err);
|
||||
CHECK(err == 1 && strstr(r, "未知工具") != NULL, "不该再有子代理工具: %s", r);
|
||||
free(r);
|
||||
|
||||
/* ---- 超大文件不能把内存吃干 ---- */
|
||||
puts("超大文件防护");
|
||||
{
|
||||
/* 稀疏文件,创建几乎不耗时;关键是客户端要在读之前就发现它太大。
|
||||
* 开头 16 KB 写成 'x':这样它是"文本"(探测的前 8 KB 里没有 NUL),
|
||||
* 否则连 read_file 都会先被二进制检测拦掉 —— 这个用例就测不到
|
||||
* "分段读大文件"了。 */
|
||||
FILE *big = fopen("huge.txt", "wb");
|
||||
CHECK(big != NULL, "创建测试文件失败");
|
||||
if (big) {
|
||||
char head[16 * 1024];
|
||||
memset(head, 'x', sizeof head);
|
||||
fwrite(head, 1, sizeof head, big);
|
||||
fseek(big, 40L * 1024 * 1024, SEEK_SET);
|
||||
fputc('x', big);
|
||||
fclose(big);
|
||||
}
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"huge.txt\",\"line\":1,\"old_string\":\"x\",\"new_string\":\"y\"}",
|
||||
&err);
|
||||
CHECK(err == 1, "超大文件的 edit_file 应被拒绝: %.120s", r);
|
||||
CHECK(strstr(r, "上限") != NULL, "应说明是超上限: %.120s", r);
|
||||
free(r);
|
||||
|
||||
/* read_file 是分段读的,应当照常可用 */
|
||||
r = call("read_file", "{\"path\":\"huge.txt\",\"limit\":1}", &err);
|
||||
CHECK(err == 0, "分段读大文件应当可用: %.120s", r);
|
||||
free(r);
|
||||
unlink("huge.txt");
|
||||
}
|
||||
|
||||
/* ---- 二进制文件:所有处理文本的工具都必须拒绝(而且要说明该用什么) ---- */
|
||||
puts("二进制拒绝");
|
||||
{
|
||||
FILE *bf = fopen("bin.dat", "wb");
|
||||
CHECK(bf != NULL, "创建二进制测试文件失败");
|
||||
if (bf) { fwrite("PNG\0\1\2\3", 1, 8, bf); fclose(bf); }
|
||||
}
|
||||
|
||||
r = call("read_file", "{\"path\":\"bin.dat\"}", &err);
|
||||
CHECK(err == 1, "read_file 读二进制应被拒绝: %s", r);
|
||||
CHECK(strstr(r, "二进制") != NULL && strstr(r, "第 4 字节") != NULL,
|
||||
"拒绝理由要指出 NUL 在哪一字节: %s", r);
|
||||
free(r);
|
||||
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"bin.dat\",\"line\":1,\"old_string\":\"PNG\",\"new_string\":\"JPG\"}",
|
||||
&err);
|
||||
CHECK(err == 1 && strstr(r, "二进制") != NULL, "edit_file 改二进制应被拒绝: %s", r);
|
||||
free(r);
|
||||
|
||||
r = call("write_file", "{\"path\":\"bin.dat\",\"content\":\"text\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "二进制") != NULL,
|
||||
"write_file 覆盖二进制应被拒绝: %s", r);
|
||||
free(r);
|
||||
|
||||
/* 被拒绝的三次都不能动到文件本身 —— 拒绝的意义就在这里 */
|
||||
{
|
||||
FILE *bf = fopen("bin.dat", "rb");
|
||||
char keep[16];
|
||||
memset(keep, 0, sizeof keep);
|
||||
size_t kn = bf ? fread(keep, 1, sizeof keep, bf) : 0;
|
||||
if (bf) fclose(bf);
|
||||
CHECK(kn == 8 && memcmp(keep, "PNG\0\1\2\3", 8) == 0,
|
||||
"被拒绝的读/写不能改动二进制文件的字节");
|
||||
}
|
||||
|
||||
/* 确认提示里要能看出改的是哪一行 */
|
||||
r = call("write_file", "{\"path\":\"confirm.txt\",\"content\":\"aaa\\nbbb\\n\"}", &err);
|
||||
free(r);
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"confirm.txt\",\"line\":2,\"old_string\":\"bbb\",\"new_string\":\"ccc\"}",
|
||||
&err);
|
||||
CHECK(err == 0 && strstr(r, "第 2 行") != NULL, "回执带行号: %s", r);
|
||||
free(r);
|
||||
|
||||
/* ---- 哪些工具会改动磁盘(决定是否弹确认) ---- */
|
||||
puts("写操作分类");
|
||||
CHECK(tool_mutating("run_command") == 1, "run_command 会执行命令,必须确认");
|
||||
CHECK(tool_mutating("write_file") == 1, "write_file 会写盘,必须确认");
|
||||
CHECK(tool_mutating("edit_file") == 1, "edit_file 会改盘,必须确认");
|
||||
CHECK(tool_mutating("read_file") == 0, "read_file 只读,不该打扰");
|
||||
CHECK(tool_mutating("list_dir") == 0, "list_dir 只读,不该打扰");
|
||||
/* read_image 会在 ligent 自己的目录里留备份(副作用),
|
||||
* 但不改用户的任何东西 —— 它是"读",不该每次都拦一道 */
|
||||
CHECK(tool_mutating("read_image") == 0, "read_image 不改用户的文件,不该打扰");
|
||||
CHECK(tool_mutating("没有这个工具") == 0, "未知工具按只读处理");
|
||||
CHECK(tool_mutating(NULL) == 0, "空名字不应崩");
|
||||
|
||||
/* ---- read_image:只收真的能内联进去的图 ----
|
||||
*
|
||||
* 两条拒绝路径是新的:格式不认识、体积超限。它们都必须**不备份** ——
|
||||
* 一张交不出去的图,留备份只会让 ligent.archive 变成垃圾场。 */
|
||||
puts("read_image:本地就把不合格的图拦掉");
|
||||
{
|
||||
char btmp[] = "/tmp/ligent_tools_img_XXXXXX";
|
||||
char *bdir = mkdtemp(btmp);
|
||||
CHECK(bdir != NULL, "建备份临时目录失败");
|
||||
if (bdir) {
|
||||
image_set_bindir(bdir);
|
||||
image_pending_take(); /* 清干净,免得上一段残留 */
|
||||
|
||||
/* 1) 内容是文本、扩展名骗人 */
|
||||
FILE *f = fopen("notanimage.png", "wb");
|
||||
if (f) { fputs("这不是图片", f); fclose(f); }
|
||||
r = call("read_image", "{\"path\":\"notanimage.png\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "不认识") != NULL,
|
||||
"非图片内容应被拒: %s", r);
|
||||
CHECK(strstr(r, "按文件内容判断") != NULL, "应说明是看内容判断的");
|
||||
free(r);
|
||||
|
||||
/* 2) 体积超限:base64 后超过 IMAGE_MAX_B64。
|
||||
* 4/3 * 原始大小 = 编码后大小,所以原始要 > 3/4 * 上限。 */
|
||||
size_t raw_needed = (size_t)(IMAGE_MAX_B64 / 4 * 3) + 4096;
|
||||
f = fopen("huge.png", "wb");
|
||||
if (f) {
|
||||
fwrite("\x89PNG\r\n\x1a\n", 1, 8, f);
|
||||
char *pad = xcalloc(1, 65536);
|
||||
size_t left = raw_needed - 8;
|
||||
while (left > 0) {
|
||||
size_t chunk = left > 65536 ? 65536 : left;
|
||||
if (fwrite(pad, 1, chunk, f) != chunk) break;
|
||||
left -= chunk;
|
||||
}
|
||||
free(pad);
|
||||
fclose(f);
|
||||
}
|
||||
r = call("read_image", "{\"path\":\"huge.png\"}", &err);
|
||||
CHECK(err == 1 && strstr(r, "太大") != NULL,
|
||||
"超限的图应被当场拒掉: %.120s", r);
|
||||
CHECK(strstr(r, "内联上限") != NULL, "应说清上限是多少");
|
||||
free(r);
|
||||
|
||||
/* 3) 合法的小图:成功,并且挂上待发附件 */
|
||||
f = fopen("ok.png", "wb");
|
||||
if (f) { fwrite("\x89PNG\r\n\x1a\n", 1, 8, f); fwrite("payload", 1, 7, f); fclose(f); }
|
||||
r = call("read_image", "{\"path\":\"ok.png\"}", &err);
|
||||
CHECK(err == 0 && strstr(r, "已读入图片") != NULL,
|
||||
"合法图片应成功: %s", r);
|
||||
CHECK(strstr(r, "base64") != NULL, "应说明会以 base64 内联");
|
||||
free(r);
|
||||
|
||||
JsonValue *att = image_pending_take();
|
||||
CHECK(att && json_array_size(att) == 1, "应挂上 1 个待发附件");
|
||||
if (att && json_array_size(att) == 1)
|
||||
CHECK(strcmp(json_get_string(json_array_get(att, 0), "type"),
|
||||
"ligent_image") == 0, "附件应是内部引用块");
|
||||
json_free(att);
|
||||
|
||||
/* 只有成功那一张留了备份 —— 两条拒绝路径都不该留下东西 */
|
||||
char *ls = xasprintf("ls %s/ligent.archive/images/*.png | wc -l", bdir);
|
||||
FILE *p = popen(ls, "r");
|
||||
int cnt = -1;
|
||||
if (p) { if (fscanf(p, "%d", &cnt) != 1) cnt = -1; pclose(p); }
|
||||
free(ls);
|
||||
CHECK(cnt == 1, "只该留 1 份备份(成功那张),实际 %d", cnt);
|
||||
|
||||
image_set_bindir(".");
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 超长单行:read_file 报出的行号必须与 edit_file 的口径一致 ----
|
||||
*
|
||||
* read_file 以前用 fgets + 固定 64 KB 缓冲,一行比缓冲长时会被切成"多行"、
|
||||
* 每块各带一个行号。于是模型按 read_file 看到的行号去 edit_file 改,要么改错
|
||||
* 地方、要么报越界 —— 而它没有任何办法察觉。工具描述和 README 都写着
|
||||
* "read_file 打出来的行号可以直接喂给 edit_file 的 line",这里把它钉住。 */
|
||||
puts("超长单行的行号口径");
|
||||
{
|
||||
FILE *lf = fopen("longline.txt", "wb");
|
||||
for (int i = 0; i < 70000; i++) fputc('a', lf); /* 跨过 64 KB 那块缓冲 */
|
||||
fputc('\n', lf);
|
||||
fputs("second line\nthird line\n", lf);
|
||||
fclose(lf);
|
||||
|
||||
r = call("read_file", "{\"path\":\"longline.txt\"}", &err);
|
||||
CHECK(err == 0, "读超长行文件失败: %s", r);
|
||||
CHECK(strstr(r, " 1\ta") != NULL, "第 1 行应以 a 开头:\n%.120s", r);
|
||||
CHECK(strstr(r, " 2\tsecond line") != NULL,
|
||||
"超长行之后的第 2 行应是 second line(它不能被算成好几行):\n%.200s", r);
|
||||
CHECK(strstr(r, " 3\tthird line") != NULL, "第 3 行应是 third line");
|
||||
CHECK(strstr(r, " 4\t") == NULL, "不应该出现第 4 行");
|
||||
free(r);
|
||||
|
||||
/* 拿 read_file 报出的行号去 edit_file 改,必须命中同一处 */
|
||||
r = call("edit_file",
|
||||
"{\"path\":\"longline.txt\",\"line\":3,"
|
||||
"\"old_string\":\"third line\",\"new_string\":\"THIRD LINE\"}", &err);
|
||||
CHECK(err == 0, "用 read_file 的行号改第 3 行失败: %s", r);
|
||||
CHECK(strstr(r, "第 3 行替换 1 处") != NULL, "回执应说清改的是第 3 行: %s", r);
|
||||
free(r);
|
||||
char *after = read_entire_file("longline.txt", NULL);
|
||||
CHECK(after && strstr(after, "THIRD LINE") != NULL, "改动应落到文件里");
|
||||
free(after);
|
||||
|
||||
/* 单行本身就超过输出上限:必须说清"这一行只给了开头",不能说成
|
||||
* "已截断,从下一行继续" —— 那会让调用方以为拿到的是完整的行。 */
|
||||
FILE *hf = fopen("hugeline.txt", "wb");
|
||||
for (int i = 0; i < 200000; i++) fputc('b', hf);
|
||||
fputc('\n', hf);
|
||||
fputs("tail\n", hf);
|
||||
fclose(hf);
|
||||
r = call("read_file", "{\"path\":\"hugeline.txt\"}", &err);
|
||||
CHECK(strstr(r, "第 1 行本身就超过") != NULL,
|
||||
"应说清是这一行超过上限:%.160s", r);
|
||||
CHECK(strstr(r, " 2\t") == NULL, "不能把剩下的半个行伪装成第 2 行");
|
||||
free(r);
|
||||
remove("hugeline.txt");
|
||||
remove("longline.txt");
|
||||
}
|
||||
|
||||
/* ---- 空目录:qsort 的第一个参数会拿到 NULL ---- */
|
||||
puts("list_dir 空目录");
|
||||
if (mkdir("emptydir", 0755) != 0) { /* 可能已存在 */ }
|
||||
r = call("list_dir", "{\"path\":\"emptydir\"}", &err);
|
||||
CHECK(err == 0 && strstr(r, "0 个条目") != NULL,
|
||||
"空目录应报 0 个条目,实际 %s", r);
|
||||
free(r);
|
||||
rmdir("emptydir");
|
||||
|
||||
/* ---- 未知工具 ---- */
|
||||
r = call("no_such_tool", "{}", &err);
|
||||
CHECK(err == 1 && strstr(r, "未知工具") != NULL, "未知工具应报错: %s", r);
|
||||
free(r);
|
||||
|
||||
/* ---- 输出截断 ---- */
|
||||
puts("output truncation");
|
||||
r = call("run_command",
|
||||
"{\"command\":\"head -c 300000 /dev/zero | tr '\\\\0' 'a'\"}", &err);
|
||||
CHECK(strlen(r) <= LIGENT_TOOL_OUTPUT_LIMIT + 200,
|
||||
"输出应被截断,实际 %zu 字节", strlen(r));
|
||||
CHECK(strstr(r, "截断") != NULL, "应提示已截断");
|
||||
free(r);
|
||||
|
||||
printf("\n%d 项检查,%d 项失败\n", checks, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user