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 // 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, "") || !strings.Contains(out, "") { t.Errorf("reminder must be wrapped in 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, "") { 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, "") { 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") } }