Files
Cyrene_v2/internal/runtime/orchestration/orchestration_test.go
T
AskaEth eb1828ee7a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/orchestration(29 文件,纯替换)
- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 裁决编号与 design 节号(02.4.3 / 11.4.2 / design/05 §9 等)逐字保留
- 复核:go build 绿;orchestration 及其子包(audit/comm)测试绿;gofmt 干净
2026-10-04 13:30:04 +08:00

258 lines
7.7 KiB
Go

package orchestration
import (
"context"
"errors"
"fmt"
"runtime"
"strings"
"testing"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration/comm"
)
func TestJoinMergesResults(t *testing.T) {
t.Parallel()
s := newScheduler(t, Options{MaxParallel: 2})
m := &mockRunner{result: &loop.Result{Text: "answer", Steps: 3, StopReason: "stop"}}
s.runner = m
ctx := context.Background()
for i := 0; i < 3; i++ {
if _, err := s.Spawn(ctx, AgentSpec{Goal: fmt.Sprintf("goal %d", i)}); err != nil {
t.Fatalf("Spawn %d: %v", i, err)
}
}
res, err := s.Join(ctx)
if err != nil {
t.Fatalf("Join: %v", err)
}
if res.Total != 3 || res.Succeeded != 3 || res.Failed != 0 || res.Aborted != 0 {
t.Fatalf("merged = %d total / %d ok / %d failed / %d aborted, want 3/3/0/0",
res.Total, res.Succeeded, res.Failed, res.Aborted)
}
if len(res.Agents) != 3 {
t.Fatalf("Agents len = %d, want 3", len(res.Agents))
}
for _, a := range res.Agents {
if a.Status != StatusDone {
t.Fatalf("agent %s status = %s, want done", a.ID, a.Status)
}
if a.Summary == "" {
t.Fatalf("agent %s has no summary", a.ID)
}
if a.Text != "answer" || a.Steps != 3 {
t.Fatalf("agent %s result = text %q steps %d, want answer/3", a.ID, a.Text, a.Steps)
}
}
if !strings.Contains(res.Summary, "3 成功") {
t.Fatalf("merged summary = %q, want a 3-成功 aggregate", res.Summary)
}
}
func TestFailureSummary(t *testing.T) {
t.Parallel()
s := newScheduler(t, Options{MaxParallel: 2})
boom := errors.New("boom")
m := &mockRunner{err: boom}
s.runner = m
sub, err := s.Spawn(context.Background(), AgentSpec{Goal: "x"})
if err != nil {
t.Fatalf("Spawn: %v", err)
}
waitFor(t, func() bool { return sub.Status() == StatusFailed }, waitTimeout)
if !strings.Contains(sub.Summary(), "boom") {
t.Fatalf("Summary = %q, want the failure recorded", sub.Summary())
}
if !errors.Is(sub.Err(), boom) {
t.Fatalf("Err = %v, want boom", sub.Err())
}
if sub.Result() != nil {
t.Fatalf("Result = %v, want nil on failure", sub.Result())
}
res, err := s.Join(context.Background())
if err != nil {
t.Fatalf("Join: %v", err)
}
if res.Total != 1 || res.Failed != 1 || res.Succeeded != 0 {
t.Fatalf("merged = %d total / %d ok / %d failed, want 1/0/1",
res.Total, res.Succeeded, res.Failed)
}
}
func TestContextIsolation(t *testing.T) {
t.Parallel()
s := newScheduler(t, Options{MaxParallel: 1})
m := &mockRunner{result: &loop.Result{Text: "done"}}
s.runner = m
specCtx := []model.Message{model.TextMessage(model.RoleUser, "hello")}
sub, err := s.Spawn(context.Background(), AgentSpec{Goal: "x", Context: specCtx})
if err != nil {
t.Fatalf("Spawn: %v", err)
}
// Spawn 之后再改调用方的切片,不得泄漏进子代理。
specCtx[0].Content[0].Text = "mutated"
res, err := s.Join(context.Background())
if err != nil {
t.Fatalf("Join: %v", err)
}
if len(res.Agents) != 1 {
t.Fatalf("Agents len = %d, want 1", len(res.Agents))
}
if m.total != 1 {
t.Fatalf("runner calls = %d, want 1", m.total)
}
// runner 收到的是隔离后的调用方上下文,加上任务本身作为末尾一条 user 消息
// (裁决 2026-10-04: 任务作为一条 user 消息注入).
got := m.captured(0)
if len(got) != 2 {
t.Fatalf("runner received %d messages, want the isolated context + the task message: %#v", len(got), got)
}
if got[0].Role != model.RoleUser || got[0].Content[0].Text != "hello" {
t.Fatalf("runner context[0] = %+v, want the isolated 'hello' user message", got[0])
}
if got[1].Role != model.RoleUser || !strings.Contains(got[1].Content[0].Text, "x") {
t.Fatalf("runner context[1] = %+v, want the task user message carrying the goal", got[1])
}
if sub.Context[0].Content[0].Text != "hello" {
t.Fatalf("sub-agent context text = %q, want hello", sub.Context[0].Content[0].Text)
}
}
func TestProgressEvents(t *testing.T) {
t.Parallel()
s := newScheduler(t, Options{MaxParallel: 1})
gate := make(chan struct{})
m := &mockRunner{gate: gate}
s.runner = m
listener := comm.New(s.Bus(), "main")
events := make(chan comm.Event, 8)
unsub, err := listener.SubscribeEvent(context.Background(), func(_ context.Context, e comm.Event) error {
events <- e
return nil
})
if err != nil {
t.Fatalf("SubscribeEvent: %v", err)
}
defer unsub()
if _, err := s.Spawn(context.Background(), AgentSpec{Goal: "x"}); err != nil {
t.Fatalf("Spawn: %v", err)
}
waitFor(t, func() bool { return m.activeCount() >= 1 }, waitTimeout)
close(gate)
names := collectEventNames(t, events, waitTimeout)
seen := make(map[string]bool)
for _, n := range names {
seen[n] = true
}
for _, want := range []string{EventDispatched, EventStarted, EventDone} {
if !seen[want] {
t.Fatalf("missing progress event %q, got %v", want, names)
}
}
}
func TestJoin_CanceledCtx(t *testing.T) {
t.Parallel()
s := newScheduler(t, Options{MaxParallel: 1, PerAgentTimeout: 2 * time.Second})
m := &mockRunner{gate: make(chan struct{})}
s.runner = m
if _, err := s.Spawn(context.Background(), AgentSpec{Goal: "x"}); err != nil {
t.Fatalf("Spawn: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
res, err := s.Join(ctx)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("Join err = %v, want DeadlineExceeded", err)
}
if res == nil {
t.Fatal("Join returned a nil merged result on canceled ctx")
}
}
func TestApprove_NoApprover(t *testing.T) {
t.Parallel()
s := newScheduler(t, Options{})
if _, err := s.Approve(context.Background(), "bob", "run", "do something"); !errors.Is(err, ErrNoApprover) {
t.Fatalf("Approve err = %v, want ErrNoApprover", err)
}
}
func TestSinkMemory_NoStore(t *testing.T) {
t.Parallel()
s := newScheduler(t, Options{})
sub, err := s.Spawn(context.Background(), AgentSpec{Goal: "x"})
if err != nil {
t.Fatalf("Spawn: %v", err)
}
if _, err := s.SinkMemory(context.Background(), sub, "remember this", "persona", "relationship"); !errors.Is(err, ErrNoMemory) {
t.Fatalf("SinkMemory err = %v, want ErrNoMemory", err)
}
}
func TestRunWithRealLoop(t *testing.T) {
t.Parallel()
ad := &mockAdapter{resp: model.Response{Content: "final answer", Model: "mock"}}
ag, err := agent.New("You are a helper.", nil, nil, agent.WithModel(ad, nil))
if err != nil {
t.Fatalf("agent.New: %v", err)
}
lp := loop.New(loop.Options{Agent: ag, MaxRounds: 3})
s := newScheduler(t, Options{Loop: lp, MaxParallel: 2})
sub, err := s.Spawn(context.Background(), AgentSpec{Goal: "x"})
if err != nil {
t.Fatalf("Spawn: %v", err)
}
waitFor(t, func() bool { return sub.Status() == StatusDone }, waitTimeout)
if sub.Result() == nil {
t.Fatal("real loop produced no result")
}
if sub.Result().Text != "final answer" {
t.Fatalf("result text = %q, want final answer", sub.Result().Text)
}
res, err := s.Join(context.Background())
if err != nil {
t.Fatalf("Join: %v", err)
}
if res.Succeeded != 1 || res.Failed != 0 {
t.Fatalf("merged = %d ok / %d failed, want 1 ok", res.Succeeded, res.Failed)
}
}
func TestNoGoroutineLeak(t *testing.T) {
// 不并行:goroutine 计数断言在并行测试下噪声很大。
s := newScheduler(t, Options{MaxParallel: 2})
m := &mockRunner{result: &loop.Result{Text: "ok"}}
s.runner = m
baseline := runtime.NumGoroutine()
ctx := context.Background()
for i := 0; i < 20; i++ {
if _, err := s.Spawn(ctx, AgentSpec{Goal: fmt.Sprintf("g%d", i)}); err != nil {
t.Fatalf("Spawn %d: %v", i, err)
}
}
if _, err := s.Join(ctx); err != nil {
t.Fatalf("Join: %v", err)
}
waitFor(t, func() bool { return runtime.NumGoroutine() <= baseline+5 }, waitTimeout)
}