416 lines
12 KiB
Go
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")
|
|
}
|