Files
Agent-System-Backend/internal/runtime/e4_escalation_test.go
T
weichen f1302ed5b3 Enable canonical versioned Go module consumption
Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements.

Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate.

Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable.

Refs: Versioned dependencies U1/U3; feature/dependency-workspace
2026-10-13 15:47:16 +00:00

113 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}