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("会话没有回到空闲") } }