package runtime import ( "context" "encoding/json" "strings" "testing" core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git" ) // ---- E4:R4 升级提醒 ---- // 滑动窗口判据:近 6 次结果中 3 次失败(可与成功交错)触发换路指令; // 每 run 一次;2 次失败不触发。 func TestEscalationOnConsecutiveToolFailures(t *testing.T) { st := &directRun{l: &LLMRunner{}, inflight: map[string]*toolSlot{}} st.noteFailureOutcome("ok") st.noteFailureOutcome("error") st.noteFailureOutcome("ok") st.noteFailureOutcome("error") if got := st.escalationReminders(); len(got) != 0 { t.Fatalf("two scattered failures must not escalate: %v", got) } st.noteFailureOutcome("ok") st.noteFailureOutcome("error") // 窗口内第 3 败(交错分布) got := st.escalationReminders() if len(got) != 1 || !strings.Contains(got[0], "停止原样重试") { t.Fatalf("three failures in the window must escalate: %v", got) } if again := st.escalationReminders(); len(again) != 0 { t.Fatalf("escalation must fire once per run: %v", again) } } // 检索连续空结果(成功但无实质内容)触发「换关键词或如实写缺失」。 func TestEscalationOnZeroHitSearches(t *testing.T) { f := &fakeRunCtx{ctx: context.Background(), prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, execOutput: "未找到相关内容"} r := &LLMRunner{Stream: scriptStream(padTurns( textDeltaTurn("", toolCall("knowledge_search", `{"query":"a"}`), toolCall("knowledge_search", `{"query":"b"}`)), textDeltaTurn("好了。"), ), nil)} if err := r.Execute(f); err != nil { t.Fatal(err) } st := &directRun{l: &LLMRunner{}, inflight: map[string]*toolSlot{}} st.noteEvidenceCall("knowledge_search", "ok", "未找到相关内容") if got := st.escalationReminders(); len(got) != 0 { t.Fatalf("one zero hit must not escalate: %v", got) } st.noteEvidenceCall("knowledge_search", "ok", "无结果") got := st.escalationReminders() if len(got) != 1 || !strings.Contains(got[0], "材料中无此信息") { t.Fatalf("two zero hits must escalate: %v", got) } // 有实质结果后计数重置。 st.noteEvidenceCall("knowledge_search", "ok", "营收 3,200 万元") if st.zeroHitSearches != 0 { t.Fatal("substantive hit must reset zero-hit count") } } // ---- E4:结构化 source_id 账本 ---- func TestEvidenceLedgerTagsSource(t *testing.T) { f := &fakeRunCtx{ctx: context.Background(), prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, execOutput: "上半年营收 3,200 万元。"} r := &LLMRunner{Stream: scriptStream(padTurns( textDeltaTurn("", toolCall("knowledge_search", `{"query":"营收"}`)), textDeltaTurn("总结完成。"), ), nil)} if err := r.Execute(f); err != nil { t.Fatal(err) } // 结果文本必须带来源标记,且账本登记了对应条目(直接经 runTool 断言)。 st := &directRun{l: &LLMRunner{}, rc: f, eff: f.Settings(), inflight: map[string]*toolSlot{}} res := st.runTool("knowledge_search", `{}`) if !strings.Contains(res.Content, "【src_1|knowledge_search】") { t.Fatalf("tool result must carry the source tag: %.120s", res.Content) } if len(st.ledger) != 1 || st.ledger[0].ID != "src_1" || !strings.Contains(st.ledger[0].Text, "3,200") { t.Fatalf("ledger = %+v", st.ledger) } // 第二次登记递增编号。 res2 := st.runTool("fs_read_file", `{}`) if !strings.Contains(res2.Content, "【src_2|fs_read_file】") { t.Fatalf("second tag must be src_2: %.120s", res2.Content) } } // 非证据工具不登记、不加标记。 func TestEvidenceLedgerSkipsNonEvidenceTools(t *testing.T) { f := &fakeRunCtx{} st := &directRun{l: &LLMRunner{}, rc: f, eff: f.Settings(), inflight: map[string]*toolSlot{}} res := st.runTool("time_now", `{}`) if strings.Contains(res.Content, "src_") || len(st.ledger) != 0 { t.Fatalf("non-evidence tool must not enter the ledger: %.80s %+v", res.Content, st.ledger) } } // 材料读(inputs/)打标;模型自产文件的读与 grep 不打——尾注会污染 // read→edit→build 编辑循环(E4 首轮 eval 实测教训,回归锚点)。 func TestEvidenceLedgerOnlyTagsMaterialReads(t *testing.T) { if !isEvidenceRead("read_file", json.RawMessage(`{"path":"inputs/年报.docx.md"}`)) { t.Fatal("inputs/ read must count as evidence") } if isEvidenceRead("read_file", json.RawMessage(`{"path":"方案.md"}`)) { t.Fatal("own-workspace read must NOT count as evidence") } if isEvidenceRead("grep", json.RawMessage(`{"pattern":"营收"}`)) { t.Fatal("grep must never count as evidence") } }