@@ -764,6 +764,71 @@ func TestProcessFileProviderDevinSameSizeSameMtimeTranscriptRewriteReparses(t *t
764764 }
765765}
766766
767+ func TestProcessFileProviderDevinRepeatedHashRewriteIgnoresStaleHashedCache (t * testing.T ) {
768+ root := t .TempDir ()
769+ dbPath , transcriptPath := writeProcessProviderDevinFixture (
770+ t ,
771+ root ,
772+ "session-repeated-hash" ,
773+ "Initial reply" ,
774+ 1700000000000 ,
775+ 1700000005000 ,
776+ )
777+ virtualPath := parser .VirtualSourcePath (dbPath , "session-repeated-hash" )
778+ file := parser.DiscoveredFile {
779+ Path : virtualPath ,
780+ Agent : parser .AgentDevin ,
781+ }
782+ database := dbtest .OpenTestDB (t )
783+ engine := NewEngine (database , EngineConfig {
784+ AgentDirs : map [parser.AgentType ][]string {
785+ parser .AgentDevin : {root },
786+ },
787+ Machine : "devbox" ,
788+ })
789+
790+ first := engine .processFile (context .Background (), file )
791+ require .NoError (t , first .err )
792+ require .Len (t , first .results , 1 )
793+ require .Len (t , first .results [0 ].Messages , 2 )
794+ assert .Equal (t , "Initial reply" , first .results [0 ].Messages [1 ].Content )
795+ initialMtime := first .results [0 ].Session .File .Mtime
796+ initialHash := first .results [0 ].Session .File .Hash
797+ require .NotZero (t , initialMtime )
798+ require .NotEmpty (t , initialHash )
799+ require .Contains (t , first .cacheKey , "?source_hash=" + initialHash )
800+ writeProcessProviderDevinResult (t , engine , first )
801+
802+ engine .cacheSkip (first .cacheKey , initialMtime )
803+
804+ writeProcessProviderDevinTranscript (t , transcriptPath , "Changed reply" )
805+ initialTime := time .Unix (0 , initialMtime )
806+ require .NoError (t , os .Chtimes (transcriptPath , initialTime , initialTime ))
807+ second := engine .processFile (context .Background (), file )
808+ require .NoError (t , second .err )
809+ assert .False (t , second .skip )
810+ require .Len (t , second .results , 1 )
811+ require .Len (t , second .results [0 ].Messages , 2 )
812+ assert .Equal (t , "Changed reply" , second .results [0 ].Messages [1 ].Content )
813+ changedHash := second .results [0 ].Session .File .Hash
814+ require .NotEmpty (t , changedHash )
815+ require .NotEqual (t , initialHash , changedHash )
816+ require .Contains (t , second .cacheKey , "?source_hash=" + changedHash )
817+ writeProcessProviderDevinResult (t , engine , second )
818+ engine .clearSkip (second .cacheKey )
819+
820+ writeProcessProviderDevinTranscript (t , transcriptPath , "Initial reply" )
821+ require .NoError (t , os .Chtimes (transcriptPath , initialTime , initialTime ))
822+
823+ third := engine .processFile (context .Background (), file )
824+ require .NoError (t , third .err )
825+ assert .False (t , third .skip )
826+ require .Len (t , third .results , 1 )
827+ require .Len (t , third .results [0 ].Messages , 2 )
828+ assert .Equal (t , "Initial reply" , third .results [0 ].Messages [1 ].Content )
829+ assert .Equal (t , initialHash , third .results [0 ].Session .File .Hash )
830+ }
831+
767832func TestSyncAllProviderDevinMissingDBPreservesArchive (t * testing.T ) {
768833 root := t .TempDir ()
769834 dbPath , _ := writeProcessProviderDevinFixture (
@@ -843,6 +908,27 @@ func writeProcessProviderForgeDB(t *testing.T, root string) string {
843908 return dbPath
844909}
845910
911+ func writeProcessProviderDevinResult (
912+ t * testing.T ,
913+ engine * Engine ,
914+ result processResult ,
915+ ) {
916+ t .Helper ()
917+ require .Len (t , result .results , 1 )
918+ written , _ , failed := engine .writeBatch (
919+ []pendingWrite {{
920+ sess : result .results [0 ].Session ,
921+ msgs : result .results [0 ].Messages ,
922+ usageEvents : result .results [0 ].UsageEvents ,
923+ forceReplace : result .forceReplace ,
924+ }},
925+ syncWriteDefault ,
926+ false ,
927+ )
928+ require .Equal (t , 0 , failed )
929+ require .Equal (t , 1 , written )
930+ }
931+
846932func writeProcessProviderDevinFixture (
847933 t * testing.T ,
848934 root string ,
0 commit comments