@@ -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+ }
0 commit comments