Files
jules/engine/prompt/prompt_test.go
T
2026-06-06 18:52:20 +03:00

416 lines
12 KiB
Go

package prompt
import (
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"m8sh.su/d/jules/database"
"m8sh.su/d/jules/engine/user"
)
func newTestUser() *user.User {
return &user.User{
User: &database.User{
ID: uuid.New(),
Language: "en",
Timezone: "Europe/Moscow",
PreferredChat: "telegram",
BindCode: uuid.New(),
ContactCode: uuid.New(),
Role: "free",
},
Chats: []database.Chat{},
Facts: []database.Fact{},
Contacts: []database.Contact{},
IncomingNotifications: []database.Notification{},
OutgoingNotifications: []database.Notification{},
RecentActions: []database.Action{},
}
}
func TestBuild_ContainsBase(t *testing.T) {
u := newTestUser()
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "MUST FOLLOW RULES")
assert.Contains(t, result, "AVAILABLE ACTIONS")
}
func TestBuild_OnboardingTriggered(t *testing.T) {
u := newTestUser()
u.Language = ""
u.Timezone = ""
u.Facts = nil
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "ONBOARDING")
assert.Contains(t, result, "English?")
}
func TestBuild_OnboardingNotTriggered_EnoughFacts(t *testing.T) {
u := newTestUser()
u.Facts = []database.Fact{
{Value: "fact1"},
{Value: "fact2"},
{Value: "fact3"},
}
result := Build(u, "telegram", "hello")
assert.NotContains(t, result, "ONBOARDING")
}
func TestBuild_OnboardingNotTriggered_LanguageAndTz(t *testing.T) {
u := newTestUser()
u.Facts = []database.Fact{{Value: "f1"}, {Value: "f2"}}
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "ONBOARDING")
}
func TestBuild_BehaviourMapping_AllGendersAndAges(t *testing.T) {
tests := []struct {
name string
facts []string
contains string
}{
{"man young", []string{"GENDER MALE", "AGE YOUNG"}, "YOUNG MAN"},
{"man middle", []string{"GENDER MALE", "AGE MIDDLE"}, "MIDDLE-AGED MAN"},
{"man old", []string{"GENDER MALE", "AGE OLD"}, "ELDERLY MAN"},
{"woman young", []string{"GENDER FEMALE", "AGE YOUNG"}, "YOUNG WOMAN"},
{"woman middle", []string{"GENDER FEMALE", "AGE MIDDLE"}, "MIDDLE-AGED WOMAN"},
{"woman old", []string{"GENDER FEMALE", "AGE OLD"}, "ELDERLY WOMAN"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := newTestUser()
u.Facts = make([]database.Fact, len(tt.facts))
for i, f := range tt.facts {
u.Facts[i] = database.Fact{Value: f}
}
result := Build(u, "telegram", "hello")
assert.Contains(t, result, tt.contains)
})
}
}
func TestBuild_NoBehaviourMatched(t *testing.T) {
u := newTestUser()
u.Facts = nil
result := Build(u, "telegram", "hello")
assert.NotContains(t, result, "YOU ARE TALKING TO A")
}
func TestBuild_MessageSource(t *testing.T) {
u := newTestUser()
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "EXTRACT FACTS FROM MESSAGES")
assert.Contains(t, result, "PROCESS FOLLOWING MESSAGE")
assert.Contains(t, result, "PLATFORM:telegram")
assert.Contains(t, result, "MESSAGE:hello")
assert.NotContains(t, result, "YOU ARE PROCESSING A USER NOTIFICATION")
}
func TestBuild_DatabaseSource(t *testing.T) {
u := newTestUser()
result := Build(u, database.DatabaseSource, "notification content")
assert.Contains(t, result, "YOU ARE PROCESSING A USER NOTIFICATION")
assert.Contains(t, result, "PROCESS FOLLOWING NOTIFICAION")
assert.Contains(t, result, "notification content")
assert.NotContains(t, result, "EXTRACT FACTS FROM MESSAGES")
}
func TestBuild_WithRepeatOn(t *testing.T) {
u := newTestUser()
result := Build(u, database.DatabaseSource, "test", RepeatOn("daily at 09:00"))
assert.Contains(t, result, "CREATE NEXT NOTIFICAION")
assert.Contains(t, result, "REPETITION RULES: daily at 09:00")
}
func TestBuild_WithErrors(t *testing.T) {
u := newTestUser()
err1 := errors.New("parse: action array not found in output")
err2 := errors.New("add_notification: invalid time")
result := Build(u, "telegram", "hello", err1, err2)
assert.Contains(t, result, "PREVIOUS EXECUTION CAUSED ERRORS")
assert.Contains(t, result, "=== ERROR CONTEXT ===")
assert.Contains(t, result, strings.ToUpper(err1.Error()))
assert.Contains(t, result, strings.ToUpper(err2.Error()))
}
func TestBuild_WithErrors_DatabaseSource(t *testing.T) {
u := newTestUser()
err := errors.New("something failed")
result := Build(u, database.DatabaseSource, "test", err)
assert.Contains(t, result, "PREVIOUS EXECUTION CAUSED ERRORS")
assert.Contains(t, result, strings.ToUpper(err.Error()))
}
func TestBuild_UserContext_Chats(t *testing.T) {
u := newTestUser()
u.Chats = []database.Chat{
{Platform: "telegram", Identifier: "123"},
{Platform: "whatsapp", Identifier: "456"},
}
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "CHATS:")
assert.Contains(t, result, " - telegram")
assert.Contains(t, result, " - whatsapp")
}
func TestBuild_UserContext_Facts(t *testing.T) {
u := newTestUser()
u.Facts = []database.Fact{
{Value: "loves coffee"},
{Value: "works remotely"},
}
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "FACTS:")
assert.Contains(t, result, " - loves coffee")
assert.Contains(t, result, " - works remotely")
}
func TestBuild_UserContext_Contacts(t *testing.T) {
u := newTestUser()
u.Contacts = []database.Contact{
{Name: "Mom"},
{Name: "Best Friend"},
}
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "CONTACTS:")
assert.Contains(t, result, " - Mom")
assert.Contains(t, result, " - Best Friend")
}
func TestBuild_UserContext_IncomingNotifications_FiltersSystem(t *testing.T) {
u := newTestUser()
u.IncomingNotifications = []database.Notification{
{ID: uuid.New(), Content: "call mom", ScheduledAt: time.Now(), RepeatOn: "daily"},
{ID: uuid.New(), Content: "SYSTEM: hidden", ScheduledAt: time.Now(), RepeatOn: ""},
{ID: uuid.New(), Content: "system: also hidden", ScheduledAt: time.Now(), RepeatOn: ""},
}
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "call mom")
assert.NotContains(t, result, "SYSTEM: hidden")
assert.NotContains(t, result, "system: also hidden")
}
func TestBuild_UserContext_OutgoingNotifications_FiltersSystem(t *testing.T) {
u := newTestUser()
u.OutgoingNotifications = []database.Notification{
{ID: uuid.New(), Content: "reminder for friend", ScheduledAt: time.Now(), RepeatOn: ""},
{ID: uuid.New(), Content: "SYSTEM: secret", ScheduledAt: time.Now(), RepeatOn: ""},
}
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "reminder for friend")
assert.NotContains(t, result, "SYSTEM: secret")
}
func TestBuild_UserContext_RecentActions(t *testing.T) {
u := newTestUser()
u.RecentActions = []database.Action{
{Type: "user_msg", Content: "hello", ExecutedAt: time.Now()},
{Type: "jules_msg", Content: "hi back", ExecutedAt: time.Now()},
}
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "ACTIONS")
assert.Contains(t, result, "user_msg")
assert.Contains(t, result, "jules_msg")
}
func TestBuild_UserContext_EmptyTimezone(t *testing.T) {
u := newTestUser()
u.Timezone = ""
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "CRITICAL! ASK USER ABOUT HIS LOCATION/TIMEZONE")
}
func TestBuild_UserContext_Identifiers(t *testing.T) {
u := newTestUser()
result := Build(u, "telegram", "hello")
assert.Contains(t, result, fmt.Sprintf("BIND CODE: %s", u.BindCode.String()))
assert.Contains(t, result, fmt.Sprintf("CONTACT CODE: %s", u.ContactCode.String()))
assert.Contains(t, result, "LANGUAGE: en")
assert.Contains(t, result, "SELECTED CHAT: telegram")
}
func TestBuild_TimeContext_ValidTimezone(t *testing.T) {
u := newTestUser()
u.Timezone = "Europe/Moscow"
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "=== TIME CONTEXT ===")
assert.Contains(t, result, "CURRENT TIME:")
assert.Contains(t, result, "NEXT 7 DAYS:")
assert.Contains(t, result, "Monday")
}
func TestBuild_TimeContext_InvalidTimezone(t *testing.T) {
u := newTestUser()
u.Timezone = "Mars/City"
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "=== TIME CONTEXT ===")
assert.Contains(t, result, "CURRENT TIME:")
}
func TestBuild_TimeContext_EmptyTimezone(t *testing.T) {
u := newTestUser()
u.Timezone = ""
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "=== TIME CONTEXT ===")
assert.Contains(t, result, "CURRENT TIME:")
}
func TestEjectRepeatedOn(t *testing.T) {
tests := []struct {
name string
params []any
want string
}{
{"empty", nil, ""},
{"no repeat on", []any{1, "string", errors.New("err")}, ""},
{"has repeat on", []any{RepeatOn("daily"), 42}, "daily"},
{"has repeat on among errors", []any{errors.New("err"), RepeatOn("weekly")}, "weekly"},
{"multiple repeat on returns first", []any{RepeatOn("first"), RepeatOn("second")}, "first"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ejectRepeatedOn(tt.params...)
assert.Equal(t, tt.want, got)
})
}
}
func TestBuildErrorContext(t *testing.T) {
tests := []struct {
name string
params []any
contains []string
absent []string
}{
{
name: "empty",
params: nil,
contains: nil,
absent: []string{"ERROR CONTEXT"},
},
{
name: "no errors",
params: []any{1, "string", RepeatOn("daily")},
contains: nil,
absent: []string{"ERROR CONTEXT"},
},
{
name: "one error",
params: []any{errors.New("something broke")},
contains: []string{"=== ERROR CONTEXT ===", "SOMETHING BROKE"},
},
{
name: "multiple errors",
params: []any{errors.New("err1"), 42, errors.New("err2")},
contains: []string{
"=== ERROR CONTEXT ===",
"ERR1",
"ERR2",
},
},
{
name: "non-error interface wrapping error",
params: []any{fmt.Errorf("wrapped: %w", errors.New("inner"))},
contains: []string{"WRAPPED: INNER"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildErrorContext(tt.params...)
for _, c := range tt.contains {
assert.Contains(t, result, c)
}
for _, a := range tt.absent {
assert.NotContains(t, result, a)
}
})
}
}
func TestCheckOnboarding(t *testing.T) {
tests := []struct {
name string
lang string
tz string
facts int
expected bool
}{
{"all unset", "", "", 0, true},
{"no language", "", "UTC", 5, true},
{"no timezone", "en", "", 5, true},
{"few facts", "en", "UTC", 2, true},
{"enough facts but no lang", "", "UTC", 5, true},
{"complete", "en", "UTC", 3, false},
{"more than enough", "en", "UTC", 10, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := &user.User{
User: &database.User{
Language: tt.lang,
Timezone: tt.tz,
},
}
for i := 0; i < tt.facts; i++ {
u.Facts = append(u.Facts, database.Fact{Value: fmt.Sprintf("fact_%d", i)})
}
assert.Equal(t, tt.expected, checkOnboarding(u))
})
}
}
func TestCheckFacts(t *testing.T) {
u := &user.User{
Facts: []database.Fact{
{Value: "GENDER MALE"},
{Value: "AGE YOUNG"},
{Value: "likes pizza"},
},
}
assert.True(t, checkFacts(u, "GENDER MALE"))
assert.True(t, checkFacts(u, "GENDER MALE", "AGE YOUNG"))
assert.True(t, checkFacts(u, "likes pizza"))
assert.False(t, checkFacts(u, "GENDER FEMALE"))
assert.False(t, checkFacts(u, "GENDER MALE", "AGE OLD"))
assert.False(t, checkFacts(u, "nonexistent"))
assert.True(t, checkFacts(u)) // empty = vacuously true
}
func TestBuildMessage(t *testing.T) {
result := buildMessage("telegram", "hello world")
assert.Contains(t, result, "PROCESS FOLLOWING MESSAGE")
assert.Contains(t, result, "PLATFORM:telegram")
assert.Contains(t, result, "MESSAGE:hello world")
}
func TestBuildNotification(t *testing.T) {
t.Run("without repeat", func(t *testing.T) {
result := buildNotification("test content", "")
assert.Contains(t, result, "PROCESS FOLLOWING NOTIFICAION")
assert.Contains(t, result, "CONTENT: test content")
assert.NotContains(t, result, "REPETITION RULES")
})
t.Run("with repeat", func(t *testing.T) {
result := buildNotification("test content", "daily at 09:00")
assert.Contains(t, result, "PROCESS FOLLOWING NOTIFICAION")
assert.Contains(t, result, "CONTENT: test content")
assert.Contains(t, result, "REPETITION RULES: daily at 09:00")
})
}
func TestBuild_ActionsPromptPartIncluded(t *testing.T) {
u := newTestUser()
result := Build(u, "telegram", "hello")
assert.Contains(t, result, "AVAILABLE ACTIONS")
assert.Contains(t, result, "OUTPUT EXAMPLE")
}