Skip to content

Commit 824bdf9

Browse files
authored
Merge pull request #1332 from wakatime/bugfix/offline-ai-heartbeats
Fix saving AI heartbeats to offline db
2 parents 0989388 + f44aa93 commit 824bdf9

File tree

3 files changed

+86
-3
lines changed

3 files changed

+86
-3
lines changed

cmd/heartbeat/ai_sync_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/gandarez/go-realpath"
1414
cmdheartbeat "github.com/wakatime/wakatime-cli/cmd/heartbeat"
1515
"github.com/wakatime/wakatime-cli/pkg/ini"
16+
"github.com/wakatime/wakatime-cli/pkg/offline"
1617

1718
"github.com/spf13/viper"
1819
"github.com/stretchr/testify/assert"
@@ -210,3 +211,64 @@ func TestRunAISyncActivity_UsesAlternateProject(t *testing.T) {
210211
assert.Equal(t, 0, code)
211212
assert.Equal(t, 1, numCalls)
212213
}
214+
215+
func TestRunAISyncActivity_RateLimitedWithoutEntity_SavesOffline(t *testing.T) {
216+
resetSingleton(t)
217+
218+
home := t.TempDir()
219+
t.Setenv("HOME", home)
220+
t.Setenv("USERPROFILE", home)
221+
222+
tmpDir := t.TempDir()
223+
224+
tmpDir, err := realpath.Realpath(tmpDir)
225+
require.NoError(t, err)
226+
227+
copyFile(t, "testdata/main.go", filepath.Join(tmpDir, "main.go"))
228+
229+
entity, err := filepath.Abs(filepath.Join(tmpDir, "main.go"))
230+
require.NoError(t, err)
231+
232+
entity = strings.ReplaceAll(entity, "\\", "/")
233+
234+
transcriptDir := filepath.Join(home, ".claude", "projects", "sample-project")
235+
require.NoError(t, os.MkdirAll(transcriptDir, 0o755))
236+
237+
transcriptPath := filepath.Join(transcriptDir, "session.jsonl")
238+
transcript := strings.Join([]string{
239+
"{\"timestamp\":\"2026-03-18T12:00:00Z\",\"version\":\"2.1.45\"," +
240+
"\"toolUseResult\":{\"filePath\":\"" + entity + "\"," +
241+
"\"structuredPatch\":[{\"oldLines\":3,\"newLines\":5},{\"oldLines\":4,\"newLines\":1}]}}",
242+
}, "\n") + "\n"
243+
require.NoError(t, os.WriteFile(transcriptPath, []byte(transcript), 0o644))
244+
245+
tmpInternalFile, err := os.CreateTemp(t.TempDir(), "wakatime-internal-config")
246+
require.NoError(t, err)
247+
248+
defer tmpInternalFile.Close()
249+
250+
offlineQueueFile, err := os.CreateTemp(t.TempDir(), "offline-queue-file")
251+
require.NoError(t, err)
252+
253+
defer offlineQueueFile.Close()
254+
255+
v := viper.New()
256+
v.Set("heartbeat-rate-limit-seconds", 120)
257+
v.Set("internal-config", tmpInternalFile.Name())
258+
v.Set("internal.heartbeats_last_sent_at", time.Now().Format(ini.DateFormat))
259+
v.Set("internal.ai_heartbeats_last_parsed_at", time.Date(2026, 3, 18, 11, 0, 0, 0, time.UTC).Format(ini.DateFormat))
260+
v.Set("key", "00000000-0000-4000-8000-000000000000")
261+
v.Set("offline-queue-file", offlineQueueFile.Name())
262+
v.Set("plugin", "plugin/0.0.1")
263+
v.Set("project", "myproject")
264+
v.Set("project-folder", "/path/to/project")
265+
v.Set("timeout", 5)
266+
267+
code, err := cmdheartbeat.RunAISyncActivity(t.Context(), v)
268+
require.NoError(t, err)
269+
assert.Equal(t, 0, code)
270+
271+
offlineCount, err := offline.CountHeartbeats(t.Context(), offlineQueueFile.Name())
272+
require.NoError(t, err)
273+
assert.Equal(t, 1, offlineCount)
274+
}

cmd/heartbeat/heartbeat.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ func sendPreparedHeartbeats(
267267
LastSentAt: params.Offline.LastSentAt,
268268
Timeout: params.Offline.RateLimit,
269269
}) {
270-
err := offlinecmd.SaveHeartbeats(ctx, v, queueFilepath, heartbeats)
270+
err := offlinecmd.SaveHeartbeatsWithParams(ctx, v, queueFilepath, heartbeats, params)
271271
if err == nil {
272272
return nil
273273
}
@@ -288,7 +288,7 @@ func sendPreparedHeartbeats(
288288
logger.Debugf("save %d extra heartbeat(s) to offline queue", len(extraHeartbeats))
289289

290290
go func(done chan<- bool) {
291-
if err := offlinecmd.SaveHeartbeats(ctx, v, queueFilepath, extraHeartbeats); err != nil {
291+
if err := offlinecmd.SaveHeartbeatsWithParams(ctx, v, queueFilepath, extraHeartbeats, params); err != nil {
292292
logger.Errorf("failed to save extra heartbeats to offline queue: %s", err)
293293
}
294294

@@ -300,7 +300,7 @@ func sendPreparedHeartbeats(
300300

301301
sender, err := buildHandle(ctx, v, params, queueFilepath)
302302
if err != nil {
303-
if err := offlinecmd.SaveHeartbeats(ctx, v, queueFilepath, heartbeats); err != nil {
303+
if err := offlinecmd.SaveHeartbeatsWithParams(ctx, v, queueFilepath, heartbeats, params); err != nil {
304304
logger.Errorf("failed to save extra heartbeats to offline queue: %s", err)
305305
}
306306

cmd/offline/offline.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,27 @@ func SaveHeartbeats(
3030
return fmt.Errorf("failed to load command parameters: %w", err)
3131
}
3232

33+
return saveHeartbeats(ctx, v, queueFilepath, heartbeats, params)
34+
}
35+
36+
// SaveHeartbeatsWithParams saves heartbeats to the offline db using already loaded command params.
37+
func SaveHeartbeatsWithParams(
38+
ctx context.Context,
39+
v *viper.Viper,
40+
queueFilepath string,
41+
heartbeats []heartbeat.Heartbeat,
42+
params params.Params,
43+
) error {
44+
return saveHeartbeats(ctx, v, queueFilepath, heartbeats, params)
45+
}
46+
47+
func saveHeartbeats(
48+
ctx context.Context,
49+
v *viper.Viper,
50+
queueFilepath string,
51+
heartbeats []heartbeat.Heartbeat,
52+
params params.Params,
53+
) error {
3354
logger := log.Extract(ctx)
3455

3556
setLogFields(ctx, params)

0 commit comments

Comments
 (0)