ci / vet-and-test (pull_request) Successful in 43s
Why: the round-6 real-model dual-speed run showed MiniMax writing its reasoning as inline <think> blocks; finishChild's partsText fed them into the background notice turn and the inbox result card verbatim. Evidence: new runmgr test covers closed-block stripping with body preserved and conservative handling of unclosed blocks; go test ./... 32 pkgs green. Impact: partsText removes closed <think>...</think> spans before summary, notice, and card assembly; unclosed blocks are kept as-is. Refs: Dev task feature/dual-speed-real-path.
181 lines
6.4 KiB
Go
181 lines
6.4 KiB
Go
package runtime
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
agentllm "agent-native/runtime/llm"
|
||
core "agent-system-core"
|
||
)
|
||
|
||
// 双速提醒经内核 Reminders 钩子进入下一请求的消息(不是响应部件):
|
||
// 这里捕获每次模型请求的原文,检索提醒标记。
|
||
type dualSpeedRecorder struct {
|
||
clock *stepClock
|
||
turns [][]agentllm.Event
|
||
request []string
|
||
base agentllm.StreamFunc
|
||
}
|
||
|
||
// stepClock 每 tick 前进 5 秒;runDirect 起 0 时刻,模型每被调用一次 tick 一格,
|
||
// 20 次调用即 100 秒量级,足以压过默认 75 秒阈值。
|
||
type stepClock struct{ step int64 }
|
||
|
||
func (c *stepClock) now() time.Time { return time.Unix(0, c.step*int64(5*time.Second)) }
|
||
func (c *stepClock) tick() { c.step++ }
|
||
|
||
func (rec *dualSpeedRecorder) stream(base agentllm.StreamFunc) agentllm.StreamFunc {
|
||
return func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
|
||
rec.clock.tick()
|
||
for _, m := range msgs {
|
||
if b, err := json.Marshal(m); err == nil {
|
||
rec.request = append(rec.request, string(b))
|
||
}
|
||
}
|
||
return base(ctx, model, msgs, tools, opts)
|
||
}
|
||
}
|
||
|
||
func (rec *dualSpeedRecorder) nudged() bool {
|
||
for _, r := range rec.request {
|
||
if strings.Contains(r, "任务快慢双速") {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func runDualSpeed(t *testing.T, turns [][]agentllm.Event, setup func(rc *fakeRunCtx)) *dualSpeedRecorder {
|
||
t.Helper()
|
||
rec := &dualSpeedRecorder{clock: &stepClock{}, turns: turns}
|
||
r := &LLMRunner{Stream: rec.stream(scriptStream(turns, nil)), Now: rec.clock.now}
|
||
rc := &fakeRunCtx{ctx: context.Background(), prof: verifyOn()}
|
||
if setup != nil {
|
||
setup(rc)
|
||
}
|
||
if err := r.Execute(rc); err != nil {
|
||
t.Fatalf("Execute: %v", err)
|
||
}
|
||
return rec
|
||
}
|
||
|
||
func toolOnlyTurns(n int) [][]agentllm.Event {
|
||
var turns [][]agentllm.Event
|
||
for i := 0; i < n; i++ {
|
||
// 每次调用用独立 ID(toolCall 帮手的 ID 固定,重复会撞内核的工具槽位)。
|
||
call := agentllm.ToolCall{ID: fmt.Sprintf("c-ds-%d", i), Name: "make_report", Arguments: "{}"}
|
||
turns = append(turns, textDeltaTurn("", call))
|
||
}
|
||
return turns
|
||
}
|
||
|
||
// 长时间纯工具轮、用户零答复 → 阈值后恰好注入一次双速提醒。
|
||
func TestDualSpeedNudgeFiresOnceAfterThreshold(t *testing.T) {
|
||
turns := append(toolOnlyTurns(20), textDeltaTurn("完成。"))
|
||
rec := runDualSpeed(t, turns, nil)
|
||
if !rec.nudged() {
|
||
t.Fatal("dual-speed nudge missing after threshold with no user reply")
|
||
}
|
||
count := 0
|
||
for _, r := range rec.request {
|
||
count += strings.Count(r, "任务快慢双速")
|
||
}
|
||
if count > 1 {
|
||
t.Fatalf("nudge injected %d times, want at most once per request window", count)
|
||
}
|
||
}
|
||
|
||
// 短任务(阈值内完成)不提醒。
|
||
func TestDualSpeedNudgeQuietBeforeThreshold(t *testing.T) {
|
||
turns := append(toolOnlyTurns(1), textDeltaTurn("完成。"))
|
||
rec := runDualSpeed(t, turns, nil)
|
||
if rec.nudged() {
|
||
t.Fatal("nudge fired before the threshold")
|
||
}
|
||
}
|
||
|
||
// 设置 runtime.dualSpeedNudgeSeconds=0 → 关闭,超时也不提醒。
|
||
func TestDualSpeedNudgeDisabledBySetting(t *testing.T) {
|
||
turns := append(toolOnlyTurns(20), textDeltaTurn("完成。"))
|
||
rec := runDualSpeed(t, turns, func(rc *fakeRunCtx) {
|
||
rc.runtimeExtra = map[string]any{"dualSpeedNudgeSeconds": float64(0)}
|
||
})
|
||
if rec.nudged() {
|
||
t.Fatal("nudge fired although disabled via runtime.dualSpeedNudgeSeconds=0")
|
||
}
|
||
}
|
||
|
||
// 模型已给出面向用户的答复(快通道成立)→ 即使后续事实核查续写拖过阈值也不提醒。
|
||
func TestDualSpeedNudgeSkippedWhenReplied(t *testing.T) {
|
||
turns := [][]agentllm.Event{
|
||
textDeltaTurn("", toolCall("make_report", `{"content":"准确率 95%"}`)),
|
||
textDeltaTurn("完整总结:产出物与要点。"),
|
||
textDeltaTurn("已修正并重新登记。"),
|
||
}
|
||
rec := runDualSpeed(t, turns, func(rc *fakeRunCtx) {
|
||
rc.verify = true
|
||
rc.execArtifact = &core.Artifact{ID: "art_0123456789ab", Name: "报告.md"}
|
||
rc.execArtifactContent = []byte("# 报告\n\n准确率 95%。")
|
||
})
|
||
if rec.nudged() {
|
||
t.Fatal("nudge fired although the model already gave its user reply")
|
||
}
|
||
}
|
||
|
||
// 已把深度工作放后台(慢通道成立)→ 不再提醒。
|
||
func TestDualSpeedNudgeSkippedWhenDelegated(t *testing.T) {
|
||
turns := [][]agentllm.Event{
|
||
textDeltaTurn("", toolCall("delegate_task", `{"name":"深度解析","task":"逐页解析","background":true}`)),
|
||
}
|
||
turns = append(turns, toolOnlyTurns(19)...)
|
||
turns = append(turns, textDeltaTurn("后台进行中,先给结论。"))
|
||
rec := runDualSpeed(t, turns, func(rc *fakeRunCtx) {
|
||
rc.bgID = "run_bg_1"
|
||
})
|
||
if rec.nudged() {
|
||
t.Fatal("nudge fired although background delegation already happened")
|
||
}
|
||
}
|
||
|
||
// 首次被无视后按 45s 间隔升级重复(最多 3 次),第二次起为硬性措辞。
|
||
func TestDualSpeedNudgeEscalates(t *testing.T) {
|
||
turns := append(toolOnlyTurns(35), textDeltaTurn("完成。")) // 35×5s=175s → 75s 起、120s、165s 三档
|
||
rec := runDualSpeed(t, turns, nil)
|
||
marks, hard := 0, 0
|
||
for _, r := range rec.request {
|
||
marks += strings.Count(r, "用户已经等待")
|
||
hard += strings.Count(r, "硬性要求")
|
||
}
|
||
if marks < 1 {
|
||
t.Fatalf("escalation missing: requests=%d", len(rec.request))
|
||
}
|
||
if marks > 2 {
|
||
t.Fatalf("escalated %d times, want at most 2 repeats after the first", marks)
|
||
}
|
||
}
|
||
|
||
// 入口前置:带附件的任务第 0 回合注入快慢双速选项;无附件不注入;阈值关闭不注入。
|
||
func TestDualSpeedEntryHintWithAttachments(t *testing.T) {
|
||
withAtt := runDualSpeed(t, [][]agentllm.Event{textDeltaTurn("完成。")}, func(rc *fakeRunCtx) {
|
||
rc.attachments = []core.ParsedDocument{{Meta: core.DocMeta{Filename: "meeting.docx"}}}
|
||
})
|
||
if !strings.Contains(strings.Join(withAtt.request, "\n"), "快慢双速") {
|
||
t.Fatal("entry hint missing for an attachment-bearing task")
|
||
}
|
||
noAtt := runDualSpeed(t, [][]agentllm.Event{textDeltaTurn("完成。")}, nil)
|
||
if strings.Contains(strings.Join(noAtt.request, "\n"), "本任务附带材料") {
|
||
t.Fatal("entry hint fired without attachments")
|
||
}
|
||
disabled := runDualSpeed(t, [][]agentllm.Event{textDeltaTurn("完成。")}, func(rc *fakeRunCtx) {
|
||
rc.attachments = []core.ParsedDocument{{Meta: core.DocMeta{Filename: "meeting.docx"}}}
|
||
rc.runtimeExtra = map[string]any{"dualSpeedNudgeSeconds": float64(0)}
|
||
})
|
||
if strings.Contains(strings.Join(disabled.request, "\n"), "本任务附带材料") {
|
||
t.Fatal("entry hint fired although the nudge setting is disabled")
|
||
}
|
||
}
|