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) } }