first commit
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/smallnest/pigo/internal/agentcore"
|
||||
"github.com/smallnest/pigo/internal/agenttool"
|
||||
"github.com/smallnest/pigo/internal/provider"
|
||||
)
|
||||
|
||||
// wrapStreamCapture wraps a StreamFn to record every text-block message the
|
||||
// request carried into seen, so a test can assert what the model was sent.
|
||||
func wrapStreamCapture(inner provider.StreamFn, seen *[]string) provider.StreamFn {
|
||||
return func(ctx context.Context, model string, llm provider.LlmContext, cfg provider.StreamConfig) (*provider.AssistantMessageEventStream, error) {
|
||||
for _, m := range llm.Messages {
|
||||
if um, ok := m.(agentcore.UserMessage); ok {
|
||||
*seen = append(*seen, agentcore.ContentToText(um.Content))
|
||||
}
|
||||
}
|
||||
return inner(ctx, model, llm, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
// reminderTextsInRequest drives one turn and returns the <system-reminder>
|
||||
// message texts the provider stream actually received (the request-shaped list),
|
||||
// so a test can assert what the model saw without touching persisted state.
|
||||
func reminderTextsInRequest(t *testing.T, reg *ReminderRegistry, agentCtx *agentcore.AgentContext) []string {
|
||||
t.Helper()
|
||||
var seen []string
|
||||
streamFn := scriptedStream([]agentcore.AssistantMessage{
|
||||
{RoleField: agentcore.RoleAssistant, StopReason: agentcore.StopReasonEndTurn, Content: agentcore.ContentList{agentcore.NewTextContent("ok")}},
|
||||
})
|
||||
// Wrap the stream so we can inspect the LlmContext it is handed.
|
||||
cfg := newRunCfg(nil)
|
||||
cfg.Reminders = reg
|
||||
cfg.LoopConfig.Stream = wrapStreamCapture(streamFn, &seen)
|
||||
collectStream(t, agentLoop(context.Background(), agentCtx, cfg))
|
||||
return seen
|
||||
}
|
||||
|
||||
func TestWrapSystemReminderLabelsBackgroundContext(t *testing.T) {
|
||||
out := WrapSystemReminder("body here")
|
||||
if !strings.Contains(out, "<system-reminder>") || !strings.Contains(out, "</system-reminder>") {
|
||||
t.Errorf("reminder must be wrapped in <system-reminder> tags, got %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "NOT a message or instruction from the user") {
|
||||
t.Errorf("reminder must be labeled as background context, not a user instruction, got %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "body here") {
|
||||
t.Errorf("reminder must contain the body, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReminderInjectedWhenConditionHolds(t *testing.T) {
|
||||
fired := ReminderFunc{NameField: "always", Fn: func(ctx context.Context, msgs agentcore.MessageList) (string, bool) {
|
||||
return "budget is low", true
|
||||
}}
|
||||
reg := NewReminderRegistry(fired)
|
||||
agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}
|
||||
|
||||
seen := reminderTextsInRequest(t, reg, agentCtx)
|
||||
found := false
|
||||
for _, s := range seen {
|
||||
if strings.Contains(s, "budget is low") && strings.Contains(s, "<system-reminder>") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected a system-reminder in the request, saw %v", seen)
|
||||
}
|
||||
// Ephemeral: the reminder must NOT be written back into the persisted history.
|
||||
for _, m := range agentCtx.Messages {
|
||||
if um, ok := m.(agentcore.UserMessage); ok {
|
||||
if strings.Contains(agentcore.ContentToText(um.Content), "system-reminder") {
|
||||
t.Errorf("reminder leaked into persisted message history: %+v", um)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReminderNotInjectedWhenConditionFails(t *testing.T) {
|
||||
silent := ReminderFunc{NameField: "never", Fn: func(ctx context.Context, msgs agentcore.MessageList) (string, bool) {
|
||||
return "", false
|
||||
}}
|
||||
reg := NewReminderRegistry(silent)
|
||||
agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}
|
||||
|
||||
seen := reminderTextsInRequest(t, reg, agentCtx)
|
||||
for _, s := range seen {
|
||||
if strings.Contains(s, "system-reminder") {
|
||||
t.Errorf("no reminder should be injected when the provider declines, saw %q", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReminderPreservesInnerTransform(t *testing.T) {
|
||||
var innerRan bool
|
||||
reg := NewReminderRegistry(ReminderFunc{NameField: "always", Fn: func(ctx context.Context, msgs agentcore.MessageList) (string, bool) {
|
||||
return "note", true
|
||||
}})
|
||||
inner := func(ctx context.Context, msgs agentcore.MessageList) agentcore.MessageList {
|
||||
innerRan = true
|
||||
return msgs
|
||||
}
|
||||
wrapped := reg.wrapTransform(inner)
|
||||
out := wrapped(context.Background(), agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}})
|
||||
if !innerRan {
|
||||
t.Errorf("wrapTransform must call the inner TransformContext")
|
||||
}
|
||||
if len(out) != 2 {
|
||||
t.Fatalf("expected original + 1 reminder message, got %d", len(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodoReminderProvider(t *testing.T) {
|
||||
store := agenttool.NewTodoStore()
|
||||
p := &TodoReminderProvider{Store: store}
|
||||
|
||||
// Empty store: silent.
|
||||
if _, ok := p.Reminder(context.Background(), nil); ok {
|
||||
t.Errorf("empty todo store must not fire a reminder")
|
||||
}
|
||||
|
||||
// All completed: silent.
|
||||
store.Set([]agenttool.TodoItem{{Content: "done", Status: agenttool.TodoCompleted}})
|
||||
if _, ok := p.Reminder(context.Background(), nil); ok {
|
||||
t.Errorf("fully-completed todo list must not fire a reminder")
|
||||
}
|
||||
|
||||
// Incomplete work: fires with the rendered list.
|
||||
store.Set([]agenttool.TodoItem{
|
||||
{Content: "write code", Status: agenttool.TodoInProgress},
|
||||
{Content: "review", Status: agenttool.TodoPending},
|
||||
})
|
||||
body, ok := p.Reminder(context.Background(), nil)
|
||||
if !ok {
|
||||
t.Fatalf("incomplete todo list must fire a reminder")
|
||||
}
|
||||
if !strings.Contains(body, "write code") || !strings.Contains(body, "review") {
|
||||
t.Errorf("reminder body should render the todo items, got %q", body)
|
||||
}
|
||||
|
||||
// nil store: never fires.
|
||||
nilP := &TodoReminderProvider{}
|
||||
if _, ok := nilP.Reminder(context.Background(), nil); ok {
|
||||
t.Errorf("nil todo store must not fire a reminder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodoReminderEndToEndInjection(t *testing.T) {
|
||||
store := agenttool.NewTodoStore()
|
||||
store.Set([]agenttool.TodoItem{{Content: "unfinished task", Status: agenttool.TodoInProgress}})
|
||||
reg := NewReminderRegistry(&TodoReminderProvider{Store: store})
|
||||
agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{agentcore.UserMessage{RoleField: agentcore.RoleUser}}}
|
||||
|
||||
seen := reminderTextsInRequest(t, reg, agentCtx)
|
||||
found := false
|
||||
for _, s := range seen {
|
||||
if strings.Contains(s, "unfinished task") && strings.Contains(s, "<system-reminder>") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("todo reminder should be injected into the request, saw %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoalReminderOnlyWhenActive(t *testing.T) {
|
||||
st := agenttool.NewGoalState()
|
||||
p := &GoalReminderProvider{State: st}
|
||||
|
||||
// Idle: no reminder.
|
||||
if _, ok := p.Reminder(context.Background(), nil); ok {
|
||||
t.Fatal("idle goal should not inject a reminder")
|
||||
}
|
||||
|
||||
// Active: injects the objective.
|
||||
st.Start("g1", "build the feature", 0)
|
||||
body, ok := p.Reminder(context.Background(), nil)
|
||||
if !ok {
|
||||
t.Fatal("active goal should inject a reminder")
|
||||
}
|
||||
if !strings.Contains(body, "build the feature") {
|
||||
t.Errorf("reminder body missing objective: %q", body)
|
||||
}
|
||||
|
||||
// Completed: silent again.
|
||||
st.MarkComplete("done")
|
||||
if _, ok := p.Reminder(context.Background(), nil); ok {
|
||||
t.Fatal("completed goal should not inject a reminder")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user