chore: 规范标点
This commit is contained in:
1 parent
15e79887fa
commit
3dd2231c70
81 files changed
+1751
-1751
No files matched your search
+99
-99
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# run_e2e.sh - 端到端测试:用本地 mock 服务端跑通 ligent 的完整链路
|
||||
# run_e2e.sh - 端到端测试: 用本地 mock 服务端跑通 ligent 的完整链路
|
||||
set -u
|
||||
|
||||
# 把脚本自己的 stdin 断掉.
|
||||
#
|
||||
# 这不是洁癖:ligent 在写操作前会问 (Y/n), 而它在 stdin 不是终端时默认放行.
|
||||
# 这不是洁癖: ligent 在写操作前会问 (Y/n), 而它在 stdin 不是终端时默认放行.
|
||||
# 于是"直接调用 $BIN"的用例在 CI 里一路通过, 在开发者的终端里却会停下来
|
||||
# 等人敲回车 —— 同一份脚本, 两种行为. 交互用例都走 pty 驱动脚本, 不靠这里
|
||||
# 的 stdin, 所以断开它不会误伤任何东西.
|
||||
@@ -12,7 +12,7 @@ exec </dev/null
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
# 默认测刚编出来的那个, 但允许覆盖 —— 静态产物 (ligent-static)也应当能过同一
|
||||
# 套用例, 否则"静态构建可用"就只等于"它能启动":
|
||||
# 套用例, 否则"静态构建可用"就只等于"它能启动":
|
||||
# BIN=$ROOT/ligent-static tests/run_e2e.sh
|
||||
BIN="${BIN:-$ROOT/ligent}"
|
||||
PASS=0
|
||||
@@ -44,32 +44,32 @@ PORT="$port"
|
||||
export LIGENT_HISTORY="$ROOT/build/e2e.history"
|
||||
rm -f "$LIGENT_HISTORY" "$LIGENT_HISTORY.tmp"
|
||||
|
||||
# 默认关掉能力探测:它会在每个会话的第一次真实请求前插进一串试探请求,
|
||||
# 默认关掉能力探测: 它会在每个会话的第一次真实请求前插进一串试探请求,
|
||||
# 而下面几十段用例都是按"请求序列"写的, 会被它整体推后一位.
|
||||
# 探测本身由第 51 段专门覆盖 (那段自己把这个变量打开).
|
||||
export LIGENT_PROBE=0
|
||||
|
||||
# 界面语言锁成中文:下面这些断言写的是中文文案, 而开发者的 LANG 或 CI 的
|
||||
# 界面语言锁成中文: 下面这些断言写的是中文文案, 而开发者的 LANG 或 CI 的
|
||||
# C locale 会让同一份脚本在两种语言下各红一半. 英文路径由第 59 段专门覆盖.
|
||||
export LIGENT_LANG=zh
|
||||
|
||||
# 配置隔离. ligent 读的是"二进制旁边那份 ligent.json", 而被测二进制就在仓库根
|
||||
# —— 于是开发者自己那份 (改过 temperature、reasoning_effort……)会混进请求体,
|
||||
# 让"默认请求长什么样"这类断言时绿时红:同一份代码, 换个人的机器结论就不同.
|
||||
# 让"默认请求长什么样"这类断言时绿时红: 同一份代码, 换个人的机器结论就不同.
|
||||
# 测试得自证, 所以这里指向一份空配置; 要测配置加载的用例自己带 -c.
|
||||
export LIGENT_CONFIG="$ROOT/build/tmp/e2e-empty.json"
|
||||
printf '{}\n' > "$LIGENT_CONFIG"
|
||||
|
||||
# 存档目录 (<二进制目录>/ligent.archive)是**运行期状态**, 不是构建产物:
|
||||
# 存档目录 (<二进制目录>/ligent.archive)是**运行期状态**, 不是构建产物:
|
||||
# 里面是用户自己的 /export 存档和图片备份. 而下面几十段用例为了隔离会反复
|
||||
# `rm -rf` 它, 被测二进制又常常就在仓库根 —— 也就是说直接跑 e2e 会把人家的
|
||||
# 存档删掉. 所以开机先看一眼:非空就整体挪到临时目录 (打印路径, 内容不动),
|
||||
# 存档删掉. 所以开机先看一眼: 非空就整体挪到临时目录 (打印路径, 内容不动),
|
||||
# 之后也不再碰它. 挪不动就直接停 —— 宁可测试不跑, 也不能把东西删了.
|
||||
ARCHIVE_DIR="$(dirname "$BIN")/ligent.archive"
|
||||
if [ -d "$ARCHIVE_DIR" ] && [ -n "$(ls -A "$ARCHIVE_DIR" 2>/dev/null)" ]; then
|
||||
SAVED_ARCHIVE="$(mktemp -d "${TMPDIR:-/tmp}/ligent.archive.saved.XXXXXX")"
|
||||
if mv "$ARCHIVE_DIR" "$SAVED_ARCHIVE/ligent.archive"; then
|
||||
echo "注意:$ARCHIVE_DIR 里有东西, 已整体移到"
|
||||
echo "注意: $ARCHIVE_DIR 里有东西, 已整体移到"
|
||||
echo " $SAVED_ARCHIVE/ligent.archive (e2e 需要这个目录是干净的)"
|
||||
echo
|
||||
else
|
||||
@@ -180,7 +180,7 @@ printf '%s' "$OUT" | grep -q $'\033\[' && r=1 || r=0
|
||||
check "--no-color 时不含任何 ANSI 转义" $r
|
||||
|
||||
echo
|
||||
echo "== 9. 网络健壮性:瞬态失败重试 =="
|
||||
echo "== 9. 网络健壮性: 瞬态失败重试 =="
|
||||
OUT="$("$BIN" --api-key test --base-url "$BASE" --no-confirm --no-auto-save --no-color "RETRY_TEST" 2>&1)"
|
||||
contains "重试后成功" "$OUT"; check "503 之后重试并最终成功" $?
|
||||
contains "重试 (第 1 次)" "$OUT"; check "第一次失败后有退避重试" $?
|
||||
@@ -202,7 +202,7 @@ contains "已重试 1 次" "$OUT"; check "错误信息说明重试了几次" $?
|
||||
contains "mock 持续不可用" "$OUT"; check "服务端的说明被透传" $?
|
||||
|
||||
echo
|
||||
echo "== 12. 流式传输中途断连:保留已收到的内容 =="
|
||||
echo "== 12. 流式传输中途断连: 保留已收到的内容 =="
|
||||
OUT="$("$BIN" --api-key test --base-url "$BASE" --no-confirm --no-auto-save --no-color "TRUNCATE_TEST" 2>&1)"
|
||||
contains "半途崩溃" "$OUT"; check "已收到的部分内容被保留" $?
|
||||
contains "传输中断" "$OUT"; check "明确提示传输中断" $?
|
||||
@@ -225,8 +225,8 @@ OUT="$("$BIN" --api-key test --base-url "$BASE" --no-confirm --no-auto-save --no
|
||||
contains "超过" "$OUT"; check "非流式路径同样有上限" $?
|
||||
|
||||
echo
|
||||
echo "== 15. 上下文超限:如实报错, 历史一个字节都不动 =="
|
||||
# 这里以前测的是"超限后自动丢弃最老的一轮再重试". 那套机制已经整个删掉:
|
||||
echo "== 15. 上下文超限: 如实报错, 历史一个字节都不动 =="
|
||||
# 这里以前测的是"超限后自动丢弃最老的一轮再重试". 那套机制已经整个删掉:
|
||||
# 判据只是错误文案里的关键词, 代价却是**不可逆地**删掉几轮对话. 现在的行为是
|
||||
# 把服务端的原始错误报出来、历史原样保留, 怎么处理由用户决定 (/back、/reset).
|
||||
OUT="$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
@@ -236,7 +236,7 @@ contains "第一轮" "$OUT"; check "第一轮正常" $?
|
||||
contains "maximum context length" "$OUT"; check "超限时报出服务端的原始错误" $?
|
||||
contains "已丢弃最老" "$OUT" && r=1 || r=0
|
||||
check "不再自动丢弃历史" $r
|
||||
# 两轮 user + 第一轮的 assistant = 3 条:第二轮报错没有留下残缺消息, 也没删东西
|
||||
# 两轮 user + 第一轮的 assistant = 3 条: 第二轮报错没有留下残缺消息, 也没删东西
|
||||
contains "历史消息 3 条" "$OUT"
|
||||
check "历史条数原样 (system 不计)" $?
|
||||
|
||||
@@ -267,7 +267,7 @@ contains "example.invalid" "$OUT"; check "报出重定向目标地址" $?
|
||||
|
||||
echo
|
||||
echo "== 18. --verbose 不得泄露 API key =="
|
||||
# 必须有 --no-confirm:这一轮会触发工具调用, 缺了它就会在真终端上问 (Y/n),
|
||||
# 必须有 --no-confirm: 这一轮会触发工具调用, 缺了它就会在真终端上问 (Y/n),
|
||||
# 整个套件停在那里等人敲回车. 脚本开头的 exec </dev/null 是兜底, 这里写明
|
||||
# 是为了不依赖那个兜底.
|
||||
OUT="$("$BIN" --api-key "sk-VERBOSE-LEAK-CHECK-9876" --base-url "$BASE" \
|
||||
@@ -299,7 +299,7 @@ echo
|
||||
echo "== 21. REPL 行编辑 (方向键 / Home / End / 退格 / 历史) =="
|
||||
# 这个套件要跑十几秒. 用 $(...) 捕获的话, 输出被接到管道会变成块缓冲,
|
||||
# 中途十几秒一点动静都没有 —— 看着就像卡住了在等输入. 所以用 tee 边走边打;
|
||||
# python 的 -u 和 sed 的 -u 都不能省:stdout 接了管道就变块缓冲,
|
||||
# python 的 -u 和 sed 的 -u 都不能省: stdout 接了管道就变块缓冲,
|
||||
# 得每一环都关掉才对.
|
||||
python3 -u tests/repl_edit_check.py "$BIN" "$BASE" 2>&1 \
|
||||
| sed -u 's/^/ /' | tee build/edit_check.out
|
||||
@@ -372,7 +372,7 @@ PYEOF
|
||||
check "存档是自描述的 JSON (name/parent/created/messages)" $?
|
||||
|
||||
echo
|
||||
echo "== 30b. 谱系命名:分支看得见 =="
|
||||
echo "== 30b. 谱系命名: 分支看得见 =="
|
||||
AROOT="$(ls "$ARCHIVE_DIR" | sed 's/\.json$//' | head -1)"
|
||||
OUT="$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--extra=--auto-save --prompt "/import $AROOT" --prompt "n" --prompt "分支甲" \
|
||||
@@ -424,7 +424,7 @@ OUT="$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--prompt "/import $SECRET" --prompt "y" --prompt "/exit" --wait 1.2 2>&1)"
|
||||
contains "紫水晶" "$OUT"; check "回答 y 时打印历史内容" $?
|
||||
contains "助手" "$OUT"; check "打印时带角色标签" $?
|
||||
# 完整输出:system 提示词也展开, 不是只报个体积
|
||||
# 完整输出: system 提示词也展开, 不是只报个体积
|
||||
contains "你是用户的 AI 助手" "$OUT"; check "system 提示词全文展开" $?
|
||||
# 工具调用的参数要完整, 不能只报字节数
|
||||
contains '"command": "echo hello-from-tool"' "$OUT"; check "工具调用参数完整打印" $?
|
||||
@@ -449,7 +449,7 @@ printf '%s' "$OUT" | grep -q $'\033\[2J' ; check "发出清屏转义序列" $?
|
||||
N1="$(printf '%s' "$OUT" | tr '\r' '\n' | grep -oE '历史消息 [0-9]+ 条' | head -1)"
|
||||
N2="$(printf '%s' "$OUT" | tr '\r' '\n' | grep -oE '历史消息 [0-9]+ 条' | tail -1)"
|
||||
[ -n "$N1" ] && [ "$N1" = "$N2" ]; check "/cls 前后历史条数不变 ($N1)" $?
|
||||
# /cls 和 /clear 是两回事:/clear 是 /reset 的别名
|
||||
# /cls 和 /clear 是两回事: /clear 是 /reset 的别名
|
||||
contains "已清空对话历史" "$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--prompt "你好" --prompt "/clear" --wait 1.2 2>&1)"
|
||||
check "/clear 仍然清空对话 (没被 /cls 抢走)" $?
|
||||
@@ -609,7 +609,7 @@ check "关闭确认期间不再询问" $r
|
||||
|
||||
echo
|
||||
echo "== 41. 思考模式与级别 =="
|
||||
# 默认:不发 thinking / reasoning_effort, 由服务端决定
|
||||
# 默认: 不发 thinking / reasoning_effort, 由服务端决定
|
||||
OUT="$("$BIN" --api-key test --base-url "$BASE" --no-confirm --no-auto-save --no-color \
|
||||
"THINK_TEST 默认" 2>&1)"
|
||||
contains "THINK[echo=yes thinking=none effort=none]" "$OUT"; check "默认不发思考字段" $?
|
||||
@@ -640,7 +640,7 @@ OUT="$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--wait 0.6 2>&1)"
|
||||
contains "由服务端决定" "$OUT"; check "/think 无参数显示当前设置" $?
|
||||
contains "思考级别已设为 high" "$OUT"; check "/think LEVEL 设置级别" $?
|
||||
contains "思考级别:high" "$OUT"; check "级别被记住" $?
|
||||
contains "思考级别: high" "$OUT"; check "级别被记住" $?
|
||||
contains "关闭 (发 thinking.type=disabled)" "$OUT"; check "/think off 关闭思考" $?
|
||||
contains "无法识别" "$OUT"; check "/think 非法值给出提示" $?
|
||||
|
||||
@@ -666,7 +666,7 @@ echo "== 44. /meta 元编辑对话历史 =="
|
||||
ARCHIVE_DIR="$(dirname "$BIN")/ligent.archive"
|
||||
rm -rf "$ARCHIVE_DIR"
|
||||
|
||||
# 全都在同一个进程里做完:谱系 (g_lineage)是进程内状态,
|
||||
# 全都在同一个进程里做完: 谱系 (g_lineage)是进程内状态,
|
||||
# 换一个 ligent 进程它就不知道上一份存档叫什么, 会另起一个新根.
|
||||
OUT="$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--extra=--no-confirm --extra=--no-auto-save --extra=--no-history \
|
||||
@@ -702,7 +702,7 @@ python3 - "$ARCHIVE_DIR/$META_NAME.json" <<'PYEOF'
|
||||
import json, sys
|
||||
d = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
msgs = d.get("messages", [])
|
||||
# 结构必须仍然自洽:带 tool_calls 的 assistant 后面必须紧跟 tool
|
||||
# 结构必须仍然自洽: 带 tool_calls 的 assistant 后面必须紧跟 tool
|
||||
ok = True
|
||||
for i, m in enumerate(msgs):
|
||||
if m.get("role") == "assistant" and m.get("tool_calls"):
|
||||
@@ -713,7 +713,7 @@ PYEOF
|
||||
check "删改之后工具调用配对仍然完整" $?
|
||||
|
||||
# meta 模式里的未知命令不该被当成给模型的指令
|
||||
# 单引号很关键:双引号里 bash 会先把 $((6*7)) 算成 42,
|
||||
# 单引号很关键: 双引号里 bash 会先把 $((6*7)) 算成 42,
|
||||
# 那断言就成了"回显里有没有 42", 毫无意义.
|
||||
# 命令写成"输出和输入不同"的样子, 因为行编辑器会把敲进去的字原样回显,
|
||||
# 直接 grep 命令文本永远命中, 证明不了它有没有被执行.
|
||||
@@ -727,7 +727,7 @@ rm -rf "$ARCHIVE_DIR"
|
||||
|
||||
echo
|
||||
echo "== 45. 确认提示上的每种回答 =="
|
||||
# 重点:在确认提示上按 Ctrl-C 必须等于拒绝.
|
||||
# 重点: 在确认提示上按 Ctrl-C 必须等于拒绝.
|
||||
# confirm_tool 用 fgets 读输入, 而 SIGINT 会让它 EINTR 返回 NULL ——
|
||||
# 以前 NULL 被当成"没人能回答, 按默认值放行", 于是 Ctrl-C 批准了写盘.
|
||||
OUT="$(python3 tests/confirm_check.py "$BIN" "$BASE" 2>&1)"
|
||||
@@ -745,7 +745,7 @@ contains "连通性" "$OUT"; check "报告了连通性" $?
|
||||
contains "HTTP 状态" "$OUT"; check "报告了 HTTP 状态码" $?
|
||||
contains "结论" "$OUT"; check "给出了结论" $?
|
||||
|
||||
# 认证这一节必须区分三种情况:密钥对 / 密钥错 / 根本没配.
|
||||
# 认证这一节必须区分三种情况: 密钥对 / 密钥错 / 根本没配.
|
||||
# 以前 http_probe 压根不带密钥, 于是任何要求认证的端点都回 401, 而 doctor
|
||||
# 一律说"密钥被拒绝" —— 把用户引到一个根本不存在的问题上.
|
||||
AUTHED="http://127.0.0.1:$PORT/authed"
|
||||
@@ -758,7 +758,7 @@ OUT="$(env -u LIGENT_API_KEY -u DEEPSEEK_API_KEY -u OPENAI_API_KEY \
|
||||
contains "端点要求认证, 但当前没有配置 API key" "$OUT"; check "没配密钥时不说"被拒绝"" $?
|
||||
contains "不是网络问题" "$OUT"; check "并说明这跟网络无关" $?
|
||||
|
||||
# 404 也不能被说成"密钥没问题":请求根本没走到认证
|
||||
# 404 也不能被说成"密钥没问题": 请求根本没走到认证
|
||||
OUT="$(env -u LIGENT_API_KEY "$BIN" doctor --api-key good-key \
|
||||
--base-url "http://127.0.0.1:$PORT/nope" 2>&1)"
|
||||
contains "这个地址不存在" "$OUT"; check "404 报地址不对" $?
|
||||
@@ -773,8 +773,8 @@ contains "example.invalid" "$OUT"; check "报出跳转目标" $?
|
||||
contains "不自动跟随重定向" "$OUT"; check "解释了为什么不跟随" $?
|
||||
|
||||
echo
|
||||
echo "== 48. read_image:图片以 base64 内联发出, 历史里只留备份引用 =="
|
||||
# 自己造测试图:一张最小的合法 PNG, 和一个"扩展名是 .png、内容不是图片"的文件,
|
||||
echo "== 48. read_image: 图片以 base64 内联发出, 历史里只留备份引用 =="
|
||||
# 自己造测试图: 一张最小的合法 PNG, 和一个"扩展名是 .png、内容不是图片"的文件,
|
||||
# 以及一张故意超过内联上限的大图. 不能依赖 build/ 里已有的东西 ——
|
||||
# make clean 会把它整个删掉.
|
||||
mkdir -p build/tmp
|
||||
@@ -803,7 +803,7 @@ OUT="$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
contains "已读入图片" "$OUT"; check "read_image 读入成功" $?
|
||||
contains "base64" "$OUT"; check "工具结果说明了会以 base64 内联" $?
|
||||
|
||||
# 关键:模型在下一轮**真的看到了**内联图片 (图片只能出现在 user 消息里,
|
||||
# 关键: 模型在下一轮**真的看到了**内联图片 (图片只能出现在 user 消息里,
|
||||
# 而 read_image 是工具、结果只能是字符串, 所以中间补了一条附件消息)
|
||||
contains "IMAGE[data:image/png:" "$OUT"; check "模型收到了内联 base64 图片" $?
|
||||
contains "历史消息 5 条" "$OUT"
|
||||
@@ -821,7 +821,7 @@ sys.exit(0 if ok else 1)
|
||||
PYEOF
|
||||
check "备份名形如 <时间戳>_<4位>.<ext>" $?
|
||||
|
||||
# 这是这一版最要紧的不变量:**归档里不能有图片字节**.
|
||||
# 这是这一版最要紧的不变量: **归档里不能有图片字节**.
|
||||
# 请求里发的是 base64, 但存进历史 / 归档的必须是那个几十字节的引用.
|
||||
python3 - "$ARCHIVE_DIR" "build/tmp/dot.png" <<'PYEOF'
|
||||
import base64, glob, json, sys
|
||||
@@ -829,7 +829,7 @@ import base64, glob, json, sys
|
||||
d, png_path = sys.argv[1], sys.argv[2]
|
||||
raw = open(glob.glob(d + "/*.json")[0], encoding="utf-8").read()
|
||||
|
||||
# 判据只能是**图片载荷本身**. 不能拿 "base64" 这个词, 也不能拿 "data:image":
|
||||
# 判据只能是**图片载荷本身**. 不能拿 "base64" 这个词, 也不能拿 "data:image":
|
||||
# 工具结果里会正当地写"base64 内联后约…", mock 的回复里还会出现
|
||||
# "IMAGE[data:image/png:92]" 这种对收到内容的描述. 真正该消失的是那串载荷.
|
||||
payload = base64.b64encode(open(png_path, "rb").read()).decode()
|
||||
@@ -862,7 +862,7 @@ check "归档里只有对备份的引用、没有图片字节" $?
|
||||
rm -rf "$ARCHIVE_DIR"
|
||||
|
||||
echo
|
||||
echo "== 49. read_image:服务端拒图时把图片摘掉重试, 而不是每轮都失败 =="
|
||||
echo "== 49. read_image: 服务端拒图时把图片摘掉重试, 而不是每轮都失败 =="
|
||||
rm -rf "$ARCHIVE_DIR"
|
||||
# 三轮必须在**同一个** repl 进程里. mock 的 told_once 是一次性的, 重开一个
|
||||
# 进程它就不再拒了; 而且"降级要就地生效"这件事本身也只能在同一会话里验.
|
||||
@@ -880,7 +880,7 @@ contains "IMAGE[none]" "$OUT"; check "重试时请求里确实没有图片了" $
|
||||
contains "不符合 API 所需的格式标准" "$OUT"; check "模型收到了"请规范化图片"的说明" $?
|
||||
contains "请规范化图片" "$OUT"; check "说明里给了下一步动作" $?
|
||||
|
||||
# 降级是**就地**的:同一会话的下一轮也不该再有图片,
|
||||
# 降级是**就地**的: 同一会话的下一轮也不该再有图片,
|
||||
# 否则每轮都会重新踩同一个 4xx, 用户只能 /reset.
|
||||
LAST_IMG="$(printf '%s\n' "$OUT" | grep -o 'IMAGE\[[^]]*\]' | tail -1)"
|
||||
[ "$LAST_IMG" = "IMAGE[none]" ]; check "下一轮也是干净的 ($LAST_IMG)" $?
|
||||
@@ -890,14 +890,14 @@ N="$(ls "$ARCHIVE_DIR"/images/*.png 2>/dev/null | wc -l)"
|
||||
[ "$N" -eq 1 ]; check "备份没有被重复写 ($N 份)" $?
|
||||
rm -rf "$ARCHIVE_DIR"
|
||||
|
||||
echo "== 50. read_image:不认识的格式、以及过大的图, 都要在本地拒绝 =="
|
||||
echo "== 50. read_image: 不认识的格式、以及过大的图, 都要在本地拒绝 =="
|
||||
rm -rf "$ARCHIVE_DIR"
|
||||
OUT="$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--extra=--no-confirm --extra=--no-auto-save --extra=--no-history \
|
||||
--prompt "IMAGE_BAD_TEST 读一个假图片" --prompt "/exit" --wait 1.3 2>&1)"
|
||||
contains "不认识的图片格式" "$OUT"; check "按内容判断并拒绝 (扩展名是 .png 也没用)" $?
|
||||
|
||||
# 过大的图:本地当场拒, 不备份、不发请求
|
||||
# 过大的图: 本地当场拒, 不备份、不发请求
|
||||
OUT="$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--extra=--no-confirm --extra=--no-auto-save --extra=--no-history \
|
||||
--prompt "IMAGE_OVERSIZE_TEST 读一张大图" --prompt "/exit" --wait 1.5 2>&1)"
|
||||
@@ -907,7 +907,7 @@ N="$(ls "$ARCHIVE_DIR"/images/*.png 2>/dev/null | wc -l)"
|
||||
[ "$N" -eq 0 ]; check "被拒的图不备份 ($N 份)" $?
|
||||
rm -rf "$ARCHIVE_DIR"
|
||||
|
||||
echo "== 51. 能力探测序列:按模型能力决定暴露哪些工具 =="
|
||||
echo "== 51. 能力探测序列: 按模型能力决定暴露哪些工具 =="
|
||||
# 这一段自己把 LIGENT_PROBE 打开 (脚本全局是关的, 理由见文件开头).
|
||||
PROBE="LIGENT_PROBE=1"
|
||||
run_probe() { # run_probe <模型名> <额外参数...>
|
||||
@@ -917,17 +917,17 @@ run_probe() { # run_probe <模型名> <额外参数...>
|
||||
--extra="--model" --extra="$model" "$@" 2>&1
|
||||
}
|
||||
|
||||
# --- 正常模型:探测跑过, read_image 在 ---
|
||||
# --- 正常模型: 探测跑过, read_image 在 ---
|
||||
OUT="$(run_probe probe-model --prompt "TOOLS_ECHO_TEST 看看工具表" --prompt "/exit" --wait 2.0)"
|
||||
contains "TOOLS[" "$OUT"; check "探测之后请求正常发出" $?
|
||||
printf '%s\n' "$OUT" | grep -q 'TOOLS\[[^]]*read_image'
|
||||
check "支持图片的模型:工具表里有 read_image" $?
|
||||
check "支持图片的模型: 工具表里有 read_image" $?
|
||||
printf '%s\n' "$OUT" | grep -q 'TOOLS\[[^]]*run_command'
|
||||
check "其他工具不受影响" $?
|
||||
contains "模型能力:多模态 (图片输入)支持" "$OUT"; check "探测后打一条结论日志" $?
|
||||
contains "模型能力: 多模态 (图片输入)支持" "$OUT"; check "探测后打一条结论日志" $?
|
||||
contains "工具调用 支持" "$OUT"; check "日志里也说明工具调用能力" $?
|
||||
|
||||
# --- 模型明确不收图片:read_image 必须从工具表里消失 ---
|
||||
# --- 模型明确不收图片: read_image 必须从工具表里消失 ---
|
||||
OUT="$(run_probe no-vision-model --prompt "TOOLS_ECHO_TEST 看看工具表" \
|
||||
--prompt "/tools" --prompt "/exit" --wait 2.0)"
|
||||
contains "多模态 (图片输入)不支持 (已隐藏 read_image)" "$OUT"
|
||||
@@ -942,13 +942,13 @@ printf '%s\n' "$OUT" | grep -q 'TOOLS\[[^]]*run_command'
|
||||
check "只摘掉 read_image, 别的工具留着" $?
|
||||
contains "read_image 已隐藏" "$OUT"; check "/tools 不列已经藏掉的工具" $?
|
||||
|
||||
# --- 模型完全不收 tools:整张表都不发 ---
|
||||
# --- 模型完全不收 tools: 整张表都不发 ---
|
||||
OUT="$(run_probe no-tools-model --prompt "TOOLS_ECHO_TEST 看看工具表" --prompt "/exit" --wait 2.0)"
|
||||
contains "工具调用 不支持 (不再发送工具表)" "$OUT"
|
||||
check "如实说明了工具调用能力与动作" $?
|
||||
contains "TOOLS[]" "$OUT"; check "工具表确实是空的" $?
|
||||
|
||||
# --- 探测结论不确定:一律按"支持"处理, 宁可多暴露 ---
|
||||
# --- 探测结论不确定: 一律按"支持"处理, 宁可多暴露 ---
|
||||
# broken-probe 模型只在**探测请求**上回 401, 真实请求照常.
|
||||
OUT="$(run_probe broken-probe-model --prompt "TOOLS_ECHO_TEST 看看工具表" --prompt "/exit" --wait 2.0)"
|
||||
contains "多模态 (图片输入)未测" "$OUT"; check "探不出来时如实说明是未测" $?
|
||||
@@ -956,11 +956,11 @@ contains "没测出来的一律按「支持」处理" "$OUT"; check "说明按
|
||||
printf '%s\n' "$OUT" | grep -q 'TOOLS\[[^]]*read_image'
|
||||
check "结论不确定时不藏工具 (宁可多暴露)" $?
|
||||
|
||||
# --- 关掉探测:一个试探请求都不发, 工具表保持完整 ---
|
||||
# --- 关掉探测: 一个试探请求都不发, 工具表保持完整 ---
|
||||
OUT="$(env LIGENT_PROBE=0 python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--extra=--no-confirm --extra=--no-auto-save --extra=--no-history \
|
||||
--prompt "TOOLS_ECHO_TEST 看看工具表" --prompt "/exit" --wait 1.6 2>&1)"
|
||||
if printf '%s\n' "$OUT" | grep -q '检测模型能力\|模型能力:'; then
|
||||
if printf '%s\n' "$OUT" | grep -q '检测模型能力\|模型能力: '; then
|
||||
check "--no-probe 时既不该发探测请求, 也不该有结论日志" 1
|
||||
else
|
||||
check "--no-probe 时既不该发探测请求, 也不该有结论日志" 0
|
||||
@@ -968,8 +968,8 @@ fi
|
||||
printf '%s\n' "$OUT" | grep -q 'TOOLS\[[^]]*read_image'
|
||||
check "--no-probe 时工具表保持完整" $?
|
||||
|
||||
echo "== 52. /model 不带参数:打印在线列表, 输入行里选序号或直接写名字 =="
|
||||
# 这一段刻意不用光标菜单, 而是 /import 那种"列表 + 普通输入行":
|
||||
echo "== 52. /model 不带参数: 打印在线列表, 输入行里选序号或直接写名字 =="
|
||||
# 这一段刻意不用光标菜单, 而是 /import 那种"列表 + 普通输入行":
|
||||
# 既能填序号, 也能直接写一个**列表里没有**的模型名 (内部模型就走这条).
|
||||
run_model() { # run_model <要依次输入的行...>; 第一行固定是 /model
|
||||
local a=()
|
||||
@@ -979,7 +979,7 @@ run_model() { # run_model <要依次输入的行...>; 第一行固定是 /model
|
||||
--prompt "/model" "${a[@]}" --prompt "/exit" --wait 1.4 2>&1
|
||||
}
|
||||
|
||||
# --- 列表本身:编号、当前模型被标出、去重与坏条目被跳过 ---
|
||||
# --- 列表本身: 编号、当前模型被标出、去重与坏条目被跳过 ---
|
||||
OUT="$(run_model "2")"
|
||||
contains "正在获取模型列表 ($BASE/models)" "$OUT"; check "用的是标准的 /models 路径" $?
|
||||
contains "服务端列出了 3 个模型" "$OUT"
|
||||
@@ -991,7 +991,7 @@ contains "选择序号, 或直接输入模型名" "$OUT"; check "输入行的提
|
||||
# --- 按序号选 ---
|
||||
contains "模型已切换为 alpha-model" "$OUT"; check "填序号能选中对应模型" $?
|
||||
|
||||
# --- 直接写一个**列表里没有**的名字:这是内部模型的入口, 必须允许 ---
|
||||
# --- 直接写一个**列表里没有**的名字: 这是内部模型的入口, 必须允许 ---
|
||||
OUT="$(run_model "internal-model-v9")"
|
||||
contains "模型已切换为 internal-model-v9" "$OUT"
|
||||
check "能直接写列表里没有的模型名 (内部模型走这条)" $?
|
||||
@@ -1010,7 +1010,7 @@ else
|
||||
check "取消之后什么都没改" 0
|
||||
fi
|
||||
|
||||
# --- 序号越界:报错并取消, 不能当成模型名去用 ---
|
||||
# --- 序号越界: 报错并取消, 不能当成模型名去用 ---
|
||||
OUT="$(run_model "9")"
|
||||
contains "序号 9 超出范围" "$OUT"; check "序号越界要报出来" $?
|
||||
if printf '%s\n' "$OUT" | grep -q "模型已切换"; then
|
||||
@@ -1036,7 +1036,7 @@ else
|
||||
check "带参数时不去拉列表" 0
|
||||
fi
|
||||
|
||||
# --- 服务端返回空列表:说清楚, 并提示还能直接指定名字 ---
|
||||
# --- 服务端返回空列表: 说清楚, 并提示还能直接指定名字 ---
|
||||
printf 'empty' > build/tmp/models_mode
|
||||
OUT="$(run_model "1")"
|
||||
rm -f build/tmp/models_mode
|
||||
@@ -1044,7 +1044,7 @@ contains "拿不到模型列表" "$OUT"; check "空列表时如实报错" $?
|
||||
contains "空的模型列表" "$OUT"; check "说清是空列表而不是网络故障" $?
|
||||
contains "/model <名字>" "$OUT"; check "提示还能直接指定名字" $?
|
||||
|
||||
# --- 服务端返回的不是 JSON:也不能崩, 同样给出可用的退路 ---
|
||||
# --- 服务端返回的不是 JSON: 也不能崩, 同样给出可用的退路 ---
|
||||
printf 'garbage' > build/tmp/models_mode
|
||||
OUT="$(run_model "1")"
|
||||
rm -f build/tmp/models_mode
|
||||
@@ -1058,11 +1058,11 @@ rm -f build/tmp/models_mode
|
||||
contains "服务端列出了 2 个模型" "$OUT"; check "裸数组形状也能解析" $?
|
||||
contains "模型已切换为 beta-model" "$OUT"; check "裸数组里也能按序号选" $?
|
||||
|
||||
echo "== 53. 一条 assistant 里两个工具调用 + 图片:附件不能插在 tool 消息中间 =="
|
||||
echo "== 53. 一条 assistant 里两个工具调用 + 图片: 附件不能插在 tool 消息中间 =="
|
||||
# 回归用例. 曾经的写法是"每推完一条 tool 消息就把图片附件推出去", 于是
|
||||
# assistant 带两个调用、第一个是 read_image 时, 历史变成
|
||||
# assistant(tool_calls) / tool(读图) / user(附件) / tool(另一个)
|
||||
# 中间那条 user 把 tool 段截断了, 服务端回 400:
|
||||
# 中间那条 user 把 tool 段截断了, 服务端回 400:
|
||||
# An assistant message with 'tool_calls' must be followed by tool messages
|
||||
# responding to each 'tool_call_id'. (insufficient tool messages ...)
|
||||
# 而且这段历史留在上下文里, 之后每一轮都会 400 —— 用户只能 /reset.
|
||||
@@ -1090,11 +1090,11 @@ else
|
||||
fi
|
||||
rm -rf "$ARCHIVE_DIR"
|
||||
|
||||
# 顺带验一下修复路径本身:mock 校验会让坏历史当场 400, 所以这里换个角度 ——
|
||||
# 顺带验一下修复路径本身: mock 校验会让坏历史当场 400, 所以这里换个角度 ——
|
||||
# 直接看 agent_repair_tool_pairing 的单元测试 (test_agent).
|
||||
|
||||
echo "== 54. /back 报的条数不含 system 提示词 =="
|
||||
# 用户报过一个:聊一句「hi」再 /back, 屏幕显示"历史剩 1 条", 但那段对话其实
|
||||
# 用户报过一个: 聊一句「hi」再 /back, 屏幕显示"历史剩 1 条", 但那段对话其实
|
||||
# 已经空了 —— 那 1 条是 system 提示词. system 是配置不是对话, 不该算进"历史".
|
||||
# 现在 /history、/back、/unback、/export、/import 统一用"不含 system"的口径.
|
||||
OUT="$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
@@ -1107,7 +1107,7 @@ check "/history 也报 0 条 (与 /back 口径一致)" $?
|
||||
contains "历史回到 4 条" "$OUT"; check "/unback 之后回到 4 条" $?
|
||||
contains "不含 system" "$OUT"; check "说清楚这个口径不含 system 提示词" $?
|
||||
|
||||
echo "== 55. /include:把文件附带进下一条消息 =="
|
||||
echo "== 55. /include: 把文件附带进下一条消息 =="
|
||||
mkdir -p build/tmp/inc/sub
|
||||
printf 'int main(void){return 0;}\n' > build/tmp/inc/a.c
|
||||
printf 'hello text\n' > build/tmp/inc/b.txt
|
||||
@@ -1131,7 +1131,7 @@ inc() { # inc <prompt...>; 先切到场景目录 (/cd 是 REPL 命令, 没有同
|
||||
--prompt "/cd build/tmp/inc" "$@" --wait 1.5 2>&1
|
||||
}
|
||||
|
||||
# --- glob 参数:命中几个就附带几个 ---
|
||||
# --- glob 参数: 命中几个就附带几个 ---
|
||||
OUT="$(inc --prompt "/include *.c" --prompt "INCLUDE_TEST 看看" --prompt "/exit")"
|
||||
contains "已附带 1 个文件" "$OUT"; check "glob 展开并暂存" $?
|
||||
contains "INC[text=2 img=0 files=a.c]" "$OUT"
|
||||
@@ -1143,11 +1143,11 @@ OUT="$(inc --prompt "/include *.c" --prompt "/include *.txt" \
|
||||
--prompt "INCLUDE_TEST 两个" --prompt "/exit")"
|
||||
contains "INC[text=3 img=0" "$OUT"; check "两次附带的文件都发出去了" $?
|
||||
|
||||
# --- 图片:存备份、以 ligent_image 引用参与请求 ---
|
||||
# --- 图片: 存备份、以 ligent_image 引用参与请求 ---
|
||||
OUT="$(inc --prompt "/include pic.png" --prompt "INCLUDE_TEST 看图" --prompt "/export" --prompt "/exit")"
|
||||
contains "〔图片〕" "$OUT"; check "识别出是图片并标注" $?
|
||||
contains "INC[text=1 img=1" "$OUT"; check "图片以 image_url 内联发出" $?
|
||||
# 备份必须落在**二进制所在目录**, 不是当前工作目录 —— 这一条是有来历的:
|
||||
# 备份必须落在**二进制所在目录**, 不是当前工作目录 —— 这一条是有来历的:
|
||||
# image_set_bindir() 曾经从来没被调用过, 于是备份跟着 cwd 走, /cd 到别处
|
||||
# 带图就会写到那儿, 之后从别处发消息会因为找不到备份把图降级掉.
|
||||
# 测试一直从二进制所在目录跑, "." 恰好等于正确值, 所以一直没暴露.
|
||||
@@ -1171,7 +1171,7 @@ rm -rf "$ARCHIVE_DIR"
|
||||
# --- 内容不是图片的 .png 按文本附带 (认内容不认扩展名) ---
|
||||
OUT="$(inc --prompt "/include fake.png" --prompt "INCLUDE_TEST 假的" --prompt "/exit")"
|
||||
contains "INC[text=2 img=0 files=fake.png]" "$OUT"
|
||||
check "按内容判断:不是图片就当文本带" $?
|
||||
check "按内容判断: 不是图片就当文本带" $?
|
||||
|
||||
# --- 二进制被跳过 ---
|
||||
printf '\x00\x01\x02binary' > build/tmp/inc/bin.dat
|
||||
@@ -1183,14 +1183,14 @@ contains "二进制" "$OUT"; check "说清跳过原因" $?
|
||||
OUT="$(inc --prompt "/include 不存在的模式*.zzz" --prompt "/exit")"
|
||||
contains "没有匹配到文件" "$OUT"; check "匹配不到要说出来" $?
|
||||
|
||||
# --- 交互式:进目录、退回来、选文件 ---
|
||||
# --- 交互式: 进目录、退回来、选文件 ---
|
||||
# 序号 3 是 sub/ (a.c / b.txt / sub 排序后), 进去再回车结束;
|
||||
# 然后回头选一个文件.
|
||||
OUT="$(inc --prompt "/include" --prompt "3" --prompt "" \
|
||||
--prompt "/include" --prompt "1" --prompt "" --prompt "/exit")"
|
||||
contains "进入交互式选择" "$OUT"; check "无参数进入交互式" $?
|
||||
contains " (目录)" "$OUT"; check "目录被标出来" $?
|
||||
contains "当前目录:" "$OUT"; check "显示了当前目录" $?
|
||||
contains "当前目录: " "$OUT"; check "显示了当前目录" $?
|
||||
contains "已选中" "$OUT"; check "选中后有回执" $?
|
||||
|
||||
# --- clear ---
|
||||
@@ -1199,7 +1199,7 @@ OUT="$(inc --prompt "/include *.c" --prompt "/include clear" \
|
||||
contains "已清空待附带的文件" "$OUT"; check "/include clear 清空暂存" $?
|
||||
contains "INC[text=1 img=0" "$OUT"; check "清空之后只发用户那句话" $?
|
||||
|
||||
# --- 模型不支持视觉时:文本照带, 图片跳过并说明 ---
|
||||
# --- 模型不支持视觉时: 文本照带, 图片跳过并说明 ---
|
||||
OUT="$(env LIGENT_PROBE=1 python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--extra=--no-confirm --extra=--no-auto-save --extra=--no-history \
|
||||
--extra=--model --extra=no-vision-model \
|
||||
@@ -1220,8 +1220,8 @@ OUT="$(env LIGENT_PROBE=1 python3 tests/repl_drive.py --bin "$BIN" --base-url "$
|
||||
contains "INC[text=1 img=1" "$OUT"; check "支持视觉时图片正常附带" $?
|
||||
rm -rf "$ARCHIVE_DIR"
|
||||
|
||||
echo "== 56. run_command elevated:经 sudo 提权执行单条命令 =="
|
||||
# 全程用**替身 sudo**:不需要真权限, 也永远不会碰到谁的真实密码.
|
||||
echo "== 56. run_command elevated: 经 sudo 提权执行单条命令 =="
|
||||
# 全程用**替身 sudo**: 不需要真权限, 也永远不会碰到谁的真实密码.
|
||||
# 它把 argv 记下来, 于是"ligent 到底怎么调 sudo 的"能断言到参数级.
|
||||
FAKEBIN="$ROOT/build/tmp/fakebin"
|
||||
mkdir -p "$FAKEBIN"
|
||||
@@ -1242,7 +1242,7 @@ elev() { # elev <额外环境变量...> -- <repl 参数...>
|
||||
PATH="$FAKEBIN:$PATH" FAKE_SUDO_LOG="$SULOG" "$@" 2>&1
|
||||
}
|
||||
|
||||
# --- 模型请求提权:命令经 sudo 以 root 跑, 输出照常被捕获 ---
|
||||
# --- 模型请求提权: 命令经 sudo 以 root 跑, 输出照常被捕获 ---
|
||||
OUT="$(elev python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--extra=--no-confirm --extra=--no-auto-save --extra=--no-history \
|
||||
--prompt "ELEVATED_TEST 提权跑个命令" --prompt "/exit" --wait 1.8)"
|
||||
@@ -1250,10 +1250,10 @@ contains "提权后的输出" "$OUT"; check "提权命令真的执行了, 输出
|
||||
contains "ELEV_SAW[提权后的输出" "$OUT"; check "输出回填给了模型" $?
|
||||
grep -q '^-u root -- /bin/sh -c echo 提权后的输出$' "$SULOG"
|
||||
check "调用形式是 sudo -u root -- <argv> (argv 形式, 不经多余 shell)" $?
|
||||
# 这一条是这次设计的核心:**不能带 -n**. 带 -n 就意味着"不许提问",
|
||||
# 这一条是这次设计的核心: **不能带 -n**. 带 -n 就意味着"不许提问",
|
||||
# 于是需要密码时直接失败 —— 而用户明确要的是"提权要有身份验证".
|
||||
printf '%s\n' "$(cat "$SULOG")" | grep -q -- '-n' && r=1 || r=0
|
||||
[ "$r" -eq 0 ]; check "不带 -n:需要密码时要让 sudo 能问出来" $?
|
||||
[ "$r" -eq 0 ]; check "不带 -n: 需要密码时要让 sudo 能问出来" $?
|
||||
|
||||
# --- 确认提示必须先把要跑的东西摆出来 ---
|
||||
OUT="$(elev python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
@@ -1262,7 +1262,7 @@ OUT="$(elev python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
contains "即将执行 run_command" "$OUT"; check "提权命令同样过确认提示" $?
|
||||
contains "提权后的输出" "$OUT"; check "确认之后才执行" $?
|
||||
|
||||
# --- 认证失败:模型要收到能理解的话, 而不是一片空白 ---
|
||||
# --- 认证失败: 模型要收到能理解的话, 而不是一片空白 ---
|
||||
OUT="$(PATH="$FAKEBIN:$PATH" FAKE_SUDO_LOG="$SULOG" FAKE_SUDO_REJECT=1 \
|
||||
python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--extra=--no-confirm --extra=--no-auto-save --extra=--no-history \
|
||||
@@ -1271,7 +1271,7 @@ contains "提权命令失败" "$OUT"; check "提权失败要告诉模型" $?
|
||||
contains "认证没有通过" "$OUT"; check "说清是认证没过 (而不是命令本身的问题)" $?
|
||||
contains "不要反复重试" "$OUT"; check "明确叫它别反复重试" $?
|
||||
|
||||
# --- 没有控制终端时:直接拒绝, 绝不发起一个等不到输入的进程 ---
|
||||
# --- 没有控制终端时: 直接拒绝, 绝不发起一个等不到输入的进程 ---
|
||||
# setsid 摘掉控制终端, 正是"没人能输密码"的情形.
|
||||
if command -v setsid >/dev/null 2>&1; then
|
||||
OUT="$(setsid "$BIN" --api-key test --base-url "$BASE" --no-color \
|
||||
@@ -1283,11 +1283,11 @@ if command -v setsid >/dev/null 2>&1; then
|
||||
# 而且它必须是**立刻**失败, 不能卡在那儿等密码
|
||||
contains "没有可用的控制终端" "$OUT" || true
|
||||
else
|
||||
echo " (跳过:没有 setsid)"
|
||||
echo " (跳过: 没有 setsid)"
|
||||
fi
|
||||
|
||||
echo "== 57. 离线回放:把 /log 录下来的真实会话当夹具 =="
|
||||
# 这是 /log 存在的**真正理由**:录一次真的, 之后离线反复回放.
|
||||
echo "== 57. 离线回放: 把 /log 录下来的真实会话当夹具 =="
|
||||
# 这是 /log 存在的**真正理由**: 录一次真的, 之后离线反复回放.
|
||||
# 录的是原始字节 (请求体 + 未解析的响应原文), 所以回放时 http.c 的分块读取、
|
||||
# llm.c 的 SSE 行切分与 tool_calls 增量重组, 全都在真实字节上跑.
|
||||
FIX="$ROOT/tests/fixtures/session.log"
|
||||
@@ -1311,7 +1311,7 @@ sys.exit(0 if s.connect_ex(('127.0.0.1',$1))==0 else 1)" && break
|
||||
done
|
||||
}
|
||||
|
||||
# --- 用离线服务端跑**真的** ligent:它应当沿着录制里的路径走完 ---
|
||||
# --- 用离线服务端跑**真的** ligent: 它应当沿着录制里的路径走完 ---
|
||||
replay_serve 8899 "$FIX" --strict
|
||||
OUT="$("$BIN" --api-key test --base-url "http://127.0.0.1:8899/v1" --no-color \
|
||||
--no-confirm --no-auto-save --no-history --no-probe "please run it" 2>&1)"
|
||||
@@ -1320,7 +1320,7 @@ kill $RPID 2>/dev/null; wait $RPID 2>/dev/null
|
||||
[ "$RC" -eq 0 ]; check "在录制上跑完一轮 (退出码 $RC)" $?
|
||||
contains "hello-from-tool" "$OUT"; check "工具调用从被切碎的 SSE 里正确重组" $?
|
||||
contains "FINAL:" "$OUT"; check "拿到了录制里的最终回答" $?
|
||||
# strict:请求的**结构**必须和录制时一致 —— 这才是"报文形状没变"的断言
|
||||
# strict: 请求的**结构**必须和录制时一致 —— 这才是"报文形状没变"的断言
|
||||
if grep -q "结构" "$RLOG"; then
|
||||
check "请求结构与录制一致" 1
|
||||
grep -A2 "结构" "$RLOG" | head -4
|
||||
@@ -1328,7 +1328,7 @@ else
|
||||
check "请求结构与录制一致 (strict 没报差异)" 0
|
||||
fi
|
||||
|
||||
# --- 错误体:录下来的 400 也要能被原样解析出可读信息 ---
|
||||
# --- 错误体: 录下来的 400 也要能被原样解析出可读信息 ---
|
||||
replay_serve 8898 "$ROOT/tests/fixtures/error.log" --strict
|
||||
OUT="$("$BIN" --api-key test --base-url "http://127.0.0.1:8898/v1" --no-color \
|
||||
--no-confirm --no-auto-save --no-history --no-probe --retries 0 \
|
||||
@@ -1340,7 +1340,7 @@ contains "mock 模拟的服务端错误" "$OUT"; check "错误体从真实字节
|
||||
|
||||
# --- 请求比录制多时, 回放器要明确报出来 (而不是静静回错东西)---
|
||||
replay_serve 8897 "$ROOT/tests/fixtures/error.log" --strict
|
||||
# 录制里只有 1 次往返. 跑两次:第二次就没东西可回了.
|
||||
# 录制里只有 1 次往返. 跑两次: 第二次就没东西可回了.
|
||||
for _ in 1 2; do
|
||||
"$BIN" --api-key test --base-url "http://127.0.0.1:8897/v1" --no-color \
|
||||
--no-confirm --no-auto-save --no-history --no-probe --retries 0 \
|
||||
@@ -1350,8 +1350,8 @@ kill $RPID 2>/dev/null; wait $RPID 2>/dev/null
|
||||
contains "没有更多响应" "$(cat "$RLOG")"
|
||||
check "录制用完后明确报出来 (而不是静静回错东西)" $?
|
||||
|
||||
echo "== 58. 工具输出里混进非法 UTF-8:不能让之后每个请求都被拒 =="
|
||||
# 坏字节的来源很日常:某条命令在非 UTF-8 locale 下打出来的路径、被截断的多
|
||||
echo "== 58. 工具输出里混进非法 UTF-8: 不能让之后每个请求都被拒 =="
|
||||
# 坏字节的来源很日常: 某条命令在非 UTF-8 locale 下打出来的路径、被截断的多
|
||||
# 字节字符、二进制文件里混的一小段. 它会跟着工具结果进对话历史, 再被序列化
|
||||
# 进请求体 —— 序列化器若原样吐出, DeepSeek 回
|
||||
# 400 ... invalid unicode code point
|
||||
@@ -1366,10 +1366,10 @@ case "$OUT" in
|
||||
*"替换字符=0"*) rc=1;; # 一个都没替换 —— 坏字节原样发出去了
|
||||
*) rc=0;;
|
||||
esac
|
||||
check "请求体是合法 UTF-8:服务端没拒, 且坏字节被换成了 U+FFFD" $rc
|
||||
check "请求体是合法 UTF-8: 服务端没拒, 且坏字节被换成了 U+FFFD" $rc
|
||||
contains "读完了" "$OUT"; check "这一轮正常跑完" $?
|
||||
|
||||
echo "== 59. 界面语言:--lang / LIGENT_LANG / LC_* =="
|
||||
echo "== 59. 界面语言: --lang / LIGENT_LANG / LC_* =="
|
||||
# 前面几十段都跑在顶层锁死的 zh 下 (见文件开头的 export). 这一段专门走英文,
|
||||
# 因为"翻译漏了"和"翻译错了"只有真的把每条路径跑一遍才看得出来.
|
||||
OUT="$(LIGENT_LANG=en "$BIN" --help 2>&1)"
|
||||
@@ -1404,19 +1404,19 @@ contains "只接受 auto / en / zh" "$OUT"; check "拒绝时说清可选值" $?
|
||||
|
||||
# doctor 是最长的一段界面文案, 分节括号也要跟着语言走
|
||||
OUT="$(LIGENT_LANG=en "$BIN" doctor --offline 2>&1)"
|
||||
# 注意:方括号在 case 模式里是字符类, 所以这里只断言括号里的文字
|
||||
# 注意: 方括号在 case 模式里是字符类, 所以这里只断言括号里的文字
|
||||
contains "binary & config location" "$OUT"; check "英文分节标题" $?
|
||||
contains "Bottom line:" "$OUT"; check "英文结论行" $?
|
||||
OUT="$(LIGENT_LANG=zh "$BIN" doctor --offline 2>&1)"
|
||||
contains "【二进制与配置位置】" "$OUT"; check "中文分节用【】" $?
|
||||
contains "结论:" "$OUT"; check "中文结论行" $?
|
||||
contains "结论: " "$OUT"; check "中文结论行" $?
|
||||
|
||||
echo
|
||||
echo "== 60. 单轮往返次数:默认不限, 正数才是上限 =="
|
||||
echo "== 60. 单轮往返次数: 默认不限, 正数才是上限 =="
|
||||
# 默认 (配置文件里没写 max_steps)要能跑完 4 次工具往返 —— 这正是"默认不限"
|
||||
# 的可观测形式:任何小上限都会在这里当场暴露.
|
||||
# 的可观测形式: 任何小上限都会在这里当场暴露.
|
||||
OUT="$("$BIN" --api-key test --base-url "$BASE" --no-confirm --no-auto-save --no-color "MANYSTEPS_TEST" 2>&1)"
|
||||
contains "STEPS_DONE[4]" "$OUT"; check "默认不限往返次数:4 次工具往返跑完" $?
|
||||
contains "STEPS_DONE[4]" "$OUT"; check "默认不限往返次数: 4 次工具往返跑完" $?
|
||||
contains "run_command" "$OUT"; check "每一步的工具调用都真实执行了" $?
|
||||
|
||||
# 显式写 0 也是不限 (0 = 不限是这条接口的约定)
|
||||
@@ -1442,9 +1442,9 @@ OUT="$("$BIN" --print-config 2>&1)"
|
||||
contains "max_steps : 不限" "$OUT"; check "--print-config 显示不限" $?
|
||||
|
||||
echo
|
||||
echo "== 61. edit_file 的调用行:\"路径\" [行号], 成功后给带行号的新旧对照 =="
|
||||
echo "== 61. edit_file 的调用行: \"路径\" [行号], 成功后给带行号的新旧对照 =="
|
||||
# 以前这一行是把整个参数 JSON 原样打出来 (截 256 字节)—— 对 edit_file 来说那是
|
||||
# 一整段 old_string/new_string:既刷屏, 又看不出到底要改哪里. 现在摘要只有
|
||||
# 一整段 old_string/new_string: 既刷屏, 又看不出到底要改哪里. 现在摘要只有
|
||||
# "哪个文件、哪几行" (路径带引号、行号用方括号), 改动内容用带行号的 -/+ 两段摆开.
|
||||
mkprobe() {
|
||||
python3 -c "
|
||||
@@ -1460,7 +1460,7 @@ check "调用行是 \"路径\" [行号], 不再抖 JSON" $?
|
||||
contains " │ - 2 OLD1" "$OUT"; check "旧内容逐行显示, 且带文件行号" $?
|
||||
contains " │ - 9 OLD8" "$OUT"; check "行号随行递增 (2..9)" $?
|
||||
contains " │ + 2 NEW" "$OUT"
|
||||
check "新内容也带行号 (两侧起始行号相同:替换就落在原处)" $?
|
||||
check "新内容也带行号 (两侧起始行号相同: 替换就落在原处)" $?
|
||||
contains "还有 4 行" "$OUT"; check "每侧最多 8 行, 其余折成一句" $?
|
||||
contains " │ - 10 OLD9" "$OUT" && r=1 || r=0
|
||||
check "第 9 行起不再逐行铺开" $r
|
||||
@@ -1469,10 +1469,10 @@ check "参数 JSON 一个字节都不上屏" $r
|
||||
contains "DONE[" "$OUT"; check "工具确实执行了" $?
|
||||
|
||||
echo
|
||||
echo "== 62. 确认提示:先摆带行号的新旧对照, 再问 Y/n =="
|
||||
echo "== 62. 确认提示: 先摆带行号的新旧对照, 再问 Y/n =="
|
||||
# 这一版把确认提示里的"替换 <旧> -> <新>"删掉了 —— 两边各截 60 字符、压成一行,
|
||||
# 看不出动了几行、动的是哪几行. 现在摆的是和事后展示同一份带行号 diff,
|
||||
# 而且必须在提问**之前**:先让人点头、再让人看改什么, 那是反的.
|
||||
# 而且必须在提问**之前**: 先让人点头、再让人看改什么, 那是反的.
|
||||
mkprobe
|
||||
OUT="$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--extra=--confirm --extra=--no-auto-save --extra=--no-color \
|
||||
@@ -1490,7 +1490,7 @@ i_diff = t.find("- 2 OLD1")
|
||||
i_ask = t.find("继续?")
|
||||
raise SystemExit(0 if 0 <= i_diff < i_ask else 1)
|
||||
PY
|
||||
check "顺序:先摆 diff、再提问" $?
|
||||
check "顺序: 先摆 diff、再提问" $?
|
||||
contains "替换:" "$OUT" && r=1 || r=0
|
||||
check "不再出现「替换: 旧 -> 新」那一行" $r
|
||||
# 确认过的改动不该在执行后再摆一遍 diff (否则像是要改两次)
|
||||
@@ -1498,7 +1498,7 @@ contains "已在 build/tmp/edit_probe.txt 第 2 行替换 1 处" "$OUT"
|
||||
check "确认之后只给一句回执, 不重复 diff" $?
|
||||
|
||||
echo
|
||||
echo "== 63. edit_file 失败时:同样只有摘要 + 错误 =="
|
||||
echo "== 63. edit_file 失败时: 同样只有摘要 + 错误 =="
|
||||
mkprobe
|
||||
OUT="$("$BIN" --api-key test --base-url "$BASE" --no-confirm --no-auto-save --no-color "EDITTEST_FAIL" 2>&1)"
|
||||
contains '{/} edit_file "build/tmp/edit_probe.txt" [999]' "$OUT"
|
||||
@@ -1508,14 +1508,14 @@ contains '"old_string"' "$OUT" && r=1 || r=0
|
||||
check "失败时同样不抖 JSON" $r
|
||||
|
||||
echo
|
||||
echo "== 64. /probe:手动测一次能力 (绕开 --no-probe 与缓存)=="
|
||||
echo "== 64. /probe: 手动测一次能力 (绕开 --no-probe 与缓存)=="
|
||||
# 这一段的 e2e 全局设了 LIGENT_PROBE=0 (各处用例都按"请求序列"写, 别被探测整体推后),
|
||||
# 正好拿来验证 /probe 的两条语义:无视那个开关, 也不吃缓存 (敲两次就测两次).
|
||||
# 正好拿来验证 /probe 的两条语义: 无视那个开关, 也不吃缓存 (敲两次就测两次).
|
||||
OUT="$(python3 tests/repl_drive.py --bin "$BIN" --base-url "$BASE" \
|
||||
--prompt "/probe" --wait 1.5 \
|
||||
--prompt "/probe" --wait 1.5 --prompt "/exit" 2>&1)"
|
||||
contains "模型能力:" "$OUT"; check "--no-probe 下 /probe 仍会真的探测" $?
|
||||
n=$(printf '%s' "$OUT" | grep -c "模型能力:")
|
||||
contains "模型能力: " "$OUT"; check "--no-probe 下 /probe 仍会真的探测" $?
|
||||
n=$(printf '%s' "$OUT" | grep -c "模型能力: ")
|
||||
[ "${n:-0}" -ge 2 ]; check "每次 /probe 都重测一遍 (不吃缓存), 实际 $n 次" $?
|
||||
contains "支持" "$OUT"; check "结论照启动时的样子打出来" $?
|
||||
|
||||
@@ -1526,7 +1526,7 @@ OUT="$("$BIN" --help 2>&1)"
|
||||
contains "/probe" "$OUT"; check "--help 的 REPL 命令列表里也有" $?
|
||||
|
||||
echo
|
||||
echo "== 65. /history:一行摘要 + 完整对话 =="
|
||||
echo "== 65. /history: 一行摘要 + 完整对话 =="
|
||||
# 摘要行报的是"不含 system"的口径 (与 /usage、/back、/export 一致), 下面列的
|
||||
# 是含 system 的全部消息 —— 两个数不同, 所以第二行得自己说清楚, 否则用户会
|
||||
# 以为打印漏了或者多了一条.
|
||||
|
||||
Reference in new issue
Block a user