test(qa): pin the answer-run boundary used to trim narration

Adds the production CRA5 shape (narration run + tool steps + final answer run
that opens with a lead-in and an image) and a control case where the last
answer run is interrupted by a tool call and the heuristic still applies.
This commit is contained in:
姚章浩
2026-09-18 15:58:14 +08:00
parent 149e94b34b
commit dfce8cb9f6
+83
View File
@@ -68,3 +68,86 @@ func TestAskEventsQuickKeepsFullAnswer(t *testing.T) {
t.Fatalf("quick answer must be untouched, got %q", result.Answer)
}
}
// Production shape (turn efae317e, 给我CRA5的负载图): the agent narrates its
// steps, then publishes the answer — lead-in, image and all — as its own answer
// run. Cutting at the first structural line threw the lead-in and the image
// away; the run boundary keeps them.
func cra5Fixture() (string, string, string) {
narration := "用户想要 CRA5(CR5A)的末端负载曲线图。让我先在知识库中搜索相关的产品手册和 FAQ。"
body := "我已经找到了 CRA5(CR5A)的末端负载曲线图,并确认了负载曲线的读法说明。下面给出完整回答。\n\n" +
"CRA5(即 CR5A)的末端负载曲线如下:\n\n" +
"![CR5A 末端负载曲线](resource://FsVe56UYMz64sm8NsUZNrg)\n\n" +
"---\n\n" +
"## 一、负载曲线怎么读\n\n" +
"负载曲线反映的是:末端负载能力与负载质心偏心距离成负相关。"
return narration, body, narration + body
}
func TestAskEventsReasoningKeepsLeadInAndImageFromAnswerRun(t *testing.T) {
narration, body, answer := cra5Fixture()
// The heuristic on its own drops the lead-in and the image; the observation
// must win over it.
if heuristic := reasoningmode.TrimReasoningNarration(answer); heuristic == body {
t.Fatal("fixture no longer exposes the heuristic defect")
}
repo := newMemoryRepository()
client := &fakeEventStreamingWeKnora{
fakeWeKnora: &fakeWeKnora{},
events: []weknora.StreamEvent{
{ResponseType: "answer", FullAnswer: narration, AnswerRunID: "step-1-answer", AnswerRunStart: 0},
{ResponseType: "tool_call", ToolName: "knowledge_search"},
{ResponseType: "tool_result", ToolName: "knowledge_search"},
{ResponseType: "thinking"},
{ResponseType: "answer", FullAnswer: answer, Done: true, AnswerRunID: "final-answer", AnswerRunStart: len(narration)},
},
streamResult: weknora.ChatResult{RequestID: "request-cra5", Answer: answer},
}
service, err := New(repo, fixedResolver{}, client, session.GroupShared)
if err != nil {
t.Fatal(err)
}
req := streamQARequest("message-cra5")
req.ReasoningExecution = string(reasoningmode.ExecutionReasoning)
result, err := service.AskEvents(context.Background(), req, func(weknora.StreamEvent) {})
if err != nil {
t.Fatalf("AskEvents: %v", err)
}
if result.Answer != body {
t.Fatalf("delivered answer must keep the answer body (lead-in + image), got %q", result.Answer)
}
}
func TestAskEventsReasoningFallsBackWhenFinalRunInterrupted(t *testing.T) {
_, _, answer := reasoningAnswerFixture()
narration := "我需要先确认这台设备的型号和手册。"
repo := newMemoryRepository()
client := &fakeEventStreamingWeKnora{
fakeWeKnora: &fakeWeKnora{},
events: []weknora.StreamEvent{
{ResponseType: "answer", FullAnswer: narration, AnswerRunID: "step-1-answer", AnswerRunStart: 0},
{ResponseType: "tool_call", ToolName: "knowledge_search"},
{ResponseType: "tool_result", ToolName: "knowledge_search"},
// The last answer run keeps running across another tool call, so the
// stream cannot prove where the body starts: fall back to the heuristic.
{ResponseType: "answer", FullAnswer: answer, AnswerRunID: "step-2-answer", AnswerRunStart: len(narration)},
{ResponseType: "tool_call", ToolName: "knowledge_search"},
{ResponseType: "tool_result", ToolName: "knowledge_search"},
{ResponseType: "answer", FullAnswer: answer, Done: true, AnswerRunID: "step-2-answer", AnswerRunStart: len(narration)},
},
streamResult: weknora.ChatResult{RequestID: "request-interrupted", Answer: answer},
}
service, err := New(repo, fixedResolver{}, client, session.GroupShared)
if err != nil {
t.Fatal(err)
}
req := streamQARequest("message-interrupted")
req.ReasoningExecution = string(reasoningmode.ExecutionReasoning)
result, err := service.AskEvents(context.Background(), req, func(weknora.StreamEvent) {})
if err != nil {
t.Fatalf("AskEvents: %v", err)
}
if want := reasoningmode.TrimReasoningNarration(answer); result.Answer != want {
t.Fatalf("interrupted run must fall back to the heuristic: want %q, got %q", want, result.Answer)
}
}