fix(loop): keep cross-batch voice success and gate speak without dispatcher (M57)
两处 review 必修(review-m57 r1): P2-1 跨批覆盖:applyBatchVoice 原先无条件用后一批的语音结论覆盖 前一批,于是「先成功合成 abc.wav、下一批换个参数再试却失败」会把 Result.Voice 退化成 tts_failed —— 音频已落盘,退回文本既无收益也 违背那次表达。改为守 captureVoice 的同一条优先级:已落盘的音频不被 后续批次的失败抹掉;先降级后成功仍然覆盖(后到的成功即最终语音)。 P2-2 nil-dispatcher 安全门:speak 由循环自己承载、无 dispatcher 也 能执行(invokeOne 短路),但 applyToolAdmissions 在 dispatcher 缺席 时整段跳过安全门,使「没配 dispatcher」成为绕过 design/11 §3 的 旁路。新增 applyDispatcherlessVoiceGate:与策略引擎对未登记工具的 口径一致(默认关闭、只有 owner 放行),拒绝走同一条 not_allowed 软结果与 tool.reject.not_allowed 审计通道;并改正该处过时注释。 两条回归测试都做过改前红/改后绿: - TestApplyBatchVoice_KeepsCrossBatchSuccess:注释掉守卫即报 「voice = ... FallbackReason:tts_failed, want abc.wav」 - TestApplyToolAdmissions_DispatcherlessSpeakGate:去掉 else 分支即报 「env group_member run = true, want false」 验收:go build ./... / go vet ./... / go test ./... 全绿。
This commit is contained in:
1 parent
c9da488749
commit
a0b4c941e4
5 files changed
+159
-4
No files matched your search
@@ -8,7 +8,9 @@ import (
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
mvoice "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/multimodal/voice"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool"
|
||||
)
|
||||
|
||||
@@ -235,6 +237,75 @@ func TestRun_Stranger_NotAllowed_Blocked(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyToolAdmissions_DispatcherlessSpeakGate 锁住退化配置下的同一道
|
||||
// 安全门:speak 由循环自己承载,没有 dispatcher 也能执行(invokeOne 的
|
||||
// 短路),所以「没配 dispatcher」绝不能变成绕过 design/11 §3 的旁路 ——
|
||||
// 群聊/陌生人的 speak 必须在这里被拦成 not_allowed 软结果并留下审计,
|
||||
// 主人环境照常放行。
|
||||
func TestApplyToolAdmissions_DispatcherlessSpeakGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
var mu sync.Mutex
|
||||
var audited []AuditEvent
|
||||
l := New(Options{
|
||||
VoiceSynth: &stubSynth{audio: mvoice.Audio{Format: "wav", Data: []byte{1}}, provider: "p"},
|
||||
VoiceStore: &stubStore{ref: VoiceRef{Name: "a.wav", MIME: "audio/wav", Size: 1}},
|
||||
OnAudit: func(ev AuditEvent) {
|
||||
mu.Lock()
|
||||
audited = append(audited, ev)
|
||||
mu.Unlock()
|
||||
},
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
env restrict.ActorEnv
|
||||
allow bool
|
||||
}{
|
||||
{restrict.ActorGroupMember, false},
|
||||
{restrict.ActorStranger, false},
|
||||
{restrict.ActorOwner, true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
mu.Lock()
|
||||
audited = nil
|
||||
mu.Unlock()
|
||||
|
||||
calls := []agent.ToolCall{{Name: speakToolName, Args: map[string]any{"text": "晚安"}}}
|
||||
actions := []toolCallAction{{run: true}}
|
||||
l.applyToolAdmissions(WithSessionEnv(context.Background(), SessionEnv{ActorEnv: tc.env}), nil, calls, actions)
|
||||
|
||||
if actions[0].run != tc.allow {
|
||||
t.Fatalf("env %s run = %v, want %v (reason %q)", tc.env, actions[0].run, tc.allow, actions[0].reason)
|
||||
}
|
||||
mu.Lock()
|
||||
got := append([]AuditEvent(nil), audited...)
|
||||
mu.Unlock()
|
||||
if tc.allow {
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("env %s audited %+v, want none", tc.env, got)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(actions[0].reason, "not_allowed") {
|
||||
t.Fatalf("env %s reason = %q, want a not_allowed soft result", tc.env, actions[0].reason)
|
||||
}
|
||||
if len(got) != 1 || got[0].What != auditToolRejectNotAllowed || got[0].Who != string(tc.env) {
|
||||
t.Fatalf("env %s audited %+v, want one %s by %s", tc.env, got, auditToolRejectNotAllowed, tc.env)
|
||||
}
|
||||
}
|
||||
|
||||
// speak 未接线时没有可执行的旁路,门不该动任何调用。
|
||||
actions := []toolCallAction{{run: true}}
|
||||
New(Options{}).applyToolAdmissions(
|
||||
WithSessionEnv(context.Background(), SessionEnv{ActorEnv: restrict.ActorGroupMember}),
|
||||
nil,
|
||||
[]agent.ToolCall{{Name: speakToolName}},
|
||||
actions,
|
||||
)
|
||||
if !actions[0].run {
|
||||
t.Fatalf("unwired speak blocked (reason %q), want untouched", actions[0].reason)
|
||||
}
|
||||
}
|
||||
|
||||
// mustSetPolicy 把 ToolPolicy 应用到具名工具,出错即让测试失败。
|
||||
func mustSetPolicy(t *testing.T, reg *registry.Registry, name string, pol registry.ToolPolicy) {
|
||||
t.Helper()
|
||||
|
||||
Reference in new issue
Block a user