156 lines
5.6 KiB
Go
156 lines
5.6 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"iter"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/Autumn-27/artex/db"
|
|
"github.com/Autumn-27/norma/llm"
|
|
)
|
|
|
|
type directionCaptureProvider struct {
|
|
mu sync.Mutex
|
|
requests []llm.CompletionRequest
|
|
}
|
|
|
|
func (p *directionCaptureProvider) Stream(_ context.Context, req llm.CompletionRequest) iter.Seq2[llm.StreamEvent, error] {
|
|
p.mu.Lock()
|
|
p.requests = append(p.requests, req)
|
|
p.mu.Unlock()
|
|
return func(yield func(llm.StreamEvent, error) bool) {
|
|
if !yield(llm.StreamEvent{Type: llm.SETextDelta, Text: "同步完成"}, nil) {
|
|
return
|
|
}
|
|
if !yield(llm.StreamEvent{Type: llm.SEMessageDelta, StopReason: "end_turn"}, nil) {
|
|
return
|
|
}
|
|
yield(llm.StreamEvent{Type: llm.SEMessageStop}, nil)
|
|
}
|
|
}
|
|
|
|
func (p *directionCaptureProvider) Complete(_ context.Context, req llm.CompletionRequest) (llm.Message, string, llm.Usage, error) {
|
|
p.mu.Lock()
|
|
p.requests = append(p.requests, req)
|
|
p.mu.Unlock()
|
|
return llm.Message{Role: llm.RoleAssistant, Content: []llm.ContentBlock{llm.TextBlock("同步完成")}}, "end_turn", llm.Usage{}, nil
|
|
}
|
|
|
|
func (p *directionCaptureProvider) lastRequest(t *testing.T) llm.CompletionRequest {
|
|
t.Helper()
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if len(p.requests) == 0 {
|
|
t.Fatal("provider received no completion request")
|
|
}
|
|
return p.requests[len(p.requests)-1]
|
|
}
|
|
|
|
func lastUserText(t *testing.T, req llm.CompletionRequest) string {
|
|
t.Helper()
|
|
for i := len(req.Messages) - 1; i >= 0; i-- {
|
|
if req.Messages[i].Role == llm.RoleUser {
|
|
return req.Messages[i].Text()
|
|
}
|
|
}
|
|
t.Fatal("completion request has no user message")
|
|
return ""
|
|
}
|
|
|
|
type directionAgentFixture struct {
|
|
d *db.DB
|
|
task *db.Task
|
|
store *db.ExplorationStore
|
|
intentID int64
|
|
activity db.Activity
|
|
}
|
|
|
|
func newDirectionAgentFixture(t *testing.T) *directionAgentFixture {
|
|
t.Helper()
|
|
d := testDB(t)
|
|
task, err := d.CreateTask("agent direction sync", "synchronize worker steering", nil, 0, 0)
|
|
if err != nil {
|
|
d.Close()
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = d.DeleteTask(task.ID)
|
|
_ = d.Close()
|
|
})
|
|
store := d.Exploration(task.ExplorationID)
|
|
intentID, err := store.AddIntent(map[string]any{"summary": "原始接口枚举方向"}, 1, nil, "planner")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if claimed, err := store.ClaimIntent(intentID, "work#direction"); err != nil || !claimed {
|
|
t.Fatalf("claim direction fixture: claimed=%v err=%v", claimed, err)
|
|
}
|
|
const requestID = "agent-direction-sync-1"
|
|
const direction = "停止接口枚举,立即聚焦登录后的水平越权"
|
|
if _, err := store.ReserveIntentIntervention(intentID, requestID, direction); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if changed, err := store.CompareAndSetIntentState(intentID, "running", "paused"); err != nil || !changed {
|
|
t.Fatalf("pause direction fixture: changed=%v err=%v", changed, err)
|
|
}
|
|
activity, duplicate, err := store.AcceptIntentIntervention(intentID, requestID, direction)
|
|
if err != nil || duplicate {
|
|
t.Fatalf("accept direction fixture: activity=%+v duplicate=%v err=%v", activity, duplicate, err)
|
|
}
|
|
return &directionAgentFixture{d: d, task: task, store: store, intentID: intentID, activity: activity}
|
|
}
|
|
|
|
func TestGraphOverviewIncludesLatestWorkerDirection(t *testing.T) {
|
|
f := newDirectionAgentFixture(t)
|
|
overview := NewToolSet(f.store, "planner").graphOverviewData()
|
|
directions, ok := overview["worker_directions"].([]map[string]any)
|
|
if !ok || len(directions) != 1 {
|
|
t.Fatalf("worker_directions=%T %+v", overview["worker_directions"], overview["worker_directions"])
|
|
}
|
|
if directions[0]["activity_id"] != f.activity.ID || directions[0]["intent_id"] != f.intentID ||
|
|
directions[0]["intent_summary"] != "原始接口枚举方向" ||
|
|
directions[0]["message"] != "停止接口枚举,立即聚焦登录后的水平越权" {
|
|
t.Fatalf("worker direction overview=%+v", directions[0])
|
|
}
|
|
}
|
|
|
|
func TestPlannerRequestIncludesWorkerMessageTriggerAndPersistedDirection(t *testing.T) {
|
|
f := newDirectionAgentFixture(t)
|
|
provider := &directionCaptureProvider{}
|
|
planner := NewPlanner(provider, "test-model", t.TempDir(), nil, 0, 1)
|
|
triggerDetail := "本轮只验证登录后的对象级授权"
|
|
_, _, err := planner.Plan(context.Background(), f.task.ID, nil, f.store, f.task.Goal, []TriggerEvent{{
|
|
Kind: TriggerWorkerMessage, ActivityID: f.activity.ID, IntentID: f.intentID, Detail: triggerDetail,
|
|
}}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input := lastUserText(t, provider.lastRequest(t))
|
|
for _, want := range []string{"【本次触发本轮的实际变动】", triggerDetail, `"worker_directions"`, "停止接口枚举,立即聚焦登录后的水平越权"} {
|
|
if !strings.Contains(input, want) {
|
|
t.Fatalf("Planner input missing %q:\n%s", want, input)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMainAgentNextTurnIncludesPersistedWorkerDirection(t *testing.T) {
|
|
f := newDirectionAgentFixture(t)
|
|
provider := &directionCaptureProvider{}
|
|
mainAgent := NewMainAgent(provider, "test-model", t.TempDir(), nil, 0, 1)
|
|
const userMessage = "汇报当前执行方向"
|
|
if _, err := mainAgent.Chat(context.Background(), f.task.ID, nil, f.store, f.task.Goal, userMessage, nil, nil, nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input := lastUserText(t, provider.lastRequest(t))
|
|
for _, want := range []string{"【系统同步的 Worker 最新人工方向】", "停止接口枚举,立即聚焦登录后的水平越权", "【用户本轮消息】", userMessage} {
|
|
if !strings.Contains(input, want) {
|
|
t.Fatalf("Main Agent input missing %q:\n%s", want, input)
|
|
}
|
|
}
|
|
if strings.Index(input, "【系统同步的 Worker 最新人工方向】") > strings.Index(input, "【用户本轮消息】") {
|
|
t.Fatalf("Main Agent context is not separated before current user message:\n%s", input)
|
|
}
|
|
}
|