summaryrefslogtreecommitdiff
path: root/cmd/internal/hexai-action/main_test.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-07 11:26:10 +0300
committerPaul Buetow <paul@buetow.org>2025-09-07 11:26:10 +0300
commit8889949ad3851bfbf36ff5b73128286d67c88201 (patch)
tree0f515ae6ee3da898dea113799c09e943f3e3f8fb /cmd/internal/hexai-action/main_test.go
parent7c0266e94378f6121719939c6d53915eb72eed3e (diff)
tiding up
Diffstat (limited to 'cmd/internal/hexai-action/main_test.go')
-rw-r--r--cmd/internal/hexai-action/main_test.go357
1 files changed, 334 insertions, 23 deletions
diff --git a/cmd/internal/hexai-action/main_test.go b/cmd/internal/hexai-action/main_test.go
index 9603826..16bb2ed 100644
--- a/cmd/internal/hexai-action/main_test.go
+++ b/cmd/internal/hexai-action/main_test.go
@@ -1,44 +1,355 @@
package main
import (
+ "context"
+ "fmt"
"io"
"os"
"path/filepath"
+ "strings"
"testing"
+
+ "codeberg.org/snonux/hexai/internal/tmux"
)
-// TestOpenIO_InOutFiles verifies that openIO opens the specified files
-// and that writing via the returned writer persists to disk.
-func TestOpenIO_InOutFiles(t *testing.T) {
+func TestShouldRunInTmux_Preferences(t *testing.T) {
+ // no-tmux overrides
+ if shouldRunInTmux(false, true) {
+ t.Fatal("expected false when no-tmux is set")
+ }
+ // force tmux overrides
+ if !shouldRunInTmux(true, false) {
+ t.Fatal("expected true when -tmux is set")
+ }
+}
+
+func TestShouldRunInTmux_Auto(t *testing.T) {
+ oldIsTTY := isTTYFn
+ oldAvail := tmuxAvailableFn
+ t.Cleanup(func() { isTTYFn = oldIsTTY; tmuxAvailableFn = oldAvail })
+ // Simulate Helix :pipe (no TTY) and tmux available
+ isTTYFn = func(_ uintptr) bool { return false }
+ tmuxAvailableFn = func() bool { return true }
+ if !shouldRunInTmux(false, false) {
+ t.Fatal("expected true when not TTY and tmux available")
+ }
+ // Simulate TTY present: prefer inline
+ isTTYFn = func(_ uintptr) bool { return true }
+ if shouldRunInTmux(false, false) {
+ t.Fatal("expected false when TTY present")
+ }
+ // Simulate tmux not available
+ isTTYFn = func(_ uintptr) bool { return false }
+ tmuxAvailableFn = func() bool { return false }
+ if shouldRunInTmux(false, false) {
+ t.Fatal("expected false when tmux unavailable")
+ }
+}
+
+func TestPersistStdin_WritesFile(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "in.txt")
+ // Point os.Stdin to a temp file with content
+ src := filepath.Join(dir, "src.txt")
+ if err := os.WriteFile(src, []byte("hello world"), 0o600); err != nil {
+ t.Fatalf("write src: %v", err)
+ }
+ f, err := os.Open(src)
+ if err != nil { t.Fatalf("open src: %v", err) }
+ old := os.Stdin
+ os.Stdin = f
+ t.Cleanup(func() { os.Stdin = old; _ = f.Close() })
+ if err := persistStdin(path); err != nil {
+ t.Fatalf("persistStdin error: %v", err)
+ }
+ b, err := os.ReadFile(path)
+ if err != nil { t.Fatalf("read out: %v", err) }
+ if string(b) != "hello world" {
+ t.Fatalf("unexpected content %q", string(b))
+ }
+}
+
+func TestEchoThrough(t *testing.T) {
+ dir := t.TempDir()
+ in := filepath.Join(dir, "in.txt")
+ out := filepath.Join(dir, "out.txt")
+ if err := os.WriteFile(in, []byte("hello"), 0o600); err != nil {
+ t.Fatalf("write in: %v", err)
+ }
+ if err := echoThrough(in, out); err != nil {
+ t.Fatalf("echoThrough: %v", err)
+ }
+ b, _ := os.ReadFile(out)
+ if string(b) != "hello" {
+ t.Fatalf("unexpected: %q", string(b))
+ }
+}
+
+func TestEchoThrough_StdinStdout(t *testing.T) {
+ // set stdin
+ rIn, wIn, _ := os.Pipe()
+ _, _ = wIn.Write([]byte("PIPE"))
+ _ = wIn.Close()
+ oldIn := os.Stdin
+ os.Stdin = rIn
+ defer func() { os.Stdin = oldIn; _ = rIn.Close() }()
+ // capture stdout
+ r, w, _ := os.Pipe()
+ oldOut := os.Stdout
+ os.Stdout = w
+ defer func() { os.Stdout = oldOut; _ = r.Close(); _ = w.Close() }()
+ if err := echoThrough("", ""); err != nil { t.Fatalf("echoThrough: %v", err) }
+ _ = w.Close()
+ data, _ := io.ReadAll(r)
+ if string(data) != "PIPE" {
+ t.Fatalf("stdout: %q", string(data))
+ }
+}
+
+func TestWaitForFile(t *testing.T) {
dir := t.TempDir()
- inPath := filepath.Join(dir, "in.txt")
- outPath := filepath.Join(dir, "out.txt")
+ p := filepath.Join(dir, "x")
+ go func() {
+ // create shortly after
+ f, _ := os.Create(p)
+ defer f.Close()
+ f.WriteString("ok")
+ }()
+ if err := waitForFile(p, 2_000_000_000); err != nil { // 2s
+ t.Fatalf("waitForFile: %v", err)
+ }
+}
- // Prepare input file
- want := "hello world"
- if err := os.WriteFile(inPath, []byte(want), 0o600); err != nil {
- t.Fatalf("write infile: %v", err)
+func TestCatFileToStdout(t *testing.T) {
+ dir := t.TempDir()
+ p := filepath.Join(dir, "f")
+ if err := os.WriteFile(p, []byte("abc"), 0o600); err != nil {
+ t.Fatalf("write: %v", err)
+ }
+ // capture stdout
+ old := os.Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+ defer func() { os.Stdout = old; _ = r.Close(); _ = w.Close() }()
+ if err := catFileToStdout(p); err != nil {
+ t.Fatalf("catFileToStdout: %v", err)
+ }
+ _ = w.Close()
+ buf, _ := io.ReadAll(r)
+ if string(buf) != "abc" {
+ t.Fatalf("stdout = %q", string(buf))
}
+}
+
+func TestRunInTmuxParent_Stubbed(t *testing.T) {
+ dir := t.TempDir()
+ // set stdin content
+ src := filepath.Join(dir, "stdin.txt")
+ _ = os.WriteFile(src, []byte("input"), 0o600)
+ f, _ := os.Open(src)
+ oldStdin := os.Stdin
+ os.Stdin = f
+ defer func() { os.Stdin = oldStdin; _ = f.Close() }()
- in, out, cin, cout, err := openIO(inPath, outPath)
- if err != nil {
- t.Fatalf("openIO: %v", err)
+ // capture stdout
+ oldStdout := os.Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+ defer func() { os.Stdout = oldStdout; _ = r.Close(); _ = w.Close() }()
+
+ // stub seams
+ oldExec := osExecutableFn
+ oldSplit := splitRunFn
+ oldRun := hexaiactionRun
+ osExecutableFn = func() (string, error) { return "/bin/hexai-action", nil }
+ splitRunFn = func(opts tmux.SplitOpts, argv []string) error {
+ // find -outfile path and write content to simulate child
+ for i := 0; i < len(argv)-1; i++ {
+ if argv[i] == "-outfile" && i+1 < len(argv) {
+ _ = os.WriteFile(argv[i+1], []byte("OUT:"+strings.Join(argv, ",")), 0o600)
+ break
+ }
+ }
+ return nil
+ }
+ // Ensure child mode won't try to run the real TUI if invoked in tests.
+ hexaiactionRun = func(_ context.Context, _ io.Reader, w io.Writer, _ io.Writer) error {
+ _, _ = io.WriteString(w, "child-stub")
+ return nil
}
- defer cin()
- defer cout()
+ defer func() { osExecutableFn = oldExec; splitRunFn = oldSplit; hexaiactionRun = oldRun }()
- // Copy through to simulate main's behavior
- if _, err := io.Copy(out.(io.Writer), in); err != nil {
- t.Fatalf("copy: %v", err)
+ if err := runInTmuxParent("", "v", 33); err != nil {
+ t.Fatalf("runInTmuxParent: %v", err)
}
+ _ = w.Close()
+ got, _ := io.ReadAll(r)
+ if !strings.HasPrefix(string(got), "OUT:") {
+ t.Fatalf("unexpected stdout: %q", string(got))
+ }
+}
- // Verify outfile content
- got, err := os.ReadFile(outPath)
- if err != nil {
- t.Fatalf("read outfile: %v", err)
+func TestRunChild_StubbedOutfile(t *testing.T) {
+ dir := t.TempDir()
+ in := filepath.Join(dir, "in.txt")
+ out := filepath.Join(dir, "out.txt")
+ _ = os.WriteFile(in, []byte("sel"), 0o600)
+ old := hexaiactionRun
+ hexaiactionRun = func(_ context.Context, _ io.Reader, w io.Writer, _ io.Writer) error {
+ _, _ = io.WriteString(w, "RESULT")
+ return nil
}
- if string(got) != want {
- t.Fatalf("mismatch: got %q want %q", string(got), want)
+ defer func() { hexaiactionRun = old }()
+ if err := runChild(in, out); err != nil {
+ t.Fatalf("runChild: %v", err)
+ }
+ b, _ := os.ReadFile(out)
+ if string(b) != "RESULT" {
+ t.Fatalf("unexpected outfile: %q", string(b))
}
}
+func TestRunChild_StubbedStdout(t *testing.T) {
+ dir := t.TempDir()
+ in := filepath.Join(dir, "in.txt")
+ _ = os.WriteFile(in, []byte("sel"), 0o600)
+ // capture stdout
+ oldStdout := os.Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+ defer func() { os.Stdout = oldStdout; _ = r.Close(); _ = w.Close() }()
+ old := hexaiactionRun
+ hexaiactionRun = func(_ context.Context, _ io.Reader, w io.Writer, _ io.Writer) error {
+ _, _ = io.WriteString(w, "STDOUT-RESULT")
+ return nil
+ }
+ defer func() { hexaiactionRun = old }()
+ if err := runChild(in, ""); err != nil {
+ t.Fatalf("runChild: %v", err)
+ }
+ _ = w.Close()
+ data, _ := io.ReadAll(r)
+ if string(data) != "STDOUT-RESULT" {
+ t.Fatalf("stdout: %q", string(data))
+ }
+}
+
+func TestRunChild_ErrorFallback(t *testing.T) {
+ dir := t.TempDir()
+ in := filepath.Join(dir, "in.txt")
+ out := filepath.Join(dir, "out.txt")
+ _ = os.WriteFile(in, []byte("INPUT"), 0o600)
+ old := hexaiactionRun
+ hexaiactionRun = func(_ context.Context, _ io.Reader, _ io.Writer, _ io.Writer) error {
+ return fmt.Errorf("boom")
+ }
+ defer func() { hexaiactionRun = old }()
+ if err := runChild(in, out); err != nil {
+ t.Fatalf("runChild: %v", err)
+ }
+ b, _ := os.ReadFile(out)
+ if string(b) != "INPUT" {
+ t.Fatalf("expected fallback echo, got %q", string(b))
+ }
+}
+
+func TestWaitForFile_Timeout(t *testing.T) {
+ dir := t.TempDir()
+ p := filepath.Join(dir, "nope")
+ if err := waitForFile(p, 10_000_000); err == nil { // 10ms
+ t.Fatal("expected timeout error")
+ }
+}
+
+func TestOpenIO_InfileOutfile(t *testing.T) {
+ dir := t.TempDir()
+ in := filepath.Join(dir, "i")
+ out := filepath.Join(dir, "o")
+ _ = os.WriteFile(in, []byte("X"), 0o600)
+ r, w, ci, co, err := openIO(in, out)
+ if err != nil { t.Fatalf("openIO: %v", err) }
+ defer ci(); defer co()
+ if _, err := io.Copy(w, r); err != nil { t.Fatalf("copy: %v", err) }
+ b, _ := os.ReadFile(out)
+ if string(b) != "X" { t.Fatalf("got %q", string(b)) }
+}
+
+func TestRunInTmuxParent_ExecutableError(t *testing.T) {
+ old := osExecutableFn
+ osExecutableFn = func() (string, error) { return "", fmt.Errorf("no exe") }
+ defer func() { osExecutableFn = old }()
+ // set stdin content
+ r, w, _ := os.Pipe()
+ _, _ = w.Write([]byte("x"))
+ _ = w.Close()
+ oldIn := os.Stdin
+ os.Stdin = r
+ defer func() { os.Stdin = oldIn; _ = r.Close() }()
+ if err := runInTmuxParent("", "v", 33); err == nil {
+ t.Fatal("expected error from missing executable")
+ }
+}
+
+func TestRunInTmuxParent_SplitError(t *testing.T) {
+ oldExec := osExecutableFn
+ osExecutableFn = func() (string, error) { return "/bin/hexai-action", nil }
+ oldSplit := splitRunFn
+ splitRunFn = func(_ tmux.SplitOpts, _ []string) error { return fmt.Errorf("split failed") }
+ defer func() { osExecutableFn = oldExec; splitRunFn = oldSplit }()
+ // set stdin
+ r, w, _ := os.Pipe()
+ _, _ = w.Write([]byte("x"))
+ _ = w.Close()
+ oldIn := os.Stdin
+ os.Stdin = r
+ defer func() { os.Stdin = oldIn; _ = r.Close() }()
+ if err := runInTmuxParent("", "v", 33); err == nil {
+ t.Fatal("expected split error")
+ }
+}
+
+func TestEchoThrough_OutfileError(t *testing.T) {
+ dir := t.TempDir()
+ in := filepath.Join(dir, "i.txt")
+ _ = os.WriteFile(in, []byte("x"), 0o600)
+ // Outfile inside non-existent subdir -> Create should fail
+ out := filepath.Join(dir, "nope", "out.txt")
+ if err := echoThrough(in, out); err == nil {
+ t.Fatal("expected echoThrough outfile error")
+ }
+}
+
+func TestPersistStdin_Error(t *testing.T) {
+ // Parent directory missing -> Create should fail
+ dir := t.TempDir()
+ p := filepath.Join(dir, "missing", "x.txt")
+ // set stdin to something
+ r, w, _ := os.Pipe()
+ _, _ = w.Write([]byte("x"))
+ _ = w.Close()
+ old := os.Stdin
+ os.Stdin = r
+ defer func() { os.Stdin = old; _ = r.Close() }()
+ if err := persistStdin(p); err == nil {
+ t.Fatal("expected persistStdin error")
+ }
+}
+
+func TestCatFileToStdout_Error(t *testing.T) {
+ if err := catFileToStdout("/nonexistent/path/file.txt"); err == nil {
+ t.Fatal("expected error for missing file")
+ }
+}
+
+func TestOpenIO_Errors(t *testing.T) {
+ // Non-existent infile
+ if _, _, _, _, err := openIO("/definitely/missing/file.txt", ""); err == nil {
+ t.Fatal("expected infile error")
+ }
+ // Outfile in missing dir
+ dir := t.TempDir()
+ out := filepath.Join(dir, "nope", "x.txt")
+ if _, _, _, _, err := openIO("", out); err == nil {
+ t.Fatal("expected outfile error")
+ }
+}