package keylog import ( "os" "path/filepath" "strings" "testing" "time" ) func TestHourBucket(t *testing.T) { t.Parallel() when := time.Date(2026, 8, 28, 13, 45, 0, 0, time.FixedZone("EEST", 3*3600)) if got := HourBucket(when); got != "2026-08-28-13" { t.Fatalf("HourBucket() = %q, want 2026-08-28-13", got) } } func TestLogFilename(t *testing.T) { t.Parallel() if got := LogFilename("2026-08-28-13"); got != "2026-08-28-13.log" { t.Fatalf("LogFilename() = %q", got) } } func TestValidLogFilename(t *testing.T) { t.Parallel() cases := map[string]bool{ "2026-08-28-13.log": true, "2026-01-01-00.log": true, "../2026-08-28-13.log": true, "notes.log": false, "2026-08-28.log": false, "2026-08-28-13.txt": false, "": false, } for name, want := range cases { if got := ValidLogFilename(name); got != want { t.Fatalf("ValidLogFilename(%q) = %v, want %v", name, got, want) } } } func TestSectionHeader(t *testing.T) { t.Parallel() got := sectionHeader("injected", "Notepad") want := "[injected · Notepad]\n" if got != want { t.Fatalf("sectionHeader() = %q, want %q", got, want) } } func TestWriterTranscript(t *testing.T) { dir := t.TempDir() writer, err := NewWriter(dir) if err != nil { t.Fatalf("NewWriter: %v", err) } t.Cleanup(func() { _ = writer.Close() }) when := time.Date(2026, 8, 28, 13, 0, 0, 0, time.Local) events := []Event{ {Time: when, Window: "Cursor - main.go", Text: "hello"}, {Time: when, Window: "Cursor - main.go", Text: " world"}, {Time: when, Window: "Notepad", Injected: true, Text: "ai "}, {Time: when, Window: "Notepad", Injected: true, Text: "typed"}, } for _, event := range events { if err := writer.Write(event); err != nil { t.Fatalf("Write: %v", err) } } if err := writer.Close(); err != nil { t.Fatalf("Close: %v", err) } data, err := os.ReadFile(filepath.Join(dir, LogFilename(HourBucket(when)))) if err != nil { t.Fatalf("read file: %v", err) } text := string(data) wantParts := []string{ "[user · Cursor - main.go]", "hello world", "[injected · Notepad]", "ai typed", } for _, part := range wantParts { if !strings.Contains(text, part) { t.Fatalf("file = %q, missing %q", text, part) } } if strings.Count(text, "[user · Cursor - main.go]") != 1 { t.Fatalf("expected one user section header, got %q", text) } } func TestWriterRotation(t *testing.T) { dir := t.TempDir() writer, err := NewWriter(dir) if err != nil { t.Fatalf("NewWriter: %v", err) } t.Cleanup(func() { _ = writer.Close() }) zone := time.FixedZone("EEST", 3*3600) first := time.Date(2026, 8, 28, 13, 59, 0, 0, zone) second := time.Date(2026, 8, 28, 14, 0, 0, 0, zone) if err := writer.Write(Event{Time: first, Window: "Notepad", Text: "a"}); err != nil { t.Fatalf("Write first: %v", err) } if err := writer.Write(Event{Time: second, Window: "Notepad", Text: "b"}); err != nil { t.Fatalf("Write second: %v", err) } if err := writer.Close(); err != nil { t.Fatalf("Close: %v", err) } firstData, err := os.ReadFile(filepath.Join(dir, "2026-08-28-13.log")) if err != nil { t.Fatalf("read first file: %v", err) } secondData, err := os.ReadFile(filepath.Join(dir, "2026-08-28-14.log")) if err != nil { t.Fatalf("read second file: %v", err) } if !strings.Contains(string(firstData), "a") { t.Fatalf("first file = %q", firstData) } if !strings.Contains(string(secondData), "b") { t.Fatalf("second file = %q", secondData) } } func TestPruneOldLogs(t *testing.T) { dir := t.TempDir() oldPath := filepath.Join(dir, "2020-01-01-12.log") newPath := filepath.Join(dir, "2026-08-28-13.log") if err := os.WriteFile(oldPath, []byte("old"), 0o600); err != nil { t.Fatalf("write old: %v", err) } if err := os.WriteFile(newPath, []byte("new"), 0o600); err != nil { t.Fatalf("write new: %v", err) } oldTime := time.Now().AddDate(0, 0, -30) if err := os.Chtimes(oldPath, oldTime, oldTime); err != nil { t.Fatalf("Chtimes old: %v", err) } if err := PruneOldLogs(dir, 7); err != nil { t.Fatalf("PruneOldLogs: %v", err) } if _, err := os.Stat(oldPath); !os.IsNotExist(err) { t.Fatalf("old file still present") } if _, err := os.Stat(newPath); err != nil { t.Fatalf("new file missing: %v", err) } } func TestOpenValidation(t *testing.T) { if _, _, err := Open("bad-name.log"); err != os.ErrInvalid { t.Fatalf("Open bad-name.log = %v, want ErrInvalid", err) } }