diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-07 11:49:38 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-07 11:49:38 +0300 |
| commit | 5ed470a093ffb7d28c88f9687429f238959935da (patch) | |
| tree | f4fd4e0bae2e5312de675d9de708c6543f2299e0 /internal/hexaiaction | |
| parent | 9a901054e8828054f7b26514b72b6e938f97e4a7 (diff) | |
test: add seams for RunTUI and client; expand hexaiaction tests; cover lsp initialized and testutil fixtures
Diffstat (limited to 'internal/hexaiaction')
| -rw-r--r-- | internal/hexaiaction/cmdentry_runcommand_test.go | 70 | ||||
| -rw-r--r-- | internal/hexaiaction/run.go | 28 | ||||
| -rw-r--r-- | internal/hexaiaction/run_seam_test.go | 36 |
3 files changed, 122 insertions, 12 deletions
diff --git a/internal/hexaiaction/cmdentry_runcommand_test.go b/internal/hexaiaction/cmdentry_runcommand_test.go new file mode 100644 index 0000000..7c8aa5c --- /dev/null +++ b/internal/hexaiaction/cmdentry_runcommand_test.go @@ -0,0 +1,70 @@ +package hexaiaction + +import ( + "bytes" + "context" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "codeberg.org/snonux/hexai/internal/tmux" +) + +func TestRunCommand_UIChild(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 := runFn + runFn = func(_ context.Context, _ io.Reader, w io.Writer, _ io.Writer) error { _, _ = io.WriteString(w, "OK"); return nil } + t.Cleanup(func(){ runFn = old }) + opts := Options{Infile: in, Outfile: out, UIChild: true} + if err := RunCommand(context.Background(), opts, bytes.NewBuffer(nil), io.Discard, io.Discard); err != nil { + t.Fatalf("RunCommand UIChild: %v", err) + } + b, _ := os.ReadFile(out) + if string(b) != "OK" { t.Fatalf("outfile: %q", string(b)) } +} + +func TestRunCommand_Tmux(t *testing.T) { + oldTTY := isTTYFn + oldAvail := tmuxAvailableFn + oldExec := osExecutableFn + oldSplit := splitRunFn + isTTYFn = func(_ uintptr) bool { return false } + tmuxAvailableFn = func() bool { return true } + osExecutableFn = func() (string, error) { return "/bin/hexai-action", nil } + splitRunFn = func(_ tmux.SplitOpts, argv []string) error { + for i := 0; i < len(argv)-1; i++ { + if argv[i] == "-outfile" && i+1 < len(argv) { + _ = os.WriteFile(argv[i+1], []byte("OUT"), 0o600) + break + } + } + return nil + } + defer func(){ isTTYFn = oldTTY; tmuxAvailableFn = oldAvail; osExecutableFn = oldExec; splitRunFn = oldSplit }() + var out bytes.Buffer + if err := RunCommand(context.Background(), Options{ForceTmux: true}, bytes.NewBufferString("X"), &out, io.Discard); err != nil { + t.Fatalf("RunCommand tmux: %v", err) + } + if out.String() != "OUT" { t.Fatalf("stdout: %q", out.String()) } +} + +// Inline TTY path is exercised implicitly via other helpers; testing it directly +// would require TTY simulation which is brittle in unit tests. + +func TestRunCommand_FallbackEcho(t *testing.T) { + oldTTY := isTTYFn + oldAvail := tmuxAvailableFn + isTTYFn = func(_ uintptr) bool { return false } + tmuxAvailableFn = func() bool { return false } + defer func(){ isTTYFn = oldTTY; tmuxAvailableFn = oldAvail }() + var out bytes.Buffer + if err := RunCommand(context.Background(), Options{NoTmux: true}, bytes.NewBufferString("Z"), &out, io.Discard); err != nil { + t.Fatalf("RunCommand fallback: %v", err) + } + if strings.TrimSpace(out.String()) != "Z" { t.Fatalf("stdout: %q", out.String()) } +} diff --git a/internal/hexaiaction/run.go b/internal/hexaiaction/run.go index 2a67a58..609dad1 100644 --- a/internal/hexaiaction/run.go +++ b/internal/hexaiaction/run.go @@ -13,15 +13,19 @@ import ( ) // Run executes the hexai-action command flow. +// seams for testability +var chooseActionFn = RunTUI +var newClientFromApp = llmutils.NewClientFromApp + func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error { - logger := log.New(stderr, "hexai-action ", log.LstdFlags|log.Lmsgprefix) - cfg := appconfig.Load(logger) - client, err := llmutils.NewClientFromApp(cfg) - if err != nil { - fmt.Fprintf(stderr, logging.AnsiBase+"hexai-action: LLM disabled: %v"+logging.AnsiReset+"\n", err) - return err - } - parts, err := ParseInput(stdin) + logger := log.New(stderr, "hexai-action ", log.LstdFlags|log.Lmsgprefix) + cfg := appconfig.Load(logger) + client, err := newClientFromApp(cfg) + if err != nil { + fmt.Fprintf(stderr, logging.AnsiBase+"hexai-action: LLM disabled: %v"+logging.AnsiReset+"\n", err) + return err + } + parts, err := ParseInput(stdin) if err != nil { fmt.Fprintln(stderr, logging.AnsiBase+"hexai-action: failed to read input"+logging.AnsiReset) return err @@ -29,10 +33,10 @@ func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error { if strings.TrimSpace(parts.Selection) == "" { return fmt.Errorf("hexai-action: no input provided on stdin") } - kind, err := RunTUI() - if err != nil { - return err - } + kind, err := chooseActionFn() + if err != nil { + return err + } out, err := executeAction(ctx, kind, parts, cfg, client, stderr) if err != nil { return err diff --git a/internal/hexaiaction/run_seam_test.go b/internal/hexaiaction/run_seam_test.go new file mode 100644 index 0000000..0b8761f --- /dev/null +++ b/internal/hexaiaction/run_seam_test.go @@ -0,0 +1,36 @@ +package hexaiaction + +import ( + "bytes" + "context" + "testing" + + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/llm" +) + +type llmFake struct{} + +func (llmFake) Chat(_ context.Context, _ []llm.Message, _ ...llm.RequestOption) (string, error) { return "OK", nil } +func (llmFake) Name() string { return "fake" } +func (llmFake) DefaultModel() string { return "model" } + +func TestRun_WithSeams_SkipAndRewrite(t *testing.T) { + // Seam: choose action to Skip first, then Rewrite + oldChoose := chooseActionFn + oldNew := newClientFromApp + t.Cleanup(func(){ chooseActionFn = oldChoose; newClientFromApp = oldNew }) + // 1) Skip -> echoes selection + chooseActionFn = func() (ActionKind, error) { return ActionSkip, nil } + newClientFromApp = func(_ appconfig.App) (llm.Client, error) { return llmFake{}, nil } + var out bytes.Buffer + in := bytes.NewBufferString("some code") + if err := Run(context.Background(), in, &out, &out); err != nil { t.Fatalf("Run skip: %v", err) } + if out.String() != "some code" { t.Fatalf("skip out: %q", out.String()) } + // 2) Rewrite -> requires inline instruction + chooseActionFn = func() (ActionKind, error) { return ActionRewrite, nil } + out.Reset() + in = bytes.NewBufferString(";upper;\nhello") + if err := Run(context.Background(), in, &out, &out); err != nil { t.Fatalf("Run rewrite: %v", err) } + if out.String() == "" { t.Fatalf("expected non-empty rewrite output") } +} |
