Files
Agent-System-Backend/internal/runtime/material_flow_test.go
T
weichen 02858f486b Add deterministic material-intake background path for attachment sends
Why: the dual-speed entry hint is advisory only; real runs (MiniMax, 3 attachments, no text) end with a clarifying question and zero delegation, so background cards and result backfill never start.
Evidence: new unit tests in runmgr/material_test.go and runtime/material_flow_test.go all pass; full go test ./... exit 0 (local, SELF).
Impact: attachment-carrying runs now guarantee a material-organization background task (pre-spawn on attachment-only/heavy sends, stop-hook backstop otherwise when the model did not delegate); subagent receives inline parsed text with budget plus knowledge_search guidance; content-key dedup prevents duplicate spawns (in-flight, completed, resend); model-authored delegate_task gains a materials appendix; settings runtime.materialIntake (default on) and runtime.materialIntakeChars added.
Refs: Dev task .task/feature-material-intake-flow.md I1; user ruling 2026-10-11.
2026-10-11 16:45:49 +00:00

122 lines
4.3 KiB
Go

package runtime
import (
"context"
"strings"
"testing"
agentllm "agent-native/runtime/llm"
core "agent-system-core"
)
// matCtx 在 fakeRunCtx 之上实现材料整理的两个可选接口(附件即保证),
// 记录 EnsureMaterial 调用次数以断言 stop 兜底的幂等。
type matCtx struct {
*fakeRunCtx
note string
ensureN int
lastNote string
}
func (m *matCtx) MaterialNote() string { return m.note }
func (m *matCtx) EnsureMaterial() string {
m.ensureN++
if m.ensureN > 1 {
return ""
}
return m.lastNote
}
func matAttachments() []core.ParsedDocument {
return []core.ParsedDocument{
{Meta: core.DocMeta{Filename: "纪要.docx"}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "决定 11 月上线"}}},
{Meta: core.DocMeta{Filename: "意见.pdf"}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "预算存疑"}}},
{Meta: core.DocMeta{Filename: "清单.md"}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "12 项需求"}}},
}
}
func runMaterial(t *testing.T, turns [][]agentllm.Event, rc *matCtx) *dualSpeedRecorder {
t.Helper()
rec := &dualSpeedRecorder{clock: &stepClock{}, turns: turns}
r := &LLMRunner{Stream: rec.stream(scriptStream(turns, nil)), Now: rec.clock.now}
if err := r.Execute(rc); err != nil {
t.Fatalf("Execute: %v", err)
}
return rec
}
func requestsContaining(rec *dualSpeedRecorder, needle string) int {
n := 0
for _, req := range rec.request {
if strings.Contains(req, needle) {
n++
}
}
return n
}
// 预启动说明替换建议式提示:第 0 回合注入事实说明而非「可能属于重任务」。
func TestTurn0MaterialNoteReplacesAdvisory(t *testing.T) {
rc := &matCtx{fakeRunCtx: &fakeRunCtx{ctx: context.Background(), prof: verifyOn()}, note: "系统已自动启动材料整理后台任务(收件箱进度卡「材料整理」已创建)。"}
rc.attachments = matAttachments()
rec := runMaterial(t, [][]agentllm.Event{textDeltaTurn("收到,先给每份材料一句话概览。")}, rc)
if requestsContaining(rec, "系统已自动启动材料整理后台任务") == 0 {
t.Fatal("material note not injected at turn 0")
}
if requestsContaining(rec, "可能属于重任务") != 0 {
t.Fatal("advisory text should be replaced when material note present")
}
}
// 未预启动(文字+小材料):保留建议式入口提示。
func TestTurn0AdvisoryWhenNoNote(t *testing.T) {
rc := &matCtx{fakeRunCtx: &fakeRunCtx{ctx: context.Background(), prof: verifyOn()}}
rc.attachments = matAttachments()
rec := runMaterial(t, [][]agentllm.Event{textDeltaTurn("概览如下。")}, rc)
if requestsContaining(rec, "可能属于重任务") == 0 {
t.Fatal("advisory hint missing when no material note")
}
}
// stop 兜底:模型问一句澄清就收场(用户实测现象)→ 系统续跑一轮要求告知;
// ensureMaterial 幂等保证只续跑一次。
func TestStopMaterialBackstopContinuesOnce(t *testing.T) {
rc := &matCtx{
fakeRunCtx: &fakeRunCtx{ctx: context.Background(), prof: verifyOn()},
lastNote: "系统已自动启动材料整理后台任务",
}
rc.attachments = matAttachments()
turns := [][]agentllm.Event{
textDeltaTurn("请问你需要哪种产出?"),
textDeltaTurn("材料正在后台整理,完成后自动汇报;先给概览。"),
}
rec := runMaterial(t, turns, rc)
if requestsContaining(rec, "系统已自动启动材料整理后台任务") == 0 {
t.Fatal("backstop reason not injected as continuation")
}
if rc.ensureN != 1 {
t.Fatalf("EnsureMaterial calls = %d, want exactly 1", rc.ensureN)
}
}
// 模型已自行委派后台(delegatedBackground)→ 尊重模型判断,不触发兜底。
func TestStopMaterialBackstopSkippedWhenDelegated(t *testing.T) {
rc := &matCtx{
fakeRunCtx: &fakeRunCtx{ctx: context.Background(), prof: verifyOn(), bgID: "run_bg_1"},
}
rc.attachments = matAttachments()
delegate := toolCall("delegate_task", `{"name":"深度解析","task":"解析材料","background":true}`)
turns := [][]agentllm.Event{
textDeltaTurn("", delegate),
textDeltaTurn("已在后台解析,先给即时反馈。"),
}
rec := runMaterial(t, turns, rc)
if rc.ensureN != 0 {
t.Fatalf("EnsureMaterial calls = %d, want 0 (model delegated)", rc.ensureN)
}
if requestsContaining(rec, "自动启动材料整理后台任务") != 0 {
t.Fatal("backstop should not fire when model delegated")
}
}