style: 应用 clang-format

This commit is contained in:
pluv committed 2026-10-04 13:50:33 +08:00
1 parent 016a45bc6e
commit d1612f1d31
65 files changed
+7288 -5983

No files matched your search

+187 -102
View File
@@ -1,31 +1,30 @@
/* test_tools.c - 工具层测试 */
#include "tools.h"
#include "image.h"
#include "json.h"
#include "tools.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <unistd.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"); \
} \
#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)
{
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) {
@@ -37,12 +36,17 @@ static char *call(const char *name, const char *args_json, int *is_error)
return r;
}
int main(void)
{
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; }
if (!dir) {
perror("mkdtemp");
return 2;
}
if (chdir(dir) != 0) {
perror("chdir");
return 2;
}
/* ---- 工具 schema 合法性 ---- */
puts("tools_schema");
@@ -62,19 +66,22 @@ int main(void)
{
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");
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;
if (nm && strcmp(nm, "read_image") == 0)
has_image = 1;
}
CHECK(has_image, "schema 里应有 read_image");
}
{
/* line 是集合型参数: schema 里必须是 array (元素 integer),
/* 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");
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");
@@ -86,18 +93,19 @@ int main(void)
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;
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 : "(缺)");
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 : "(缺)");
CHECK(it && strcmp(it, "integer") == 0,
"line 的元素应是 integer, 实际 %s", it ? it : "(缺)");
}
/* 序列化一遍确保可发给服务端 */
@@ -137,8 +145,10 @@ int main(void)
/* ---- write_file / read_file ---- */
puts("write_file / read_file");
r = call("write_file",
"{\"path\":\"sub/dir/a.txt\",\"content\":\"line1\\nline2\\nline3\\n\"}", &err);
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);
@@ -149,7 +159,8 @@ int main(void)
CHECK(strstr(r, "1\t") != NULL, "应带行号: %s", r);
free(r);
r = call("read_file", "{\"path\":\"sub/dir/a.txt\",\"offset\":2,\"limit\":1}", &err);
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);
@@ -162,7 +173,8 @@ int main(void)
puts("edit_file");
r = call("edit_file",
"{\"path\":\"sub/dir/a.txt\",\"line\":2,\"old_string\":\"line2\","
"\"new_string\":\"LINE-TWO\"}", &err);
"\"new_string\":\"LINE-TWO\"}",
&err);
CHECK(err == 0, "按行号替换应成功: %s", r);
CHECK(strstr(r, "第 2 行") != NULL, "回执里应带上行号: %s", r);
free(r);
@@ -173,9 +185,11 @@ int main(void)
/* 省略 line 不再是错误: 那是"全文件替换"这条逃生门 (见下面那组用例) */
/* 行号对不上: 必须报出它**真正在哪一行**, 否则模型只能反复猜 */
r = call("edit_file",
"{\"path\":\"sub/dir/a.txt\",\"line\":3,\"old_string\":\"LINE-TWO\","
"\"new_string\":\"x\"}", &err);
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);
@@ -183,7 +197,8 @@ int main(void)
/* 越界行号: 要连文件总行数一起说清楚 */
r = call("edit_file",
"{\"path\":\"sub/dir/a.txt\",\"line\":99,\"old_string\":\"x\","
"\"new_string\":\"y\"}", &err);
"\"new_string\":\"y\"}",
&err);
CHECK(err == 1 && strstr(r, "只有 3 行") != NULL,
"越界行号应报错并给出总行数: %s", r);
free(r);
@@ -192,16 +207,19 @@ int main(void)
* (这正是"全文唯一"最烦人的地方——文件一长, 随便一个短串都不唯一) */
r = call("edit_file",
"{\"path\":\"sub/dir/a.txt\",\"line\":1,\"old_string\":\"line\","
"\"new_string\":\"LINE\"}", &err);
"\"new_string\":\"LINE\"}",
&err);
CHECK(err == 0, "同一行里唯一即可, 别的行的重复不该拦: %s", r);
free(r);
/* 同一行里出现两处 → 仍然拒绝 (行内也要定得下来) */
r = call("write_file", "{\"path\":\"dup.txt\",\"content\":\"foo foo\\n\"}", &err);
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\"}",
"{\"path\":\"dup.txt\",\"line\":1,\"old_string\":\"foo\",\"new_"
"string\":\"bar\"}",
&err);
CHECK(err == 1 && strstr(r, "第 1 行里有 2 处") != NULL,
"同一行里多匹配应拒绝: %s", r);
@@ -209,8 +227,10 @@ int main(void)
/* replace_all 仍然换全文件, 但 line 必须指向其中真实的一处 */
r = call("edit_file",
"{\"path\":\"dup.txt\",\"line\":1,\"old_string\":\"foo\",\"new_string\":\"bar\","
"\"replace_all\":true}", &err);
"{\"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);
@@ -218,8 +238,10 @@ int main(void)
"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);
"{\"path\":\"dup.txt\",\"line\":7,\"old_string\":\"bar\",\"new_"
"string\":\"x\","
"\"replace_all\":true}",
&err);
CHECK(err == 1, "replace_all 也必须锚定在真实的一行上: %s", r);
free(r);
@@ -228,14 +250,16 @@ int main(void)
* "每行都改一次": 真改动的仍是唯一那一处. */
puts("edit_file: line 集合");
r = call("write_file",
"{\"path\":\"set.txt\",\"content\":\"aaa\\nbbb\\nccc\\nddd\\n\"}", &err);
"{\"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);
"\"new_string\":\"AAA\"}",
&err);
CHECK(err == 0, "集合圈住的替换应成功: %s", r);
CHECK(strstr(r, "第 1、2 行") != NULL, "回执里应带上整个集合: %s", r);
free(r);
@@ -244,40 +268,48 @@ int main(void)
free(r);
/* old_string 跨行时, 把覆盖到的几行都填进集合——不必赌起点是第几行 */
r = call("edit_file",
"{\"path\":\"set.txt\",\"line\":[2,3],\"old_string\":\"bbb\\nccc\","
"\"new_string\":\"B\\nC\"}", &err);
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);
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);
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);
"\"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);
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);
"\"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);
"\"new_string\":\"y\"}",
&err);
CHECK(err == 1 && strstr(r, "只有 4 行") != NULL,
"集合里的越界行号应报错并给出总行数: %s", r);
free(r);
@@ -285,25 +317,30 @@ int main(void)
/* 写法本身不对的三种: 空数组、非整数元素、一次给太多 */
r = call("edit_file",
"{\"path\":\"set.txt\",\"line\":[],\"old_string\":\"x\","
"\"new_string\":\"y\"}", &err);
"\"new_string\":\"y\"}",
&err);
CHECK(err == 1 && strstr(r, "空数组") != NULL, "空数组应报错: %s", r);
CHECK(strstr(r, "省略") != NULL, "空数组的报错要提示可以整个省略 line: %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);
"\"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);
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);
"{\"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);
@@ -312,29 +349,39 @@ int main(void)
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);
/* 省略 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);
"\"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\":[");
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\"}");
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);
CHECK(err == 1 && strstr(r, "一次最多") != NULL, "行号过多应报错: %s",
r);
free(r);
}
@@ -348,21 +395,23 @@ int main(void)
CHECK(err == 1, "不存在的目录应报错: %s", r);
free(r);
/* 这两个工具都已经删掉: 搜索交给模型自己用 run_command 里的真 grep,
/* 这两个工具都已经删掉: 搜索交给模型自己用 run_command 里的真 grep,
* 子代理 (spawn_subagent)整个功能也去掉了. 留着断言是为了钉住
* "不要再冒出一个半吊子的替代品". */
r = call("grep", "{\"pattern\":\"x\",\"path\":\".\"}", &err);
CHECK(err == 1 && strstr(r, "未知工具") != NULL, "不该再有 grep 工具: %s", r);
CHECK(err == 1 && strstr(r, "未知工具") != NULL, "不该再有 grep 工具: %s",
r);
free(r);
r = call("spawn_subagent", "{}", &err);
CHECK(err == 1 && strstr(r, "未知工具") != NULL, "不该再有子代理工具: %s", r);
CHECK(err == 1 && strstr(r, "未知工具") != NULL, "不该再有子代理工具: %s",
r);
free(r);
/* ---- 超大文件不能把内存吃干 ---- */
puts("超大文件防护");
{
/* 稀疏文件, 创建几乎不耗时; 关键是客户端要在读之前就发现它太大.
* 开头 16 KB 写成 'x': 这样它是"文本" (探测的前 8 KB 里没有 NUL),
/* 稀疏文件, 创建几乎不耗时; 关键是客户端要在读之前就发现它太大.
* 开头 16 KB 写成 'x': 这样它是"文本" (探测的前 8 KB 里没有 NUL),
* 否则连 read_file 都会先被二进制检测拦掉 —— 这个用例就测不到
* "分段读大文件"了. */
FILE *big = fopen("huge.txt", "wb");
@@ -376,7 +425,8 @@ int main(void)
fclose(big);
}
r = call("edit_file",
"{\"path\":\"huge.txt\",\"line\":1,\"old_string\":\"x\",\"new_string\":\"y\"}",
"{\"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);
@@ -389,12 +439,16 @@ int main(void)
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); }
if (bf) {
fwrite("PNG\0\1\2\3", 1, 8, bf);
fclose(bf);
}
}
r = call("read_file", "{\"path\":\"bin.dat\"}", &err);
@@ -404,9 +458,11 @@ int main(void)
free(r);
r = call("edit_file",
"{\"path\":\"bin.dat\",\"line\":1,\"old_string\":\"PNG\",\"new_string\":\"JPG\"}",
"{\"path\":\"bin.dat\",\"line\":1,\"old_string\":\"PNG\",\"new_"
"string\":\"JPG\"}",
&err);
CHECK(err == 1 && strstr(r, "二进制") != NULL, "edit_file 改二进制应被拒绝: %s", r);
CHECK(err == 1 && strstr(r, "二进制") != NULL,
"edit_file 改二进制应被拒绝: %s", r);
free(r);
r = call("write_file", "{\"path\":\"bin.dat\",\"content\":\"text\"}", &err);
@@ -420,30 +476,35 @@ int main(void)
char keep[16];
memset(keep, 0, sizeof keep);
size_t kn = bf ? fread(keep, 1, sizeof keep, bf) : 0;
if (bf) fclose(bf);
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);
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\"}",
"{\"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("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 自己的目录里留备份 (副作用),
/* read_image 会在 ligent 自己的目录里留备份 (副作用),
* 但不改用户的任何东西 —— 它是"读", 不该每次都拦一道 */
CHECK(tool_mutating("read_image") == 0, "read_image 不改用户的文件, 不该打扰");
CHECK(tool_mutating("read_image") == 0,
"read_image 不改用户的文件, 不该打扰");
CHECK(tool_mutating("没有这个工具") == 0, "未知工具按只读处理");
CHECK(tool_mutating(NULL) == 0, "空名字不应崩");
@@ -458,18 +519,21 @@ int main(void)
CHECK(bdir != NULL, "建备份临时目录失败");
if (bdir) {
image_set_bindir(bdir);
image_pending_take(); /* 清干净, 以免上一段残留 */
image_pending_take(); /* 清干净, 以免上一段残留 */
/* 1) 内容是文本、扩展名骗人 */
FILE *f = fopen("notanimage.png", "wb");
if (f) { fputs("这不是图片", f); fclose(f); }
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.
/* 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");
@@ -479,7 +543,8 @@ int main(void)
size_t left = raw_needed - 8;
while (left > 0) {
size_t chunk = left > 65536 ? 65536 : left;
if (fwrite(pad, 1, chunk, f) != chunk) break;
if (fwrite(pad, 1, chunk, f) != chunk)
break;
left -= chunk;
}
free(pad);
@@ -493,7 +558,11 @@ int main(void)
/* 3) 合法的小图: 成功, 并且挂上待发附件 */
f = fopen("ok.png", "wb");
if (f) { fwrite("\x89PNG\r\n\x1a\n", 1, 8, f); fwrite("payload", 1, 7, f); fclose(f); }
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);
@@ -504,14 +573,20 @@ int main(void)
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, "附件应是内部引用块");
"ligent_image") == 0,
"附件应是内部引用块");
json_free(att);
/* 只有成功那一张留了备份 —— 两条拒绝路径都不该留下东西 */
char *ls = xasprintf("ls %s/ligent.archive/images/*.png | wc -l", bdir);
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); }
if (p) {
if (fscanf(p, "%d", &cnt) != 1)
cnt = -1;
pclose(p);
}
free(ls);
CHECK(cnt == 1, "只该留 1 份备份 (成功那张), 实际 %d", cnt);
@@ -522,13 +597,15 @@ int main(void)
/* ---- 超长单行: read_file 报出的行号必须与 edit_file 的口径一致 ----
*
* read_file 以前用 fgets + 固定 64 KB 缓冲, 一行比缓冲长时会被切成"多行"、
* 每块各带一个行号. 于是模型按 read_file 看到的行号去 edit_file 改, 要么改错
* 地方、要么报越界 —— 而它没有任何办法察觉. 工具描述和 README 都写着
* "read_file 打出来的行号可以直接喂给 edit_file 的 line", 这里把它钉住. */
* 每块各带一个行号. 于是模型按 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 那块缓冲 */
for (int i = 0; i < 70000; i++)
fputc('a', lf); /* 跨过 64 KB 那块缓冲 */
fputc('\n', lf);
fputs("second line\nthird line\n", lf);
fclose(lf);
@@ -537,17 +614,22 @@ int main(void)
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");
"超长行之后的第 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);
"\"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);
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, "改动应落到文件里");
@@ -556,7 +638,8 @@ int main(void)
/* 单行本身就超过输出上限: 必须说清"这一行只给了开头", 不能说成
* "已截断, 从下一行继续" —— 那会让调用方以为拿到的是完整的行. */
FILE *hf = fopen("hugeline.txt", "wb");
for (int i = 0; i < 200000; i++) fputc('b', hf);
for (int i = 0; i < 200000; i++)
fputc('b', hf);
fputc('\n', hf);
fputs("tail\n", hf);
fclose(hf);
@@ -571,7 +654,8 @@ int main(void)
/* ---- 空目录: qsort 的第一个参数会拿到 NULL ---- */
puts("list_dir 空目录");
if (mkdir("emptydir", 0755) != 0) { /* 可能已存在 */ }
if (mkdir("emptydir", 0755) != 0) { /* 可能已存在 */
}
r = call("list_dir", "{\"path\":\"emptydir\"}", &err);
CHECK(err == 0 && strstr(r, "0 个条目") != NULL,
"空目录应报 0 个条目, 实际 %s", r);
@@ -586,7 +670,8 @@ int main(void)
/* ---- 输出截断 ---- */
puts("output truncation");
r = call("run_command",
"{\"command\":\"head -c 300000 /dev/zero | tr '\\\\0' 'a'\"}", &err);
"{\"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, "应提示已截断");