This commit is contained in:
zhaopeng 2026-02-15 23:28:09 +05:30 committed by GitHub
commit afddee7b90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 401 additions and 13 deletions

View file

@ -14,6 +14,7 @@ import (
"io" "io"
"io/fs" "io/fs"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -138,6 +139,8 @@ func main() {
migrateCmd() migrateCmd()
case "auth": case "auth":
authCmd() authCmd()
case "install":
installCmd()
case "cron": case "cron":
cronCmd() cronCmd()
case "skills": case "skills":
@ -207,6 +210,7 @@ func printHelp() {
fmt.Println(" agent Interact with the agent directly") fmt.Println(" agent Interact with the agent directly")
fmt.Println(" auth Manage authentication (login, logout, status)") fmt.Println(" auth Manage authentication (login, logout, status)")
fmt.Println(" gateway Start picoclaw gateway") fmt.Println(" gateway Start picoclaw gateway")
fmt.Println(" install Install/uninstall launchd service (macOS)")
fmt.Println(" status Show picoclaw status") fmt.Println(" status Show picoclaw status")
fmt.Println(" cron Manage scheduled tasks") fmt.Println(" cron Manage scheduled tasks")
fmt.Println(" migrate Migrate from OpenClaw to PicoClaw") fmt.Println(" migrate Migrate from OpenClaw to PicoClaw")
@ -1410,3 +1414,205 @@ func skillsShowCmd(loader *skills.SkillsLoader, skillName string) {
fmt.Println("----------------------") fmt.Println("----------------------")
fmt.Println(content) fmt.Println(content)
} }
// installCmd handles the install subcommand
func installCmd() {
if len(os.Args) < 3 {
doInstall()
return
}
switch os.Args[2] {
case "--help", "-h":
installHelp()
case "--uninstall":
uninstallInstall()
case "--status":
statusInstall()
default:
fmt.Printf("Unknown install option: %s\n", os.Args[2])
installHelp()
}
}
// installHelp displays help information for install command
func installHelp() {
fmt.Println("\nInstall PicoClaw as a macOS service (launch agent)")
fmt.Println()
fmt.Println("Usage: picoclaw install [options]")
fmt.Println()
fmt.Println("Options:")
fmt.Println(" --uninstall Uninstall the launch agent")
fmt.Println(" --status Show installation status")
fmt.Println(" --help, -h Show this help message")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" picoclaw install Install as launch agent")
fmt.Println(" picoclaw install --uninstall Uninstall the launch agent")
fmt.Println(" picoclaw install --status Check installation status")
}
// doInstall executes the installation
func doInstall() {
execPath, err := findExecutable()
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Found picoclaw at: %s\n", execPath)
// Generate plist content
plistContent := generatePlist(execPath)
// Get plist path
home, err := os.UserHomeDir()
if err != nil {
fmt.Printf("Error getting home directory: %v\n", err)
os.Exit(1)
}
plistDir := filepath.Join(home, "Library", "LaunchAgents")
plistPath := filepath.Join(plistDir, "io.picoclaw.gateway.plist")
// Create directory if not exists
if err := os.MkdirAll(plistDir, 0755); err != nil {
fmt.Printf("Error creating LaunchAgents directory: %v\n", err)
os.Exit(1)
}
// Write plist file
if err := os.WriteFile(plistPath, []byte(plistContent), 0644); err != nil {
fmt.Printf("Error writing plist file: %v\n", err)
os.Exit(1)
}
fmt.Printf("Created plist at: %s\n", plistPath)
// Load the launch agent
cmd := exec.Command("launchctl", "load", plistPath)
if output, err := cmd.CombinedOutput(); err != nil {
fmt.Printf("Error loading launch agent: %v\n", err)
fmt.Printf("Output: %s\n", string(output))
os.Exit(1)
}
fmt.Println("\n✓ PicoClaw gateway installed successfully!")
fmt.Println("The gateway will start automatically on login.")
}
// findExecutable finds the picoclaw executable path
func findExecutable() (string, error) {
// 使用 exec.LookPath 查找
path, err := exec.LookPath("picoclaw")
if err == nil {
return path, nil
}
// 如果不是 "not found" 错误,返回原错误
if !strings.Contains(err.Error(), "not found") {
return "", err
}
// 尝试常见路径
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("cannot find home directory: %w", err)
}
commonPaths := []string{
"/usr/local/bin/picoclaw",
"/usr/bin/picoclaw",
filepath.Join(home, "go", "bin", "picoclaw"),
}
for _, p := range commonPaths {
if _, err := os.Stat(p); err == nil {
return p, nil
}
}
return "", fmt.Errorf("picoclaw not found. Please run 'go install' first or ensure picoclaw is in your PATH")
}
// generatePlist generates the plist content for launch agent
func generatePlist(execPath string) string {
currentPath := os.Getenv("PATH")
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>io.picoclaw.gateway</string>
<key>ProgramArguments</key>
<array>
<string>%s</string>
<string>gateway</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>%s</string>
</dict>
</dict>
</plist>`, execPath, currentPath)
}
// uninstallInstall uninstalls the launch agent
func uninstallInstall() {
home, _ := os.UserHomeDir()
plistPath := filepath.Join(home, "Library", "LaunchAgents", "io.picoclaw.gateway.plist")
if _, err := os.Stat(plistPath); os.IsNotExist(err) {
fmt.Println("PicoClaw is not installed.")
return
}
// Unload the launch agent
cmd := exec.Command("launchctl", "unload", plistPath)
if output, err := cmd.CombinedOutput(); err != nil {
fmt.Printf("Error unloading launch agent: %v\n", err)
fmt.Printf("Output: %s\n", string(output))
}
// Remove plist file
if err := os.Remove(plistPath); err != nil {
fmt.Printf("Error removing plist file: %v\n", err)
os.Exit(1)
}
fmt.Println("✓ PicoClaw gateway uninstalled successfully!")
}
// statusInstall shows the installation status
func statusInstall() {
home, _ := os.UserHomeDir()
plistPath := filepath.Join(home, "Library", "LaunchAgents", "io.picoclaw.gateway.plist")
fmt.Println("\nPicoClaw Gateway Status:")
fmt.Println("------------------------")
if _, err := os.Stat(plistPath); os.IsNotExist(err) {
fmt.Println("Status: Not installed")
fmt.Println("Run 'picoclaw install' to install as launch agent")
return
}
fmt.Println("Status: Installed")
fmt.Printf("Plist: %s\n", plistPath)
// Check if loaded
cmd := exec.Command("launchctl", "list", "io.picoclaw.gateway")
output, err := cmd.CombinedOutput()
if err == nil {
fmt.Println("\nLaunch agent is loaded.")
if len(output) > 0 {
fmt.Println(string(output))
}
} else {
fmt.Println("\nLaunch agent is not running.")
}
}

7
go.mod
View file

@ -19,8 +19,6 @@ require (
golang.org/x/oauth2 v0.35.0 golang.org/x/oauth2 v0.35.0
) )
require ( require (
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
@ -28,9 +26,9 @@ require (
github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/github/copilot-sdk/go v0.1.23 github.com/github/copilot-sdk/go v0.1.23
github.com/google/jsonschema-go v0.4.2 // indirect github.com/go-resty/resty/v2 v2.17.2 // indirect
github.com/go-resty/resty/v2 v2.17.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/grbit/go-json v0.11.0 // indirect github.com/grbit/go-json v0.11.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
@ -47,5 +45,4 @@ require (
golang.org/x/net v0.50.0 // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
) )

6
go.sum
View file

@ -36,8 +36,8 @@ github.com/github/copilot-sdk/go v0.1.23 h1:uExtO/inZQndCZMiSAA1hvXINiz9tqo/MZgQ
github.com/github/copilot-sdk/go v0.1.23/go.mod h1:GdwwBfMbm9AABLEM3x5IZKw4ZfwCYxZ1BgyytmZenQ0= github.com/github/copilot-sdk/go v0.1.23/go.mod h1:GdwwBfMbm9AABLEM3x5IZKw4ZfwCYxZ1BgyytmZenQ0=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
@ -58,6 +58,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

View file

@ -29,6 +29,7 @@ type TelegramChannel struct {
transcriber *voice.GroqTranscriber transcriber *voice.GroqTranscriber
placeholders sync.Map // chatID -> messageID placeholders sync.Map // chatID -> messageID
stopThinking sync.Map // chatID -> thinkingCancel stopThinking sync.Map // chatID -> thinkingCancel
threadIDs sync.Map // chatIDStr -> MessageThreadID
} }
type thinkingCancel struct { type thinkingCancel struct {
@ -124,7 +125,7 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
return fmt.Errorf("telegram bot not running") return fmt.Errorf("telegram bot not running")
} }
chatID, err := parseChatID(msg.ChatID) chatID, threadID, err := parseTelegramChatID(msg.ChatID)
if err != nil { if err != nil {
return fmt.Errorf("invalid chat ID: %w", err) return fmt.Errorf("invalid chat ID: %w", err)
} }
@ -144,6 +145,7 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
c.placeholders.Delete(msg.ChatID) c.placeholders.Delete(msg.ChatID)
editMsg := tu.EditMessageText(tu.ID(chatID), pID.(int), htmlContent) editMsg := tu.EditMessageText(tu.ID(chatID), pID.(int), htmlContent)
editMsg.ParseMode = telego.ModeHTML editMsg.ParseMode = telego.ModeHTML
// Note: EditMessageText doesn't require MessageThreadID as it edits existing message
if _, err = c.bot.EditMessageText(ctx, editMsg); err == nil { if _, err = c.bot.EditMessageText(ctx, editMsg); err == nil {
return nil return nil
@ -154,6 +156,11 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
tgMsg := tu.Message(tu.ID(chatID), htmlContent) tgMsg := tu.Message(tu.ID(chatID), htmlContent)
tgMsg.ParseMode = telego.ModeHTML tgMsg.ParseMode = telego.ModeHTML
// Set thread ID if present
if threadID != 0 {
tgMsg.MessageThreadID = threadID
}
if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil { if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil {
logger.ErrorCF("telegram", "HTML parse failed, falling back to plain text", map[string]interface{}{ logger.ErrorCF("telegram", "HTML parse failed, falling back to plain text", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@ -195,6 +202,16 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
chatID := message.Chat.ID chatID := message.Chat.ID
c.chatIDs[senderID] = chatID c.chatIDs[senderID] = chatID
// Check for message thread (forum topic)
messageThreadID := message.MessageThreadID
chatIDStr := fmt.Sprintf("%d", chatID)
if messageThreadID != 0 {
// Store thread ID for later use
c.threadIDs.Store(chatIDStr, messageThreadID)
// Encode thread ID into chatID string (similar to Slack pattern)
chatIDStr = fmt.Sprintf("%d:%d", chatID, messageThreadID)
}
content := "" content := ""
mediaPaths := []string{} mediaPaths := []string{}
localFiles := []string{} // 跟踪需要清理的本地文件 localFiles := []string{} // 跟踪需要清理的本地文件
@ -301,11 +318,16 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
logger.DebugCF("telegram", "Received message", map[string]interface{}{ logger.DebugCF("telegram", "Received message", map[string]interface{}{
"sender_id": senderID, "sender_id": senderID,
"chat_id": fmt.Sprintf("%d", chatID), "chat_id": fmt.Sprintf("%d", chatID),
"thread_id": messageThreadID,
"preview": utils.Truncate(content, 50), "preview": utils.Truncate(content, 50),
}) })
// Thinking indicator // Thinking indicator - include thread ID if present
err := c.bot.SendChatAction(ctx, tu.ChatAction(tu.ID(chatID), telego.ChatActionTyping)) chatAction := tu.ChatAction(tu.ID(chatID), telego.ChatActionTyping)
if messageThreadID != 0 {
chatAction.MessageThreadID = messageThreadID
}
err := c.bot.SendChatAction(ctx, chatAction)
if err != nil { if err != nil {
logger.ErrorCF("telegram", "Failed to send chat action", map[string]interface{}{ logger.ErrorCF("telegram", "Failed to send chat action", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@ -313,7 +335,6 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
} }
// Stop any previous thinking animation // Stop any previous thinking animation
chatIDStr := fmt.Sprintf("%d", chatID)
if prevStop, ok := c.stopThinking.Load(chatIDStr); ok { if prevStop, ok := c.stopThinking.Load(chatIDStr); ok {
if cf, ok := prevStop.(*thinkingCancel); ok && cf != nil { if cf, ok := prevStop.(*thinkingCancel); ok && cf != nil {
cf.Cancel() cf.Cancel()
@ -324,7 +345,12 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
_, thinkCancel := context.WithTimeout(ctx, 5*time.Minute) _, thinkCancel := context.WithTimeout(ctx, 5*time.Minute)
c.stopThinking.Store(chatIDStr, &thinkingCancel{fn: thinkCancel}) c.stopThinking.Store(chatIDStr, &thinkingCancel{fn: thinkCancel})
pMsg, err := c.bot.SendMessage(ctx, tu.Message(tu.ID(chatID), "Thinking... 💭")) // Send "Thinking..." message - include thread ID if present
thinkingMsg := tu.Message(tu.ID(chatID), "Thinking... 💭")
if messageThreadID != 0 {
thinkingMsg.MessageThreadID = messageThreadID
}
pMsg, err := c.bot.SendMessage(ctx, thinkingMsg)
if err == nil { if err == nil {
pID := pMsg.MessageID pID := pMsg.MessageID
c.placeholders.Store(chatIDStr, pID) c.placeholders.Store(chatIDStr, pID)
@ -338,7 +364,12 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
"is_group": fmt.Sprintf("%t", message.Chat.Type != "private"), "is_group": fmt.Sprintf("%t", message.Chat.Type != "private"),
} }
c.HandleMessage(senderID, fmt.Sprintf("%d", chatID), content, mediaPaths, metadata) if messageThreadID != 0 {
metadata["message_thread_id"] = fmt.Sprintf("%d", messageThreadID)
metadata["is_topic_message"] = "true"
}
c.HandleMessage(senderID, chatIDStr, content, mediaPaths, metadata)
} }
func (c *TelegramChannel) downloadPhoto(ctx context.Context, fileID string) string { func (c *TelegramChannel) downloadPhoto(ctx context.Context, fileID string) string {
@ -386,6 +417,30 @@ func parseChatID(chatIDStr string) (int64, error) {
return id, err return id, err
} }
// parseTelegramChatID extracts chatID and threadID from a combined chatID string
// Format: "chatID" or "chatID:threadID"
func parseTelegramChatID(chatIDStr string) (chatID int64, threadID int, err error) {
parts := strings.SplitN(chatIDStr, ":", 2)
var id int64
_, err = fmt.Sscanf(parts[0], "%d", &id)
if err != nil {
return 0, 0, err
}
chatID = id
if len(parts) > 1 {
var tid int
_, err = fmt.Sscanf(parts[1], "%d", &tid)
if err != nil {
return chatID, 0, fmt.Errorf("invalid thread ID '%s': %w", parts[1], err)
}
threadID = tid
}
return chatID, threadID, nil
}
func markdownToTelegramHTML(text string) string { func markdownToTelegramHTML(text string) string {
if text == "" { if text == "" {
return "" return ""

View file

@ -0,0 +1,128 @@
package channels
import (
"testing"
)
func TestParseTelegramChatID(t *testing.T) {
tests := []struct {
name string
chatIDStr string
wantChatID int64
wantThreadID int
wantErr bool
}{
{
name: "chat only",
chatIDStr: "123456789",
wantChatID: 123456789,
wantThreadID: 0,
wantErr: false,
},
{
name: "negative chat ID (private chat)",
chatIDStr: "-987654321",
wantChatID: -987654321,
wantThreadID: 0,
wantErr: false,
},
{
name: "chat with thread",
chatIDStr: "123456789:42",
wantChatID: 123456789,
wantThreadID: 42,
wantErr: false,
},
{
name: "negative chat with thread",
chatIDStr: "-987654321:100",
wantChatID: -987654321,
wantThreadID: 100,
wantErr: false,
},
{
name: "invalid chat ID",
chatIDStr: "invalid",
wantChatID: 0,
wantThreadID: 0,
wantErr: true,
},
{
name: "invalid thread ID",
chatIDStr: "123456789:invalid",
wantChatID: 123456789,
wantThreadID: 0,
wantErr: true,
},
{
name: "empty string",
chatIDStr: "",
wantChatID: 0,
wantThreadID: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
chatID, threadID, err := parseTelegramChatID(tt.chatIDStr)
if (err != nil) != tt.wantErr {
t.Errorf("parseTelegramChatID(%q) error = %v, wantErr %v", tt.chatIDStr, err, tt.wantErr)
return
}
if !tt.wantErr {
if chatID != tt.wantChatID {
t.Errorf("parseTelegramChatID(%q) chatID = %d, want %d", tt.chatIDStr, chatID, tt.wantChatID)
}
if threadID != tt.wantThreadID {
t.Errorf("parseTelegramChatID(%q) threadID = %d, want %d", tt.chatIDStr, threadID, tt.wantThreadID)
}
}
})
}
}
func TestParseChatID(t *testing.T) {
tests := []struct {
name string
chatIDStr string
want int64
wantErr bool
}{
{
name: "positive chat ID",
chatIDStr: "123456789",
want: 123456789,
wantErr: false,
},
{
name: "negative chat ID",
chatIDStr: "-987654321",
want: -987654321,
wantErr: false,
},
{
name: "invalid chat ID",
chatIDStr: "invalid",
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseChatID(tt.chatIDStr)
if (err != nil) != tt.wantErr {
t.Errorf("parseChatID(%q) error = %v, wantErr %v", tt.chatIDStr, err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("parseChatID(%q) = %d, want %d", tt.chatIDStr, got, tt.want)
}
})
}
}