Skip to content

Commit ffc4df9

Browse files
committed
fix: align canned coach timezone filtering
1 parent a21c0cd commit ffc4df9

2 files changed

Lines changed: 146 additions & 3 deletions

File tree

internal/server/insights.go

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -526,9 +526,13 @@ func (s *Server) listCannedCoachSessions(
526526
if req.Filters != nil {
527527
filters = *req.Filters
528528
}
529+
loc := cannedCoachLocation(filters.Timezone)
530+
dateFrom, dateTo := cannedCoachUTCDateBounds(
531+
req.DateFrom, req.DateTo, loc,
532+
)
529533
filter := db.SessionFilter{
530-
DateFrom: req.DateFrom,
531-
DateTo: req.DateTo,
534+
DateFrom: dateFrom,
535+
DateTo: dateTo,
532536
Project: req.Project,
533537
Machine: filters.Machine,
534538
Agent: filters.Agent,
@@ -545,10 +549,91 @@ func (s *Server) listCannedCoachSessions(
545549
if err != nil {
546550
return nil, err
547551
}
548-
out = append(out, page.Sessions...)
552+
for _, session := range page.Sessions {
553+
if cannedCoachSessionInDateRange(
554+
session, req.DateFrom, req.DateTo, loc,
555+
) {
556+
out = append(out, session)
557+
}
558+
}
549559
if page.NextCursor == "" {
550560
return out, nil
551561
}
552562
filter.Cursor = page.NextCursor
553563
}
554564
}
565+
566+
func cannedCoachLocation(name string) *time.Location {
567+
loc, err := time.LoadLocation(name)
568+
if err != nil {
569+
return time.UTC
570+
}
571+
return loc
572+
}
573+
574+
func cannedCoachUTCDateBounds(
575+
from, to string,
576+
loc *time.Location,
577+
) (string, string) {
578+
start, err := time.ParseInLocation("2006-01-02", from, loc)
579+
if err != nil {
580+
return from, to
581+
}
582+
end, err := time.ParseInLocation("2006-01-02", to, loc)
583+
if err != nil {
584+
return from, to
585+
}
586+
end = end.AddDate(0, 0, 1).Add(-time.Nanosecond)
587+
return start.UTC().Format("2006-01-02"),
588+
end.UTC().Format("2006-01-02")
589+
}
590+
591+
func cannedCoachSessionInDateRange(
592+
session db.Session,
593+
from, to string,
594+
loc *time.Location,
595+
) bool {
596+
date := cannedCoachSessionLocalDate(session, loc)
597+
if date == "" {
598+
return false
599+
}
600+
if from != "" && date < from {
601+
return false
602+
}
603+
if to != "" && date > to {
604+
return false
605+
}
606+
return true
607+
}
608+
609+
func cannedCoachSessionLocalDate(
610+
session db.Session,
611+
loc *time.Location,
612+
) string {
613+
ts := session.CreatedAt
614+
if session.StartedAt != nil && *session.StartedAt != "" {
615+
ts = *session.StartedAt
616+
}
617+
t, ok := cannedCoachLocalTime(ts, loc)
618+
if !ok {
619+
if len(ts) >= 10 {
620+
return ts[:10]
621+
}
622+
return ""
623+
}
624+
return t.Format("2006-01-02")
625+
}
626+
627+
func cannedCoachLocalTime(
628+
ts string,
629+
loc *time.Location,
630+
) (time.Time, bool) {
631+
t, err := time.Parse(time.RFC3339Nano, ts)
632+
if err != nil {
633+
t, err = time.Parse("2006-01-02T15:04:05Z", ts)
634+
if err != nil {
635+
return time.Time{}, false
636+
}
637+
}
638+
return t.In(loc), true
639+
}

internal/server/insights_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,64 @@ func TestGenerateCannedInsight_UsesSessionFilterPayload(t *testing.T) {
663663
assert.NotContains(t, generatedPrompts[1], codexPrompt)
664664
}
665665

666+
func TestGenerateCannedInsight_CoachSummaryUsesFilterTimezone(t *testing.T) {
667+
var generatedPrompt string
668+
stubGen := func(
669+
_ context.Context, _ string, prompt string, _ insight.LogFunc,
670+
) (insight.Result, error) {
671+
generatedPrompt = prompt
672+
return insight.Result{
673+
Agent: "claude",
674+
Model: "test-model",
675+
Content: `{
676+
"schema_version":"llm_insight.v1",
677+
"kind":"prompt_maturity_review",
678+
"summary":"Prompt maturity evidence is scoped to the requested local day.",
679+
"confidence":"medium",
680+
"recommendations":[{
681+
"title":"Keep local-day Coach scope aligned",
682+
"rationale":"Coach prompt maturity uses the same local-date filter as the canned payload.",
683+
"actions":["Generate recommendations from the selected local day"],
684+
"evidence_refs":["coach:prompt_maturity"],
685+
"impact":"medium",
686+
"effort":"low"
687+
}],
688+
"risks":[],
689+
"evidence_refs":["coach:prompt_maturity"]
690+
}`,
691+
}, nil
692+
}
693+
te := setupWithServerOpts(t, []server.Option{
694+
server.WithGenerateStreamFunc(stubGen),
695+
})
696+
697+
localDayPrompt := "Implement local-day filtering with acceptance criteria and verification steps"
698+
previousLocalDayPrompt := "Implement previous-day filtering with acceptance criteria and verification steps"
699+
te.seedSession(t, "local-day-match", "my-app", 4, func(s *db.Session) {
700+
started := "2025-01-16T07:30:00Z"
701+
ended := "2025-01-16T07:45:00Z"
702+
s.StartedAt = &started
703+
s.EndedAt = &ended
704+
s.FirstMessage = &localDayPrompt
705+
})
706+
te.seedSession(t, "previous-local-day", "my-app", 4, func(s *db.Session) {
707+
started := "2025-01-15T01:00:00Z"
708+
ended := "2025-01-15T01:15:00Z"
709+
s.StartedAt = &started
710+
s.EndedAt = &ended
711+
s.FirstMessage = &previousLocalDayPrompt
712+
})
713+
714+
payload := `{"type":"llm_canned","kind":"prompt_maturity_review","date_from":"2025-01-15","date_to":"2025-01-15","project":"my-app","agent":"claude","llm_opt_in":true,"filters":{"timezone":"America/Los_Angeles","include_one_shot":false,"automated_scope":"human"}}`
715+
w := te.post(t, "/api/v1/insights/generate", payload)
716+
assertStatus(t, w, http.StatusOK)
717+
718+
assert.Contains(t, generatedPrompt, `"timezone":"America/Los_Angeles"`)
719+
assert.Contains(t, generatedPrompt, `"session_count":1`)
720+
assert.Contains(t, generatedPrompt, localDayPrompt)
721+
assert.NotContains(t, generatedPrompt, previousLocalDayPrompt)
722+
}
723+
666724
func TestGenerateCannedInsight_RejectsOversizedFocus(t *testing.T) {
667725
te := setup(t)
668726
longFocus := strings.Repeat("x", insight.MaxCannedFocusRunes+1)

0 commit comments

Comments
 (0)