Skip to content

Commit 8145ec3

Browse files
committed
Fix Codex token parsing
1 parent 3aa83e7 commit 8145ec3

File tree

5 files changed

+546
-97
lines changed

5 files changed

+546
-97
lines changed

pkg/ai/ai.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,43 @@ func mergeHeartbeatCounts(dst *heartbeat.Heartbeat, src heartbeat.Heartbeat) {
498498
dst.HumanLineChanges = addIntPointers(dst.HumanLineChanges, src.HumanLineChanges)
499499
dst.AIPromptLength += src.AIPromptLength
500500
dst.AIInputTokens += src.AIInputTokens
501+
501502
dst.AIOutputTokens += src.AIOutputTokens
502-
dst.AIPromptLength += src.AIPromptLength
503+
if dst.Project == nil || *dst.Project == "" {
504+
dst.Project = src.Project
505+
}
506+
507+
if dst.ProjectAlternate == "" {
508+
dst.ProjectAlternate = src.ProjectAlternate
509+
}
510+
511+
if dst.Branch == nil || *dst.Branch == "" {
512+
dst.Branch = src.Branch
513+
}
514+
515+
if dst.BranchAlternate == "" {
516+
dst.BranchAlternate = src.BranchAlternate
517+
}
518+
519+
if dst.Language == nil || *dst.Language == "" {
520+
dst.Language = src.Language
521+
}
522+
523+
if dst.LanguageAlternate == "" {
524+
dst.LanguageAlternate = src.LanguageAlternate
525+
}
526+
527+
if dst.ProjectOverride == "" {
528+
dst.ProjectOverride = src.ProjectOverride
529+
}
530+
531+
if dst.ProjectPath == "" {
532+
dst.ProjectPath = src.ProjectPath
533+
}
534+
535+
if dst.ProjectPathOverride == "" {
536+
dst.ProjectPathOverride = src.ProjectPathOverride
537+
}
503538
}
504539

505540
func addIntPointers(dst *int, src *int) *int {

pkg/ai/ai_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,87 @@ func TestWithAISyncSkipsTwoMinuteAICodingForHumanEditsWithChanges(t *testing.T)
652652
assert.Equal(t, "ai coding", categoriesByEntity[humanWithinTwoMinutesNoChanges])
653653
}
654654

655+
func TestWithAISync_ProducesExpectedHeartbeats(t *testing.T) {
656+
home := t.TempDir()
657+
t.Setenv("HOME", home)
658+
t.Setenv("USERPROFILE", home)
659+
660+
now := time.Now()
661+
transcriptDir := filepath.Join(home, ".codex", "sessions", now.Format("2026"), now.Format("04"), now.Format("15"))
662+
require.NoError(t, os.MkdirAll(transcriptDir, 0o755))
663+
664+
transcript := "rollout-2026-04-15T18-46-36-019d9353-333c-7c41-a909-26f5c6221a5a.jsonl"
665+
transcriptPath := filepath.Join(transcriptDir, transcript)
666+
copyFile(t, filepath.Join("testdata", transcript), transcriptPath)
667+
668+
after := time.Date(2026, 4, 16, 0, 0, 0, 0, time.UTC)
669+
670+
tmpInternal, err := os.CreateTemp(t.TempDir(), "wakatime-internal")
671+
require.NoError(t, err)
672+
673+
defer tmpInternal.Close()
674+
675+
v := viper.New()
676+
v.Set("internal-config", tmpInternal.Name())
677+
v.Set("internal.ai_heartbeats_last_parsed_at", after.Format(ini.DateFormat))
678+
679+
handle := ai.WithAISync(ai.Config{
680+
Plugin: "plugin/0.0.1",
681+
V: v,
682+
})(func(_ context.Context, hh []heartbeat.Heartbeat) ([]heartbeat.Result, error) {
683+
results := make([]heartbeat.Result, len(hh))
684+
for i := range hh {
685+
results[i] = heartbeat.Result{Heartbeat: hh[i]}
686+
}
687+
688+
return results, nil
689+
})
690+
691+
heartbeats, err := handle(t.Context(), []heartbeat.Heartbeat{})
692+
require.NoError(t, err)
693+
694+
require.Len(t, heartbeats, 3)
695+
696+
h := heartbeats[0].Heartbeat
697+
assert.Equal(t, float64(1776297745), h.Time)
698+
assert.Equal(t, heartbeat.FileType, h.EntityType)
699+
assert.Equal(t, "/Users/user/git/wakatime-cli/templates/index.html", h.Entity)
700+
assert.Equal(t, "ai coding", h.Category)
701+
assert.False(t, *h.IsWrite)
702+
assert.Equal(t, int64(105134), h.AIInputTokens)
703+
assert.Equal(t, int64(479), h.AIOutputTokens)
704+
assert.Equal(t, 29, h.AIPromptLength)
705+
assert.Nil(t, h.AILineChanges)
706+
assert.Equal(t, "/Users/user/git/wakatime-cli", h.ProjectPathOverride)
707+
assert.Equal(t, "019d9353-333c-7c41-a909-26f5c6221a5a", h.AISession)
708+
709+
h = heartbeats[1].Heartbeat
710+
assert.Equal(t, float64(1776297784), h.Time)
711+
assert.Equal(t, heartbeat.FileType, h.EntityType)
712+
assert.Equal(t, "/Users/user/git/wakatime-cli/templates/index.html", h.Entity)
713+
assert.Equal(t, "ai coding", h.Category)
714+
assert.True(t, *h.IsWrite)
715+
assert.Equal(t, int64(326506), h.AIInputTokens)
716+
assert.Equal(t, int64(1442), h.AIOutputTokens)
717+
assert.Equal(t, 10, h.AIPromptLength)
718+
assert.Equal(t, -1, *h.AILineChanges)
719+
assert.Equal(t, "/Users/user/git/wakatime-cli", h.ProjectPathOverride)
720+
assert.Equal(t, "019d9353-333c-7c41-a909-26f5c6221a5a", h.AISession)
721+
722+
h = heartbeats[2].Heartbeat
723+
assert.Equal(t, float64(1776297789), h.Time)
724+
assert.Equal(t, heartbeat.FileType, h.EntityType)
725+
assert.Equal(t, "/Users/user/git/wakatime-cli/static/css/index.less", h.Entity)
726+
assert.Equal(t, "ai coding", h.Category)
727+
assert.True(t, *h.IsWrite)
728+
assert.Equal(t, int64(110200), h.AIInputTokens)
729+
assert.Equal(t, int64(223), h.AIOutputTokens)
730+
assert.Zero(t, h.AIPromptLength)
731+
assert.Equal(t, 23, *h.AILineChanges)
732+
assert.Equal(t, "/Users/user/git/wakatime-cli", h.ProjectPathOverride)
733+
assert.Equal(t, "019d9353-333c-7c41-a909-26f5c6221a5a", h.AISession)
734+
}
735+
655736
func resetSingleton(t *testing.T) {
656737
t.Helper()
657738

0 commit comments

Comments
 (0)