▍ humdrum codex / ticktock v0.0.2
license AGPL-3.0
2.7 KB raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package autotrack

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

// TestEncodeLineByteParityWithPython pins the exact byte shape of a real line
// from ~/.local/share/ticktock/activity-2026-07-07.jsonl written by the
// Python daemon (json.dumps ", "/": " separators, key order, no escaping).
func TestEncodeLineByteParityWithPython(t *testing.T) {
	r := Record{
		Start: time.Date(2026, 7, 7, 8, 59, 19, 0, time.Local),
		End:   time.Date(2026, 7, 7, 9, 0, 9, 0, time.Local),
		Secs:  49, App: "Trace",
	}
	want := `{"start": "2026-07-07T08:59:19", "end": "2026-07-07T09:00:09", "secs": 49, "app": "Trace", "title": "", "url": "", "domain": "", "idle": false}`
	if got := r.EncodeLine(); got != want {
		t.Errorf("byte mismatch:\n got %s\nwant %s", got, want)
	}
}

func TestEncodeLineNoHTMLEscaping(t *testing.T) {
	r := Record{
		Start: time.Date(2026, 7, 7, 10, 0, 0, 0, time.Local),
		End:   time.Date(2026, 7, 7, 10, 1, 0, 0, time.Local),
		Secs:  60, App: "Safari", Title: `R&D <notes> — café "x"`,
		URL: "https://example.com/a?b=1&c=2", Domain: "example.com",
	}
	got := r.EncodeLine()
	if !strings.Contains(got, `"title": "R&D <notes> — café \"x\""`) {
		t.Errorf("ensure_ascii=False parity broken: %s", got)
	}
	if !strings.Contains(got, `"url": "https://example.com/a?b=1&c=2"`) {
		t.Errorf("url should not be HTML-escaped: %s", got)
	}
}

func TestWriteSegmentDropsShortAndAppends(t *testing.T) {
	dir := filepath.Join(t.TempDir(), "data") // must be created by WriteSegment
	start := time.Date(2026, 7, 7, 9, 0, 0, 0, time.Local)

	short := &Segment{Ctx: Ctx{App: "Blip"}, Start: start, End: start.Add(10 * time.Second)}
	if err := WriteSegment(dir, short, 15); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(LogPath(dir, start)); !os.IsNotExist(err) {
		t.Fatal("sub-minSecs segment must be dropped (no file written)")
	}

	long := &Segment{Ctx: Ctx{App: "Trace", Title: "doc"}, Start: start, End: start.Add(50 * time.Second)}
	if err := WriteSegment(dir, long, 15); err != nil {
		t.Fatal(err)
	}
	if err := WriteSegment(dir, long, 15); err != nil { // append, not truncate
		t.Fatal(err)
	}
	b, err := os.ReadFile(LogPath(dir, start))
	if err != nil {
		t.Fatal(err)
	}
	lines := strings.Split(strings.TrimRight(string(b), "\n"), "\n")
	if len(lines) != 2 {
		t.Fatalf("want 2 appended lines, got %d: %q", len(lines), string(b))
	}
	want := `{"start": "2026-07-07T09:00:00", "end": "2026-07-07T09:00:50", "secs": 50, "app": "Trace", "title": "doc", "url": "", "domain": "", "idle": false}`
	if lines[0] != want {
		t.Errorf("line mismatch:\n got %s\nwant %s", lines[0], want)
	}
	if err := WriteSegment(dir, nil, 15); err != nil {
		t.Errorf("nil segment must be a no-op, got %v", err)
	}
}