remove flaky Go test files per request

This commit is contained in:
DBT
2026-02-23 16:19:33 +00:00
parent 6848f8f674
commit 81674e30f6
16 changed files with 0 additions and 1632 deletions

View File

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

View File

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

View File

@@ -1,38 +0,0 @@
package agent
import (
"strings"
"testing"
)
func TestTruncateMemoryTextRuneSafe(t *testing.T) {
in := "你好世界这是一个测试"
out := truncateMemoryText(in, 6)
if strings.Contains(out, "<22>") {
t.Fatalf("expected rune-safe truncation, got invalid rune replacement: %q", out)
}
}
func TestCompressMemoryForPromptPrefersStructuredLines(t *testing.T) {
in := `
# Long-term Memory
plain paragraph line 1
plain paragraph line 2
- bullet one
- bullet two
another paragraph
`
out := compressMemoryForPrompt(in, 4, 200)
if !strings.Contains(out, "# Long-term Memory") {
t.Fatalf("expected heading in digest, got: %q", out)
}
if !strings.Contains(out, "- bullet one") {
t.Fatalf("expected bullet in digest, got: %q", out)
}
if strings.Contains(out, "plain paragraph line 2") {
t.Fatalf("expected paragraph compression to keep first line only, got: %q", out)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,175 +0,0 @@
package providers
import (
"strings"
"testing"
)
func TestMapChatCompletionResponse_CompatFunctionCallXML(t *testing.T) {
raw := []byte(`{
"choices":[
{
"finish_reason":"stop",
"message":{
"content":"I need to check the current state and understand what was last worked on before proceeding.\n\n<function_call><invoke><toolname>exec</toolname><parameters><command>cd /root/clawgo && git status</command></parameters></invoke></function_call>\n\n<function_call><invoke><tool_name>read_file</tool_name><parameters><path>/root/.clawgo/workspace/memory/MEMORY.md</path></parameters></invoke></function_call>"
}
}
]
}`)
resp, err := parseChatCompletionsResponse(raw)
if err != nil {
t.Fatalf("parseChatCompletionsResponse error: %v", err)
}
if resp == nil {
t.Fatalf("expected response")
}
if len(resp.ToolCalls) != 2 {
t.Fatalf("expected 2 tool calls, got %d", len(resp.ToolCalls))
}
if resp.ToolCalls[0].Name != "exec" {
t.Fatalf("expected first tool exec, got %q", resp.ToolCalls[0].Name)
}
if got, ok := resp.ToolCalls[0].Arguments["command"].(string); !ok || got == "" {
t.Fatalf("expected first tool command arg, got %#v", resp.ToolCalls[0].Arguments)
}
if resp.ToolCalls[1].Name != "read_file" {
t.Fatalf("expected second tool read_file, got %q", resp.ToolCalls[1].Name)
}
if got, ok := resp.ToolCalls[1].Arguments["path"].(string); !ok || got == "" {
t.Fatalf("expected second tool path arg, got %#v", resp.ToolCalls[1].Arguments)
}
if resp.Content == "" {
t.Fatalf("expected non-empty cleaned content")
}
if containsFunctionCallMarkup(resp.Content) {
t.Fatalf("expected function call markup removed from content, got %q", resp.Content)
}
}
func TestNormalizeAPIBase_CompatibilityPaths(t *testing.T) {
tests := []struct {
in string
want string
}{
{"http://localhost:8080/v1/chat/completions", "http://localhost:8080/v1/chat/completions"},
{"http://localhost:8080/v1/responses", "http://localhost:8080/v1/responses"},
{"http://localhost:8080/v1", "http://localhost:8080/v1"},
{"http://localhost:8080/v1/", "http://localhost:8080/v1"},
}
for _, tt := range tests {
got := normalizeAPIBase(tt.in)
if got != tt.want {
t.Fatalf("normalizeAPIBase(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestNormalizeProtocol(t *testing.T) {
tests := []struct {
in string
want string
}{
{"", ProtocolChatCompletions},
{"chat_completions", ProtocolChatCompletions},
{"responses", ProtocolResponses},
{"invalid", ProtocolChatCompletions},
}
for _, tt := range tests {
got := normalizeProtocol(tt.in)
if got != tt.want {
t.Fatalf("normalizeProtocol(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestParseCompatFunctionCalls_NoMarkup(t *testing.T) {
calls, cleaned := parseCompatFunctionCalls("hello")
if len(calls) != 0 {
t.Fatalf("expected 0 calls, got %d", len(calls))
}
if cleaned != "hello" {
t.Fatalf("expected content unchanged, got %q", cleaned)
}
}
func TestEndpointForResponsesCompact(t *testing.T) {
tests := []struct {
base string
relative string
want string
}{
{"http://localhost:8080/v1", "/responses/compact", "http://localhost:8080/v1/responses/compact"},
{"http://localhost:8080/v1/responses", "/responses/compact", "http://localhost:8080/v1/responses/compact"},
{"http://localhost:8080/v1/responses/compact", "/responses", "http://localhost:8080/v1/responses"},
{"http://localhost:8080/v1/responses/compact", "/responses/compact", "http://localhost:8080/v1/responses/compact"},
}
for _, tt := range tests {
got := endpointFor(tt.base, tt.relative)
if got != tt.want {
t.Fatalf("endpointFor(%q, %q) = %q, want %q", tt.base, tt.relative, got, tt.want)
}
}
}
func TestToResponsesInputItems_AssistantUsesOutputText(t *testing.T) {
items := toResponsesInputItems(Message{
Role: "assistant",
Content: "hello",
})
if len(items) != 1 {
t.Fatalf("expected 1 item, got %d", len(items))
}
content, ok := items[0]["content"].([]map[string]interface{})
if !ok || len(content) == 0 {
t.Fatalf("unexpected content shape: %#v", items[0]["content"])
}
gotType, _ := content[0]["type"].(string)
if gotType != "output_text" {
t.Fatalf("assistant content type = %q, want output_text", gotType)
}
}
func TestToResponsesInputItems_AssistantPreservesToolCalls(t *testing.T) {
items := toResponsesInputItems(Message{
Role: "assistant",
Content: "",
ToolCalls: []ToolCall{
{
ID: "call_abc",
Name: "exec_command",
Arguments: map[string]interface{}{
"cmd": "pwd",
},
},
},
})
if len(items) != 1 {
t.Fatalf("expected 1 item, got %d", len(items))
}
gotType, _ := items[0]["type"].(string)
if gotType != "function_call" {
t.Fatalf("item type = %q, want function_call", gotType)
}
gotCallID, _ := items[0]["call_id"].(string)
if gotCallID != "call_abc" {
t.Fatalf("call_id = %q, want call_abc", gotCallID)
}
gotName, _ := items[0]["name"].(string)
if gotName != "exec_command" {
t.Fatalf("name = %q, want exec_command", gotName)
}
gotArgs, _ := items[0]["arguments"].(string)
if !strings.Contains(gotArgs, "\"cmd\":\"pwd\"") {
t.Fatalf("arguments = %q, want serialized cmd", gotArgs)
}
}
func containsFunctionCallMarkup(s string) bool {
return len(s) > 0 && (strings.Contains(s, "<function_call>") || strings.Contains(s, "</function_call>"))
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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