Files
Cyrene_v2/internal/services/gateway/history_toolcalls_gap_test.go
T
AskaEth c907a34510 feat(gateway): 面 1 补齐 —— 消息读取面 toolCalls[](工具回看)
前提由 FV2 提供(图自 v0.77 起带每路 turnId、每路各落一行)。本片:
- 迁移 0023:flow_graphs 加「按路查询键」(写入时回填图的 TurnID),老行按缺席处理
- GET /api/v1/sessions/{id}/messages 加**可选** toolCalls[],形状严格照 Docs/api-client.md
  v0.76 ① 已登记的:{name, status(ok|error|unknown), durationMs, argsSummary, resultSummary}
- 🔴 图取不到 ⇒ **字段整体缺席**(绝不回空数组冒充「没调过工具」)
- 一条批量 SQL(按页内 turnId 集合),禁 N+1;单条 ≤20 个工具、resultSummary ≤500 字符(常量+依据)
- 只放图里已有的、已脱敏内容,不放宽脱敏通道
2026-10-06 09:03:55 +08:00

90 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gateway
// 本文件钉住**单路回合的工具回看面缺口**(切片 CODE-TC 的诚实边界 —— 本用例钉的是
// **现状,不是期望行为**)。
//
// 事实链(三处代码,全部可核对):
//
// 1. 发布器建图时把「这一路的路标识」取自 `loop.flowOwner` → `ParallelLegFrom(ctx)`
// (`internal/runtime/loop/flow_branch.go`);
// 2. `WithParallelLeg` **只给真并行路**挂(`internal/services/gateway/session_parallel.go`
// 的 `beginLegLocked`:`if parallel { ... }`),普通回合 / 队首续跑路都不挂;
// 3. 于是普通回合的图 `TurnID == ""`(既有回归 `internal/runtime/loop/flow_lane_test.go`
// 的 `TestFlowPublisher_SingleLegUnchanged` 正是这么钉的),落盘后
// `flow_graphs.lane_turn_id` 为空 ⇒ 按消息的 `turn_id` **查不到图** ⇒ `toolCalls`
// 整体缺席(这是契约允许的「图取不到」,但**不是**我们要的覆盖率)。
//
// 因此本特性在当前代码下只覆盖**真并行路**的回复。要覆盖普通回合,必须让发布器把
// **ctx 上的权威 turnId**(`loop.TurnIDFrom`,普通回合也有)盖在图上 —— 那是
// `internal/runtime/loop/**` 的改动,**不属于本切片的面**,故此处只如实钉住。
//
// ⚠️ 本用例**红了**只可能意味着 loop 已经把权威 turnId 盖上了单路图:那时请把它改成
// 「单路回合也能回看到工具」的**正向断言**(并把 `loop` 侧那条改动一并交代),
// **不要**删掉它。
import (
"context"
"testing"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
)
// TestSingleLegRoundLacksLaneTurnID_现状缺口 演练一次**普通回合**(没有
// `WithParallelLeg`,与生产里 Web 单路聊天逐字同形):它的图确实落了盘、也确实带工具
// 节点,但图上没有路标识 ⇒ 消息侧无论用哪个值都查不到它 ⇒ `toolCalls` 缺席。
func TestSingleLegRoundLacksLaneTurnID_现状缺口(t *testing.T) {
t.Parallel()
env := newToolCallEnv(t, Deps{}, nil, true)
ctx := context.Background()
// 走生产那条落盘路径(发布器 → flowgraph.SQLStore.RecordRound)。
pub := flowPublisherWithStore(env.store, func(string) bool { return true })
pub.StartRound(history.MainSessionID, "问题")
sp := &trace.Span{
ID: "span-tool", Name: "tool.web_search", Kind: trace.SpanKindTool, StartedAt: time.Now(),
// 生产里的 span 由 Tracer 建(Props 已初始化);这里手搓一个,必须自己给 map,
// 否则 SetProp 会往 nil map 里写。
Props: map[string]any{},
}
pub.SpanStarted(history.MainSessionID, sp)
_ = sp.SetProp("tool", "web_search")
_ = sp.SetProp("result", "晴,18~26℃")
sp.Finish(trace.SpanStatusOK)
pub.SpanFinished(history.MainSessionID, sp)
pub.EndRound(history.MainSessionID, "答复")
snap := pub.LastGraph(history.MainSessionID)
if snap == nil {
t.Fatal("单路回合应当建出一张图")
}
if snap.TurnID != "" {
t.Fatalf("现状已变:单路图带上了 turnId=%q(loop 已盖上权威回合标识)—— 请把本用例改成"+
"「单路也能回看」的正向断言,不要删掉它", snap.TurnID)
}
// 库里那一行的 `lane_turn_id` 为空:拿消息侧的 turnId 查不到,拿图 id 也查不到
// (两者不同源,这正是 v0.76 登记的阻塞,只被 v0.77 解掉了一半)。
found, err := env.store.GraphsByTurnID(ctx, history.MainSessionID, []string{"turn-abc", snap.ID})
if err != nil {
t.Fatalf("GraphsByTurnID: %v", err)
}
if len(found) != 0 {
t.Fatalf("单路图竟然按路查到了:%v(现状已变,请更新本用例)", found)
}
// 端到端:这条普通回合的回复在读接口上没有 toolCalls(缺席,不是空数组)。
if err := env.history.Append(ctx, history.Message{
SessionID: history.MainSessionID, Role: history.RoleAssistant, Content: "答复", TurnID: "turn-abc", CreatedAt: 1000,
}); err != nil {
t.Fatalf("seed: %v", err)
}
tok := signAny(t, env.jwt, "acct", auth.RoleOwner)
raw := env.pageMessages(t, tok)["答复"]
if v, present := raw["toolCalls"]; present {
t.Fatalf("单路回合本应查不到图(缺口),却给出了 toolCalls=%s", v)
}
}