Files
HomeAgent/internal/plugins/clawhubadapter/python_bin_test.go
JianFeeeee 7052644431 fix(core): contextoverflow 改回空中断(按原始设计)+ Python 解释器可配置
## 1. contextoverflow:L4 空中断(推翻三处偏移)

用户口径:**原始设计是注入 L4 空中断,目的是触发语义相关性裁剪,
不含任何消息,就是一个空中断。**此前的实现有三处偏移:

① **中断带正文** —— `Build` 生成「已裁剪 N 条低相关事件到文档记忆…」,
   于是 L4 变成一条要模型回应的消息,白烧一整轮推理。
② **裁剪在 raise 之前做完** —— 先 `pruneByQuery` 再 raise,中断沦为
   「裁剪已完成」的回执,晚了一步。
③ **上游 contextfull 后原地重跑** —— `StepPrepare` 重建后拿**同一份**
   超限内容再发一次,必然再超限,快速烧光 recoverBudget。
   (这条是修① ② 时暴露的连带问题。)

改动:
· `VectorContextOverflow.Build` 返回空串 ⇒ 空中断;级别仍是 L4
· 删除 `overflowNotice` 与 `vectorArgs.Pruned`(中断不需要知道裁了多少)
· `handleContextOverflow` 不再裁剪,只发空中断,如实返回 0
· 新增 `pruneForAirInterrupt`:裁剪由空中断任务**真正执行时**触发,
  query 取上下文里最后一条真实用户输入作相关性基准
· 新增 `isAirInterrupt`:只认「内核来源 + type=interrupt + 无正文」,
  插件带正文的中断不受影响(否则"插件发了个空中断"会变成无声裁剪)
· `prepareInputTask` 识别空中断 → 裁剪 → terminalSkipped
  (不进 LLM、不写上下文、不发回复)
· 上游 contextfull 后改为 `outcomeSuspended`(让位等裁剪),不再原地重跑

顺带记录偏移成因(`raiseContextOverflow` 注释):当时的**观察**是对的
(L4 看不到被打断者上下文 ⇒ 模型会把"没检索到"推断成"不存在"),
但**结论**错了——它把"补偿 L4 看不到上下文"当成中断该带正文的理由。
模型需要的不是"知道有内容被移走",而是**留下来的内容与当前问题相关**。

判据(均经变异验证):
· `TestVectors_OverflowIsAirInterrupt` —— 正文必须为空 + 级别 L4
  变异:Build 改回非空 ⇒ 红
· `TestOverflow_RaisesAirInterruptWithoutPruning` —— 发中断那步不改上下文
  变异:加回 pruneByQuery ⇒ 红
· `TestAirInterrupt_ResolvedAsValidInput` —— 空中断须被认作有效输入
· `TestAirInterrupt_RejectsOrdinaryEmptyInput` —— 普通空包/插件带正文
  中断**不得**误判
· 改写 5 条断言"发中断即裁剪"的旧测试(T1/T2/T5/大事件/自适应),
  保留它们原本守的东西(超限判定、恢复预算、终止保护),
  只把裁剪步骤显式移到 `pruneForAirInterrupt`

## 2. clawhubadapter:Python 解释器与工作目录可配置

真机事故:`bilibili-all-in-one` 声明 requirements 含 httpx,系统 Python
没装 ⇒ 启动即 `ModuleNotFoundError`。而解释器此前是**硬编码猜**的
(先 /usr/bin/python3,再 /usr/local/bin/python3)。

「猜」没有正确解:不同部署的 Python 布局完全不同(发行版 Python、
pyenv、uv、conda、venv…)。而往系统 Python 里 pip install 是**错的**:
PEP 668 明确禁止,`--break-system-packages` 会破坏 apt 包一致性。

新增两个配置项(与既有 skills_dir/simulator_dir 同一风格):
· `python_bin` —— 解释器绝对路径,最高优先
· `skill_workdir` —— skill 子进程工作目录

回退顺序:配置 → `<skill>/.venv/bin/python` → 系统 python3。
**配错必须响亮**:配置的路径不存在时返回 error,不静默退回系统解释器
(否则用户以为配好了,实际跑在缺依赖的 Python 上 —— 又一次静默降级)。

判据:`TestPickPython_ConfiguredWins`(配置优先于 venv)/
`TestPickPython_VenvWhenNotConfigured` / `TestPickPython_MissingConfiguredIsLoud`

`launchProcessIn` 是 `launchProcess` 的工作目录变体(不改既有 4 个调用点)。
2026-10-25 21:39:12 +08:00

64 lines
2.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package clawhubadapter
import (
"os"
"path/filepath"
"testing"
)
// ── Python 解释器选择优先级 ──────────────────────────────────
//
// 背景(2026-10-25 真机事故):bilibili skill 声明 requirements 含 httpx,
// 系统 Python 没装 ⇒ 启动即 ModuleNotFoundError。而此前解释器是**硬编码猜**的:
// 先 /usr/bin/python3,再 /usr/local/bin/python3。
//
// 「猜」没有正确解:不同部署的 Python 布局完全不同(发行版 Python、
// pyenv、uv、conda、venv…)。所以改为配置显式指定 + venv 探测兜底。
// 判据:配置了 python_bin 且可用 ⇒ 必须用它,优先于 venv 与系统解释器。
//
// 这是"配置生效"的唯一证据。没有它,配了却仍走系统解释器也不会被发现
// (正是本次事故的形态:静默降级)。
func TestPickPython_ConfiguredWins(t *testing.T) {
dir := t.TempDir()
// 造一个假的 venv 解释器,确保"配置优先于 venv"被真正验证
venvBin := filepath.Join(dir, ".venv", "bin")
os.MkdirAll(venvBin, 0755)
venvPy := filepath.Join(venvBin, "python")
os.WriteFile(venvPy, []byte("#!/bin/sh\n"), 0755)
cfgPy := filepath.Join(dir, "custom-python")
os.WriteFile(cfgPy, []byte("#!/bin/sh\n"), 0755)
got := pickPythonFor("bilibili", dir, cfgPy)
if got != cfgPy {
t.Fatalf("配置的 python_bin 应最高优先,期望 %s,实际 %s", cfgPy, got)
}
}
// 判据:没配 python_bin 但 skill 有 venv ⇒ 用 venv。
func TestPickPython_VenvWhenNotConfigured(t *testing.T) {
dir := t.TempDir()
venvBin := filepath.Join(dir, ".venv", "bin")
os.MkdirAll(venvBin, 0755)
venvPy := filepath.Join(venvBin, "python")
os.WriteFile(venvPy, []byte("#!/bin/sh\n"), 0755)
if got := pickPythonFor("x", dir, ""); got != venvPy {
t.Fatalf("无配置时应优先 skill 自带 venv,期望 %s,实际 %s", venvPy, got)
}
}
// 判据:配置了但**不存在** ⇒ 必须报错,不能静默退回系统解释器。
//
// 静默退回的后果:用户以为配好了,实际跑在缺依赖的 Python 上 ——
// 这正是本次要修的「静默降级」那一族。
func TestPickPython_MissingConfiguredIsLoud(t *testing.T) {
dir := t.TempDir()
bad := filepath.Join(dir, "no-such-python")
_, err := resolvePython("x", dir, bad)
if err == nil {
t.Fatal("配置的 python_bin 不存在时必须报错(静默退回会让用户以为配置生效)")
}
}