Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions pkg/channels/weixin/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,53 @@ type syncCursorFile struct {
GetUpdatesBuf string `json:"get_updates_buf"`
}

func syncDirForHome(home string) string {
return filepath.Join(home, "channels", "weixin", "sync")
}

func probeSyncDirWritable(home string) error {
syncDir := syncDirForHome(home)
if err := os.MkdirAll(syncDir, 0o700); err != nil {
return err
}

probeFile, err := os.CreateTemp(syncDir, ".write-probe-*")
if err != nil {
return err
}
probePath := probeFile.Name()
if err := probeFile.Close(); err != nil {
_ = os.Remove(probePath)
return err
}
if err := os.Remove(probePath); err != nil {
return err
}
return nil
}

func picoclawHomeDir() string {
if home := os.Getenv(config.EnvHome); home != "" {
return home
}
userHome, _ := os.UserHomeDir()
return filepath.Join(userHome, ".picoclaw")

if userHome, err := os.UserHomeDir(); err == nil && userHome != "" {
home := filepath.Join(userHome, ".picoclaw")
if probeErr := probeSyncDirWritable(home); probeErr == nil {
return home
} else {
logger.WarnCF(
"weixin",
"Default picoclaw home is not writable; using temp directory for sync cursor",
map[string]any{
"path": home,
"error": probeErr.Error(),
},
)
}
}

return filepath.Join(os.TempDir(), "picoclaw")
}

func buildWeixinSyncBufPath(cfg config.WeixinConfig) string {
Expand Down
63 changes: 63 additions & 0 deletions pkg/channels/weixin/weixin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"errors"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"testing"
"time"

Expand Down Expand Up @@ -159,6 +161,67 @@ func TestBuildWeixinSyncBufPathUsesPicoclawHome(t *testing.T) {
}
}

func TestBuildWeixinSyncBufPathFallsBackWhenHomeIsUnusable(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("HOME handling differs on windows")
}

t.Setenv(config.EnvHome, "")
unusableHome := filepath.Join(t.TempDir(), "home-file")
if err := os.WriteFile(unusableHome, []byte("not-a-dir"), 0o600); err != nil {
t.Fatalf("WriteFile(unusableHome) error = %v", err)
}
t.Setenv("HOME", unusableHome)

wxCfg := config.WeixinConfig{
BaseURL: "https://ilinkai.weixin.qq.com/",
}
wxCfg.SetToken("token-123")
got := buildWeixinSyncBufPath(wxCfg)
wantDir := filepath.Join(os.TempDir(), "picoclaw", "channels", "weixin", "sync")
if filepath.Dir(got) != wantDir {
t.Fatalf("sync path dir = %q, want %q", filepath.Dir(got), wantDir)
}
}

func TestBuildWeixinSyncBufPathFallsBackWhenSyncDirUnwritable(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("permission mode behavior differs on windows")
}
if os.Geteuid() == 0 {
t.Skip("root can usually bypass directory write permissions")
}

t.Setenv(config.EnvHome, "")
home := t.TempDir()
t.Setenv("HOME", home)

syncDir := filepath.Join(home, ".picoclaw", "channels", "weixin", "sync")
if err := os.MkdirAll(syncDir, 0o700); err != nil {
t.Fatalf("MkdirAll(syncDir) error = %v", err)
}
if err := os.Chmod(syncDir, 0o500); err != nil {
t.Fatalf("Chmod(syncDir, 0500) error = %v", err)
}
t.Cleanup(func() { _ = os.Chmod(syncDir, 0o700) })

if probeFile, err := os.CreateTemp(syncDir, ".probe-*"); err == nil {
_ = probeFile.Close()
_ = os.Remove(probeFile.Name())
t.Skip("environment does not enforce non-writable directory permissions")
}

wxCfg := config.WeixinConfig{
BaseURL: "https://ilinkai.weixin.qq.com/",
}
wxCfg.SetToken("token-123")
got := buildWeixinSyncBufPath(wxCfg)
wantDir := filepath.Join(os.TempDir(), "picoclaw", "channels", "weixin", "sync")
if filepath.Dir(got) != wantDir {
t.Fatalf("sync path dir = %q, want %q", filepath.Dir(got), wantDir)
}
}

func TestSessionPauseGuard(t *testing.T) {
ch := &WeixinChannel{
typingCache: make(map[string]typingTicketCacheEntry),
Expand Down
Loading