@@ -432,6 +432,117 @@ func TestSendHeartbeats_WithAIParsingBatchAppliedOnce(t *testing.T) {
432432 assert .Eventually (t , func () bool { return numCalls == 1 }, time .Second , 50 * time .Millisecond )
433433}
434434
435+ func TestWithAISyncMarksHumanHeartbeatsAsAICoding (t * testing.T ) {
436+ home := t .TempDir ()
437+ t .Setenv ("HOME" , home )
438+ t .Setenv ("USERPROFILE" , home )
439+
440+ entity , err := filepath .Abs ("testdata/main.go" )
441+ require .NoError (t , err )
442+
443+ entity = strings .ReplaceAll (entity , "\\ " , "/" )
444+
445+ local , err := filepath .Abs ("testdata/localfile.go" )
446+ require .NoError (t , err )
447+
448+ local = strings .ReplaceAll (local , "\\ " , "/" )
449+
450+ transcriptDir := filepath .Join (home , ".claude" , "projects" , "sample-project" )
451+ require .NoError (t , os .MkdirAll (transcriptDir , 0o755 ))
452+
453+ transcriptPath := filepath .Join (transcriptDir , "session.jsonl" )
454+ transcript := strings .Join ([]string {
455+ "{\" timestamp\" :\" 2026-03-18T12:00:00Z\" ,\" version\" :\" 2.1.45\" ," +
456+ "\" toolUseResult\" :{\" filePath\" :\" " + entity + "\" ," +
457+ "\" structuredPatch\" :[{\" oldLines\" :3,\" newLines\" :5}]}}" ,
458+ "{\" timestamp\" :\" 2026-03-18T12:30:00Z\" ,\" toolUseResult\" :{" +
459+ "\" filePath\" :\" " + local + "\" ,\" content\" :\" first\\ nsecond\\ nthird\" }}" ,
460+ }, "\n " ) + "\n "
461+ require .NoError (t , os .WriteFile (transcriptPath , []byte (transcript ), 0o644 ))
462+
463+ tmpInternal , err := os .CreateTemp (t .TempDir (), "wakatime-internal" )
464+ require .NoError (t , err )
465+
466+ defer tmpInternal .Close ()
467+
468+ v := viper .New ()
469+ v .Set ("internal-config" , tmpInternal .Name ())
470+ v .Set ("internal.ai_heartbeats_last_parsed_at" , time .Date (2026 , 3 , 18 , 11 , 0 , 0 , 0 , time .UTC ).Format (ini .DateFormat ))
471+
472+ handle := ai .WithAISync (ai.Config {
473+ Plugin : "plugin/0.0.1" ,
474+ V : v ,
475+ })(func (_ context.Context , hh []heartbeat.Heartbeat ) ([]heartbeat.Result , error ) {
476+ results := make ([]heartbeat.Result , len (hh ))
477+ for i := range hh {
478+ results [i ] = heartbeat.Result {Heartbeat : hh [i ]}
479+ }
480+
481+ return results , nil
482+ })
483+
484+ humanWithinTwoMinutes := "/tmp/human-within-two-minutes.go"
485+ humanWithinThirtyMinutesNoChangesBeforeEdit := "/tmp/human-within-thirty-minutes-no-changes-before-edit.go"
486+ humanWithinThirtyMinutesWithChanges := "/tmp/human-within-thirty-minutes-with-changes.go"
487+ humanWithinThirtyMinutesNoChangesAfterEdit := "/tmp/human-within-thirty-minutes-no-changes-after-edit.go"
488+ humanOutsideThirtyMinutes := "/tmp/human-outside-thirty-minutes.go"
489+
490+ got , err := handle (t .Context (), []heartbeat.Heartbeat {
491+ {
492+ Entity : humanWithinTwoMinutes ,
493+ EntityType : heartbeat .FileType ,
494+ Category : "debugging" ,
495+ HumanLineChanges : heartbeat .PointerTo (3 ),
496+ Time : 1773835260.1 ,
497+ UserAgent : "editor/1.2.3" ,
498+ },
499+ {
500+ Entity : humanWithinThirtyMinutesNoChangesBeforeEdit ,
501+ EntityType : heartbeat .FileType ,
502+ Category : "debugging" ,
503+ HumanLineChanges : heartbeat .PointerTo (0 ),
504+ Time : 1773836999.1 ,
505+ UserAgent : "editor/1.2.3" ,
506+ },
507+ {
508+ Entity : humanWithinThirtyMinutesWithChanges ,
509+ EntityType : heartbeat .FileType ,
510+ Category : "debugging" ,
511+ HumanLineChanges : heartbeat .PointerTo (1 ),
512+ Time : 1773838500.2 ,
513+ UserAgent : "editor/1.2.3" ,
514+ },
515+ {
516+ Entity : humanWithinThirtyMinutesNoChangesAfterEdit ,
517+ EntityType : heartbeat .FileType ,
518+ Category : "debugging" ,
519+ HumanLineChanges : heartbeat .PointerTo (0 ),
520+ Time : 1773838500.1 ,
521+ UserAgent : "editor/1.2.3" ,
522+ },
523+ {
524+ Entity : humanOutsideThirtyMinutes ,
525+ EntityType : heartbeat .FileType ,
526+ Category : "debugging" ,
527+ HumanLineChanges : heartbeat .PointerTo (0 ),
528+ Time : 1773839100.1 ,
529+ UserAgent : "editor/1.2.3" ,
530+ },
531+ })
532+ require .NoError (t , err )
533+
534+ categoriesByEntity := make (map [string ]string , len (got ))
535+ for _ , result := range got {
536+ categoriesByEntity [result .Heartbeat .Entity ] = result .Heartbeat .Category
537+ }
538+
539+ assert .Equal (t , "ai coding" , categoriesByEntity [humanWithinTwoMinutes ])
540+ assert .Equal (t , "ai coding" , categoriesByEntity [humanWithinThirtyMinutesNoChangesBeforeEdit ])
541+ assert .Equal (t , "debugging" , categoriesByEntity [humanWithinThirtyMinutesWithChanges ])
542+ assert .Equal (t , "debugging" , categoriesByEntity [humanWithinThirtyMinutesNoChangesAfterEdit ])
543+ assert .Equal (t , "debugging" , categoriesByEntity [humanOutsideThirtyMinutes ])
544+ }
545+
435546func resetSingleton (t * testing.T ) {
436547 t .Helper ()
437548
0 commit comments