package runtime import ( "context" "strings" "testing" core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git" ) // 事实核查:交付物里材料找不到出处的百分比被抽出并组成反馈;找到的不报。 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) } }