167 lines
4.4 KiB
Go
167 lines
4.4 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|