package main import ( "context" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/kami/maven/internal/ipc" ) type fakeCore struct { ipc.CoreAPI facts map[string]ipc.Fact // composite key "key|source" → Fact writeLog []ipc.WriteFactReq writeErr error readErr error } func (f *fakeCore) LatestFactBySource(_ context.Context, key, source string) (ipc.Fact, error) { if f.readErr != nil { return ipc.Fact{}, f.readErr } if f.facts == nil { return ipc.Fact{}, ipc.ErrNoFact } fk := key + "|" + source fact, ok := f.facts[fk] if !ok { return ipc.Fact{}, ipc.ErrNoFact } return fact, nil } func (f *fakeCore) WriteFact(_ context.Context, req ipc.WriteFactReq) (int64, error) { if f.writeErr != nil { return 0, f.writeErr } if f.facts == nil { f.facts = make(map[string]ipc.Fact) } fk := req.Key + "|" + req.Source f.facts[fk] = ipc.Fact{ Key: req.Key, Value: req.Value, Source: req.Source, } f.writeLog = append(f.writeLog, req) return int64(len(f.writeLog)), nil } // --------------------------------------------------------------------------- // Parsing tests // --------------------------------------------------------------------------- func TestParseICal(t *testing.T) { now := time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC) body := []byte(`BEGIN:VCALENDAR BEGIN:VEVENT DTSTART:20260703T090000Z DTEND:20260703T100000Z SUMMARY:Morning standup END:VEVENT BEGIN:VEVENT DTSTART:20260703T140000Z DTEND:20260703T150000Z SUMMARY:Team sync END:VEVENT BEGIN:VEVENT DTSTART:20260702T140000Z DTEND:20260702T150000Z SUMMARY:Yesterday retro END:VEVENT BEGIN:VEVENT DTSTART:20260704T090000Z DTEND:20260704T100000Z SUMMARY:Tomorrow standup END:VEVENT BEGIN:VEVENT DTSTART;VALUE=DATE:20260704 DTEND;VALUE=DATE:20260705 SUMMARY:All-day event END:VEVENT END:VCALENDAR`) events := parseICal(body, now) if len(events) != 2 { t.Fatalf("got %d events, want 2 (today events, no all-day/past/future)", len(events)) } // Morning standup — overlaps today. if events[0].summary != "Morning standup" { t.Errorf("events[0].summary = %q, want %q", events[0].summary, "Morning standup") } wantStart0 := time.Date(2026, 7, 3, 9, 0, 0, 0, time.UTC) if !events[0].start.Equal(wantStart0) { t.Errorf("events[0].start = %v, want %v", events[0].start, wantStart0) } wantEnd0 := time.Date(2026, 7, 3, 10, 0, 0, 0, time.UTC) if !events[0].end.Equal(wantEnd0) { t.Errorf("events[0].end = %v, want %v", events[0].end, wantEnd0) } // Team sync — overlaps today. if events[1].summary != "Team sync" { t.Errorf("events[1].summary = %q, want %q", events[1].summary, "Team sync") } wantStart1 := time.Date(2026, 7, 3, 14, 0, 0, 0, time.UTC) if !events[1].start.Equal(wantStart1) { t.Errorf("events[1].start = %v, want %v", events[1].start, wantStart1) } wantEnd1 := time.Date(2026, 7, 3, 15, 0, 0, 0, time.UTC) if !events[1].end.Equal(wantEnd1) { t.Errorf("events[1].end = %v, want %v", events[1].end, wantEnd1) } } func TestParseVEVENT(t *testing.T) { // Normal event with TZID in DTSTART and UTC DTEND. block := "DTSTART;TZID=Europe/Moscow:20260703T130000\nDTEND:20260703T140000Z\nSUMMARY:Stand up meeting" e := parseVEVENT(block) if e == nil { t.Fatal("expected non-nil icalEvent") } wantStart := time.Date(2026, 7, 3, 13, 0, 0, 0, time.UTC) if !e.start.Equal(wantStart) { t.Errorf("start = %v, want %v", e.start, wantStart) } wantEnd := time.Date(2026, 7, 3, 14, 0, 0, 0, time.UTC) if !e.end.Equal(wantEnd) { t.Errorf("end = %v, want %v", e.end, wantEnd) } if e.summary != "Stand up meeting" { t.Errorf("summary = %q, want %q", e.summary, "Stand up meeting") } // All-day event (VALUE=DATE) → nil. allDay := "DTSTART;VALUE=DATE:20260703\nDTEND;VALUE=DATE:20260704\nSUMMARY:All-day" if e2 := parseVEVENT(allDay); e2 != nil { t.Error("expected nil for all-day event") } } func TestParseDT(t *testing.T) { tests := []struct { name string line string want time.Time wantOK bool }{ { name: "UTC", line: "DTEND:20260703T100000Z", want: time.Date(2026, 7, 3, 10, 0, 0, 0, time.UTC), wantOK: true, }, { name: "local time", line: "DTSTART;TZID=Europe/Moscow:20260703T130000", want: time.Date(2026, 7, 3, 13, 0, 0, 0, time.UTC), wantOK: true, }, { name: "all-day", line: "DTSTART;VALUE=DATE:20260703", want: time.Time{}, wantOK: false, }, { name: "invalid", line: "DTSTART:garbage", want: time.Time{}, wantOK: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := parseDT(tt.line) if ok != tt.wantOK { t.Errorf("ok = %v, want %v", ok, tt.wantOK) } if !got.Equal(tt.want) { t.Errorf("got = %v, want %v", got, tt.want) } }) } } func TestSafeKey(t *testing.T) { tests := []struct { input string want string }{ {"Stand up meeting", "Stand-up-meeting"}, {"Hello_World", "Hello-World"}, {"special@#$chars!!", "specialchars"}, {"ALL_CAPS_123", "ALL-CAPS-123"}, } for _, tt := range tests { got := safeKey(tt.input) if got != tt.want { t.Errorf("safeKey(%q) = %q, want %q", tt.input, got, tt.want) } } } // --------------------------------------------------------------------------- // Core logic tests // --------------------------------------------------------------------------- func TestWriteIfChanged(t *testing.T) { ctx := context.Background() now := time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC) t.Run("no previous fact writes", func(t *testing.T) { fc := &fakeCore{} p := &poller{core: fc} err := p.writeIfChanged(ctx, "test_key", "poll:caldav", "hello", now) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(fc.writeLog) != 1 { t.Fatalf("expected 1 write, got %d", len(fc.writeLog)) } if fc.writeLog[0].Value != "hello" { t.Errorf("value = %q, want %q", fc.writeLog[0].Value, "hello") } if fc.writeLog[0].Key != "test_key" { t.Errorf("key = %q, want %q", fc.writeLog[0].Key, "test_key") } if fc.writeLog[0].Source != "poll:caldav" { t.Errorf("source = %q, want %q", fc.writeLog[0].Source, "poll:caldav") } if fc.writeLog[0].Kind != "env" { t.Errorf("kind = %q, want %q", fc.writeLog[0].Kind, "env") } }) t.Run("same value skips write", func(t *testing.T) { fc := &fakeCore{ facts: map[string]ipc.Fact{ "test_key|poll:caldav": {Value: "hello"}, }, } p := &poller{core: fc} err := p.writeIfChanged(ctx, "test_key", "poll:caldav", "hello", now) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(fc.writeLog) != 0 { t.Errorf("expected 0 writes, got %d", len(fc.writeLog)) } }) t.Run("different value writes", func(t *testing.T) { fc := &fakeCore{ facts: map[string]ipc.Fact{ "test_key|poll:caldav": {Value: "old"}, }, } p := &poller{core: fc} err := p.writeIfChanged(ctx, "test_key", "poll:caldav", "new", now) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(fc.writeLog) != 1 { t.Fatalf("expected 1 write, got %d", len(fc.writeLog)) } if fc.writeLog[0].Value != "new" { t.Errorf("value = %q, want %q", fc.writeLog[0].Value, "new") } }) t.Run("read error other than ErrNoFact returns error", func(t *testing.T) { fc := &fakeCore{readErr: fmt.Errorf("connection refused")} p := &poller{core: fc} err := p.writeIfChanged(ctx, "fail_key", "poll:caldav", "x", now) if err == nil { t.Fatal("expected error, got nil") } }) t.Run("write error returns error", func(t *testing.T) { fc := &fakeCore{ facts: map[string]ipc.Fact{}, writeErr: fmt.Errorf("disk full"), } p := &poller{core: fc} err := p.writeIfChanged(ctx, "test_key", "poll:caldav", "hello", now) if err == nil { t.Fatal("expected error, got nil") } }) } // --------------------------------------------------------------------------- // Poll cycle test // --------------------------------------------------------------------------- func TestPollOnce(t *testing.T) { now := time.Now().UTC() start := now.Add(-2 * time.Hour).Truncate(time.Second) end := now.Add(2 * time.Hour).Truncate(time.Second) ical := fmt.Sprintf("BEGIN:VCALENDAR\nBEGIN:VEVENT\nDTSTART:%sT%sZ\nDTEND:%sT%sZ\nSUMMARY:Current meeting\nEND:VEVENT\nEND:VCALENDAR", start.Format("20060102"), start.Format("150405"), end.Format("20060102"), end.Format("150405")) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(ical)) })) defer srv.Close() fc := &fakeCore{} p := &poller{ core: fc, http: srv.Client(), url: srv.URL, } p.pollOnce(context.Background()) if len(fc.writeLog) != 2 { t.Fatalf("expected 2 writes (calendar_busy + calendar_event), got %d", len(fc.writeLog)) } // First write: calendar_busy = "true" busyReq := fc.writeLog[0] if busyReq.Key != "calendar_busy" { t.Errorf("first write key = %q, want %q", busyReq.Key, "calendar_busy") } if busyReq.Value != "true" { t.Errorf("calendar_busy value = %q, want %q", busyReq.Value, "true") } if busyReq.Source != "poll:caldav" { t.Errorf("source = %q, want %q", busyReq.Source, "poll:caldav") } if busyReq.Kind != "env" { t.Errorf("kind = %q, want %q", busyReq.Kind, "env") } if busyReq.Ts.IsZero() { t.Errorf("calendar_busy ts is zero") } // Second write: calendar_event__ = " @ HH:MM-HH:MM" eventReq := fc.writeLog[1] expectedKey := "calendar_event_" + start.Format("20060102") + "_Current-meeting" if eventReq.Key != expectedKey { t.Errorf("event key = %q, want %q", eventReq.Key, expectedKey) } expectedVal := "Current meeting @ " + start.Format("15:04") + "-" + end.Format("15:04") if eventReq.Value != expectedVal { t.Errorf("event value = %q, want %q", eventReq.Value, expectedVal) } if eventReq.Source != "poll:caldav" { t.Errorf("event source = %q, want %q", eventReq.Source, "poll:caldav") } if eventReq.Kind != "env" { t.Errorf("event kind = %q, want %q", eventReq.Kind, "env") } if !eventReq.Ts.Equal(start) { t.Errorf("event ts = %v, want %v", eventReq.Ts, start) } }