前提由 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 字符(常量+依据)
- 只放图里已有的、已脱敏内容,不放宽脱敏通道
90 lines
4.3 KiB
Go
90 lines
4.3 KiB
Go
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)
|
||
}
|
||
}
|