mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
b3a7b7ad64
* feat: add agent self-evolution * fix ci * delete unused doc * fix lint * fix evolution review issues
311 lines
9.5 KiB
Go
311 lines
9.5 KiB
Go
package evolution_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/evolution"
|
|
)
|
|
|
|
func TestOrganizer_BuildRulesCreatesRuleRecord(t *testing.T) {
|
|
ok := true
|
|
cases := []evolution.LearningRecord{
|
|
{
|
|
ID: "case-1",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000000, 0).UTC(),
|
|
Summary: "weather shanghai",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
ActiveSkillNames: []string{"weather"},
|
|
},
|
|
{
|
|
ID: "case-2",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000100, 0).UTC(),
|
|
Summary: "weather beijing",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
ActiveSkillNames: []string{"weather"},
|
|
},
|
|
{
|
|
ID: "case-3",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000200, 0).UTC(),
|
|
Summary: "weather hangzhou",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
ActiveSkillNames: []string{"weather"},
|
|
},
|
|
}
|
|
|
|
org := evolution.NewOrganizer(evolution.OrganizerOptions{
|
|
MinCaseCount: 3,
|
|
MinSuccessRate: 0.7,
|
|
Now: func() time.Time { return time.Unix(1700001000, 0).UTC() },
|
|
})
|
|
|
|
rules, err := org.BuildRules(cases)
|
|
if err != nil {
|
|
t.Fatalf("BuildRules: %v", err)
|
|
}
|
|
if len(rules) != 1 {
|
|
t.Fatalf("len(rules) = %d, want 1", len(rules))
|
|
}
|
|
|
|
rule := rules[0]
|
|
if rule.Kind != evolution.RecordKindRule {
|
|
t.Fatalf("Kind = %q, want %q", rule.Kind, evolution.RecordKindRule)
|
|
}
|
|
if rule.EventCount != 3 {
|
|
t.Fatalf("EventCount = %d, want 3", rule.EventCount)
|
|
}
|
|
if len(rule.SourceRecordIDs) != 3 {
|
|
t.Fatalf("SourceRecordIDs = %v", rule.SourceRecordIDs)
|
|
}
|
|
if rule.MaturityScore <= 0 {
|
|
t.Fatalf("MaturityScore = %v, want > 0", rule.MaturityScore)
|
|
}
|
|
if len(rule.WinningPath) != 1 || rule.WinningPath[0] != "weather" {
|
|
t.Fatalf("WinningPath = %v, want [weather]", rule.WinningPath)
|
|
}
|
|
}
|
|
|
|
func TestOrganizer_BuildRulesSkipsImmatureCluster(t *testing.T) {
|
|
ok := true
|
|
cases := []evolution.LearningRecord{
|
|
{
|
|
ID: "case-1",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000000, 0).UTC(),
|
|
Summary: "release build linux",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
},
|
|
}
|
|
|
|
org := evolution.NewOrganizer(evolution.OrganizerOptions{
|
|
MinCaseCount: 3,
|
|
MinSuccessRate: 0.7,
|
|
})
|
|
|
|
rules, err := org.BuildRules(cases)
|
|
if err != nil {
|
|
t.Fatalf("BuildRules: %v", err)
|
|
}
|
|
if len(rules) != 0 {
|
|
t.Fatalf("len(rules) = %d, want 0", len(rules))
|
|
}
|
|
}
|
|
|
|
func TestOrganizer_BuildRulesPrefersFinalSuccessfulPathFromAttemptTrail(t *testing.T) {
|
|
ok := true
|
|
cases := []evolution.LearningRecord{
|
|
{
|
|
ID: "case-1",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000000, 0).UTC(),
|
|
Summary: "weather shanghai",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
AttemptTrail: &evolution.AttemptTrail{
|
|
AttemptedSkills: []string{"geocode", "weather"},
|
|
FinalSuccessfulPath: []string{"geocode", "weather"},
|
|
},
|
|
ActiveSkillNames: []string{"geocode", "weather"},
|
|
},
|
|
{
|
|
ID: "case-2",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000100, 0).UTC(),
|
|
Summary: "weather beijing",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
AttemptTrail: &evolution.AttemptTrail{
|
|
AttemptedSkills: []string{"browser", "weather"},
|
|
FinalSuccessfulPath: []string{"geocode", "weather"},
|
|
},
|
|
ActiveSkillNames: []string{"browser", "weather"},
|
|
},
|
|
{
|
|
ID: "case-3",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000200, 0).UTC(),
|
|
Summary: "weather hangzhou",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
AttemptTrail: &evolution.AttemptTrail{
|
|
AttemptedSkills: []string{"maps", "weather"},
|
|
FinalSuccessfulPath: []string{"geocode", "weather"},
|
|
},
|
|
ActiveSkillNames: []string{"maps", "weather"},
|
|
},
|
|
}
|
|
|
|
org := evolution.NewOrganizer(evolution.OrganizerOptions{
|
|
MinCaseCount: 3,
|
|
MinSuccessRate: 0.7,
|
|
Now: func() time.Time { return time.Unix(1700001000, 0).UTC() },
|
|
})
|
|
|
|
rules, err := org.BuildRules(cases)
|
|
if err != nil {
|
|
t.Fatalf("BuildRules: %v", err)
|
|
}
|
|
if len(rules) != 1 {
|
|
t.Fatalf("len(rules) = %d, want 1", len(rules))
|
|
}
|
|
if got := rules[0].WinningPath; len(got) != 2 || got[0] != "geocode" || got[1] != "weather" {
|
|
t.Fatalf("WinningPath = %v, want [geocode weather]", got)
|
|
}
|
|
}
|
|
|
|
func TestOrganizer_BuildRulesCapturesLateAddedSkillHintFromSnapshots(t *testing.T) {
|
|
ok := true
|
|
cases := []evolution.LearningRecord{
|
|
{
|
|
ID: "case-1",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000000, 0).UTC(),
|
|
Summary: "weather shanghai",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
AttemptTrail: &evolution.AttemptTrail{
|
|
AttemptedSkills: []string{"geocode", "weather"},
|
|
FinalSuccessfulPath: []string{"geocode", "weather"},
|
|
SkillContextSnapshots: []evolution.SkillContextSnapshot{
|
|
{Sequence: 1, Trigger: "initial_build", SkillNames: []string{"geocode"}},
|
|
{Sequence: 2, Trigger: "context_retry_rebuild", SkillNames: []string{"geocode", "weather"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "case-2",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000100, 0).UTC(),
|
|
Summary: "weather beijing",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
AttemptTrail: &evolution.AttemptTrail{
|
|
AttemptedSkills: []string{"browser", "weather"},
|
|
FinalSuccessfulPath: []string{"geocode", "weather"},
|
|
SkillContextSnapshots: []evolution.SkillContextSnapshot{
|
|
{Sequence: 1, Trigger: "initial_build", SkillNames: []string{"geocode"}},
|
|
{Sequence: 2, Trigger: "context_retry_rebuild", SkillNames: []string{"geocode", "weather"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "case-3",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000200, 0).UTC(),
|
|
Summary: "weather hangzhou",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
AttemptTrail: &evolution.AttemptTrail{
|
|
AttemptedSkills: []string{"maps", "weather"},
|
|
FinalSuccessfulPath: []string{"geocode", "weather"},
|
|
SkillContextSnapshots: []evolution.SkillContextSnapshot{
|
|
{Sequence: 1, Trigger: "initial_build", SkillNames: []string{"geocode"}},
|
|
{Sequence: 2, Trigger: "context_retry_rebuild", SkillNames: []string{"geocode", "weather"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
org := evolution.NewOrganizer(evolution.OrganizerOptions{
|
|
MinCaseCount: 3,
|
|
MinSuccessRate: 0.7,
|
|
Now: func() time.Time { return time.Unix(1700001000, 0).UTC() },
|
|
})
|
|
|
|
rules, err := org.BuildRules(cases)
|
|
if err != nil {
|
|
t.Fatalf("BuildRules: %v", err)
|
|
}
|
|
if len(rules) != 1 {
|
|
t.Fatalf("len(rules) = %d, want 1", len(rules))
|
|
}
|
|
if got := rules[0].LateAddedSkills; len(got) != 1 || got[0] != "weather" {
|
|
t.Fatalf("LateAddedSkills = %v, want [weather]", got)
|
|
}
|
|
if got := rules[0].FinalSnapshotTrigger; got != "context_retry_rebuild" {
|
|
t.Fatalf("FinalSnapshotTrigger = %q, want context_retry_rebuild", got)
|
|
}
|
|
}
|
|
|
|
func TestOrganizer_BuildRulesUsesAddedSkillNamesWithoutSnapshots(t *testing.T) {
|
|
ok := true
|
|
cases := []evolution.LearningRecord{
|
|
{
|
|
ID: "case-1",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000000, 0).UTC(),
|
|
Summary: "weather shanghai",
|
|
UserGoal: "check weather in shanghai",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
UsedSkillNames: []string{"geocode", "weather"},
|
|
AddedSkillNames: []string{"weather"},
|
|
},
|
|
{
|
|
ID: "case-2",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000100, 0).UTC(),
|
|
Summary: "weather beijing",
|
|
UserGoal: "check weather in beijing",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
UsedSkillNames: []string{"geocode", "weather"},
|
|
AddedSkillNames: []string{"weather"},
|
|
},
|
|
{
|
|
ID: "case-3",
|
|
Kind: evolution.RecordKindCase,
|
|
WorkspaceID: "ws-1",
|
|
CreatedAt: time.Unix(1700000200, 0).UTC(),
|
|
Summary: "weather hangzhou",
|
|
UserGoal: "check weather in hangzhou",
|
|
Status: evolution.RecordStatus("new"),
|
|
Success: &ok,
|
|
UsedSkillNames: []string{"geocode", "weather"},
|
|
AddedSkillNames: []string{"weather"},
|
|
},
|
|
}
|
|
|
|
org := evolution.NewOrganizer(evolution.OrganizerOptions{
|
|
MinCaseCount: 3,
|
|
MinSuccessRate: 0.7,
|
|
Now: func() time.Time { return time.Unix(1700001000, 0).UTC() },
|
|
})
|
|
|
|
rules, err := org.BuildRules(cases)
|
|
if err != nil {
|
|
t.Fatalf("BuildRules: %v", err)
|
|
}
|
|
if len(rules) != 1 {
|
|
t.Fatalf("len(rules) = %d, want 1", len(rules))
|
|
}
|
|
if got := rules[0].WinningPath; len(got) != 2 || got[0] != "geocode" || got[1] != "weather" {
|
|
t.Fatalf("WinningPath = %v, want [geocode weather]", got)
|
|
}
|
|
if got := rules[0].LateAddedSkills; len(got) != 1 || got[0] != "weather" {
|
|
t.Fatalf("LateAddedSkills = %v, want [weather]", got)
|
|
}
|
|
if got := rules[0].FinalSnapshotTrigger; got != "loaded_during_task" {
|
|
t.Fatalf("FinalSnapshotTrigger = %q, want loaded_during_task", got)
|
|
}
|
|
}
|