Files
gastown/internal/cmd/account_test.go
Johann Dirry 3d5a66f850 Fixing unit tests on windows (#813)
* Add Windows stub for orphan cleanup

* Fix account switch tests on Windows

* Make query session events test portable

* Disable beads daemon in query session events test

* Add Windows bd stubs for sling tests

* Make expandOutputPath test OS-agnostic

* Make role_agents test Windows-friendly

* Make config path tests OS-agnostic

* Make HealthCheckStateFile test OS-agnostic

* Skip orphan process check on Windows

* Normalize sparse checkout detail paths

* Make dog path tests OS-agnostic

* Fix bare repo refspec config on Windows

* Add Windows process detection for locks

* Add Windows CI workflow

* Make mail path tests OS-agnostic

* Skip plugin file mode test on Windows

* Skip tmux-dependent polecat tests on Windows

* Normalize polecat paths and AGENTS.md content

* Make beads init failure test Windows-friendly

* Skip rig agent bead init test on Windows

* Make XDG path tests OS-agnostic

* Make exec tests portable on Windows

* Adjust atomic write tests for Windows

* Make wisp tests Windows-friendly

* Make workspace find tests OS-agnostic

* Fix Windows rig add integration test

* Make sling var logging Windows-friendly

* Fix sling attached molecule update ordering

---------

Co-authored-by: Johann Dirry <johann.dirry@microsea.at>
2026-01-20 14:17:35 -08:00

314 lines
8.7 KiB
Go

package cmd
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/config"
)
// setupTestTownForAccount creates a minimal Gas Town workspace with accounts.
func setupTestTownForAccount(t *testing.T) (townRoot string, accountsDir string) {
t.Helper()
townRoot = t.TempDir()
// Create mayor directory with required files
mayorDir := filepath.Join(townRoot, "mayor")
if err := os.MkdirAll(mayorDir, 0755); err != nil {
t.Fatalf("mkdir mayor: %v", err)
}
// Create town.json
townConfig := &config.TownConfig{
Type: "town",
Version: config.CurrentTownVersion,
Name: "test-town",
PublicName: "Test Town",
CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
}
townConfigPath := filepath.Join(mayorDir, "town.json")
if err := config.SaveTownConfig(townConfigPath, townConfig); err != nil {
t.Fatalf("save town.json: %v", err)
}
// Create empty rigs.json
rigsConfig := &config.RigsConfig{
Version: 1,
Rigs: make(map[string]config.RigEntry),
}
rigsPath := filepath.Join(mayorDir, "rigs.json")
if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
t.Fatalf("save rigs.json: %v", err)
}
// Create accounts directory
accountsDir = filepath.Join(t.TempDir(), "claude-accounts")
if err := os.MkdirAll(accountsDir, 0755); err != nil {
t.Fatalf("mkdir accounts: %v", err)
}
return townRoot, accountsDir
}
func setTestHome(t *testing.T, fakeHome string) {
t.Helper()
t.Setenv("HOME", fakeHome)
if runtime.GOOS != "windows" {
return
}
t.Setenv("USERPROFILE", fakeHome)
drive := filepath.VolumeName(fakeHome)
if drive == "" {
return
}
t.Setenv("HOMEDRIVE", drive)
t.Setenv("HOMEPATH", strings.TrimPrefix(fakeHome, drive))
}
func TestAccountSwitch(t *testing.T) {
t.Run("switch between accounts", func(t *testing.T) {
townRoot, accountsDir := setupTestTownForAccount(t)
// Create fake home directory for ~/.claude
fakeHome := t.TempDir()
setTestHome(t, fakeHome)
// Create account config directories
workConfigDir := filepath.Join(accountsDir, "work")
personalConfigDir := filepath.Join(accountsDir, "personal")
if err := os.MkdirAll(workConfigDir, 0755); err != nil {
t.Fatalf("mkdir work config: %v", err)
}
if err := os.MkdirAll(personalConfigDir, 0755); err != nil {
t.Fatalf("mkdir personal config: %v", err)
}
// Create accounts.json with two accounts
accountsPath := filepath.Join(townRoot, "mayor", "accounts.json")
accountsCfg := config.NewAccountsConfig()
accountsCfg.Accounts["work"] = config.Account{
Email: "steve@work.com",
ConfigDir: workConfigDir,
}
accountsCfg.Accounts["personal"] = config.Account{
Email: "steve@personal.com",
ConfigDir: personalConfigDir,
}
accountsCfg.Default = "work"
if err := config.SaveAccountsConfig(accountsPath, accountsCfg); err != nil {
t.Fatalf("save accounts.json: %v", err)
}
// Create initial symlink to work account
claudeDir := filepath.Join(fakeHome, ".claude")
if err := os.Symlink(workConfigDir, claudeDir); err != nil {
t.Fatalf("create symlink: %v", err)
}
// Change to town root
originalWd, _ := os.Getwd()
defer os.Chdir(originalWd)
if err := os.Chdir(townRoot); err != nil {
t.Fatalf("chdir: %v", err)
}
// Run switch to personal
cmd := &cobra.Command{}
err := runAccountSwitch(cmd, []string{"personal"})
if err != nil {
t.Fatalf("runAccountSwitch failed: %v", err)
}
// Verify symlink points to personal
target, err := os.Readlink(claudeDir)
if err != nil {
t.Fatalf("readlink: %v", err)
}
if target != personalConfigDir {
t.Errorf("symlink target = %q, want %q", target, personalConfigDir)
}
// Verify default was updated
loadedCfg, err := config.LoadAccountsConfig(accountsPath)
if err != nil {
t.Fatalf("load accounts: %v", err)
}
if loadedCfg.Default != "personal" {
t.Errorf("default = %q, want 'personal'", loadedCfg.Default)
}
})
t.Run("already on target account", func(t *testing.T) {
townRoot, accountsDir := setupTestTownForAccount(t)
fakeHome := t.TempDir()
setTestHome(t, fakeHome)
workConfigDir := filepath.Join(accountsDir, "work")
if err := os.MkdirAll(workConfigDir, 0755); err != nil {
t.Fatalf("mkdir work config: %v", err)
}
accountsPath := filepath.Join(townRoot, "mayor", "accounts.json")
accountsCfg := config.NewAccountsConfig()
accountsCfg.Accounts["work"] = config.Account{
Email: "steve@work.com",
ConfigDir: workConfigDir,
}
accountsCfg.Default = "work"
if err := config.SaveAccountsConfig(accountsPath, accountsCfg); err != nil {
t.Fatalf("save accounts.json: %v", err)
}
// Create symlink already pointing to work
claudeDir := filepath.Join(fakeHome, ".claude")
if err := os.Symlink(workConfigDir, claudeDir); err != nil {
t.Fatalf("create symlink: %v", err)
}
originalWd, _ := os.Getwd()
defer os.Chdir(originalWd)
if err := os.Chdir(townRoot); err != nil {
t.Fatalf("chdir: %v", err)
}
// Switch to work (should be no-op)
cmd := &cobra.Command{}
err := runAccountSwitch(cmd, []string{"work"})
if err != nil {
t.Fatalf("runAccountSwitch failed: %v", err)
}
// Symlink should still point to work
target, err := os.Readlink(claudeDir)
if err != nil {
t.Fatalf("readlink: %v", err)
}
if target != workConfigDir {
t.Errorf("symlink target = %q, want %q", target, workConfigDir)
}
})
t.Run("nonexistent account", func(t *testing.T) {
townRoot, accountsDir := setupTestTownForAccount(t)
fakeHome := t.TempDir()
setTestHome(t, fakeHome)
workConfigDir := filepath.Join(accountsDir, "work")
if err := os.MkdirAll(workConfigDir, 0755); err != nil {
t.Fatalf("mkdir work config: %v", err)
}
accountsPath := filepath.Join(townRoot, "mayor", "accounts.json")
accountsCfg := config.NewAccountsConfig()
accountsCfg.Accounts["work"] = config.Account{
Email: "steve@work.com",
ConfigDir: workConfigDir,
}
accountsCfg.Default = "work"
if err := config.SaveAccountsConfig(accountsPath, accountsCfg); err != nil {
t.Fatalf("save accounts.json: %v", err)
}
originalWd, _ := os.Getwd()
defer os.Chdir(originalWd)
if err := os.Chdir(townRoot); err != nil {
t.Fatalf("chdir: %v", err)
}
// Switch to nonexistent account
cmd := &cobra.Command{}
err := runAccountSwitch(cmd, []string{"nonexistent"})
if err == nil {
t.Fatal("expected error for nonexistent account")
}
})
t.Run("real directory gets moved", func(t *testing.T) {
townRoot, accountsDir := setupTestTownForAccount(t)
fakeHome := t.TempDir()
setTestHome(t, fakeHome)
workConfigDir := filepath.Join(accountsDir, "work")
personalConfigDir := filepath.Join(accountsDir, "personal")
// Don't create workConfigDir - it will be created by moving ~/.claude
if err := os.MkdirAll(personalConfigDir, 0755); err != nil {
t.Fatalf("mkdir personal config: %v", err)
}
accountsPath := filepath.Join(townRoot, "mayor", "accounts.json")
accountsCfg := config.NewAccountsConfig()
accountsCfg.Accounts["work"] = config.Account{
Email: "steve@work.com",
ConfigDir: workConfigDir,
}
accountsCfg.Accounts["personal"] = config.Account{
Email: "steve@personal.com",
ConfigDir: personalConfigDir,
}
accountsCfg.Default = "work"
if err := config.SaveAccountsConfig(accountsPath, accountsCfg); err != nil {
t.Fatalf("save accounts.json: %v", err)
}
// Create ~/.claude as a real directory with a marker file
claudeDir := filepath.Join(fakeHome, ".claude")
if err := os.MkdirAll(claudeDir, 0755); err != nil {
t.Fatalf("mkdir .claude: %v", err)
}
markerFile := filepath.Join(claudeDir, "marker.txt")
if err := os.WriteFile(markerFile, []byte("test"), 0644); err != nil {
t.Fatalf("write marker: %v", err)
}
originalWd, _ := os.Getwd()
defer os.Chdir(originalWd)
if err := os.Chdir(townRoot); err != nil {
t.Fatalf("chdir: %v", err)
}
// Switch to personal
cmd := &cobra.Command{}
err := runAccountSwitch(cmd, []string{"personal"})
if err != nil {
t.Fatalf("runAccountSwitch failed: %v", err)
}
// Verify ~/.claude is now a symlink to personal
fileInfo, err := os.Lstat(claudeDir)
if err != nil {
t.Fatalf("lstat .claude: %v", err)
}
if fileInfo.Mode()&os.ModeSymlink == 0 {
t.Error("~/.claude is not a symlink")
}
target, err := os.Readlink(claudeDir)
if err != nil {
t.Fatalf("readlink: %v", err)
}
if target != personalConfigDir {
t.Errorf("symlink target = %q, want %q", target, personalConfigDir)
}
// Verify original content was moved to work config dir
movedMarker := filepath.Join(workConfigDir, "marker.txt")
if _, err := os.Stat(movedMarker); err != nil {
t.Errorf("marker file not moved to work config dir: %v", err)
}
})
}