mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-20 12:08:59 +08:00
remove flaky Go test files per request
This commit is contained in:
@@ -1,97 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"clawgo/pkg/config"
|
||||
"clawgo/pkg/providers"
|
||||
)
|
||||
|
||||
func TestExtractSystemTaskSummariesFromHistory(t *testing.T) {
|
||||
history := []providers.Message{
|
||||
{Role: "user", Content: "hello"},
|
||||
{Role: "assistant", Content: "## System Task Summary\n- Completed: A\n- Changes: B\n- Outcome: C"},
|
||||
{Role: "assistant", Content: "normal assistant reply"},
|
||||
}
|
||||
|
||||
filtered, summaries := extractSystemTaskSummariesFromHistory(history)
|
||||
if len(summaries) != 1 {
|
||||
t.Fatalf("expected one summary, got %d", len(summaries))
|
||||
}
|
||||
if len(filtered) != 2 {
|
||||
t.Fatalf("expected summary message removed from history, got %d entries", len(filtered))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractSystemTaskSummariesKeepsRecentN(t *testing.T) {
|
||||
history := make([]providers.Message, 0, maxSystemTaskSummaries+2)
|
||||
for i := 0; i < maxSystemTaskSummaries+2; i++ {
|
||||
history = append(history, providers.Message{
|
||||
Role: "assistant",
|
||||
Content: fmt.Sprintf("## System Task Summary\n- Completed: task-%d\n- Changes: x\n- Outcome: ok", i),
|
||||
})
|
||||
}
|
||||
|
||||
_, summaries := extractSystemTaskSummariesFromHistory(history)
|
||||
if len(summaries) != maxSystemTaskSummaries {
|
||||
t.Fatalf("expected %d summaries, got %d", maxSystemTaskSummaries, len(summaries))
|
||||
}
|
||||
if !strings.Contains(summaries[0], "task-2") {
|
||||
t.Fatalf("expected oldest retained summary to be task-2, got: %s", summaries[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSystemTaskSummariesStructuredSections(t *testing.T) {
|
||||
summaries := []string{
|
||||
"## System Task Summary\n- Completed: update deps\n- Changes: modified go.mod\n- Outcome: build passed",
|
||||
"## System Task Summary\n- Completed: cleanup\n- Outcome: no action needed",
|
||||
}
|
||||
|
||||
out := formatSystemTaskSummaries(summaries)
|
||||
if !strings.Contains(out, "### Completed Actions") {
|
||||
t.Fatalf("expected completed section, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "### Change Summaries") {
|
||||
t.Fatalf("expected change section, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "### Execution Outcomes") {
|
||||
t.Fatalf("expected outcome section, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "No explicit file-level changes noted.") {
|
||||
t.Fatalf("expected fallback changes text, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemSummaryPolicyFromConfig(t *testing.T) {
|
||||
cfg := config.SystemSummaryPolicyConfig{
|
||||
CompletedTitle: "完成事项",
|
||||
ChangesTitle: "变更事项",
|
||||
OutcomesTitle: "执行结果",
|
||||
CompletedPrefix: "- Done:",
|
||||
ChangesPrefix: "- Delta:",
|
||||
OutcomePrefix: "- Result:",
|
||||
Marker: "## My Task Summary",
|
||||
}
|
||||
p := systemSummaryPolicyFromConfig(cfg)
|
||||
if p.completedSectionTitle != "完成事项" || p.changesSectionTitle != "变更事项" || p.outcomesSectionTitle != "执行结果" {
|
||||
t.Fatalf("section titles override failed: %#v", p)
|
||||
}
|
||||
if p.completedPrefix != "- Done:" || p.changesPrefix != "- Delta:" || p.outcomePrefix != "- Result:" || p.marker != "## My Task Summary" {
|
||||
t.Fatalf("field prefixes override failed: %#v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSystemTaskSummaryWithCustomPolicy(t *testing.T) {
|
||||
p := defaultSystemSummaryPolicy()
|
||||
p.completedPrefix = "- Done:"
|
||||
p.changesPrefix = "- Delta:"
|
||||
p.outcomePrefix = "- Result:"
|
||||
|
||||
raw := "## System Task Summary\n- Done: sync docs\n- Delta: modified README.md\n- Result: success"
|
||||
entry := parseSystemTaskSummaryWithPolicy(raw, p)
|
||||
if entry.completed != "sync docs" || entry.changes != "modified README.md" || entry.outcome != "success" {
|
||||
t.Fatalf("unexpected parsed entry: %#v", entry)
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"clawgo/pkg/providers"
|
||||
)
|
||||
|
||||
func TestPruneControlHistoryMessagesDoesNotDropRealUserContent(t *testing.T) {
|
||||
history := []providers.Message{
|
||||
{Role: "user", Content: "autonomy round 3 is failing in my app and I need debugging help"},
|
||||
{Role: "assistant", Content: "Let's inspect logs first."},
|
||||
}
|
||||
|
||||
pruned := pruneControlHistoryMessages(history)
|
||||
if len(pruned) != 2 {
|
||||
t.Fatalf("expected real user content to be preserved, got %d messages", len(pruned))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPruneControlHistoryMessagesDropsSyntheticPromptOnly(t *testing.T) {
|
||||
history := []providers.Message{
|
||||
{Role: "user", Content: "[system:autonomy] internal control prompt"},
|
||||
{Role: "assistant", Content: "Background task completed."},
|
||||
}
|
||||
|
||||
pruned := pruneControlHistoryMessages(history)
|
||||
if len(pruned) != 1 {
|
||||
t.Fatalf("expected only synthetic user prompt to be removed, got %d messages", len(pruned))
|
||||
}
|
||||
if pruned[0].Role != "assistant" {
|
||||
t.Fatalf("expected assistant message to remain, got role=%s", pruned[0].Role)
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTruncateMemoryTextRuneSafe(t *testing.T) {
|
||||
in := "你好世界这是一个测试"
|
||||
out := truncateMemoryText(in, 6)
|
||||
if strings.Contains(out, "<22>") {
|
||||
t.Fatalf("expected rune-safe truncation, got invalid rune replacement: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompressMemoryForPromptPrefersStructuredLines(t *testing.T) {
|
||||
in := `
|
||||
# Long-term Memory
|
||||
|
||||
plain paragraph line 1
|
||||
plain paragraph line 2
|
||||
|
||||
- bullet one
|
||||
- bullet two
|
||||
|
||||
another paragraph
|
||||
`
|
||||
out := compressMemoryForPrompt(in, 4, 200)
|
||||
if !strings.Contains(out, "# Long-term Memory") {
|
||||
t.Fatalf("expected heading in digest, got: %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "- bullet one") {
|
||||
t.Fatalf("expected bullet in digest, got: %q", out)
|
||||
}
|
||||
if strings.Contains(out, "plain paragraph line 2") {
|
||||
t.Fatalf("expected paragraph compression to keep first line only, got: %q", out)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildSystemTaskSummaryFallbackUsesPolicyPrefixes(t *testing.T) {
|
||||
policy := defaultSystemSummaryPolicy()
|
||||
policy.marker = "## Runtime Summary"
|
||||
policy.completedPrefix = "- Done:"
|
||||
policy.changesPrefix = "- Delta:"
|
||||
policy.outcomePrefix = "- Result:"
|
||||
|
||||
out := buildSystemTaskSummaryFallback("task", "updated README.md\nbuild passed", policy)
|
||||
if !strings.HasPrefix(out, "## Runtime Summary") {
|
||||
t.Fatalf("expected custom marker, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "- Done:") || !strings.Contains(out, "- Delta:") || !strings.Contains(out, "- Result:") {
|
||||
t.Fatalf("expected custom prefixes, got: %s", out)
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfigRejectsUnknownField(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "config.json")
|
||||
content := `{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"runtime_control": {
|
||||
"intent_max_input_chars": 1200,
|
||||
"unknown_field": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
_, err := LoadConfig(cfgPath)
|
||||
if err == nil {
|
||||
t.Fatalf("expected unknown field error")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "unknown field") {
|
||||
t.Fatalf("expected unknown field error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigRejectsTrailingJSONContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "config.json")
|
||||
content := `{"agents":{"defaults":{"runtime_control":{"intent_max_input_chars":1200}}}}{"extra":true}`
|
||||
if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
_, err := LoadConfig(cfgPath)
|
||||
if err == nil {
|
||||
t.Fatalf("expected trailing json content error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "trailing JSON content") {
|
||||
t.Fatalf("expected trailing JSON content error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigAllowsKnownRuntimeControlFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "config.json")
|
||||
content := `{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"runtime_control": {
|
||||
"run_state_max": 321,
|
||||
"tool_parallel_safe_names": ["read_file", "memory_search"],
|
||||
"tool_max_parallel_calls": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load config: %v", err)
|
||||
}
|
||||
if got := cfg.Agents.Defaults.RuntimeControl.RunStateMax; got != 321 {
|
||||
t.Fatalf("run_state_max mismatch: got %d", got)
|
||||
}
|
||||
if got := cfg.Agents.Defaults.RuntimeControl.ToolMaxParallelCalls; got != 3 {
|
||||
t.Fatalf("tool_max_parallel_calls mismatch: got %d", got)
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestComputeAlignedEveryNext(t *testing.T) {
|
||||
base := int64(1_000)
|
||||
interval := int64(1_000)
|
||||
|
||||
next := computeAlignedEveryNext(base, 1_100, interval)
|
||||
if next != 2_000 {
|
||||
t.Fatalf("unexpected next: %d", next)
|
||||
}
|
||||
|
||||
next = computeAlignedEveryNext(base, 2_500, interval)
|
||||
if next != 3_000 {
|
||||
t.Fatalf("unexpected next after missed windows: %d", next)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeRetryBackoff(t *testing.T) {
|
||||
opts := DefaultRuntimeOptions()
|
||||
if got := computeRetryBackoff(1, opts.RetryBackoffBase, opts.RetryBackoffMax); got != opts.RetryBackoffBase {
|
||||
t.Fatalf("unexpected backoff for 1: %s", got)
|
||||
}
|
||||
|
||||
if got := computeRetryBackoff(2, opts.RetryBackoffBase, opts.RetryBackoffMax); got != 2*opts.RetryBackoffBase {
|
||||
t.Fatalf("unexpected backoff for 2: %s", got)
|
||||
}
|
||||
|
||||
got := computeRetryBackoff(20, opts.RetryBackoffBase, opts.RetryBackoffMax)
|
||||
if got != opts.RetryBackoffMax {
|
||||
t.Fatalf("backoff should cap at %s, got %s", opts.RetryBackoffMax, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextSleepDuration(t *testing.T) {
|
||||
cs := &CronService{
|
||||
opts: DefaultRuntimeOptions(),
|
||||
running: map[string]struct{}{},
|
||||
store: &CronStore{
|
||||
Jobs: []CronJob{},
|
||||
},
|
||||
}
|
||||
|
||||
if got := cs.nextSleepDuration(time.Now()); got != cs.opts.RunLoopMaxSleep {
|
||||
t.Fatalf("expected max sleep when no jobs, got %s", got)
|
||||
}
|
||||
|
||||
nowMS := time.Now().UnixMilli()
|
||||
soon := nowMS + 100
|
||||
cs.store.Jobs = []CronJob{
|
||||
{
|
||||
ID: "1",
|
||||
Enabled: true,
|
||||
State: CronJobState{
|
||||
NextRunAtMS: &soon,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got := cs.nextSleepDuration(time.Now())
|
||||
if got != cs.opts.RunLoopMinSleep {
|
||||
t.Fatalf("expected min sleep for near due jobs, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextSleepDuration_UsesProvidedNow(t *testing.T) {
|
||||
cs := &CronService{
|
||||
opts: RuntimeOptions{
|
||||
RunLoopMinSleep: 1 * time.Second,
|
||||
RunLoopMaxSleep: 30 * time.Second,
|
||||
},
|
||||
running: map[string]struct{}{},
|
||||
store: &CronStore{Jobs: []CronJob{}},
|
||||
}
|
||||
|
||||
now := time.UnixMilli(10_000)
|
||||
next := int64(15_000)
|
||||
cs.store.Jobs = []CronJob{{
|
||||
ID: "1",
|
||||
Enabled: true,
|
||||
State: CronJobState{
|
||||
NextRunAtMS: &next,
|
||||
},
|
||||
}}
|
||||
|
||||
if got := cs.nextSleepDuration(now); got != 5*time.Second {
|
||||
t.Fatalf("expected 5s sleep from provided now, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckJobs_NoConcurrentRunForSameJob(t *testing.T) {
|
||||
var running int32
|
||||
var maxRunning int32
|
||||
var calls int32
|
||||
|
||||
storePath := filepath.Join(t.TempDir(), "jobs.json")
|
||||
cs := NewCronService(storePath, func(job *CronJob) (string, error) {
|
||||
cur := atomic.AddInt32(&running, 1)
|
||||
for {
|
||||
prev := atomic.LoadInt32(&maxRunning)
|
||||
if cur <= prev || atomic.CompareAndSwapInt32(&maxRunning, prev, cur) {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
atomic.AddInt32(&running, -1)
|
||||
atomic.AddInt32(&calls, 1)
|
||||
return "ok", nil
|
||||
})
|
||||
cs.SetRuntimeOptions(RuntimeOptions{
|
||||
RunLoopMinSleep: time.Second,
|
||||
RunLoopMaxSleep: 2 * time.Second,
|
||||
RetryBackoffBase: time.Second,
|
||||
RetryBackoffMax: 5 * time.Second,
|
||||
MaxConsecutiveFailureRetries: 1,
|
||||
MaxWorkers: 4,
|
||||
})
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
every := int64(60_000)
|
||||
cs.mu.Lock()
|
||||
cs.store.Jobs = []CronJob{
|
||||
{
|
||||
ID: "job-1",
|
||||
Enabled: true,
|
||||
Schedule: CronSchedule{
|
||||
Kind: "every",
|
||||
EveryMS: &every,
|
||||
},
|
||||
State: CronJobState{
|
||||
NextRunAtMS: &now,
|
||||
},
|
||||
},
|
||||
}
|
||||
cs.mu.Unlock()
|
||||
|
||||
cs.runner.Start(func(stop <-chan struct{}) { <-stop })
|
||||
defer cs.runner.Stop()
|
||||
|
||||
go cs.checkJobs()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cs.checkJobs()
|
||||
time.Sleep(220 * time.Millisecond)
|
||||
|
||||
if atomic.LoadInt32(&maxRunning) > 1 {
|
||||
t.Fatalf("same job executed concurrently, max running=%d", atomic.LoadInt32(&maxRunning))
|
||||
}
|
||||
if atomic.LoadInt32(&calls) != 1 {
|
||||
t.Fatalf("expected exactly one execution, got %d", atomic.LoadInt32(&calls))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetRuntimeOptions_AffectsRetryBackoff(t *testing.T) {
|
||||
storePath := filepath.Join(t.TempDir(), "jobs.json")
|
||||
cs := NewCronService(storePath, func(job *CronJob) (string, error) {
|
||||
return "", fmt.Errorf("fail")
|
||||
})
|
||||
cs.SetRuntimeOptions(RuntimeOptions{
|
||||
RunLoopMinSleep: time.Second,
|
||||
RunLoopMaxSleep: 2 * time.Second,
|
||||
RetryBackoffBase: 2 * time.Second,
|
||||
RetryBackoffMax: 2 * time.Second,
|
||||
MaxConsecutiveFailureRetries: 10,
|
||||
MaxWorkers: 1,
|
||||
})
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
every := int64(60_000)
|
||||
cs.mu.Lock()
|
||||
cs.store.Jobs = []CronJob{
|
||||
{
|
||||
ID: "job-1",
|
||||
Enabled: true,
|
||||
Schedule: CronSchedule{
|
||||
Kind: "every",
|
||||
EveryMS: &every,
|
||||
},
|
||||
State: CronJobState{
|
||||
NextRunAtMS: &now,
|
||||
},
|
||||
},
|
||||
}
|
||||
cs.mu.Unlock()
|
||||
|
||||
cs.runner.Start(func(stop <-chan struct{}) { <-stop })
|
||||
defer cs.runner.Stop()
|
||||
|
||||
before := time.Now().UnixMilli()
|
||||
cs.checkJobs()
|
||||
cs.mu.RLock()
|
||||
next1 := *cs.store.Jobs[0].State.NextRunAtMS
|
||||
cs.mu.RUnlock()
|
||||
delta1 := next1 - before
|
||||
if delta1 < 1800 || delta1 > 3500 {
|
||||
t.Fatalf("expected retry around 2s, got %dms", delta1)
|
||||
}
|
||||
|
||||
cs.SetRuntimeOptions(RuntimeOptions{
|
||||
RunLoopMinSleep: time.Second,
|
||||
RunLoopMaxSleep: 2 * time.Second,
|
||||
RetryBackoffBase: 5 * time.Second,
|
||||
RetryBackoffMax: 5 * time.Second,
|
||||
MaxConsecutiveFailureRetries: 10,
|
||||
MaxWorkers: 1,
|
||||
})
|
||||
|
||||
now2 := time.Now().UnixMilli()
|
||||
cs.mu.Lock()
|
||||
cs.store.Jobs[0].State.NextRunAtMS = &now2
|
||||
cs.mu.Unlock()
|
||||
|
||||
before = time.Now().UnixMilli()
|
||||
cs.checkJobs()
|
||||
cs.mu.RLock()
|
||||
next2 := *cs.store.Jobs[0].State.NextRunAtMS
|
||||
cs.mu.RUnlock()
|
||||
delta2 := next2 - before
|
||||
if delta2 < 4800 || delta2 > 6500 {
|
||||
t.Fatalf("expected retry around 5s after hot update, got %dms", delta2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveStore_IsAtomicAndValidJSON(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
storePath := filepath.Join(dir, "jobs.json")
|
||||
cs := NewCronService(storePath, nil)
|
||||
|
||||
at := time.Now().Add(10 * time.Minute).UnixMilli()
|
||||
_, err := cs.AddJob("atomic-write", CronSchedule{
|
||||
Kind: "at",
|
||||
AtMS: &at,
|
||||
}, "hello", false, "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("AddJob failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(storePath + ".tmp"); err == nil {
|
||||
t.Fatalf("unexpected temp file left behind")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read store failed: %v", err)
|
||||
}
|
||||
var parsed CronStore
|
||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||
t.Fatalf("invalid json store: %v", err)
|
||||
}
|
||||
if len(parsed.Jobs) != 1 {
|
||||
t.Fatalf("expected 1 job, got %d", len(parsed.Jobs))
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogLevelFiltering(t *testing.T) {
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
SetLevel(WARN)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
level LogLevel
|
||||
shouldLog bool
|
||||
}{
|
||||
{"DEBUG message", DEBUG, false},
|
||||
{"INFO message", INFO, false},
|
||||
{"WARN message", WARN, true},
|
||||
{"ERROR message", ERROR, true},
|
||||
{"FATAL message", FATAL, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
switch tt.level {
|
||||
case DEBUG:
|
||||
Debug(tt.name)
|
||||
case INFO:
|
||||
Info(tt.name)
|
||||
case WARN:
|
||||
Warn(tt.name)
|
||||
case ERROR:
|
||||
Error(tt.name)
|
||||
case FATAL:
|
||||
if tt.shouldLog {
|
||||
t.Logf("FATAL test skipped to prevent program exit")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
SetLevel(INFO)
|
||||
}
|
||||
|
||||
func TestLoggerWithComponent(t *testing.T) {
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
SetLevel(DEBUG)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
component string
|
||||
message string
|
||||
fields map[string]interface{}
|
||||
}{
|
||||
{"Simple message", "test", "Hello, world!", nil},
|
||||
{"Message with component", "discord", "Discord message", nil},
|
||||
{"Message with fields", "telegram", "Telegram message", map[string]interface{}{
|
||||
"user_id": "12345",
|
||||
"count": 42,
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
switch {
|
||||
case tt.fields == nil && tt.component != "":
|
||||
InfoC(tt.component, tt.message)
|
||||
case tt.fields != nil:
|
||||
InfoF(tt.message, tt.fields)
|
||||
default:
|
||||
Info(tt.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
SetLevel(INFO)
|
||||
}
|
||||
|
||||
func TestLogLevels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
level LogLevel
|
||||
want string
|
||||
}{
|
||||
{"DEBUG level", DEBUG, "DEBUG"},
|
||||
{"INFO level", INFO, "INFO"},
|
||||
{"WARN level", WARN, "WARN"},
|
||||
{"ERROR level", ERROR, "ERROR"},
|
||||
{"FATAL level", FATAL, "FATAL"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if logLevelNames[tt.level] != tt.want {
|
||||
t.Errorf("logLevelNames[%d] = %s, want %s", tt.level, logLevelNames[tt.level], tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetGetLevel(t *testing.T) {
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
tests := []LogLevel{DEBUG, INFO, WARN, ERROR, FATAL}
|
||||
|
||||
for _, level := range tests {
|
||||
SetLevel(level)
|
||||
if GetLevel() != level {
|
||||
t.Errorf("SetLevel(%v) -> GetLevel() = %v, want %v", level, GetLevel(), level)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerHelperFunctions(t *testing.T) {
|
||||
initialLevel := GetLevel()
|
||||
defer SetLevel(initialLevel)
|
||||
|
||||
SetLevel(INFO)
|
||||
|
||||
Debug("This should not log")
|
||||
Info("This should log")
|
||||
Warn("This should log")
|
||||
Error("This should log")
|
||||
|
||||
InfoC("test", "Component message")
|
||||
InfoF("Fields message", map[string]interface{}{"key": "value"})
|
||||
|
||||
WarnC("test", "Warning with component")
|
||||
ErrorF("Error with fields", map[string]interface{}{"error": "test"})
|
||||
|
||||
SetLevel(DEBUG)
|
||||
DebugC("test", "Debug with component")
|
||||
WarnF("Warning with fields", map[string]interface{}{"key": "value"})
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMapChatCompletionResponse_CompatFunctionCallXML(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"choices":[
|
||||
{
|
||||
"finish_reason":"stop",
|
||||
"message":{
|
||||
"content":"I need to check the current state and understand what was last worked on before proceeding.\n\n<function_call><invoke><toolname>exec</toolname><parameters><command>cd /root/clawgo && git status</command></parameters></invoke></function_call>\n\n<function_call><invoke><tool_name>read_file</tool_name><parameters><path>/root/.clawgo/workspace/memory/MEMORY.md</path></parameters></invoke></function_call>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`)
|
||||
resp, err := parseChatCompletionsResponse(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("parseChatCompletionsResponse error: %v", err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
if len(resp.ToolCalls) != 2 {
|
||||
t.Fatalf("expected 2 tool calls, got %d", len(resp.ToolCalls))
|
||||
}
|
||||
|
||||
if resp.ToolCalls[0].Name != "exec" {
|
||||
t.Fatalf("expected first tool exec, got %q", resp.ToolCalls[0].Name)
|
||||
}
|
||||
if got, ok := resp.ToolCalls[0].Arguments["command"].(string); !ok || got == "" {
|
||||
t.Fatalf("expected first tool command arg, got %#v", resp.ToolCalls[0].Arguments)
|
||||
}
|
||||
|
||||
if resp.ToolCalls[1].Name != "read_file" {
|
||||
t.Fatalf("expected second tool read_file, got %q", resp.ToolCalls[1].Name)
|
||||
}
|
||||
if got, ok := resp.ToolCalls[1].Arguments["path"].(string); !ok || got == "" {
|
||||
t.Fatalf("expected second tool path arg, got %#v", resp.ToolCalls[1].Arguments)
|
||||
}
|
||||
|
||||
if resp.Content == "" {
|
||||
t.Fatalf("expected non-empty cleaned content")
|
||||
}
|
||||
if containsFunctionCallMarkup(resp.Content) {
|
||||
t.Fatalf("expected function call markup removed from content, got %q", resp.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAPIBase_CompatibilityPaths(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"http://localhost:8080/v1/chat/completions", "http://localhost:8080/v1/chat/completions"},
|
||||
{"http://localhost:8080/v1/responses", "http://localhost:8080/v1/responses"},
|
||||
{"http://localhost:8080/v1", "http://localhost:8080/v1"},
|
||||
{"http://localhost:8080/v1/", "http://localhost:8080/v1"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := normalizeAPIBase(tt.in)
|
||||
if got != tt.want {
|
||||
t.Fatalf("normalizeAPIBase(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeProtocol(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"", ProtocolChatCompletions},
|
||||
{"chat_completions", ProtocolChatCompletions},
|
||||
{"responses", ProtocolResponses},
|
||||
{"invalid", ProtocolChatCompletions},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := normalizeProtocol(tt.in)
|
||||
if got != tt.want {
|
||||
t.Fatalf("normalizeProtocol(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCompatFunctionCalls_NoMarkup(t *testing.T) {
|
||||
calls, cleaned := parseCompatFunctionCalls("hello")
|
||||
if len(calls) != 0 {
|
||||
t.Fatalf("expected 0 calls, got %d", len(calls))
|
||||
}
|
||||
if cleaned != "hello" {
|
||||
t.Fatalf("expected content unchanged, got %q", cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpointForResponsesCompact(t *testing.T) {
|
||||
tests := []struct {
|
||||
base string
|
||||
relative string
|
||||
want string
|
||||
}{
|
||||
{"http://localhost:8080/v1", "/responses/compact", "http://localhost:8080/v1/responses/compact"},
|
||||
{"http://localhost:8080/v1/responses", "/responses/compact", "http://localhost:8080/v1/responses/compact"},
|
||||
{"http://localhost:8080/v1/responses/compact", "/responses", "http://localhost:8080/v1/responses"},
|
||||
{"http://localhost:8080/v1/responses/compact", "/responses/compact", "http://localhost:8080/v1/responses/compact"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := endpointFor(tt.base, tt.relative)
|
||||
if got != tt.want {
|
||||
t.Fatalf("endpointFor(%q, %q) = %q, want %q", tt.base, tt.relative, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToResponsesInputItems_AssistantUsesOutputText(t *testing.T) {
|
||||
items := toResponsesInputItems(Message{
|
||||
Role: "assistant",
|
||||
Content: "hello",
|
||||
})
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("expected 1 item, got %d", len(items))
|
||||
}
|
||||
content, ok := items[0]["content"].([]map[string]interface{})
|
||||
if !ok || len(content) == 0 {
|
||||
t.Fatalf("unexpected content shape: %#v", items[0]["content"])
|
||||
}
|
||||
gotType, _ := content[0]["type"].(string)
|
||||
if gotType != "output_text" {
|
||||
t.Fatalf("assistant content type = %q, want output_text", gotType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToResponsesInputItems_AssistantPreservesToolCalls(t *testing.T) {
|
||||
items := toResponsesInputItems(Message{
|
||||
Role: "assistant",
|
||||
Content: "",
|
||||
ToolCalls: []ToolCall{
|
||||
{
|
||||
ID: "call_abc",
|
||||
Name: "exec_command",
|
||||
Arguments: map[string]interface{}{
|
||||
"cmd": "pwd",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("expected 1 item, got %d", len(items))
|
||||
}
|
||||
gotType, _ := items[0]["type"].(string)
|
||||
if gotType != "function_call" {
|
||||
t.Fatalf("item type = %q, want function_call", gotType)
|
||||
}
|
||||
gotCallID, _ := items[0]["call_id"].(string)
|
||||
if gotCallID != "call_abc" {
|
||||
t.Fatalf("call_id = %q, want call_abc", gotCallID)
|
||||
}
|
||||
gotName, _ := items[0]["name"].(string)
|
||||
if gotName != "exec_command" {
|
||||
t.Fatalf("name = %q, want exec_command", gotName)
|
||||
}
|
||||
gotArgs, _ := items[0]["arguments"].(string)
|
||||
if !strings.Contains(gotArgs, "\"cmd\":\"pwd\"") {
|
||||
t.Fatalf("arguments = %q, want serialized cmd", gotArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func containsFunctionCallMarkup(s string) bool {
|
||||
return len(s) > 0 && (strings.Contains(s, "<function_call>") || strings.Contains(s, "</function_call>"))
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"clawgo/pkg/providers"
|
||||
)
|
||||
|
||||
func TestSessionIndexReadWriteOpenClawFormat(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
sm := NewSessionManager(dir)
|
||||
|
||||
meta := SessionMeta{
|
||||
SessionID: "sid-1",
|
||||
SessionFile: filepath.Join(dir, "sid-1.jsonl"),
|
||||
UpdatedAt: 1770962127556,
|
||||
}
|
||||
if err := sm.saveSessionMeta("channel:chat", meta); err != nil {
|
||||
t.Fatalf("saveSessionMeta failed: %v", err)
|
||||
}
|
||||
|
||||
indexPath := filepath.Join(dir, sessionsIndexFile)
|
||||
data, err := os.ReadFile(indexPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read sessions index: %v", err)
|
||||
}
|
||||
|
||||
raw := map[string]map[string]interface{}{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
t.Fatalf("failed to parse sessions index: %v", err)
|
||||
}
|
||||
entry, ok := raw["channel:chat"]
|
||||
if !ok {
|
||||
t.Fatalf("sessions index missing key")
|
||||
}
|
||||
|
||||
if _, ok := entry["sessionId"].(string); !ok {
|
||||
t.Fatalf("sessionId should be string, got %T", entry["sessionId"])
|
||||
}
|
||||
if _, ok := entry["sessionFile"].(string); !ok {
|
||||
t.Fatalf("sessionFile should be string, got %T", entry["sessionFile"])
|
||||
}
|
||||
if _, ok := entry["updatedAt"].(float64); !ok {
|
||||
t.Fatalf("updatedAt should be number(ms), got %T", entry["updatedAt"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendAndLoadSessionHistoryOpenClawJSONL(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
sm := NewSessionManager(dir)
|
||||
|
||||
sessionKey := "telegram:chat-1"
|
||||
sm.AddMessage(sessionKey, "user", "hello")
|
||||
sm.AddMessage(sessionKey, "assistant", "world")
|
||||
|
||||
meta, ok := sm.getSession(sessionKey)
|
||||
if !ok {
|
||||
t.Fatalf("expected session meta for key")
|
||||
}
|
||||
|
||||
f, err := os.Open(meta.SessionFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open session file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
lines := make([]string, 0)
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
t.Fatalf("scan failed: %v", err)
|
||||
}
|
||||
if len(lines) < 3 {
|
||||
t.Fatalf("expected at least 3 lines(session + 2 messages), got %d", len(lines))
|
||||
}
|
||||
|
||||
var first map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(lines[0]), &first); err != nil {
|
||||
t.Fatalf("invalid first event json: %v", err)
|
||||
}
|
||||
if first["type"] != "session" {
|
||||
t.Fatalf("first event should be session, got %v", first["type"])
|
||||
}
|
||||
|
||||
all, err := sm.loadSessionHistory(meta.SessionID, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("loadSessionHistory failed: %v", err)
|
||||
}
|
||||
if len(all) != 2 {
|
||||
t.Fatalf("unexpected history len: got %d want 2", len(all))
|
||||
}
|
||||
if all[0].Content != "hello" || all[1].Content != "world" {
|
||||
t.Fatalf("unexpected loaded content: %+v", all)
|
||||
}
|
||||
|
||||
limited, err := sm.loadSessionHistory(meta.SessionID, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("loadSessionHistory(limit) failed: %v", err)
|
||||
}
|
||||
if len(limited) != 1 || limited[0].Content != "world" {
|
||||
t.Fatalf("unexpected limited history: %+v", limited)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleEventsUnderSameSessionKeyOpenClaw(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
sm := NewSessionManager(dir)
|
||||
|
||||
sessionKey := "discord:room-1"
|
||||
events := []providers.Message{
|
||||
{Role: "user", Content: "first"},
|
||||
{Role: "assistant", Content: "second"},
|
||||
{Role: "user", Content: "third"},
|
||||
}
|
||||
for _, e := range events {
|
||||
sm.AddMessageFull(sessionKey, e)
|
||||
}
|
||||
|
||||
meta, ok := sm.getSession(sessionKey)
|
||||
if !ok {
|
||||
t.Fatalf("expected session meta")
|
||||
}
|
||||
|
||||
history, err := sm.loadSessionHistory(meta.SessionID, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("loadSessionHistory failed: %v", err)
|
||||
}
|
||||
if len(history) != 3 {
|
||||
t.Fatalf("unexpected history len: got %d want 3", len(history))
|
||||
}
|
||||
if history[2].Content != "third" {
|
||||
t.Fatalf("expected latest content third, got %q", history[2].Content)
|
||||
}
|
||||
|
||||
sm2 := NewSessionManager(dir)
|
||||
reloaded := sm2.GetHistory(sessionKey)
|
||||
if len(reloaded) != 3 {
|
||||
t.Fatalf("unexpected reloaded history len: got %d want 3", len(reloaded))
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadFileToolResolvesRelativePathFromAllowedDir(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
targetPath := filepath.Join(workspace, "cmd", "clawgo", "main.go")
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(targetPath, []byte("package main"), 0644); err != nil {
|
||||
t.Fatalf("write failed: %v", err)
|
||||
}
|
||||
|
||||
tool := NewReadFileTool(workspace)
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"path": "cmd/clawgo/main.go",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if out != "package main" {
|
||||
t.Fatalf("unexpected output: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileToolAllowsParentTraversalWhenPermitted(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
parentFile := filepath.Join(filepath.Dir(workspace), "outside.txt")
|
||||
if err := os.WriteFile(parentFile, []byte("outside"), 0644); err != nil {
|
||||
t.Fatalf("write failed: %v", err)
|
||||
}
|
||||
|
||||
tool := NewReadFileTool(workspace)
|
||||
relPath, err := filepath.Rel(workspace, parentFile)
|
||||
if err != nil {
|
||||
t.Fatalf("rel failed: %v", err)
|
||||
}
|
||||
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"path": relPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if out != "outside" {
|
||||
t.Fatalf("unexpected output: %q", out)
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMemorySearchToolClampsMaxResults(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
memDir := filepath.Join(workspace, "memory")
|
||||
if err := os.MkdirAll(memDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
content := "# Long-term Memory\n\nalpha one\n\nalpha two\n"
|
||||
if err := os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte(content), 0644); err != nil {
|
||||
t.Fatalf("write failed: %v", err)
|
||||
}
|
||||
|
||||
tool := NewMemorySearchTool(workspace)
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"query": "alpha",
|
||||
"maxResults": -5,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "Found 1 memories") {
|
||||
t.Fatalf("expected clamped result count, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemorySearchToolScannerHandlesLargeLine(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
memDir := filepath.Join(workspace, "memory")
|
||||
if err := os.MkdirAll(memDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
large := strings.Repeat("x", 80*1024) + " needle"
|
||||
if err := os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte(large), 0644); err != nil {
|
||||
t.Fatalf("write failed: %v", err)
|
||||
}
|
||||
|
||||
tool := NewMemorySearchTool(workspace)
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"query": "needle",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "needle") {
|
||||
t.Fatalf("expected search hit in large line, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemorySearchToolPrefersCanonicalMemoryPath(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
memDir := filepath.Join(workspace, "memory")
|
||||
if err := os.MkdirAll(memDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("canonical"), 0644); err != nil {
|
||||
t.Fatalf("write canonical failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(workspace, "MEMORY.md"), []byte("legacy"), 0644); err != nil {
|
||||
t.Fatalf("write legacy failed: %v", err)
|
||||
}
|
||||
|
||||
tool := NewMemorySearchTool(workspace)
|
||||
files := tool.getMemoryFiles()
|
||||
for _, file := range files {
|
||||
if file == filepath.Join(workspace, "MEMORY.md") {
|
||||
t.Fatalf("legacy path should be ignored when canonical exists: %v", files)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemorySearchToolReportsFileScanWarnings(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
memDir := filepath.Join(workspace, "memory")
|
||||
if err := os.MkdirAll(memDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
|
||||
tooLargeLine := strings.Repeat("x", 2*1024*1024) + "\n"
|
||||
if err := os.WriteFile(filepath.Join(memDir, "bad.md"), []byte(tooLargeLine), 0644); err != nil {
|
||||
t.Fatalf("write bad file failed: %v", err)
|
||||
}
|
||||
|
||||
tool := NewMemorySearchTool(workspace)
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"query": "needle",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "Warning: memory_search skipped") {
|
||||
t.Fatalf("expected warning suffix when scan errors happen, got: %s", out)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatMemoryLine(t *testing.T) {
|
||||
got := formatMemoryLine("remember this", "high", "user", []string{"preference", "lang"})
|
||||
if got == "" {
|
||||
t.Fatal("empty formatted line")
|
||||
}
|
||||
if want := "importance=high"; !strings.Contains(got, want) {
|
||||
t.Fatalf("expected %q in %q", want, got)
|
||||
}
|
||||
if want := "source=user"; !strings.Contains(got, want) {
|
||||
t.Fatalf("expected %q in %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeImportance(t *testing.T) {
|
||||
if normalizeImportance("HIGH") != "high" {
|
||||
t.Fatal("expected high")
|
||||
}
|
||||
if normalizeImportance("unknown") != "medium" {
|
||||
t.Fatal("expected medium fallback")
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type basicTool struct {
|
||||
name string
|
||||
execute func(ctx context.Context, args map[string]interface{}) (string, error)
|
||||
}
|
||||
|
||||
func (t *basicTool) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *basicTool) Description() string {
|
||||
return "test tool"
|
||||
}
|
||||
|
||||
func (t *basicTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{"type": "object"}
|
||||
}
|
||||
|
||||
func (t *basicTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) {
|
||||
return t.execute(ctx, args)
|
||||
}
|
||||
|
||||
type safeTool struct {
|
||||
*basicTool
|
||||
}
|
||||
|
||||
func (t *safeTool) ParallelSafe() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type concurrencyTool struct {
|
||||
name string
|
||||
delay time.Duration
|
||||
current int32
|
||||
max int32
|
||||
}
|
||||
|
||||
func (t *concurrencyTool) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *concurrencyTool) Description() string {
|
||||
return "concurrency test tool"
|
||||
}
|
||||
|
||||
func (t *concurrencyTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{"type": "object"}
|
||||
}
|
||||
|
||||
func (t *concurrencyTool) ParallelSafe() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *concurrencyTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) {
|
||||
current := atomic.AddInt32(&t.current, 1)
|
||||
for {
|
||||
max := atomic.LoadInt32(&t.max)
|
||||
if current <= max {
|
||||
break
|
||||
}
|
||||
if atomic.CompareAndSwapInt32(&t.max, max, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(t.delay)
|
||||
atomic.AddInt32(&t.current, -1)
|
||||
return "ok", nil
|
||||
}
|
||||
|
||||
type conflictTool struct {
|
||||
name string
|
||||
delay time.Duration
|
||||
mu sync.Mutex
|
||||
active map[string]bool
|
||||
conflicts int32
|
||||
}
|
||||
|
||||
func (t *conflictTool) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *conflictTool) Description() string {
|
||||
return "resource conflict test tool"
|
||||
}
|
||||
|
||||
func (t *conflictTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{"type": "object"}
|
||||
}
|
||||
|
||||
func (t *conflictTool) ParallelSafe() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *conflictTool) ResourceKeys(args map[string]interface{}) []string {
|
||||
key, _ := args["key"].(string)
|
||||
if key == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{key}
|
||||
}
|
||||
|
||||
func (t *conflictTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) {
|
||||
key, _ := args["key"].(string)
|
||||
if key == "" {
|
||||
return "", errors.New("missing key")
|
||||
}
|
||||
defer func() {
|
||||
t.mu.Lock()
|
||||
delete(t.active, key)
|
||||
t.mu.Unlock()
|
||||
}()
|
||||
|
||||
t.mu.Lock()
|
||||
if t.active == nil {
|
||||
t.active = make(map[string]bool)
|
||||
}
|
||||
if t.active[key] {
|
||||
atomic.AddInt32(&t.conflicts, 1)
|
||||
}
|
||||
t.active[key] = true
|
||||
t.mu.Unlock()
|
||||
|
||||
time.Sleep(t.delay)
|
||||
return "ok", nil
|
||||
}
|
||||
|
||||
func TestParallelToolStableOrdering(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
tool := &safeTool{&basicTool{
|
||||
name: "echo",
|
||||
execute: func(ctx context.Context, args map[string]interface{}) (string, error) {
|
||||
delay := 0 * time.Millisecond
|
||||
switch v := args["delay"].(type) {
|
||||
case int:
|
||||
delay = time.Duration(v) * time.Millisecond
|
||||
case float64:
|
||||
delay = time.Duration(v) * time.Millisecond
|
||||
}
|
||||
if delay > 0 {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
value, _ := args["value"].(string)
|
||||
return value, nil
|
||||
},
|
||||
}}
|
||||
registry.Register(tool)
|
||||
|
||||
parallel := NewParallelTool(registry, 3, nil)
|
||||
calls := []interface{}{
|
||||
map[string]interface{}{
|
||||
"tool": "echo",
|
||||
"arguments": map[string]interface{}{"value": "first", "delay": 40},
|
||||
"id": "first",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"tool": "echo",
|
||||
"arguments": map[string]interface{}{"value": "second", "delay": 10},
|
||||
"id": "second",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"tool": "echo",
|
||||
"arguments": map[string]interface{}{"value": "third", "delay": 20},
|
||||
"id": "third",
|
||||
},
|
||||
}
|
||||
|
||||
output, err := parallel.Execute(context.Background(), map[string]interface{}{"calls": calls})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
firstIdx := strings.Index(output, "Result for first")
|
||||
secondIdx := strings.Index(output, "Result for second")
|
||||
thirdIdx := strings.Index(output, "Result for third")
|
||||
if firstIdx == -1 || secondIdx == -1 || thirdIdx == -1 {
|
||||
t.Fatalf("missing result markers in output: %s", output)
|
||||
}
|
||||
if !(firstIdx < secondIdx && secondIdx < thirdIdx) {
|
||||
t.Fatalf("results not in call order: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParallelToolErrorFormatting(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
tool := &safeTool{&basicTool{
|
||||
name: "fail",
|
||||
execute: func(ctx context.Context, args map[string]interface{}) (string, error) {
|
||||
return "", errors.New("boom")
|
||||
},
|
||||
}}
|
||||
registry.Register(tool)
|
||||
|
||||
parallel := NewParallelTool(registry, 2, nil)
|
||||
calls := []interface{}{
|
||||
map[string]interface{}{
|
||||
"tool": "fail",
|
||||
"arguments": map[string]interface{}{},
|
||||
"id": "err",
|
||||
},
|
||||
}
|
||||
|
||||
output, err := parallel.Execute(context.Background(), map[string]interface{}{"calls": calls})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(output, "Error: boom") {
|
||||
t.Fatalf("expected formatted error, got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParallelToolConcurrencyLimit(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
tool := &concurrencyTool{name: "sleep", delay: 25 * time.Millisecond}
|
||||
registry.Register(tool)
|
||||
|
||||
parallel := NewParallelTool(registry, 2, nil)
|
||||
calls := make([]interface{}, 5)
|
||||
for i := 0; i < len(calls); i++ {
|
||||
calls[i] = map[string]interface{}{
|
||||
"tool": "sleep",
|
||||
"arguments": map[string]interface{}{},
|
||||
"id": "call",
|
||||
}
|
||||
}
|
||||
|
||||
_, err := parallel.Execute(context.Background(), map[string]interface{}{"calls": calls})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if max := atomic.LoadInt32(&tool.max); max > 2 {
|
||||
t.Fatalf("expected max concurrency <= 2, got %d", max)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParallelToolResourceBatching(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
tool := &conflictTool{name: "resource", delay: 30 * time.Millisecond}
|
||||
registry.Register(tool)
|
||||
|
||||
parallel := NewParallelTool(registry, 3, nil)
|
||||
calls := []interface{}{
|
||||
map[string]interface{}{
|
||||
"tool": "resource",
|
||||
"arguments": map[string]interface{}{"key": "alpha"},
|
||||
"id": "first",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"tool": "resource",
|
||||
"arguments": map[string]interface{}{"key": "beta"},
|
||||
"id": "second",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"tool": "resource",
|
||||
"arguments": map[string]interface{}{"key": "alpha"},
|
||||
"id": "third",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := parallel.Execute(context.Background(), map[string]interface{}{"calls": calls})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if conflicts := atomic.LoadInt32(&tool.conflicts); conflicts > 0 {
|
||||
t.Fatalf("expected no resource conflicts, got %d", conflicts)
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"clawgo/pkg/providers"
|
||||
)
|
||||
|
||||
func TestSessionsToolListWithKindsAndQuery(t *testing.T) {
|
||||
tool := NewSessionsTool(func(limit int) []SessionInfo {
|
||||
return []SessionInfo{
|
||||
{Key: "telegram:1", Kind: "main", Summary: "project alpha", UpdatedAt: time.Now()},
|
||||
{Key: "cron:1", Kind: "cron", Summary: "nightly sync", UpdatedAt: time.Now()},
|
||||
}
|
||||
}, nil, "", "")
|
||||
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"action": "list",
|
||||
"kinds": []interface{}{"main"},
|
||||
"query": "alpha",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(out, "telegram:1") || strings.Contains(out, "cron:1") {
|
||||
t.Fatalf("unexpected output: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionsToolHistoryWithoutTools(t *testing.T) {
|
||||
tool := NewSessionsTool(nil, func(key string, limit int) []providers.Message {
|
||||
return []providers.Message{
|
||||
{Role: "user", Content: "hello"},
|
||||
{Role: "tool", Content: "tool output"},
|
||||
{Role: "assistant", Content: "ok"},
|
||||
}
|
||||
}, "", "")
|
||||
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"action": "history",
|
||||
"key": "telegram:1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(out), "tool output") {
|
||||
t.Fatalf("tool message should be filtered: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionsToolHistoryFromMe(t *testing.T) {
|
||||
tool := NewSessionsTool(nil, func(key string, limit int) []providers.Message {
|
||||
return []providers.Message{
|
||||
{Role: "user", Content: "u1"},
|
||||
{Role: "assistant", Content: "a1"},
|
||||
{Role: "assistant", Content: "a2"},
|
||||
}
|
||||
}, "", "")
|
||||
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"action": "history",
|
||||
"key": "telegram:1",
|
||||
"from_me": true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(out, "u1") || !strings.Contains(out, "a1") {
|
||||
t.Fatalf("unexpected filtered output: %s", out)
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"clawgo/pkg/config"
|
||||
)
|
||||
|
||||
func TestExecToolExecuteBasicCommand(t *testing.T) {
|
||||
tool := NewExecTool(config.ShellConfig{Timeout: 2 * time.Second}, ".")
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"command": "echo hello",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "hello") {
|
||||
t.Fatalf("expected output to contain hello, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecToolExecuteTimeout(t *testing.T) {
|
||||
tool := NewExecTool(config.ShellConfig{Timeout: 20 * time.Millisecond}, ".")
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{
|
||||
"command": "sleep 1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "timed out") {
|
||||
t.Fatalf("expected timeout message, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectMissingCommandFromOutput(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{in: "sh: git: not found", want: "git"},
|
||||
{in: "/bin/sh: 1: rg: not found", want: "rg"},
|
||||
{in: "bash: foo: command not found", want: "foo"},
|
||||
{in: "normal error", want: ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := detectMissingCommandFromOutput(tc.in)
|
||||
if got != tc.want {
|
||||
t.Fatalf("detectMissingCommandFromOutput(%q)=%q want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildInstallCommandCandidates_EmptyName(t *testing.T) {
|
||||
if got := buildInstallCommandCandidates(""); len(got) != 0 {
|
||||
t.Fatalf("expected empty candidates, got %v", got)
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSubagentsInfoAll(t *testing.T) {
|
||||
m := NewSubagentManager(nil, ".", nil, nil)
|
||||
m.tasks["subagent-1"] = &SubagentTask{ID: "subagent-1", Status: "completed", Label: "a", Created: 2}
|
||||
m.tasks["subagent-2"] = &SubagentTask{ID: "subagent-2", Status: "running", Label: "b", Created: 3}
|
||||
|
||||
tool := NewSubagentsTool(m, "", "")
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{"action": "info", "id": "all"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(out, "Subagents Summary") || !strings.Contains(out, "subagent-2") {
|
||||
t.Fatalf("unexpected output: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubagentsKillAll(t *testing.T) {
|
||||
m := NewSubagentManager(nil, ".", nil, nil)
|
||||
m.tasks["subagent-1"] = &SubagentTask{ID: "subagent-1", Status: "running", Label: "a", Created: 2}
|
||||
m.tasks["subagent-2"] = &SubagentTask{ID: "subagent-2", Status: "running", Label: "b", Created: 3}
|
||||
|
||||
tool := NewSubagentsTool(m, "", "")
|
||||
out, err := tool.Execute(context.Background(), map[string]interface{}{"action": "kill", "id": "all"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(out, "2") {
|
||||
t.Fatalf("unexpected kill output: %s", out)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user