32f50cd983
此前测试都直打 runGraph/evaluate,绕过真正的任务入口 Handle()——熔断、输入护栏门控、 状态机流转、token 预算、异步评测/用量这些治理逻辑的协同从未在入口级被覆盖。 新增 handle_test.go 6 例(全 fake 依赖,可断言各出口): - HappyPath:running→done + token 流出/收尾 + 异步评测落库(ok) - ToolFeedsAgent:图执行→工具→agent 全程经 Handle,工具被调、产出注入 - CircuitBreakerOpen:熔断开 → 快速 failed、不执行图 - BudgetExceeded:meta token_budget=3 → failed + 用量回写带 Exceeded - GuardrailBlocks:灰区 + LLM 分类器命中 → rejected、不进 running - EmptyTaskDropped:空 id 直接丢弃、无状态回写 配套 fakeStatus/fakeUsageSink;fakeEvalSink 加锁(Handle 异步评测在 goroutine 写)。 go test -race ./internal/eino 干净,四模块全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
4.0 KiB
Go
124 lines
4.0 KiB
Go
package eino
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
|
||
"github.com/sundynix/sundynix-dispatcher/internal/harness"
|
||
"github.com/sundynix/sundynix-dispatcher/internal/llm"
|
||
"github.com/sundynix/sundynix-shared/contract"
|
||
)
|
||
|
||
// fakeEvalSink 捕获回流的评测事件,供断言纠偏终值。并发安全(Handle 异步评测会在 goroutine 写)。
|
||
type fakeEvalSink struct {
|
||
mu sync.Mutex
|
||
ev *contract.EvalEvent
|
||
}
|
||
|
||
func (f *fakeEvalSink) PublishEval(ev *contract.EvalEvent) error {
|
||
f.mu.Lock()
|
||
f.ev = ev
|
||
f.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
// get 并发安全地取最近一次评测事件。
|
||
func (f *fakeEvalSink) get() *contract.EvalEvent {
|
||
f.mu.Lock()
|
||
defer f.mu.Unlock()
|
||
return f.ev
|
||
}
|
||
|
||
// orchForEval 组一个仅评测/纠偏所需依赖的编排器(生成器 gen + 评审 judge + 评测出口 sink)。
|
||
func orchForEval(gen *fakeLLM, judge func(ctx context.Context, sys, user string) (string, error), sink *fakeEvalSink) *Orchestrator {
|
||
return &Orchestrator{
|
||
pool: gen,
|
||
breaker: harness.NewCircuitBreaker(),
|
||
eval: harness.NewEvaluator(func() bool { return true }, judge),
|
||
exec: &fakeExec{},
|
||
evalSink: sink,
|
||
}
|
||
}
|
||
|
||
// 低分 → 自动纠偏重生成 → 新答更优 → 采纳修订并标记 Corrected,落库终值升为 ok。
|
||
func TestEvaluate_AutoRefine_AdoptsBetter(t *testing.T) {
|
||
// 评审:含 refusal 标记的初版给 1 分,重写后的正常回答给 5 分。
|
||
judge := func(_ context.Context, _, user string) (string, error) {
|
||
if strings.Contains(user, "error:") {
|
||
return `{"score":1,"reason":"差"}`, nil
|
||
}
|
||
return `{"score":5,"reason":"好"}`, nil
|
||
}
|
||
gen := &fakeLLM{ready: true, chat: func(_ []llm.ChatMessage) (string, error) {
|
||
return "光合作用是植物利用光能把二氧化碳和水合成有机物并释放氧气的过程。", nil
|
||
}}
|
||
sink := &fakeEvalSink{}
|
||
o := orchForEval(gen, judge, sink)
|
||
|
||
final := o.evaluate(&contract.Task{ID: "t1"}, "介绍光合作用", "error: 我无法回答", nil)
|
||
|
||
if !strings.Contains(final, "光合作用") {
|
||
t.Fatalf("应采纳纠偏后的好答案,got %q", final)
|
||
}
|
||
if sink.ev == nil || !sink.ev.Corrected {
|
||
t.Fatalf("应标记 Corrected=true, got %+v", sink.ev)
|
||
}
|
||
if sink.ev.Level != contract.EvalOK {
|
||
t.Errorf("纠偏后应升为 ok,got %s (overall %.2f)", sink.ev.Level, sink.ev.Overall)
|
||
}
|
||
}
|
||
|
||
// 低分 → 纠偏后未提升 → 保留原版(不退步),不标记 Corrected,终值仍 poor。
|
||
func TestEvaluate_AutoRefine_KeepsOriginalWhenNoGain(t *testing.T) {
|
||
judge := func(_ context.Context, _, _ string) (string, error) {
|
||
return `{"score":1,"reason":"差"}`, nil // 怎么写都低分
|
||
}
|
||
gen := &fakeLLM{ready: true, chat: func(_ []llm.ChatMessage) (string, error) {
|
||
return "另一个同样糟糕的 error: 回答", nil
|
||
}}
|
||
sink := &fakeEvalSink{}
|
||
o := orchForEval(gen, judge, sink)
|
||
|
||
orig := "error: 初版很差的回答"
|
||
final := o.evaluate(&contract.Task{ID: "t2"}, "问题", orig, nil)
|
||
|
||
if final != orig {
|
||
t.Fatalf("未提升应保留原版,got %q", final)
|
||
}
|
||
if sink.ev.Corrected {
|
||
t.Error("未提升不应标记 Corrected")
|
||
}
|
||
if sink.ev.Level != contract.EvalPoor {
|
||
t.Errorf("应仍为 poor,got %s", sink.ev.Level)
|
||
}
|
||
}
|
||
|
||
// 非低分(ok/warn)不触发纠偏:生成器不应被调用。
|
||
func TestEvaluate_NoRefineWhenNotPoor(t *testing.T) {
|
||
called := false
|
||
gen := &fakeLLM{ready: true, chat: func(_ []llm.ChatMessage) (string, error) {
|
||
called = true
|
||
return "不该被调用", nil
|
||
}}
|
||
judge := func(_ context.Context, _, _ string) (string, error) {
|
||
return `{"score":5,"reason":"好"}`, nil
|
||
}
|
||
sink := &fakeEvalSink{}
|
||
o := orchForEval(gen, judge, sink)
|
||
|
||
orig := "一段质量不错的正常回答内容。"
|
||
final := o.evaluate(&contract.Task{ID: "t3"}, "问题", orig, nil)
|
||
|
||
if called {
|
||
t.Error("非低分不应触发纠偏(生成器被调用了)")
|
||
}
|
||
if final != orig {
|
||
t.Errorf("无纠偏应原样返回,got %q", final)
|
||
}
|
||
if sink.ev.Corrected || sink.ev.Level != contract.EvalOK {
|
||
t.Errorf("应为未纠偏的 ok,got level=%s corrected=%v", sink.ev.Level, sink.ev.Corrected)
|
||
}
|
||
}
|