diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-22 09:40:38 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-22 09:40:38 +0200 |
| commit | f78a185364ee24bc8c46d4aa6cc96d705faab326 (patch) | |
| tree | c0944b0ba4a1069abe36271dd4090bd705c99173 /internal/shell/shell_test.go | |
| parent | a76ed355954712e945b2f5da5929ca4c10dea0d2 (diff) | |
Implement shell package with readline vi mode and tab completion (task 358)
Replaces the stub in internal/shell/shell.go with a full implementation
backed by github.com/ergochat/readline. The Shell struct wraps a readline
instance configured with vi mode, a 500-entry in-memory history limit, and
a custom prefixCompleter that delegates tab expansion to a caller-supplied
function — mirroring the Ruby CLI#setup_readline / Readline.completion_proc
pattern. History deduplication (skip empty lines and consecutive duplicate
entries) matches the Ruby shell_loop behaviour by disabling auto-save and
calling SaveToHistory only for non-empty, non-duplicate lines. A package-
level ReadPassword helper handles PIN entry before the Shell is created.
Tests skip gracefully when stdin is not a TTY.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/shell/shell_test.go')
| -rw-r--r-- | internal/shell/shell_test.go | 78 |
1 files changed, 78 insertions, 0 deletions
diff --git a/internal/shell/shell_test.go b/internal/shell/shell_test.go new file mode 100644 index 0000000..19491e3 --- /dev/null +++ b/internal/shell/shell_test.go @@ -0,0 +1,78 @@ +// Package shell_test exercises the public API of the shell package. +// Because Shell requires a real TTY for readline to initialise, tests that +// construct a Shell instance are skipped automatically when running in a +// non-interactive environment (e.g. CI pipelines). +package shell_test + +import ( + "os" + "testing" + + "codeberg.org/snonux/geheim/internal/shell" +) + +// isTTY returns true when stdin is connected to an actual terminal. +// readline.NewFromConfig will still succeed without a TTY (it falls back to +// non-interactive mode), so we do not need to skip on that basis alone. +func isTTY() bool { + fi, err := os.Stdin.Stat() + if err != nil { + return false + } + return (fi.Mode() & os.ModeCharDevice) != 0 +} + +// TestNew verifies that New returns a non-nil Shell without an error when +// given a valid (no-op) completion function. +// The test is skipped when stdin is not a TTY, because readline may behave +// differently and the intent is to test real interactive initialisation. +func TestNew(t *testing.T) { + if !isTTY() { + t.Skip("skipping TestNew: stdin is not a TTY") + } + + completionFn := func(prefix string) []string { + // Return a fixed set of candidates for testing purposes. + all := []string{"add", "get", "list", "delete"} + var matches []string + for _, c := range all { + if len(c) >= len(prefix) && c[:len(prefix)] == prefix { + matches = append(matches, c) + } + } + return matches + } + + s, err := shell.New(completionFn) + if err != nil { + t.Fatalf("New() returned unexpected error: %v", err) + } + if s == nil { + t.Fatal("New() returned nil Shell") + } + + // Close must not panic or return an error. + s.Close() +} + +// TestNewNonTTY verifies that New does not panic and either succeeds or +// returns a meaningful error when stdin is not a terminal. +func TestNewNonTTY(t *testing.T) { + if isTTY() { + t.Skip("skipping TestNewNonTTY: stdin is a TTY, need non-TTY environment") + } + + completionFn := func(prefix string) []string { return nil } + + // We accept either success or failure here — the important thing is no panic. + s, err := shell.New(completionFn) + if err != nil { + // Non-TTY environments may legitimately fail; that is acceptable. + t.Logf("New() returned error in non-TTY environment (expected): %v", err) + return + } + if s == nil { + t.Fatal("New() returned nil Shell without an error") + } + s.Close() +} |
