ci / vet-and-test (pull_request) Successful in 43s
Why: The plan's E4 remainder under the MiniMax-only constraint: the harness must detect the model going in circles and inject a change-of-strategy instruction (R4 - the model cannot be trusted to notice its own repeat-fail loop), and number sourcing needs addressable evidence: tool results carry [src_N|tool] tags so "found in materials" becomes "found in source src_N" for both the model and the fact-check notice. Why: Failure counting lives in one place (onEvent end, keyed by the slot status) because tool execution is parallel; the ledger and tag append are serialized by the same mutex, and escalation reminders fire once per condition streak - following the dual-speed nudge finding that MiniMax ignores vague reminders but follows concrete instructions. Evidence: go test ./... green; new anchors TestEscalationOnConsecutive ToolFailures, TestEscalationOnZeroHitSearches, TestEvidenceLedger TagsSource, TestEvidenceLedgerSkipsNonEvidenceTools. Live before/ after comparison runs on the Dev side per ruling 6. Impact: knowledge/web/tavily searches, fs_read_file and the workspace read_file/grep results now end with a source tag and enter the run ledger (2k-char head); two consecutive tool failures or two empty search results inject a concrete change-of-strategy reminder; base.deliverables v7 and agent-sub-research v3 explain the tags; fact-check notice points at src results. Non-evidence tools and failures are unchanged. Refs: proposal 2026-10-10-agent-delegation-routing-sandbox.md (E4, R4); Dev task feature/agent-e4-escalation-ledger.
126 lines
4.6 KiB
Go
126 lines
4.6 KiB
Go
package runtime
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"testing"
|
||
|
||
core "agent-system-core"
|
||
)
|
||
|
||
// 事实核查:交付物里材料找不到出处的百分比被抽出并组成反馈;找到的不报。
|
||
func TestUnverifiedFactsAndNotice(t *testing.T) {
|
||
material := "目标是准确率 85%以上。截止 10月12日。团队共 12 人。"
|
||
artifact := "准确率达到 95%,截止 10月20日,覆盖 3,500 用户,目标 85%。"
|
||
got := UnverifiedFacts(artifact, material, factExemptions())
|
||
joined := strings.Join(got, ",")
|
||
for _, want := range []string{"95%", "10月20日", "3,500"} {
|
||
if !strings.Contains(joined, want) {
|
||
t.Fatalf("missing %q in %v", want, got)
|
||
}
|
||
}
|
||
if strings.Contains(joined, "85%") {
|
||
t.Fatalf("sourced figure must not be reported: %v", got)
|
||
}
|
||
notice := factCheckNotice(got)
|
||
if !strings.Contains(notice, "95%") || !strings.Contains(notice, "改成材料或来源结果") {
|
||
t.Fatalf("notice malformed: %q", notice)
|
||
}
|
||
if UnverifiedFacts("", material, nil) != nil || UnverifiedFacts(artifact, "", nil) != nil {
|
||
t.Fatal("empty sides short-circuit")
|
||
}
|
||
}
|
||
|
||
// 端到端:终轮对带未核实数字的交付物给一轮修正;修正后(重新登记干净版本)放行。
|
||
func TestFactCheckContinuationOnce(t *testing.T) {
|
||
rc := &fakeRunCtx{
|
||
ctx: context.Background(),
|
||
prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}},
|
||
verify: true,
|
||
execArtifact: &core.Artifact{ID: "art_0123456789ab", Name: "报告.md"},
|
||
execArtifactContent: []byte("# 报告\n\n准确率 95%。"),
|
||
}
|
||
// 第一轮:登记带编造数字的工件 -> 事实核查续跑一次;第二轮:干净版本通过。
|
||
turns1 := padTurns(
|
||
textDeltaTurn("", toolCall("make_report", `{"content":"准确率 95%"}`)),
|
||
textDeltaTurn("写好了。"),
|
||
)
|
||
turns2 := padTurns(
|
||
textDeltaTurn("材料里是 85%。"),
|
||
)
|
||
r := &LLMRunner{Stream: scriptStream(padTurns(turns1...), nil), VerifyStream: passedStream()}
|
||
if err := r.Execute(rc); err != nil {
|
||
t.Fatalf("Execute: %v", err)
|
||
}
|
||
sawFact := false
|
||
for _, p := range rc.assistant {
|
||
if p.Check != nil && !p.Check.Passed && strings.Contains(p.Check.Reason, "事实核查") {
|
||
sawFact = true
|
||
}
|
||
}
|
||
if !sawFact {
|
||
t.Fatalf("fact check continuation note missing: %+v", rc.assistant)
|
||
}
|
||
_ = turns2
|
||
}
|
||
|
||
// ---- E3:标注感知豁免 ----
|
||
|
||
func TestMarkedAsEstimate(t *testing.T) {
|
||
cases := []struct {
|
||
name, text, tok string
|
||
want bool
|
||
}{
|
||
{"前置标注", "目标完成率 85%,分三个阶段推进", "85%", true},
|
||
{"后置标注", "完成率 85%(预计)", "85%", true},
|
||
{"裸数字", "当前完成率 85%,尚有差距", "85%", false},
|
||
{"邻域外有标注词", "目标是建成样板。完成率 85%", "85%", false},
|
||
{"多次出现其一标注", "实际 70%。目标 85% 年内达成", "85%", true},
|
||
{"推测标注", "用户规模约 120,000(推测)", "120,000", true},
|
||
}
|
||
for _, c := range cases {
|
||
if got := MarkedAsEstimate(c.text, c.tok); got != c.want {
|
||
t.Errorf("%s: MarkedAsEstimate(%q) = %v, want %v", c.name, c.tok, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestUnverifiedFactsMarksPass(t *testing.T) {
|
||
material := "材料:上半年营收 3,200 万元。"
|
||
// 「目标 85%」已标注 → 放行;裸 95% → 仍拦。
|
||
artifact := "上半年营收 3,200 万元,达成率目标 85%,试点扩面后裸数字 95%。"
|
||
got := UnverifiedFacts(artifact, material, factExemptions())
|
||
if len(got) != 1 || got[0] != "95%" {
|
||
t.Fatalf("unverified = %v(应只拦未标注的 95%%)", got)
|
||
}
|
||
}
|
||
|
||
// ---- E3:两轮修正的轮次逻辑 ----
|
||
|
||
func TestFactCheckGateRounds(t *testing.T) {
|
||
st := &directRun{}
|
||
first := []string{"95%", "10月20日"}
|
||
|
||
// 第一轮:拦截并给修正通知。
|
||
notice, cont := st.factCheckGate(first)
|
||
if !cont || !strings.Contains(notice, "95%") || strings.Contains(notice, "最后一次拦截") {
|
||
t.Fatalf("round1: cont=%v notice=%q", cont, notice)
|
||
}
|
||
// token 完全没变(模型没理会):不再拦——重复拦截只挤占共享续写预算。
|
||
if _, cont := st.factCheckGate(first); cont {
|
||
t.Fatal("unchanged tokens must pass through")
|
||
}
|
||
// token 有变化(改了一个、残留一个):第二轮升级为「最后一次拦截」。
|
||
notice, cont = st.factCheckGate([]string{"10月20日"})
|
||
if !cont || !strings.Contains(notice, "最后一次拦截") || !strings.Contains(notice, "目标") {
|
||
t.Fatalf("round2: cont=%v notice=%q", cont, notice)
|
||
}
|
||
// 轮数用尽:放行。
|
||
if _, cont := st.factCheckGate([]string{"7%"}); cont {
|
||
t.Fatal("rounds exhausted must pass through")
|
||
}
|
||
if st.factCheckRounds != 2 {
|
||
t.Fatalf("rounds = %d", st.factCheckRounds)
|
||
}
|
||
}
|