docs(gateway): gateway 剩余文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批: 已推库 16 个文件 + 本批剩余文件)。 校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0); 全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
This commit is contained in:
1 parent
91e8d7a029
commit
074ffc4ebf
73 files changed
+2529
-2529
No files matched your search
@@ -13,7 +13,7 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
|
||||
// callRecord is one invocation captured by the fake runner.
|
||||
// callRecord 是假 runner 捕获的一次调用。
|
||||
type callRecord struct {
|
||||
sessionID string
|
||||
messages []model.Message
|
||||
@@ -21,19 +21,19 @@ type callRecord struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// gatedRunner is a controllable TurnRunner. Each Run blocks until it consumes one
|
||||
// ticket from tickets, or until ctx is canceled (the interrupt path). When it
|
||||
// proceeds it records the call and returns a deterministic reply. A nil err
|
||||
// produces a reply holding the *incoming* messages as text, so a test can assert
|
||||
// exactly what context the resumed turn carried.
|
||||
// gatedRunner 是一个可控 TurnRunner。每次 Run 都会阻塞,直到它
|
||||
// 从 tickets 消费一张票,或直到 ctx 被取消(打断路径)。当它
|
||||
// 继续时,它记录这次调用并返回确定性回复。err 为 nil
|
||||
// 会产出一个把*传入*消息作为文本持有的回复,使测试能断言
|
||||
// 续跑回合究竟携带了什么上下文。
|
||||
//
|
||||
// Like the real loop runner, it never executes a turn whose context is already
|
||||
// canceled, and it counts those canceled returns so a test can wait for an
|
||||
// interrupted turn to actually stop (waitCanceled) before releasing the next
|
||||
// ticket. Without that, a turn that was canceled *while parked in the select
|
||||
// below* can still win the ticket case in the same scheduler step (both cases are
|
||||
// ready) and be recorded as if it had run — a test-harness artifact, not an
|
||||
// orchestrator ordering guarantee.
|
||||
// 与真实的 loop runner 一样,它绝不执行 context 已被取消的
|
||||
// 回合,并统计那些被取消的返回,使测试能等待一个
|
||||
// 被中断的回合真正停下(waitCanceled),然后再释放下一张
|
||||
// 票。没有这一点,一个在下面 select 中驻留时被取消的回合
|
||||
// 仍可能在同一调度步骤中赢下 ticket 分支(两个分支都
|
||||
// 就绪)并被记录成仿佛已经运行 —— 这是测试夹具的产物,不是
|
||||
// 编排器的顺序保证。
|
||||
type gatedRunner struct {
|
||||
mu sync.Mutex
|
||||
calls []callRecord
|
||||
@@ -68,8 +68,8 @@ func (g *gatedRunner) Run(ctx context.Context, sessionID string, messages []mode
|
||||
return &loop.Result{Text: fmt.Sprintf("reply-%d", reply), Steps: 1, StopReason: "stop"}, nil
|
||||
}
|
||||
|
||||
// waitCalls blocks until at least n calls are recorded and returns a copy of the
|
||||
// first n, so a test can advance the gated chain deterministically.
|
||||
// waitCalls 阻塞直到至少记录 n 次调用,并返回前 n 次的
|
||||
// 副本,使测试能确定性地推进受门控的链条。
|
||||
func (g *gatedRunner) waitCalls(n int) []callRecord {
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
@@ -85,25 +85,25 @@ func (g *gatedRunner) waitCalls(n int) []callRecord {
|
||||
panic("timed out waiting for runner calls")
|
||||
}
|
||||
|
||||
// count returns the total recorded calls.
|
||||
// count 返回已记录调用的总数。
|
||||
func (g *gatedRunner) count() int {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return len(g.calls)
|
||||
}
|
||||
|
||||
// recordCanceled notes one invocation that returned because its context was
|
||||
// canceled, i.e. a turn that never executed.
|
||||
// recordCanceled 记下一次因其 context 被取消而返回的调用,
|
||||
// 即一个从未执行的回合。
|
||||
func (g *gatedRunner) recordCanceled() {
|
||||
g.mu.Lock()
|
||||
g.canceled++
|
||||
g.mu.Unlock()
|
||||
}
|
||||
|
||||
// waitCanceled blocks until at least n invocations have stopped because their
|
||||
// context was canceled, reporting false when that never happens within the
|
||||
// deadline. It is the barrier a test uses between an Interrupt and the release of
|
||||
// the next ticket: once it holds, no canceled turn can still consume a ticket.
|
||||
// waitCanceled 阻塞直到至少 n 次调用因 context 被取消而
|
||||
// 停止,未能在期限内发生时报 false。它是测试在 Interrupt
|
||||
// 与释放下一张票之间使用的屏障:它一旦成立,就没有
|
||||
// 被取消的回合还能消费一张票。
|
||||
func (g *gatedRunner) waitCanceled(n int) bool {
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
@@ -118,10 +118,10 @@ func (g *gatedRunner) waitCanceled(n int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// waitIdle blocks until the session reports idle with an empty pending queue,
|
||||
// reporting false when it never settles within the deadline. It lets a test assert
|
||||
// the settled state deterministically instead of sleeping, and turns "a dropped
|
||||
// turn was resumed anyway" into a test failure rather than a silent pass.
|
||||
// waitIdle 阻塞直到该会话报告空闲且待处理队列为空,
|
||||
// 未能在期限内安定时报 false。它让测试能确定性地断言
|
||||
// 安定的状态而不是睡眠等待,并把「被丢弃的回合竟仍被
|
||||
// 续跑」变成测试失败,而不是静默通过。
|
||||
func waitIdle(o *Orchestrator, sessionID string) bool {
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
@@ -133,13 +133,13 @@ func waitIdle(o *Orchestrator, sessionID string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// eventRec is one state transition captured by the recording sink.
|
||||
// eventRec 是记录型汇聚点捕获的一次状态转移。
|
||||
type eventRec struct {
|
||||
sessionID, typ string
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
// recordingSink captures every emitted state transition.
|
||||
// recordingSink 捕获每一次发出的状态转移。
|
||||
type recordingSink struct {
|
||||
mu sync.Mutex
|
||||
events []eventRec
|
||||
@@ -170,12 +170,12 @@ func (s *recordingSink) last() (string, bool) {
|
||||
return s.events[len(s.events)-1].typ, true
|
||||
}
|
||||
|
||||
// userMsg builds a text-only user message.
|
||||
// userMsg 构建一条纯文本用户消息。
|
||||
func userMsg(text string) model.Message {
|
||||
return model.TextMessage(model.RoleUser, text)
|
||||
}
|
||||
|
||||
// messageText returns the concatenated text of a message's text parts.
|
||||
// messageText 返回消息文本部分拼接后的文本。
|
||||
func messageText(m model.Message) string {
|
||||
var out string
|
||||
for _, c := range m.Content {
|
||||
@@ -186,8 +186,8 @@ func messageText(m model.Message) string {
|
||||
return out
|
||||
}
|
||||
|
||||
// seedOrchestrator builds an orchestrator with a gated runner and a recording
|
||||
// sink, both returned so the test can drive the chain and assert state events.
|
||||
// seedOrchestrator 构建一个带受门控 runner 与记录型汇聚点的
|
||||
// 编排器,二者都返回,使测试能驱动链条并断言状态事件。
|
||||
func seedOrchestrator(run *gatedRunner, maxPending int) (*Orchestrator, *recordingSink) {
|
||||
sink := &recordingSink{}
|
||||
o := NewOrchestrator(OrchestratorOptions{
|
||||
@@ -200,13 +200,13 @@ func seedOrchestrator(run *gatedRunner, maxPending int) (*Orchestrator, *recordi
|
||||
return o, sink
|
||||
}
|
||||
|
||||
// readOnlyEnv returns the group-chat / stranger read-only memory lane.
|
||||
// readOnlyEnv 返回群聊 / 陌生人的只读记忆通道。
|
||||
func readOnlyEnv() loop.SessionEnv {
|
||||
return loop.SessionEnv{MemWritable: false}
|
||||
}
|
||||
|
||||
// fakeHistory records every Append so tests assert the orchestrator persists the
|
||||
// user stream and the assistant reply into the chat-history store.
|
||||
// fakeHistory 记录每一次 Append,使测试能断言编排器把用户
|
||||
// 消息流与助手回复持久化进聊天历史存储。
|
||||
type fakeHistory struct {
|
||||
mu sync.Mutex
|
||||
appends []history.Message
|
||||
@@ -286,9 +286,9 @@ func (f *fakeHistory) ListSessions(context.Context) ([]history.SessionMeta, erro
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// MarkSessionRead satisfies the HistoryStore contract for the read-cursor path (D4).
|
||||
// The orchestrator never advances a cursor, so the fake only reports the requested
|
||||
// cursor back without storing it.
|
||||
// MarkSessionRead 为读游标路径(D4)满足 HistoryStore 契约。
|
||||
// 编排器绝不推进游标,因此该假实现只把请求的游标
|
||||
// 原样报告回去,不予存储。
|
||||
func (f *fakeHistory) MarkSessionRead(_ context.Context, _ string, before time.Time) (int64, error) {
|
||||
return before.UnixMilli(), nil
|
||||
}
|
||||
@@ -326,7 +326,7 @@ func TestSubmitPersistsUserAndAssistant(t *testing.T) {
|
||||
if recs[1].SessionID != "web" || recs[1].Role != history.RoleAssistant || recs[1].Content == "" {
|
||||
t.Fatalf("assistant record = %+v", recs[1])
|
||||
}
|
||||
// No mode on the ctx → both fall back to "chat" (design/16 §5.1).
|
||||
// ctx 上没有 mode → 二者都回退到 "chat"(design/16 §5.1)。
|
||||
for i, rec := range recs {
|
||||
if rec.Mode != "chat" {
|
||||
t.Fatalf("record[%d] Mode = %q, want chat fallback", i, rec.Mode)
|
||||
@@ -344,9 +344,9 @@ func TestSubmitPersistsMessageMode(t *testing.T) {
|
||||
History: hist,
|
||||
})
|
||||
run.tickets <- struct{}{}
|
||||
// The HTTP/WS boundary threads the client's behavior mode through ctx
|
||||
// (design/01 §3, design/16 §5.1); both the user turn and the assistant reply
|
||||
// must be persisted under that mode.
|
||||
// HTTP/WS 边界把客户端的行为模式经 ctx 串下来
|
||||
// (design/01 §3、design/16 §5.1);用户回合与助手回复
|
||||
// 都必须以该模式被持久化。
|
||||
ctx := withMode(context.Background(), "code")
|
||||
sr, res := o.SubmitSync(ctx, "web", []model.Message{userMsg("改代码")}, loop.OwnerEnv())
|
||||
if !sr.Accepted || !sr.Immediate {
|
||||
@@ -377,8 +377,8 @@ func TestSubmitPersistsDeviceID(t *testing.T) {
|
||||
History: hist,
|
||||
})
|
||||
run.tickets <- struct{}{}
|
||||
// The HTTP/WS boundary threads the browser deviceID through ctx (design/16 §4.5);
|
||||
// both the user turn and the assistant reply must be tagged with it.
|
||||
// HTTP/WS 边界把浏览器 deviceID 经 ctx 串下来(design/16 §4.5);
|
||||
// 用户回合与助手回复都必须被它标注。
|
||||
ctx := withDeviceID(context.Background(), "web:chrome:a1b2c3")
|
||||
sr, res := o.SubmitSync(ctx, "web", []model.Message{userMsg("你好")}, loop.OwnerEnv())
|
||||
if !sr.Accepted || !sr.Immediate {
|
||||
@@ -441,7 +441,7 @@ func TestSubmitDoesNotPersistRejectedTurn(t *testing.T) {
|
||||
t.Parallel()
|
||||
hist := &fakeHistory{}
|
||||
o := NewOrchestrator(OrchestratorOptions{Run: newGatedRunner(0).Run, Log: slog.Default(), History: hist, MaxPending: 1})
|
||||
// Empty turn is rejected before any persistence.
|
||||
// 空回合在任何持久化之前就被拒绝。
|
||||
sr := o.Submit(context.Background(), "web", nil, loop.OwnerEnv())
|
||||
if sr.Accepted || !sr.Empty {
|
||||
t.Fatalf("empty submit = %+v, want Empty reject", sr)
|
||||
@@ -475,12 +475,12 @@ func TestSubmitBusyPendingNotDropped(t *testing.T) {
|
||||
run := newGatedRunner(4)
|
||||
o, sink := seedOrchestrator(run, 5)
|
||||
|
||||
// Turn A starts and blocks (no ticket yet), so the session is busy.
|
||||
// 回合 A 启动并阻塞(还没有票),因此会话处于忙碌。
|
||||
a := o.Submit(context.Background(), "s1", []model.Message{userMsg("A")}, loop.OwnerEnv())
|
||||
if !a.Immediate {
|
||||
t.Fatalf("A = %+v, want Immediate", a)
|
||||
}
|
||||
// B and C arrive while A is in flight: they must be queued, not dropped.
|
||||
// B 与 C 在 A 在飞时到达:它们必须被排队,而不是被丢弃。
|
||||
b := o.Submit(context.Background(), "s1", []model.Message{userMsg("B")}, readOnlyEnv())
|
||||
c := o.Submit(context.Background(), "s1", []model.Message{userMsg("C")}, loop.OwnerEnv())
|
||||
if !b.Accepted || !b.Pending || b.QueueDepth != 1 {
|
||||
@@ -493,8 +493,8 @@ func TestSubmitBusyPendingNotDropped(t *testing.T) {
|
||||
t.Fatalf("queue depth = %d, want 2", o.QueueDepth("s1"))
|
||||
}
|
||||
|
||||
// Release the chain: A, then B, then C. The emitted ack should include the
|
||||
// pending depth, and the sink should have seen pending_ack events.
|
||||
// 释放链条:A,然后 B,然后 C。发出的 ack 应包含
|
||||
// 待处理深度,且汇聚点应已看到 pending_ack 事件。
|
||||
types := sink.types()
|
||||
foundAck := false
|
||||
for _, ty := range types {
|
||||
@@ -537,27 +537,27 @@ func TestSubmitBusyResumeCarriesPreviousContext(t *testing.T) {
|
||||
run.tickets <- struct{}{} // C
|
||||
got := run.waitCalls(3)
|
||||
|
||||
// A starts from a blank turn.
|
||||
// A 从空白回合开始。
|
||||
if len(got[0].messages) != 1 || messageText(got[0].messages[0]) != "A" {
|
||||
t.Fatalf("A messages = %v, want [A]", messageTextList(got[0].messages))
|
||||
}
|
||||
// B must resume carrying A's assistant reply folded into the tail.
|
||||
// B 必须带着折进尾部的 A 助手回复续跑。
|
||||
if !matchesText(got[1].messages, "A", "B") {
|
||||
t.Fatalf("B messages = %v, want B to resume carrying the prior tail", messageTextList(got[1].messages))
|
||||
}
|
||||
// C must resume carrying A's and B's assistant replies.
|
||||
// C 必须带着 A 与 B 的助手回复续跑。
|
||||
if !matchesText(got[2].messages, "A", "C") {
|
||||
t.Fatalf("C messages = %v, want C to resume carrying the prior tail", messageTextList(got[2].messages))
|
||||
}
|
||||
// B's env must be the one the message arrived with (read-only lane).
|
||||
// B 的 env 必须是该消息到达时所用的那个(只读通道)。
|
||||
if got[1].env.MemWritable {
|
||||
t.Fatal("B resumed with MemWritable=true, want the caller's read-only env")
|
||||
}
|
||||
}
|
||||
|
||||
// matchesText reports whether a resumed turn begins with the captured first user
|
||||
// turn (so the prior context was carried over), ends with the expected new user
|
||||
// turn, and contains at least one assistant reply in between.
|
||||
// matchesText 报告一个续跑回合是否以捕获的首个用户
|
||||
// 回合开头(即先前上下文被带了过来)、以预期的新闻用户
|
||||
// 回合结尾,且中间至少含有一条助手回复。
|
||||
func matchesText(msgs []model.Message, first, last string) bool {
|
||||
if len(msgs) < 3 {
|
||||
return false
|
||||
@@ -587,7 +587,7 @@ func messageTextList(msgs []model.Message) []string {
|
||||
func TestSubmitPendingBoundedRejectsOverflow(t *testing.T) {
|
||||
t.Parallel()
|
||||
run := newGatedRunner(8)
|
||||
o, _ := seedOrchestrator(run, 3) // tight queue
|
||||
o, _ := seedOrchestrator(run, 3) // 紧凑队列
|
||||
|
||||
if sr := o.Submit(context.Background(), "s1", []model.Message{userMsg("A")}, loop.OwnerEnv()); !sr.Immediate {
|
||||
t.Fatalf("A = %+v, want Immediate", sr)
|
||||
@@ -598,7 +598,7 @@ func TestSubmitPendingBoundedRejectsOverflow(t *testing.T) {
|
||||
t.Fatalf("pending %d = %+v, want Pending depth=%d", i, sr, i+1)
|
||||
}
|
||||
}
|
||||
// Queue is at capacity (3), so the next message is refused, not dropped.
|
||||
// 队列已达容量(3),因此下一条消息被拒绝,而不是被丢弃。
|
||||
over := o.Submit(context.Background(), "s1", []model.Message{userMsg("overflow")}, loop.OwnerEnv())
|
||||
if over.Accepted || !over.PendingFull {
|
||||
t.Fatalf("overflow = %+v, want PendingFull reject", over)
|
||||
@@ -620,7 +620,7 @@ func TestInterruptCancelsTurnAndClearsPending(t *testing.T) {
|
||||
t.Fatalf("B = %+v, want Pending", sr)
|
||||
}
|
||||
|
||||
// Release no ticket: A is blocked in flight. Interrupt cancels it and clears B.
|
||||
// 不释放任何票:A 阻塞在飞。Interrupt 取消它并清除 B。
|
||||
dropped := o.Interrupt("s1")
|
||||
if dropped != 1 {
|
||||
t.Fatalf("dropped = %d, want 1", dropped)
|
||||
@@ -629,16 +629,16 @@ func TestInterruptCancelsTurnAndClearsPending(t *testing.T) {
|
||||
t.Fatalf("session not idle after interrupt: busy=%v depth=%d", o.Busy("s1"), o.QueueDepth("s1"))
|
||||
}
|
||||
|
||||
// The cancel must reach the in-flight turn (design/15 §3: interrupt is a
|
||||
// graceful context cancel) and it must stop before any further turn is
|
||||
// released: a canceled turn never executes, so it can never consume the ticket
|
||||
// meant for the next one.
|
||||
// 取消必须抵达在飞回合(design/15 §3:打断是优雅的
|
||||
// context 取消),且它必须在释放任何后续回合之前停下:
|
||||
// 被取消的回合绝不执行,因此它绝不可能消费本该属于
|
||||
// 下一个回合的票。
|
||||
if !run.waitCanceled(1) {
|
||||
t.Fatal("interrupted turn never observed its context cancellation")
|
||||
}
|
||||
|
||||
// A new message after the interrupt must start fresh (Immediate), and the
|
||||
// canceled chain must not have resumed B.
|
||||
// 打断之后的新消息必须全新开始(Immediate),且
|
||||
// 被取消的链条绝不续跑过 B。
|
||||
if sr := o.Submit(context.Background(), "s1", []model.Message{userMsg("C")}, loop.OwnerEnv()); !sr.Immediate {
|
||||
t.Fatalf("C = %+v, want Immediate after interrupt", sr)
|
||||
}
|
||||
@@ -651,18 +651,18 @@ func TestInterruptCancelsTurnAndClearsPending(t *testing.T) {
|
||||
t.Fatalf("only surviving call = %v, want the C turn", messageTextList(got[0].messages))
|
||||
}
|
||||
|
||||
// C's chain settles with nothing left to run: had the interrupt failed to
|
||||
// cancel A or to clear B, the surviving chain would have resumed a turn and the
|
||||
// session would never go idle again.
|
||||
// C 的链条安定下来且无待运行项:若打断未能取消 A
|
||||
// 或未能清除 B,存活的链条本会续跑一个回合,
|
||||
// 而该会话就再也不会回到空闲。
|
||||
if !waitIdle(o, "s1") {
|
||||
t.Fatalf("session never settled idle: busy=%v depth=%d", o.Busy("s1"), o.QueueDepth("s1"))
|
||||
}
|
||||
if n := run.count(); n != 1 {
|
||||
t.Fatalf("runner calls = %d, want 1 (neither the canceled A nor the dropped B may execute)", n)
|
||||
}
|
||||
// The canceled turn's completion is a no-op for session state (its generation is
|
||||
// stale), so it must not emit the idle event of the turn now running: exactly
|
||||
// one session_idle belongs to this scenario (C settling).
|
||||
// 被取消回合的完成对会话状态是空操作(其代次已
|
||||
// 陈旧),因此它不得发出正在运行那个回合的 idle 事件:
|
||||
// 本场景恰好有一个 session_idle(C 安定)。
|
||||
idles := 0
|
||||
for _, ty := range sink.types() {
|
||||
if ty == EventSessionIdle {
|
||||
@@ -705,8 +705,8 @@ func TestSubmitConcurrentSameSessionIsSafeAndBounded(t *testing.T) {
|
||||
t.Fatalf("unexpected submit result: %+v", sr)
|
||||
}
|
||||
}
|
||||
// With maxPending=3: exactly 1 turn runs immediately and up to 3 queue behind
|
||||
// it; the remaining senders are refused at capacity rather than dropped.
|
||||
// maxPending=3 时:恰好 1 个回合立即运行,最多 3 个排在其后;
|
||||
// 其余发送者在容量处被拒绝,而不是被丢弃。
|
||||
if immediate != 1 {
|
||||
t.Fatalf("immediate = %d, want 1", immediate)
|
||||
}
|
||||
@@ -720,7 +720,7 @@ func TestSubmitConcurrentSameSessionIsSafeAndBounded(t *testing.T) {
|
||||
t.Fatalf("queue depth = %d, want 3", o.QueueDepth("s1"))
|
||||
}
|
||||
|
||||
// Drain the chain: 1 running + 3 queued = 4 turns. No panic / no lost message.
|
||||
// 排空链条:1 个在跑 + 3 个排队 = 4 个回合。无 panic / 无消息丢失。
|
||||
for i := 0; i < 4; i++ {
|
||||
run.tickets <- struct{}{}
|
||||
}
|
||||
@@ -750,8 +750,8 @@ func TestSubmitSyncIdleReturnsResultBusyReturnsPending(t *testing.T) {
|
||||
run := newGatedRunner(2)
|
||||
o, _ := seedOrchestrator(run, 5)
|
||||
|
||||
// Idle → synchronous run returns the reply immediately. Push a ticket so the
|
||||
// gated runner's blocking select resolves during the synchronous wait.
|
||||
// 空闲 → 同步运行立即返回回复。推入一张票,使
|
||||
// 受门控 runner 的阻塞 select 在同步等待期间得以解决。
|
||||
run.tickets <- struct{}{}
|
||||
sr, res := o.SubmitSync(context.Background(), "s1", []model.Message{userMsg("hi")}, loop.OwnerEnv())
|
||||
if !sr.Accepted || !sr.Immediate {
|
||||
@@ -761,8 +761,8 @@ func TestSubmitSyncIdleReturnsResultBusyReturnsPending(t *testing.T) {
|
||||
t.Fatal("sync idle should return a result")
|
||||
}
|
||||
|
||||
// Hold a busy turn in flight via an async Submit (blocked, no ticket), then a
|
||||
// sync submit must return a Pending ack without blocking and without a result.
|
||||
// 经异步 Submit 握一个在飞的忙碌回合(阻塞,无票),随后
|
||||
// 同步 submit 必须不阻塞、无结果地返回 Pending ack。
|
||||
if sr2 := o.Submit(context.Background(), "s1", []model.Message{userMsg("A")}, loop.OwnerEnv()); !sr2.Immediate {
|
||||
t.Fatalf("async A = %+v, want Immediate", sr2)
|
||||
}
|
||||
@@ -773,7 +773,7 @@ func TestSubmitSyncIdleReturnsResultBusyReturnsPending(t *testing.T) {
|
||||
if res3 != nil {
|
||||
t.Fatal("sync busy should not return a result (must not block)")
|
||||
}
|
||||
run.tickets <- struct{}{} // let A finish so the orchestrator is idle again
|
||||
run.tickets <- struct{}{} // 让 A 结束,使编排器回到空闲
|
||||
}
|
||||
|
||||
func TestInterruptAllClearsEverySession(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user