summaryrefslogtreecommitdiff
path: root/internal/tmuxedit/history_test.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-10 09:52:34 +0200
committerPaul Buetow <paul@buetow.org>2026-02-10 09:52:34 +0200
commitec745129258ae800065e302a2a40b54488cbca08 (patch)
treec18bb5dafdcab806b3d8bb0912d083b28741d24c /internal/tmuxedit/history_test.go
parent1b62f0bddc639a9b30ff95ea2326e52f9b1e7528 (diff)
Add tmux popup history storage and consolidate state files to XDG_STATE_HOME
- Add StateDir() helper function respecting XDG_STATE_HOME (~/.local/state/hexai/) - Implement JSONL-based history storage for tmux popup submissions - New history.go with AppendHistory() and GetHistory() functions - Store timestamp, agent name, cwd, and submitted text - Comprehensive unit tests for history functionality - Integrate history append into tmux edit workflow after successful submission - Move logs from /tmp/ to persistent state directory: - hexai-lsp.log: ~/.local/state/hexai/hexai-lsp.log - hexai-tmux-edit.log: ~/.local/state/hexai/hexai-tmux-edit.log - Update README.md with File Locations section documenting XDG directories - Fix pre-existing test failures by isolating project config in unit tests - Panic on state directory creation failure instead of silent fallback All unit tests pass. Follows XDG Base Directory Specification for proper state file management with persistence across reboots. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal/tmuxedit/history_test.go')
-rw-r--r--internal/tmuxedit/history_test.go196
1 files changed, 196 insertions, 0 deletions
diff --git a/internal/tmuxedit/history_test.go b/internal/tmuxedit/history_test.go
new file mode 100644
index 0000000..8a3d8af
--- /dev/null
+++ b/internal/tmuxedit/history_test.go
@@ -0,0 +1,196 @@
+package tmuxedit
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func TestAppendHistory(t *testing.T) {
+ // Create temp directory for test
+ tmpDir := t.TempDir()
+ t.Setenv("XDG_STATE_HOME", tmpDir)
+
+ text := "test prompt text"
+ agent := "claude"
+ cwd := "/tmp/test"
+
+ // Append first entry
+ if err := AppendHistory(text, agent, cwd); err != nil {
+ t.Fatalf("AppendHistory failed: %v", err)
+ }
+
+ // Verify file was created
+ historyPath := filepath.Join(tmpDir, "hexai", "tmux-edit-history.jsonl")
+ if _, err := os.Stat(historyPath); err != nil {
+ t.Fatalf("history file not created: %v", err)
+ }
+
+ // Read and verify content
+ data, err := os.ReadFile(historyPath)
+ if err != nil {
+ t.Fatalf("cannot read history file: %v", err)
+ }
+
+ content := string(data)
+ if content == "" {
+ t.Fatal("history file is empty")
+ }
+
+ // Verify it contains expected fields
+ if !containsString(content, "test prompt text") {
+ t.Error("history doesn't contain text")
+ }
+ if !containsString(content, "claude") {
+ t.Error("history doesn't contain agent")
+ }
+ if !containsString(content, "/tmp/test") {
+ t.Error("history doesn't contain cwd")
+ }
+}
+
+func TestGetHistory(t *testing.T) {
+ tmpDir := t.TempDir()
+ t.Setenv("XDG_STATE_HOME", tmpDir)
+
+ // Append multiple entries
+ entries := []struct {
+ text string
+ agent string
+ cwd string
+ }{
+ {"first prompt", "claude", "/home/user"},
+ {"second prompt", "aider", "/tmp/project"},
+ {"third prompt", "claude", "/var/tmp"},
+ }
+
+ for _, e := range entries {
+ if err := AppendHistory(e.text, e.agent, e.cwd); err != nil {
+ t.Fatalf("AppendHistory failed: %v", err)
+ }
+ time.Sleep(10 * time.Millisecond) // Ensure different timestamps
+ }
+
+ // Get all history
+ history, err := GetHistory(0)
+ if err != nil {
+ t.Fatalf("GetHistory failed: %v", err)
+ }
+
+ if len(history) != 3 {
+ t.Fatalf("expected 3 entries, got %d", len(history))
+ }
+
+ // Verify first entry
+ if history[0].Text != "first prompt" {
+ t.Errorf("first entry text: got %q, want %q", history[0].Text, "first prompt")
+ }
+ if history[0].Agent != "claude" {
+ t.Errorf("first entry agent: got %q, want %q", history[0].Agent, "claude")
+ }
+
+ // Test limit
+ limited, err := GetHistory(2)
+ if err != nil {
+ t.Fatalf("GetHistory with limit failed: %v", err)
+ }
+ if len(limited) != 2 {
+ t.Fatalf("expected 2 entries with limit, got %d", len(limited))
+ }
+
+ // Should get the most recent 2
+ if limited[0].Text != "second prompt" {
+ t.Errorf("limited[0] should be second entry")
+ }
+ if limited[1].Text != "third prompt" {
+ t.Errorf("limited[1] should be third entry")
+ }
+}
+
+func TestGetHistory_EmptyFile(t *testing.T) {
+ tmpDir := t.TempDir()
+ t.Setenv("XDG_STATE_HOME", tmpDir)
+
+ // Get history when file doesn't exist
+ history, err := GetHistory(0)
+ if err != nil {
+ t.Fatalf("GetHistory should not error on missing file: %v", err)
+ }
+
+ if len(history) != 0 {
+ t.Errorf("expected empty history, got %d entries", len(history))
+ }
+}
+
+func TestSplitLines(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want []string
+ }{
+ {
+ name: "unix newlines",
+ input: "line1\nline2\nline3",
+ want: []string{"line1", "line2", "line3"},
+ },
+ {
+ name: "windows newlines",
+ input: "line1\r\nline2\r\nline3",
+ want: []string{"line1", "line2", "line3"},
+ },
+ {
+ name: "mixed newlines",
+ input: "line1\nline2\r\nline3",
+ want: []string{"line1", "line2", "line3"},
+ },
+ {
+ name: "trailing newline",
+ input: "line1\nline2\n",
+ want: []string{"line1", "line2"},
+ },
+ {
+ name: "empty string",
+ input: "",
+ want: []string{},
+ },
+ {
+ name: "single line no newline",
+ input: "single",
+ want: []string{"single"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := splitLines([]byte(tt.input))
+ gotStr := make([]string, len(got))
+ for i, b := range got {
+ gotStr[i] = string(b)
+ }
+
+ if len(gotStr) != len(tt.want) {
+ t.Fatalf("got %d lines, want %d", len(gotStr), len(tt.want))
+ }
+
+ for i := range gotStr {
+ if gotStr[i] != tt.want[i] {
+ t.Errorf("line %d: got %q, want %q", i, gotStr[i], tt.want[i])
+ }
+ }
+ })
+ }
+}
+
+func containsString(s, substr string) bool {
+ return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && findSubstring(s, substr))
+}
+
+func findSubstring(s, substr string) bool {
+ for i := 0; i <= len(s)-len(substr); i++ {
+ if s[i:i+len(substr)] == substr {
+ return true
+ }
+ }
+ return false
+}