▍ humdrum codex / ticktock v0.0.2
license AGPL-3.0
4.3 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
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package store

import (
	"fmt"
	"strings"
	"testing"
	"time"
)

// fakeRunner returns canned output per first-arg and records calls.
type fakeRunner struct {
	out   []byte
	err   error
	calls [][]string
}

func (f *fakeRunner) Run(name string, args ...string) ([]byte, error) {
	f.calls = append(f.calls, append([]string{name}, args...))
	return f.out, f.err
}

const dayJSON = `[
 {"description":"Admin: organize","project":"ARCHER","start_time":"2026-07-07T10:10:00-07:00","end_time":"2026-07-07T10:37:00-07:00","tags":[],"notes":"emails, folders","duration":"00:27:00"},
 {"description":"Admin","project":"ARCHER","start_time":"2026-07-07T09:01:00-07:00","end_time":"2026-07-07T09:51:00-07:00","tags":["emails"],"duration":"00:50:00"},
 {"description":"Standup","project":"ARCHER","start_time":"2026-07-07T09:51:00-07:00","end_time":"","tags":[],"duration":"00:00:00"}
]`

func TestDayKeysAreDateIndexOverAllEntriesSortedByStart(t *testing.T) {
	fr := &fakeRunner{out: []byte(dayJSON)}
	got, err := New(fr).Day("2026-07-07")
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 3 {
		t.Fatalf("want 3 entries, got %d", len(got))
	}
	// sorted by start: 09:01 (Admin), 09:51 (Standup, running), 10:10 (organize)
	wantKeys := []string{"2026-07-07-01", "2026-07-07-02", "2026-07-07-03"}
	wantDesc := []string{"Admin", "Standup", "Admin: organize"}
	for i := range got {
		if got[i].Key != wantKeys[i] {
			t.Errorf("entry %d Key = %q, want %q", i, got[i].Key, wantKeys[i])
		}
		if got[i].Description != wantDesc[i] {
			t.Errorf("entry %d Desc = %q, want %q", i, got[i].Description, wantDesc[i])
		}
	}
	// The running entry (Standup) holds slot 02 — the TASK-028 alignment.
	if !got[1].Running() {
		t.Errorf("entry 02 should be running")
	}
	if got[0].Duration != 50*time.Minute {
		t.Errorf("Admin duration = %v, want 50m", got[0].Duration)
	}
	if len(got[0].Tags) != 1 || got[0].Tags[0] != "emails" {
		t.Errorf("Admin tags = %v, want [emails]", got[0].Tags)
	}
}

func TestDayEmptyIsNoError(t *testing.T) {
	got, err := New(&fakeRunner{out: []byte("")}).Day("2026-07-07")
	if err != nil || got != nil {
		t.Fatalf("empty output: got (%v, %v), want (nil, nil)", got, err)
	}
}

func mkEntry() Entry {
	st := time.Date(2026, 7, 7, 9, 1, 0, 0, time.Local)
	et := time.Date(2026, 7, 7, 9, 51, 0, 0, time.Local)
	return Entry{Start: st, End: et, Project: "ARCHER", Description: "Admin",
		Tags: []string{"emails", ""}, Notes: "cleared inbox"}
}

func TestAddArgv(t *testing.T) {
	fr := &fakeRunner{}
	if err := New(fr).Add(mkEntry()); err != nil {
		t.Fatal(err)
	}
	got := strings.Join(fr.calls[0], " ")
	want := "tock add -p ARCHER -d Admin -s 2026-07-07 09:01 -e 2026-07-07 09:51 --tag emails --note cleared inbox"
	if got != want {
		t.Fatalf("argv:\n got=%q\nwant=%q", got, want)
	}
}

func TestSafeReplaceHappyPath(t *testing.T) {
	fr := &fakeRunner{}
	up := mkEntry()
	up.Description = "Admin: sorted"
	if err := New(fr).SafeReplace("2026-07-07-01", up, mkEntry()); err != nil {
		t.Fatal(err)
	}
	if len(fr.calls) != 2 {
		t.Fatalf("want 2 calls (remove, add), got %d", len(fr.calls))
	}
	if fr.calls[0][1] != "remove" || fr.calls[0][2] != "2026-07-07-01" {
		t.Errorf("first call not remove of the key: %v", fr.calls[0])
	}
	if fr.calls[1][1] != "add" {
		t.Errorf("second call should be add: %v", fr.calls[1])
	}
}

// restoreRunner fails the Nth "add" (0-based among add calls).
type restoreRunner struct {
	calls   [][]string
	failAdd int
	adds    int
}

func (r *restoreRunner) Run(name string, args ...string) ([]byte, error) {
	r.calls = append(r.calls, append([]string{name}, args...))
	if len(args) > 0 && args[0] == "add" {
		r.adds++
		if r.adds-1 == r.failAdd {
			return nil, fmt.Errorf("boom")
		}
	}
	return nil, nil
}

func TestSafeReplaceRestoresOriginalOnAddFailure(t *testing.T) {
	rr := &restoreRunner{failAdd: 0} // first add (the updated one) fails
	orig := mkEntry()
	up := mkEntry()
	up.Description = "bad edit"
	err := New(rr).SafeReplace("2026-07-07-01", up, orig)
	if err == nil {
		t.Fatal("want error when the updated Add fails")
	}
	// calls: remove, add(updated→fail), add(original→restore)
	if len(rr.calls) != 3 {
		t.Fatalf("want 3 calls, got %d: %v", len(rr.calls), rr.calls)
	}
	last := strings.Join(rr.calls[2], " ")
	if !strings.Contains(last, "-d Admin ") {
		t.Errorf("original not restored; last call = %q", last)
	}
}