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)
- }
-}