Files
Agent-System-Backend/internal/runtime/final_reply_test.go
weichen 18aa99cb95
ci / vet-and-test (pull_request) Successful in 1m1s
Keep the final reply clean when checks continue or the verifier is skipped
Why: real run run_b520bf2e3037 showed the user-facing reply being displaced by
internal bookkeeping - the fact-check correction round's closing text plus the
same-source verifier skip notice became the visible answer while the actual
summary was folded into the process timeline (frontend splits at the last
process part). Same tail pollution reproduced on every same-model run since.
Evidence: go test ./... -count=1 green (32 pkgs); new regression tests cover
the demotion of the pre-check summary and the skip notice becoming a check
part; three old-contract tests updated to assert notices land as process
checks instead of final text.
Impact: stop() fact-check branch now demotes the standing summary (aligned
with the gate and cap branches) and asks the model for a fresh full reply;
verifier errors record as process checks (pass-state for same-source skip,
fail-state for real errors) instead of StopDecision.Notice text; the cap
notice likewise becomes a failed check line.
Refs: Dev task feature/final-reply-pollution-fix; diagnosis in
.task/feature-dual-speed-tasks.md lineage and session analysis 2026-10-10.
2026-10-10 07:48:03 +00:00

98 lines
3.2 KiB
Go

package runtime
import (
"context"
"strings"
"testing"
core "agent-system-core"
)
// 问题一回归(run_b520bf2e3037):事实核查修正轮触发时,现场那段正式总结必须
// 降级进过程时间线;修正轮的收尾才是最终答复——否则前端按「最后一个过程部件」
// 切分后,用户只看到修正流水账,看不到原总结。
func TestFactCheckDemotesPriorSummary(t *testing.T) {
rc := &fakeRunCtx{
ctx: context.Background(),
prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model", Lightweight: "lite-model"}},
verify: true,
execArtifact: &core.Artifact{ID: "art_0123456789ab", Name: "报告.md"},
execArtifactContent: []byte("# 报告\n\n准确率 95%。"),
}
turns := padTurns(
textDeltaTurn("", toolCall("make_report", `{"content":"准确率 95%"}`)),
textDeltaTurn("已完成。## 产出物\n\n结构完整的用户总结。"),
textDeltaTurn("已修正并重新登记。"),
)
r := &LLMRunner{Stream: scriptStream(turns, nil), VerifyStream: passedStream()}
if err := r.Execute(rc); err != nil {
t.Fatalf("Execute: %v", err)
}
var summary *core.Part
visible := []string{}
sawFactCheck := false
for i := range rc.assistant {
p := rc.assistant[i]
if p.Type == core.PartText && strings.Contains(p.Text, "结构完整的用户总结") {
summary = &rc.assistant[i]
}
if p.Type == core.PartText && !p.Process {
visible = append(visible, p.Text)
}
if p.Check != nil && !p.Check.Passed && strings.Contains(p.Check.Reason, "事实核查") {
sawFactCheck = true
}
}
if summary == nil {
t.Fatal("prior summary text missing")
}
if !summary.Process {
t.Fatalf("prior summary not demoted to process: %+v", *summary)
}
if !sawFactCheck {
t.Fatal("fact check note missing")
}
if len(visible) != 1 || visible[0] != "已修正并重新登记。" {
t.Fatalf("visible final texts = %v, want only the continuation closing", visible)
}
}
// 同源跳过提示不再作为用户可见正文追加(原先经 StopDecision.Notice 顶掉正式总结),
// 改记为通过态检查行留在过程时间线。
func TestVerifierSkipIsCheckNotFinalText(t *testing.T) {
rc := &fakeRunCtx{
ctx: context.Background(),
prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, // 轻量回落=主模型 → 同源跳过
verify: true,
}
turns := padTurns(textDeltaTurn("总结完成。"))
r := &LLMRunner{Stream: scriptStream(turns, nil)} // 无 VerifyStream
if err := r.Execute(rc); err != nil {
t.Fatalf("Execute: %v", err)
}
skipCheck, noticeText := false, false
visible := []string{}
for _, p := range rc.assistant {
if p.Check != nil && p.Check.Passed && strings.Contains(p.Check.Reason, "同源模型验证") {
skipCheck = true
}
if p.Type == core.PartText {
if strings.Contains(p.Text, verifierUnavailableNotice) {
noticeText = true
}
if !p.Process {
visible = append(visible, p.Text)
}
}
}
if !skipCheck {
t.Fatal("same-source skip not recorded as a passing check")
}
if noticeText {
t.Fatal("skip notice leaked into message text parts")
}
if len(visible) != 1 || visible[0] != "总结完成。" {
t.Fatalf("visible final texts = %v", visible)
}
}