Files
Cyrene_v2/internal/services/gateway/session_parallel_notice_test.go
T
AskaEth ba1974038a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway): 并行提示接线(第二路 + 串行续跑路;OtherMessageID/Queued 取自另一在途路,无其它在途路则不挂)
2026-10-05 12:21:24 +08:00

205 lines
8.6 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
import (
"context"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// 本文件是 design/15 §2.5.6「并行路进行态摘要」**接线**的回归套件(它单独成文件,
// 使 session_parallel_test.go 不因这一组用例越过「单文件 ≤500 行」的硬约束)。
//
// 它钉住三件事:
// - 第二路起跑时它的 runCtx 上带着摘要,且 OtherMessageID 指向**另一路**的消息 id;
// - 摘要里的 Queued 取自该会话仍在 pending 的消息条数;
// - 没有其它在途路时(单路 / 串行续跑)**不挂**该提示 —— 未接线的路径零成本。
//
// 断言一律**从 runCtx 上读**(loop.ParallelNoticeFrom),因此接线一旦漏掉或对方
// 消息 id 一旦不填,这些用例立刻变红。
// ---------------------------------------------------------------------------
// 并行进行态摘要(design/15 §2.5.6):起第二路时挂给**它自己**
// ---------------------------------------------------------------------------
// firstUserMessageID 返回落库记录里第一条用户消息的权威 id(夹具由 fakeHistory
// 分配)。断言「提示指向另一路的那条消息」时用它,而不是硬编码一个 id。
func firstUserMessageID(t *testing.T, f *parallelFixture) string {
t.Helper()
for _, m := range f.history.records() {
if m.Role == history.RoleUser && m.ID != "" {
return m.ID
}
}
t.Fatal("夹具没有给第一路的用户消息落库 id,这条断言无从谈起")
return ""
}
// userMessageIDByText 返回落库记录里正文为 text 的那条用户消息的权威 id。
func userMessageIDByText(t *testing.T, f *parallelFixture, text string) string {
t.Helper()
for _, m := range f.history.records() {
if m.Role == history.RoleUser && m.Content == text && m.ID != "" {
return m.ID
}
}
t.Fatalf("落库记录里找不到正文为 %q 的用户消息", text)
return ""
}
// TestParallelNoticeAttachedToSecondLegOnly 钉住 §2.5.6 的**接线**:第二路起跑时它
// 的 runCtx 上带着「另有一路在跑」的结构化摘要,且 OtherMessageID 指向**另一路**
// 的那条消息;而第一路(起跑时本会话没有别的在途路)**不挂**该提示 —— 单路零成本。
//
// 它刻意**从 runCtx 上读**(而不是读编排器内部字段):接线漏掉时这个断言立刻变红,
// 因为提示此刻根本不在 ctx 上。
func TestParallelNoticeAttachedToSecondLegOnly(t *testing.T) {
t.Parallel()
f := newParallelFixture(5, 2)
if sr := f.o.Submit(context.Background(), "web", []model.Message{userMsg("第一问")}, loop.OwnerEnv()); !sr.Immediate {
t.Fatalf("第一路 = %+v, want Immediate", sr)
}
firstCalls := f.run.waitStarted(1)
if notice, ok := loop.ParallelNoticeFrom(firstCalls[0].ctx); ok {
t.Fatalf("第一路的 runCtx 不该挂并行提示(起跑时没有其它在途路),得到 %+v", notice)
}
second := f.o.Submit(parallelCtx(), "web", []model.Message{userMsg("第二问")}, loop.OwnerEnv())
if !second.Immediate || !second.Parallel {
t.Fatalf("第二路 = %+v, want Immediate 且 Parallel", second)
}
calls := f.run.waitStarted(2)
notice, ok := loop.ParallelNoticeFrom(calls[1].ctx)
if !ok {
t.Fatal("第二路的 runCtx 上必须挂并行提示(§2.5.6 的接线点)")
}
wantID := firstUserMessageID(t, f)
if notice.OtherMessageID != wantID {
t.Fatalf("提示里的对方消息 id = %q, want %q(另一路已落库的那条)", notice.OtherMessageID, wantID)
}
// 摘要只含元数据:末条消息 id 必须是**另一路**的,绝不能等于本路自己的
// (本路此刻还没落库,也绝不该拿自己的消息冒充对方)。
if notice.OtherMessageID == "" {
t.Fatal("提示里的对方消息 id 不该为空")
}
// Step / ToolNames 当前没有数据源 ⇒ 恒为 0 / nil,对应事实行不渲染。
if notice.Step != 0 || len(notice.ToolNames) != 0 {
t.Fatalf("Step/ToolNames 没有数据源,必须留空,得到 %d / %v", notice.Step, notice.ToolNames)
}
f.run.release(0)
f.run.release(1)
if !waitIdle(f.o, "web") {
t.Fatal("会话没有回到空闲")
}
}
// TestParallelNoticeReportsQueueDepth 钉住摘要里的排队条数取自 st.pending:那条
// 已排队、尚未被任何一路接手的消息计入 Queued,而不是被漏掉或凭空推断。
func TestParallelNoticeReportsQueueDepth(t *testing.T) {
t.Parallel()
f := newParallelFixture(5, 2)
f.o.Submit(context.Background(), "web", []model.Message{userMsg("甲")}, loop.OwnerEnv())
f.run.waitStarted(1)
// 未声明并行的忙时消息 ⇒ 入队(不进任何一路)。
if sr := f.o.Submit(context.Background(), "web", []model.Message{userMsg("乙")}, loop.OwnerEnv()); !sr.Pending {
t.Fatalf("忙时未声明并行的提交 = %+v, want Pending", sr)
}
if sr := f.o.Submit(parallelCtx(), "web", []model.Message{userMsg("丙")}, loop.OwnerEnv()); !sr.Parallel {
t.Fatalf("第三问 = %+v, want 照准并行", sr)
}
calls := f.run.waitStarted(2)
notice, ok := loop.ParallelNoticeFrom(calls[1].ctx)
if !ok {
t.Fatal("第二路的 runCtx 上必须挂并行提示")
}
if notice.Queued != 1 {
t.Fatalf("提示里的排队条数 = %d, want 1(那条尚未被接手的「乙」)", notice.Queued)
}
// 两路逐条收尾;排队的那条由结算中的一路续跑(第 3 条路),必须一并放行。
f.run.release(0)
f.run.release(1)
f.run.waitStarted(3)
f.run.release(2)
if !waitIdle(f.o, "web") {
t.Fatal("会话没有回到空闲")
}
}
// TestParallelNoticeAbsentWithoutOtherLegs 钉住「没有其它在途路就不挂提示」:
// 单路提交、以及它的**串行续跑路**(起跑时在途路已清空)都拿不到提示 —— 未接线的
// 路径零成本,行为与加入这条 ctx 通道之前逐字不变。
func TestParallelNoticeAbsentWithoutOtherLegs(t *testing.T) {
t.Parallel()
f := newParallelFixture(5, 2)
f.o.Submit(context.Background(), "web", []model.Message{userMsg("甲")}, loop.OwnerEnv())
f.run.waitStarted(1)
f.o.Submit(context.Background(), "web", []model.Message{userMsg("乙")}, loop.OwnerEnv())
// 放行第一路 ⇒ 弹队首续跑;此刻在途路已清空,续跑路不得挂任何提示。
f.run.release(0)
calls := f.run.waitStarted(2)
for i, c := range calls {
if notice, ok := loop.ParallelNoticeFrom(c.ctx); ok {
t.Fatalf("第 %d 路的 runCtx 不该挂并行提示(本会话没有其它在途路),得到 %+v", i+1, notice)
}
}
f.run.release(1)
if !waitIdle(f.o, "web") {
t.Fatal("会话没有回到空闲")
}
}
// TestParallelNoticeAttachedToResumeLeg 钉住**续跑路**的接线(主人 2026-10-05 裁决:
// 同一类串话、同一个修法):一路收尾、队列有货、而**另一并行路仍在飞**时弹出来的
// 那条续跑路,同样拿到进行态摘要,且 OtherMessageID 指向**当时仍在飞的那一路**。
//
// 「没有其它在途路的续跑路不挂提示」由 TestParallelNoticeAbsentWithoutOtherLegs 钉住,
// 两条合起来才是完整的接线口径(有则挂、无则零成本)。
func TestParallelNoticeAttachedToResumeLeg(t *testing.T) {
t.Parallel()
f := newParallelFixture(5, 2)
f.o.Submit(context.Background(), "web", []model.Message{userMsg("甲")}, loop.OwnerEnv())
f.run.waitStarted(1)
if sr := f.o.Submit(parallelCtx(), "web", []model.Message{userMsg("乙")}, loop.OwnerEnv()); !sr.Parallel {
t.Fatalf("第二路 = %+v, want 照准并行", sr)
}
live := f.run.waitStarted(2)
if notice, ok := loop.ParallelNoticeFrom(live[0].ctx); ok {
t.Fatalf("第一路不该挂并行提示(它起跑时本会话没有其它在途路),得到 %+v", notice)
}
// 忙时未声明并行的第三条 ⇒ 入队,等某一路收尾时被续跑。
if sr := f.o.Submit(context.Background(), "web", []model.Message{userMsg("丙")}, loop.OwnerEnv()); !sr.Pending {
t.Fatalf("第三条 = %+v, want Pending", sr)
}
// 放行**第一**路 ⇒ 它收尾后弹队首起续跑路;此刻第二路仍在飞。
f.run.release(0)
calls := f.run.waitStarted(3)
notice, ok := loop.ParallelNoticeFrom(calls[2].ctx)
if !ok {
t.Fatal("续跑路的 runCtx 上必须挂并行提示(当时仍有另一路在飞)")
}
wantID := userMessageIDByText(t, f, "乙")
if notice.OtherMessageID != wantID {
t.Fatalf("续跑路提示里的对方消息 id = %q, want %q(当时仍在飞的那一路)", notice.OtherMessageID, wantID)
}
// 两路逐条收尾(第二路释放在前、续跑路在后),会话回到空闲。
f.run.release(1)
f.run.release(2)
if !waitIdle(f.o, "web") {
t.Fatal("会话没有回到空闲")
}
}