@@ -733,6 +733,182 @@ func TestWithAISync_ProducesExpectedHeartbeats(t *testing.T) {
733733 assert .Equal (t , "019d9353-333c-7c41-a909-26f5c6221a5a" , h .AISession )
734734}
735735
736+ func TestWithAISync_PreservesCopilotTokensAfterMergingAppHeartbeat (t * testing.T ) {
737+ home := t .TempDir ()
738+ t .Setenv ("HOME" , home )
739+ t .Setenv ("USERPROFILE" , home )
740+
741+ workspaceDir := filepath .Join (home , "Library" , "Application Support" , "Code" , "User" , "workspaceStorage" , "workspace-1" )
742+ require .NoError (t , os .MkdirAll (filepath .Join (workspaceDir , "chatSessions" ), 0o755 ))
743+ require .NoError (t , os .MkdirAll (filepath .Join (workspaceDir , "chatEditingSessions" , "session-1" ), 0o755 ))
744+
745+ mainFile := filepath .Join (home , "project" , "main.go" )
746+ secondFile := filepath .Join (home , "project" , "util.go" )
747+
748+ require .NoError (t , os .MkdirAll (filepath .Dir (mainFile ), 0o755 ))
749+ require .NoError (t , os .WriteFile (mainFile , []byte ("package main\n " ), 0o644 ))
750+ require .NoError (t , os .WriteFile (secondFile , []byte ("package main\n func util() {}\n " ), 0o644 ))
751+
752+ sessionPath := filepath .Join (workspaceDir , "chatSessions" , "session-1.json" )
753+ session := map [string ]any {
754+ "version" : 3 ,
755+ "creationDate" : int64 (1770000000000 ),
756+ "lastMessageDate" : int64 (1770000100000 ),
757+ "sessionId" : "session-1" ,
758+ "requests" : []any {
759+ map [string ]any {
760+ "requestId" : "request-1" ,
761+ "timestamp" : int64 (1770000001000 ),
762+ "agent" : map [string ]any {
763+ "extensionVersion" : "0.42.3" ,
764+ },
765+ "message" : map [string ]any {
766+ "text" : "Update the file and explain what changed" ,
767+ },
768+ "result" : map [string ]any {
769+ "metadata" : map [string ]any {
770+ "promptTokens" : 9 ,
771+ "outputTokens" : 4 ,
772+ },
773+ },
774+ "modelState" : map [string ]any {
775+ "value" : 1 ,
776+ "completedAt" : int64 (1770000009000 ),
777+ },
778+ "variableData" : map [string ]any {
779+ "variables" : []any {
780+ map [string ]any {
781+ "kind" : "file" ,
782+ "id" : "file://" + strings .ReplaceAll (mainFile , " " , "%20" ),
783+ "value" : map [string ]any {
784+ "fsPath" : mainFile ,
785+ },
786+ },
787+ },
788+ },
789+ "response" : []any {
790+ map [string ]any {
791+ "kind" : "toolInvocationSerialized" ,
792+ "invocationMessage" : map [string ]any {
793+ "value" : "Reading files" ,
794+ "uris" : map [string ]any {
795+ "file://" + strings .ReplaceAll (secondFile , " " , "%20" ): map [string ]any {
796+ "fsPath" : secondFile ,
797+ },
798+ },
799+ },
800+ },
801+ },
802+ },
803+ },
804+ }
805+ data , err := json .Marshal (session )
806+ require .NoError (t , err )
807+ require .NoError (t , os .WriteFile (sessionPath , data , 0o644 ))
808+
809+ statePath := filepath .Join (workspaceDir , "chatEditingSessions" , "session-1" , "state.json" )
810+ state := map [string ]any {
811+ "version" : 2 ,
812+ "timeline" : map [string ]any {
813+ "fileBaselines" : []any {
814+ []any {
815+ "file://" + mainFile + "::request-1" ,
816+ map [string ]any {
817+ "content" : "package main\n " ,
818+ },
819+ },
820+ },
821+ "operations" : []any {
822+ map [string ]any {
823+ "type" : "textEdit" ,
824+ "requestId" : "request-1" ,
825+ "uri" : map [string ]any {
826+ "fsPath" : mainFile ,
827+ },
828+ "epoch" : 1 ,
829+ "edits" : []any {
830+ map [string ]any {
831+ "text" : "package main\n \n func main() {}\n " ,
832+ "range" : map [string ]any {
833+ "startLineNumber" : 1 ,
834+ "startColumn" : 1 ,
835+ "endLineNumber" : 2 ,
836+ "endColumn" : 1 ,
837+ },
838+ },
839+ },
840+ },
841+ },
842+ },
843+ }
844+ data , err = json .Marshal (state )
845+ require .NoError (t , err )
846+ require .NoError (t , os .WriteFile (statePath , data , 0o644 ))
847+
848+ after := time .Unix (1769999990 , 0 )
849+ tmpInternal , err := os .CreateTemp (t .TempDir (), "wakatime-internal" )
850+ require .NoError (t , err )
851+
852+ defer tmpInternal .Close ()
853+
854+ v := viper .New ()
855+ v .Set ("internal-config" , tmpInternal .Name ())
856+ v .Set ("internal.ai_heartbeats_last_parsed_at" , after .Format (ini .DateFormat ))
857+
858+ handle := ai .WithAISync (ai.Config {
859+ Plugin : "editor/1.0.0" ,
860+ V : v ,
861+ })(func (_ context.Context , hh []heartbeat.Heartbeat ) ([]heartbeat.Result , error ) {
862+ results := make ([]heartbeat.Result , len (hh ))
863+ for i := range hh {
864+ results [i ] = heartbeat.Result {Heartbeat : hh [i ]}
865+ }
866+
867+ return results , nil
868+ })
869+
870+ results , err := handle (t .Context (), []heartbeat.Heartbeat {})
871+ require .NoError (t , err )
872+ require .Len (t , results , 4 )
873+
874+ var (
875+ mergedRead heartbeat.Heartbeat
876+ writeEdit heartbeat.Heartbeat
877+ foundRead bool
878+ foundEdit bool
879+ )
880+
881+ for _ , result := range results {
882+ h := result .Heartbeat
883+ if h .Entity == mainFile && h .IsWrite != nil && ! * h .IsWrite && h .AIPromptLength > 0 {
884+ mergedRead = h
885+ foundRead = true
886+ }
887+
888+ if h .Entity == mainFile && h .IsWrite != nil && * h .IsWrite {
889+ writeEdit = h
890+ foundEdit = true
891+ }
892+ }
893+
894+ require .True (t , foundRead )
895+ assert .Equal (t , heartbeat .FileType , mergedRead .EntityType )
896+ assert .Equal (t , int64 (9 ), mergedRead .AIInputTokens )
897+ assert .Equal (t , int64 (4 ), mergedRead .AIOutputTokens )
898+ assert .Equal (t , len ([]rune ("Update the file and explain what changed" )), mergedRead .AIPromptLength )
899+ assert .Equal (t , filepath .Dir (mainFile ), mergedRead .ProjectPathOverride )
900+ assert .Equal (t , "session-1" , mergedRead .AISession )
901+
902+ require .True (t , foundEdit )
903+ require .NotNil (t , writeEdit .AILineChanges )
904+ assert .Equal (t , 2 , * writeEdit .AILineChanges )
905+ assert .Zero (t , writeEdit .AIInputTokens )
906+ assert .Zero (t , writeEdit .AIOutputTokens )
907+ assert .Zero (t , writeEdit .AIPromptLength )
908+ assert .Equal (t , filepath .Dir (mainFile ), writeEdit .ProjectPathOverride )
909+ assert .Equal (t , "session-1" , writeEdit .AISession )
910+ }
911+
736912func resetSingleton (t * testing.T ) {
737913 t .Helper ()
738914
0 commit comments