From 75f636f9bac240088878d55c8d0f6fcb31553c2d Mon Sep 17 00:00:00 2001 From: Micheal Wilkinson Date: Fri, 20 Mar 2026 15:26:50 +0000 Subject: [PATCH] test(rc): add failing tests for Rc command --- internal/homesick/core/rc_test.go | 190 ++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 internal/homesick/core/rc_test.go diff --git a/internal/homesick/core/rc_test.go b/internal/homesick/core/rc_test.go new file mode 100644 index 0000000..89bff37 --- /dev/null +++ b/internal/homesick/core/rc_test.go @@ -0,0 +1,190 @@ +package core_test + +import ( + "bytes" + "io" + "os" + "path/filepath" + "testing" + + "git.hrafn.xyz/aether/gosick/internal/homesick/core" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type RcSuite struct { + suite.Suite + tmpDir string + homeDir string + reposDir string + stdout *bytes.Buffer + stderr *bytes.Buffer + app *core.App +} + +func TestRcSuite(t *testing.T) { + suite.Run(t, new(RcSuite)) +} + +func (s *RcSuite) SetupTest() { + s.tmpDir = s.T().TempDir() + s.homeDir = filepath.Join(s.tmpDir, "home") + s.reposDir = filepath.Join(s.homeDir, ".homesick", "repos") + require.NoError(s.T(), os.MkdirAll(s.reposDir, 0o755)) + + s.stdout = &bytes.Buffer{} + s.stderr = &bytes.Buffer{} + s.app = &core.App{ + HomeDir: s.homeDir, + ReposDir: s.reposDir, + Stdout: s.stdout, + Stderr: s.stderr, + } +} + +func (s *RcSuite) createCastle(name string) string { + castleRoot := filepath.Join(s.reposDir, name) + require.NoError(s.T(), os.MkdirAll(castleRoot, 0o755)) + return castleRoot +} + +var _ io.Writer + +// TestRc_UnknownCastleReturnsError ensures Rc returns an error when the +// castle directory does not exist. +func (s *RcSuite) TestRc_UnknownCastleReturnsError() { + err := s.app.Rc("nonexistent") + require.Error(s.T(), err) +} + +// TestRc_NoScriptsAndNoHomesickrc is a no-op when neither .homesick.d nor +// .homesickrc are present. +func (s *RcSuite) TestRc_NoScriptsAndNoHomesickrcIsNoop() { + s.createCastle("dotfiles") + require.NoError(s.T(), s.app.Rc("dotfiles")) +} + +// TestRc_ExecutesScriptsInSortedOrder verifies that executable scripts inside +// .homesick.d are run in lexicographic (sorted) order. +func (s *RcSuite) TestRc_ExecutesScriptsInSortedOrder() { + castleRoot := s.createCastle("dotfiles") + homesickD := filepath.Join(castleRoot, ".homesick.d") + require.NoError(s.T(), os.MkdirAll(homesickD, 0o755)) + + orderFile := filepath.Join(s.tmpDir, "order.txt") + scriptA := filepath.Join(homesickD, "10_a.sh") + scriptB := filepath.Join(homesickD, "20_b.sh") + + require.NoError(s.T(), os.WriteFile(scriptA, []byte("#!/bin/sh\necho a >> "+orderFile+"\n"), 0o755)) + require.NoError(s.T(), os.WriteFile(scriptB, []byte("#!/bin/sh\necho b >> "+orderFile+"\n"), 0o755)) + + require.NoError(s.T(), s.app.Rc("dotfiles")) + + content, err := os.ReadFile(orderFile) + require.NoError(s.T(), err) + require.Equal(s.T(), "a\nb\n", string(content)) +} + +// TestRc_SkipsNonExecutableFiles ensures that files without the executable bit +// are not run. +func (s *RcSuite) TestRc_SkipsNonExecutableFiles() { + castleRoot := s.createCastle("dotfiles") + homesickD := filepath.Join(castleRoot, ".homesick.d") + require.NoError(s.T(), os.MkdirAll(homesickD, 0o755)) + + notExec := filepath.Join(homesickD, "10_script.sh") + // Write a script that would exit 1 if actually run — verify it is skipped. + require.NoError(s.T(), os.WriteFile(notExec, []byte("#!/bin/sh\nexit 1\n"), 0o644)) + + require.NoError(s.T(), s.app.Rc("dotfiles")) +} + +// TestRc_HomesickrcCreatesRubyWrapper verifies that a .homesickrc file causes +// a Ruby wrapper to be written into .homesick.d before execution. +func (s *RcSuite) TestRc_HomesickrcCreatesRubyWrapper() { + castleRoot := s.createCastle("dotfiles") + homesickRc := filepath.Join(castleRoot, ".homesickrc") + require.NoError(s.T(), os.WriteFile(homesickRc, []byte("# ruby setup code\n"), 0o644)) + + require.NoError(s.T(), s.app.Rc("dotfiles")) + + wrapperPath := filepath.Join(castleRoot, ".homesick.d", "00_homesickrc.rb") + require.FileExists(s.T(), wrapperPath) + + info, err := os.Stat(wrapperPath) + require.NoError(s.T(), err) + require.NotZero(s.T(), info.Mode()&0o111, "wrapper must be executable") + + content, err := os.ReadFile(wrapperPath) + require.NoError(s.T(), err) + require.Contains(s.T(), string(content), ".homesickrc") +} + +// TestRc_HomesickrcWrapperRunsBeforeOtherScripts ensures the wrapper file +// (00_homesickrc.rb) sorts before typical user scripts and is present in +// .homesick.d after Rc returns. +func (s *RcSuite) TestRc_HomesickrcWrapperCreatedBeforeExecution() { + castleRoot := s.createCastle("dotfiles") + homesickRc := filepath.Join(castleRoot, ".homesickrc") + require.NoError(s.T(), os.WriteFile(homesickRc, []byte("# ruby setup code\n"), 0o644)) + + homesickD := filepath.Join(castleRoot, ".homesick.d") + require.NoError(s.T(), os.MkdirAll(homesickD, 0o755)) + + // A sentinel script that records whether the wrapper already exists. + orderFile := filepath.Join(s.tmpDir, "check.txt") + sentinel := filepath.Join(homesickD, "50_check.sh") + wrapperPath := filepath.Join(homesickD, "00_homesickrc.rb") + require.NoError(s.T(), os.WriteFile(sentinel, []byte( + "#!/bin/sh\n[ -f "+wrapperPath+" ] && echo present >> "+orderFile+"\n", + ), 0o755)) + + require.NoError(s.T(), s.app.Rc("dotfiles")) + + content, err := os.ReadFile(orderFile) + require.NoError(s.T(), err) + require.Equal(s.T(), "present\n", string(content)) +} + +// TestRc_FailingScriptReturnsError ensures that a non-zero exit from a script +// propagates as an error. +func (s *RcSuite) TestRc_FailingScriptReturnsError() { + castleRoot := s.createCastle("dotfiles") + homesickD := filepath.Join(castleRoot, ".homesick.d") + require.NoError(s.T(), os.MkdirAll(homesickD, 0o755)) + + failing := filepath.Join(homesickD, "10_fail.sh") + require.NoError(s.T(), os.WriteFile(failing, []byte("#!/bin/sh\nexit 42\n"), 0o755)) + + err := s.app.Rc("dotfiles") + require.Error(s.T(), err) +} + +// TestRc_ScriptOutputForwarded verifies that stdout and stderr from scripts +// are forwarded to the App's writers. +func (s *RcSuite) TestRc_ScriptOutputForwarded() { + castleRoot := s.createCastle("dotfiles") + homesickD := filepath.Join(castleRoot, ".homesick.d") + require.NoError(s.T(), os.MkdirAll(homesickD, 0o755)) + + script := filepath.Join(homesickD, "10_output.sh") + require.NoError(s.T(), os.WriteFile(script, []byte("#!/bin/sh\necho hello\necho world >&2\n"), 0o755)) + + require.NoError(s.T(), s.app.Rc("dotfiles")) + require.Contains(s.T(), s.stdout.String(), "hello") + require.Contains(s.T(), s.stderr.String(), "world") +} + +// TestRc_ScriptsRunWithCwdSetToCastleRoot verifies scripts execute with the +// castle root as the working directory. +func (s *RcSuite) TestRc_ScriptsRunWithCwdSetToCastleRoot() { + castleRoot := s.createCastle("dotfiles") + homesickD := filepath.Join(castleRoot, ".homesick.d") + require.NoError(s.T(), os.MkdirAll(homesickD, 0o755)) + + script := filepath.Join(homesickD, "10_pwd.sh") + require.NoError(s.T(), os.WriteFile(script, []byte("#!/bin/sh\npwd\n"), 0o755)) + + require.NoError(s.T(), s.app.Rc("dotfiles")) + require.Contains(s.T(), s.stdout.String(), castleRoot) +}