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:
@@ -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" +
|
||||
"\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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user