Add keylogging functionality to the agent. Implement keylog start/stop, file listing, and download API endpoints. Update web interface to display keystroke logs and allow file downloads.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user