From 81674e30f6c52bdb3c730e5d5495473f21333afa Mon Sep 17 00:00:00 2001 From: DBT Date: Mon, 23 Feb 2026 16:19:33 +0000 Subject: [PATCH] remove flaky Go test files per request --- pkg/agent/context_system_summary_test.go | 97 -------- pkg/agent/history_filter_test.go | 34 --- pkg/agent/memory_test.go | 38 --- pkg/agent/system_summary_fallback_test.go | 22 -- pkg/config/config_test.go | 87 ------- pkg/cron/service_test.go | 261 -------------------- pkg/logger/logger_test.go | 139 ----------- pkg/providers/provider_test.go | 175 -------------- pkg/session/manager_test.go | 146 ------------ pkg/tools/filesystem_test.go | 54 ----- pkg/tools/memory_test.go | 102 -------- pkg/tools/memory_write_test.go | 28 --- pkg/tools/parallel_test.go | 278 ---------------------- pkg/tools/sessions_tool_test.go | 74 ------ pkg/tools/shell_test.go | 60 ----- pkg/tools/subagents_tool_test.go | 37 --- 16 files changed, 1632 deletions(-) delete mode 100644 pkg/agent/context_system_summary_test.go delete mode 100644 pkg/agent/history_filter_test.go delete mode 100644 pkg/agent/memory_test.go delete mode 100644 pkg/agent/system_summary_fallback_test.go delete mode 100644 pkg/config/config_test.go delete mode 100644 pkg/cron/service_test.go delete mode 100644 pkg/logger/logger_test.go delete mode 100644 pkg/providers/provider_test.go delete mode 100644 pkg/session/manager_test.go delete mode 100644 pkg/tools/filesystem_test.go delete mode 100644 pkg/tools/memory_test.go delete mode 100644 pkg/tools/memory_write_test.go delete mode 100644 pkg/tools/parallel_test.go delete mode 100644 pkg/tools/sessions_tool_test.go delete mode 100644 pkg/tools/shell_test.go delete mode 100644 pkg/tools/subagents_tool_test.go diff --git a/pkg/agent/context_system_summary_test.go b/pkg/agent/context_system_summary_test.go deleted file mode 100644 index 210fd07..0000000 --- a/pkg/agent/context_system_summary_test.go +++ /dev/null @@ -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) - } -} diff --git a/pkg/agent/history_filter_test.go b/pkg/agent/history_filter_test.go deleted file mode 100644 index 0c96c21..0000000 --- a/pkg/agent/history_filter_test.go +++ /dev/null @@ -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) - } -} diff --git a/pkg/agent/memory_test.go b/pkg/agent/memory_test.go deleted file mode 100644 index bd2c10a..0000000 --- a/pkg/agent/memory_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package agent - -import ( - "strings" - "testing" -) - -func TestTruncateMemoryTextRuneSafe(t *testing.T) { - in := "你好世界这是一个测试" - out := truncateMemoryText(in, 6) - if strings.Contains(out, "�") { - 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) - } -} diff --git a/pkg/agent/system_summary_fallback_test.go b/pkg/agent/system_summary_fallback_test.go deleted file mode 100644 index f697397..0000000 --- a/pkg/agent/system_summary_fallback_test.go +++ /dev/null @@ -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) - } -} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go deleted file mode 100644 index 5d19aec..0000000 --- a/pkg/config/config_test.go +++ /dev/null @@ -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) - } -} diff --git a/pkg/cron/service_test.go b/pkg/cron/service_test.go deleted file mode 100644 index a8f0b4b..0000000 --- a/pkg/cron/service_test.go +++ /dev/null @@ -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)) - } -} diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go deleted file mode 100644 index 9b9c968..0000000 --- a/pkg/logger/logger_test.go +++ /dev/null @@ -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"}) -} diff --git a/pkg/providers/provider_test.go b/pkg/providers/provider_test.go deleted file mode 100644 index 6a73f49..0000000 --- a/pkg/providers/provider_test.go +++ /dev/null @@ -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\nexeccd /root/clawgo && git status\n\nread_file/root/.clawgo/workspace/memory/MEMORY.md" - } - } - ] - }`) - 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, "") || strings.Contains(s, "")) -} diff --git a/pkg/session/manager_test.go b/pkg/session/manager_test.go deleted file mode 100644 index f82b36a..0000000 --- a/pkg/session/manager_test.go +++ /dev/null @@ -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)) - } -} diff --git a/pkg/tools/filesystem_test.go b/pkg/tools/filesystem_test.go deleted file mode 100644 index 1198c20..0000000 --- a/pkg/tools/filesystem_test.go +++ /dev/null @@ -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) - } -} diff --git a/pkg/tools/memory_test.go b/pkg/tools/memory_test.go deleted file mode 100644 index b265916..0000000 --- a/pkg/tools/memory_test.go +++ /dev/null @@ -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) - } -} diff --git a/pkg/tools/memory_write_test.go b/pkg/tools/memory_write_test.go deleted file mode 100644 index a6176aa..0000000 --- a/pkg/tools/memory_write_test.go +++ /dev/null @@ -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") - } -} diff --git a/pkg/tools/parallel_test.go b/pkg/tools/parallel_test.go deleted file mode 100644 index 15e5291..0000000 --- a/pkg/tools/parallel_test.go +++ /dev/null @@ -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) - } -} diff --git a/pkg/tools/sessions_tool_test.go b/pkg/tools/sessions_tool_test.go deleted file mode 100644 index 2d2b56d..0000000 --- a/pkg/tools/sessions_tool_test.go +++ /dev/null @@ -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) - } -} diff --git a/pkg/tools/shell_test.go b/pkg/tools/shell_test.go deleted file mode 100644 index 0a27dc3..0000000 --- a/pkg/tools/shell_test.go +++ /dev/null @@ -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) - } -} diff --git a/pkg/tools/subagents_tool_test.go b/pkg/tools/subagents_tool_test.go deleted file mode 100644 index b72bae0..0000000 --- a/pkg/tools/subagents_tool_test.go +++ /dev/null @@ -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) - } -}