This commit is contained in:
lxowalle 2026-04-17 17:45:57 +08:00
parent cea0fa35db
commit a188e7463c
8 changed files with 49 additions and 146 deletions

View file

@ -3,6 +3,7 @@ package auth
import (
"bytes"
"context"
"net"
"net/http"
"net/http/httptest"
"net/url"
@ -19,6 +20,19 @@ import (
"github.com/sipeed/picoclaw/pkg/config"
)
func newIPv4TestServer(t *testing.T, handler http.Handler) *httptest.Server {
t.Helper()
server := httptest.NewUnstartedServer(handler)
listener, err := net.Listen("tcp4", "127.0.0.1:0")
require.NoError(t, err)
server.Listener = listener
server.Start()
t.Cleanup(server.Close)
return server
}
func TestNewWeComCommand(t *testing.T) {
cmd := newWeComCommand()
@ -53,7 +67,7 @@ func TestBuildWeComQRCodePageURL(t *testing.T) {
}
func TestFetchWeComQRCode(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := newIPv4TestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/generate", r.URL.Path)
assert.Equal(t, wecomQRSourceID, r.URL.Query().Get("source"))
assert.Equal(t, wecomQRSourceID, r.URL.Query().Get("sourceID"))
@ -61,7 +75,6 @@ func TestFetchWeComQRCode(t *testing.T) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":{"scode":"scode-1","auth_url":"https://example.com/qr"}}`))
}))
defer server.Close()
opts := normalizeWeComQRFlowOptions(wecomQRFlowOptions{
HTTPClient: server.Client(),
@ -78,7 +91,7 @@ func TestFetchWeComQRCode(t *testing.T) {
func TestPollWeComQRCodeResult(t *testing.T) {
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := newIPv4TestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
call := calls.Add(1)
assert.Equal(t, "/query", r.URL.Path)
assert.Equal(t, "scode-1", r.URL.Query().Get("scode"))
@ -92,7 +105,6 @@ func TestPollWeComQRCodeResult(t *testing.T) {
_, _ = w.Write([]byte(`{"data":{"status":"success","bot_info":{"botid":"bot-1","secret":"secret-1"}}}`))
}
}))
defer server.Close()
var output bytes.Buffer
opts := normalizeWeComQRFlowOptions(wecomQRFlowOptions{

View file

@ -1794,43 +1794,6 @@ func (m *toolFeedbackReasoningProvider) GetDefaultModel() string {
return "tool-feedback-reasoning-model"
}
type toolFeedbackExtraContentProvider struct {
filePath string
calls int
}
func (m *toolFeedbackExtraContentProvider) Chat(
ctx context.Context,
messages []providers.Message,
tools []providers.ToolDefinition,
model string,
opts map[string]any,
) (*providers.LLMResponse, error) {
m.calls++
if m.calls == 1 {
return &providers.LLMResponse{
ToolCalls: []providers.ToolCall{{
ID: "call_explicit_read_file",
Type: "function",
Name: "read_file",
Arguments: map[string]any{"path": m.filePath},
ExtraContent: &providers.ExtraContent{
ToolFeedbackExplanation: "Read README.md first to confirm the current project structure.",
},
}},
}, nil
}
return &providers.LLMResponse{
Content: "DONE",
ToolCalls: []providers.ToolCall{},
}, nil
}
func (m *toolFeedbackExtraContentProvider) GetDefaultModel() string {
return "tool-feedback-extra-content-model"
}
func TestToolFeedbackExplanationFromResponse_UsesCurrentContentFirst(t *testing.T) {
response := &providers.LLMResponse{
Content: "Read README.md first",
@ -3934,8 +3897,14 @@ func TestProcessMessage_DoesNotLeakReasoningContentInToolFeedback(t *testing.T)
}
func TestProcessMessage_DoesNotPublishToolFeedbackForDiscordWhenDisabled(t *testing.T) {
assertToolFeedbackNotPublishedWhenDisabled(t, "discord")
}
func assertToolFeedbackNotPublishedWhenDisabled(t *testing.T, channel string) {
t.Helper()
tmpDir := t.TempDir()
heartbeatFile := filepath.Join(tmpDir, "tool-feedback-discord.txt")
heartbeatFile := filepath.Join(tmpDir, "tool-feedback-"+channel+".txt")
if err := os.WriteFile(heartbeatFile, []byte("tool feedback task"), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
@ -3961,7 +3930,7 @@ func TestProcessMessage_DoesNotPublishToolFeedbackForDiscordWhenDisabled(t *test
al := NewAgentLoop(cfg, msgBus, provider)
response, err := al.processMessage(context.Background(), testInboundMessage(bus.InboundMessage{
Channel: "discord",
Channel: channel,
SenderID: "user-1",
ChatID: "chat-1",
Content: "check tool feedback",
@ -3975,103 +3944,17 @@ func TestProcessMessage_DoesNotPublishToolFeedbackForDiscordWhenDisabled(t *test
select {
case outbound := <-msgBus.OutboundChan():
t.Fatalf("expected no outbound tool feedback for discord when disabled, got %+v", outbound)
t.Fatalf("expected no outbound tool feedback for %s when disabled, got %+v", channel, outbound)
case <-time.After(200 * time.Millisecond):
}
}
func TestProcessMessage_DoesNotPublishToolFeedbackForTelegramWhenDisabled(t *testing.T) {
tmpDir := t.TempDir()
heartbeatFile := filepath.Join(tmpDir, "tool-feedback-telegram-default.txt")
if err := os.WriteFile(heartbeatFile, []byte("tool feedback task"), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
ModelName: "test-model",
MaxTokens: 4096,
MaxToolIterations: 10,
},
},
Tools: config.ToolsConfig{
ReadFile: config.ReadFileToolConfig{
Enabled: true,
},
},
}
msgBus := bus.NewMessageBus()
provider := &toolFeedbackProvider{filePath: heartbeatFile}
al := NewAgentLoop(cfg, msgBus, provider)
response, err := al.processMessage(context.Background(), testInboundMessage(bus.InboundMessage{
Channel: "telegram",
SenderID: "user-1",
ChatID: "chat-1",
Content: "check tool feedback",
}))
if err != nil {
t.Fatalf("processMessage() error = %v", err)
}
if response != "HEARTBEAT_OK" {
t.Fatalf("processMessage() response = %q, want %q", response, "HEARTBEAT_OK")
}
select {
case outbound := <-msgBus.OutboundChan():
t.Fatalf("expected no outbound tool feedback for telegram when disabled, got %+v", outbound)
case <-time.After(200 * time.Millisecond):
}
assertToolFeedbackNotPublishedWhenDisabled(t, "telegram")
}
func TestProcessMessage_DoesNotPublishToolFeedbackForFeishuWhenDisabled(t *testing.T) {
tmpDir := t.TempDir()
heartbeatFile := filepath.Join(tmpDir, "tool-feedback-feishu-default.txt")
if err := os.WriteFile(heartbeatFile, []byte("tool feedback task"), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
ModelName: "test-model",
MaxTokens: 4096,
MaxToolIterations: 10,
},
},
Tools: config.ToolsConfig{
ReadFile: config.ReadFileToolConfig{
Enabled: true,
},
},
}
msgBus := bus.NewMessageBus()
provider := &toolFeedbackProvider{filePath: heartbeatFile}
al := NewAgentLoop(cfg, msgBus, provider)
response, err := al.processMessage(context.Background(), testInboundMessage(bus.InboundMessage{
Channel: "feishu",
SenderID: "user-1",
ChatID: "chat-1",
Content: "check tool feedback",
}))
if err != nil {
t.Fatalf("processMessage() error = %v", err)
}
if response != "HEARTBEAT_OK" {
t.Fatalf("processMessage() response = %q, want %q", response, "HEARTBEAT_OK")
}
select {
case outbound := <-msgBus.OutboundChan():
t.Fatalf("expected no outbound tool feedback for feishu when disabled, got %+v", outbound)
case <-time.After(200 * time.Millisecond):
}
assertToolFeedbackNotPublishedWhenDisabled(t, "feishu")
}
func TestProcessMessage_MessageToolPublishesOutboundWithTurnMetadata(t *testing.T) {

View file

@ -12,6 +12,7 @@ import (
"time"
"github.com/bwmarrin/discordgo"
"github.com/sipeed/picoclaw/pkg/audio/tts"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"

View file

@ -7,9 +7,9 @@ import (
"errors"
"testing"
"github.com/sipeed/picoclaw/pkg/channels"
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
"github.com/sipeed/picoclaw/pkg/channels"
)
func TestExtractContent(t *testing.T) {

View file

@ -932,7 +932,10 @@ func TestPreSendMedia_LeavesTrackedMessageForChannelSend(t *testing.T) {
}, ch)
if ch.dismissedChatID != "" {
t.Fatalf("expected tracked tool feedback cleanup to be deferred to channel media send, got %q", ch.dismissedChatID)
t.Fatalf(
"expected tracked tool feedback cleanup to be deferred to channel media send, got %q",
ch.dismissedChatID,
)
}
}

View file

@ -109,13 +109,6 @@ func successResponseWithMessageID(t *testing.T, messageID int) *ta.Response {
return &ta.Response{Ok: true, Result: b}
}
func successBoolResponse(t *testing.T) *ta.Response {
t.Helper()
b, err := json.Marshal(true)
require.NoError(t, err)
return &ta.Response{Ok: true, Result: b}
}
func successUserResponse(t *testing.T, user *telego.User) *ta.Response {
t.Helper()
b, err := json.Marshal(user)

View file

@ -3,7 +3,10 @@ package utils
import "testing"
func TestFormatToolFeedbackMessage(t *testing.T) {
got := FormatToolFeedbackMessage("read_file", "I will read README.md first to confirm the current project structure.")
got := FormatToolFeedbackMessage(
"read_file",
"I will read README.md first to confirm the current project structure.",
)
want := "\U0001f527 `read_file`\nI will read README.md first to confirm the current project structure."
if got != want {
t.Fatalf("FormatToolFeedbackMessage() = %q, want %q", got, want)
@ -27,7 +30,10 @@ func TestFormatToolFeedbackMessage_EmptyToolNameOmitsToolLine(t *testing.T) {
}
func TestFitToolFeedbackMessage_TruncatesBodyWithinSingleMessage(t *testing.T) {
got := FitToolFeedbackMessage("\U0001f527 `read_file`\nRead README.md first to confirm the current project structure.", 40)
got := FitToolFeedbackMessage(
"\U0001f527 `read_file`\nRead README.md first to confirm the current project structure.",
40,
)
want := "\U0001f527 `read_file`\nRead README.md first to..."
if got != want {
t.Fatalf("FitToolFeedbackMessage() = %q, want %q", got, want)

View file

@ -700,7 +700,8 @@ func TestHandleGetSession_DoesNotExposeLegacyToolArgumentsWhenExplanationMissing
t.Fatalf("LoadConfig() error = %v", err)
}
cfg.Agents.Defaults.ToolFeedback.MaxArgsLength = 20
if err := config.SaveConfig(configPath, cfg); err != nil {
err = config.SaveConfig(configPath, cfg)
if err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
@ -712,7 +713,11 @@ func TestHandleGetSession_DoesNotExposeLegacyToolArgumentsWhenExplanationMissing
argsJSON := `{"path":"README.md","start_line":1,"end_line":10,"extra":"abcdefghijklmnopqrstuvwxyz"}`
sessionKey := picoSessionPrefix + "detail-tool-summary-legacy-args"
if err := store.AddFullMessage(nil, sessionKey, providers.Message{Role: "user", Content: "check file"}); err != nil {
if err := store.AddFullMessage(
nil,
sessionKey,
providers.Message{Role: "user", Content: "check file"},
); err != nil {
t.Fatalf("AddFullMessage(user) error = %v", err)
}
if err := store.AddFullMessage(nil, sessionKey, providers.Message{