Files
HomeAgent/internal/security/eventpolicy_test.go
JianFeeeee 4af07d08fe fix(memory,llm): L2 预填充并入召回主路 + 回退换 provider 时换 model(视觉失效真因)
## 1. L2(冷上下文)预填充此前只走「注入文本」,等于没有
设计:prune 与 recall 是一对 —— 工具声明 ContextPolicy=prune + RecallPolicy=auto
时,工具输出既触发裁剪、也触发预填充(memoryPass → recallTextFor)。
但 L2 那一路只写在 buildSystemPrompt 里,query 用的是**注入文本**;
QQ 注入的是「来自「X」的私聊消息(message_id=…),先用 qq_get_message 取正文」,
与用户真正说的话毫无相似度 ⇒ 冷上下文预填充恒为空。
(现场:头天晚上讲的冷笑话,第二天早上完全唤不回;14 小时里
 memory recall (input…) 只有 5 次、且 query 是元信息。)

改法:把 L2 检索并入 buildMemoryContext —— 输入路用输入文本、工具路用工具正文,
两条路共用同一次召回与预算;buildSystemPrompt 里那份重复注入删掉。
中断消息仍然 NoMemory + RecallPolicy=none(不裁剪、不召回、不进记忆),
这是设计要求,未改动。

## 2. 回退换 provider 时必须换 model(视觉失效的真因)
现场日志:
  deepseek 402 Insufficient Balance
  → 回退 llmsproxy,但 req 仍带 model=「deepseek-flash」
  → llmsproxy 403 model_not_allowed
  → llmsproxy 被整源标记 unavailable(冷却)
  → 视觉回退 resolveModalFallback 跳过它 ⇒「配置了视觉却说没有视觉」

两处修正:
  · 每个 provider 用自己的 model 发请求(model 是 provider 的属性)
  · 403 区分「模型不被该 key 允许」与「凭证失效」:前者只否一个 model,
    provider 仍可用于其他 model 与视觉回退

判据:internal/agent/core/provider_model_fallback_test.go
2026-10-27 11:49:26 +08:00

103 lines
3.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 security
// 事件总线**授权策略**的判据。
//
// 用户 2026-10-27 的指正:白名单放行 / 越界拒绝属于**策略**,必须在 security
// 模块里,而不是散在 SDK 适配层。所以这些判据钉在 security 包上 ——
// 换实现(谁的适配层)都不能改变口径。
import "testing"
// ① 白名单内的事件可发布。
func TestEventPolicyAllowsListedPublish(t *testing.T) {
p := DefaultEventPolicy()
if d := p.CheckEventPublish("clawhubadapter", "skill_detected"); !d.Allowed {
t.Fatalf("白名单事件应放行: %s", d.Reason)
}
}
// ② 越界类型必须拒绝,且理由里带白名单(否则调用方只能猜)。
func TestEventPolicyRejectsUnlistedPublish(t *testing.T) {
p := DefaultEventPolicy()
for _, bad := range []string{
"raw_input", // 用户输入
"agent_output", // 对外输出
"update_request", // 能驱动升级流程
"stage", // 阶段管道内部
"system", // 系统事件
"", // 空
} {
d := p.CheckEventPublish("any", bad)
if d.Allowed {
t.Errorf("发布 %q 应被拒绝", bad)
}
if d.Reason == "" {
t.Errorf("拒绝 %q 时必须给出理由", bad)
}
}
}
// ③ 自己的命名空间允许(插件间协作),且必须有后缀、不能冒用别人。
func TestEventPolicyOwnNamespace(t *testing.T) {
p := DefaultEventPolicy()
cases := []struct {
plugin, event string
want bool
}{
{"seq", "plugin.seq.done", true},
{"seq", "plugin.seq.", false}, // 只有前缀没有后缀
{"seq", "plugin.seq", false}, // 没有点
{"seq", "plugin.other.done", false}, // 冒用别人
{"", "plugin.x.done", false}, // 没有发布者名
}
for _, c := range cases {
if got := p.CheckEventPublish(c.plugin, c.event).Allowed; got != c.want {
t.Errorf("plugin=%q event=%q 期望 %v 实际 %v", c.plugin, c.event, c.want, got)
}
}
}
// ④ 订阅只开只读事件。
func TestEventPolicySubscribeReadOnlyOnly(t *testing.T) {
p := DefaultEventPolicy()
for _, ok := range []string{"tools_ready", "security_ready", "device_ready"} {
if d := p.CheckEventSubscribe("p1", ok); !d.Allowed {
t.Errorf("%q 应可订阅: %s", ok, d.Reason)
}
}
for _, bad := range []string{"raw_input", "agent_output", "update_request"} {
if d := p.CheckEventSubscribe("p1", bad); d.Allowed {
t.Errorf("订阅 %q 应被拒绝", bad)
}
}
}
// ⑤ 载荷键数上限(事件是通知,不是数据通道)。
func TestEventPolicyPayloadKeyLimit(t *testing.T) {
p := DefaultEventPolicy()
if d := p.CheckEventPayloadSize(p.MaxPayloadKeys); !d.Allowed {
t.Fatalf("达到上限应放行: %s", d.Reason)
}
if d := p.CheckEventPayloadSize(p.MaxPayloadKeys + 1); d.Allowed {
t.Fatal("超过上限应拒绝")
}
}
// ⑥ Runtime 委托:策略挂在 Runtime 上,可替换;Disabled Runtime 也带默认策略
//
// —— 关掉凭据替换不等于放开事件授权。
func TestRuntimeEventPolicyDelegation(t *testing.T) {
r := NewDisabledRuntime()
if d := r.CheckEventPublish("p", "raw_input"); d.Allowed {
t.Fatal("Disabled Runtime 仍应拒绝越界事件(关安全≠开权限)")
}
custom := EventPolicy{Publishable: []string{"my_event"}, MaxPayloadKeys: 1}
r.SetEventPolicy(custom)
if d := r.CheckEventPublish("p", "my_event"); !d.Allowed {
t.Fatalf("替换策略后应放行: %s", d.Reason)
}
if d := r.CheckEventPayloadSize(2); d.Allowed {
t.Fatal("替换后的键数上限应生效")
}
}