@@ -724,6 +724,72 @@ describe("session cost usage", () => {
724724 } ) ;
725725 } ) ;
726726
727+ it ( "reclaims stale usage cache temp files before refreshing" , async ( ) => {
728+ const root = await makeSessionCostRoot ( "cost-cache-temp-cleanup" ) ;
729+ const sessionsDir = path . join ( root , "agents" , "main" , "sessions" ) ;
730+ await fs . mkdir ( sessionsDir , { recursive : true } ) ;
731+ const sessionFile = path . join ( sessionsDir , "sess-cache-temp-cleanup.jsonl" ) ;
732+ await fs . writeFile (
733+ sessionFile ,
734+ transcriptText ( "sess-cache-temp-cleanup" , {
735+ type : "message" ,
736+ timestamp : "2026-02-05T12:00:00.000Z" ,
737+ message : {
738+ role : "assistant" ,
739+ provider : "openai" ,
740+ model : "gpt-5.4" ,
741+ usage : {
742+ input : 10 ,
743+ output : 20 ,
744+ totalTokens : 30 ,
745+ cost : { total : 0.03 } ,
746+ } ,
747+ } ,
748+ } ) ,
749+ "utf-8" ,
750+ ) ;
751+
752+ const staleLegacyTempPath = path . join ( sessionsDir , ".usage-cost-cache.json.12345.tmp" ) ;
753+ const staleCurrentTempPath = path . join ( sessionsDir , ".usage-cost-cache.12345.tmp" ) ;
754+ const recentTempPath = path . join ( sessionsDir , ".usage-cost-cache.67890.tmp" ) ;
755+ const lockTempPath = path . join ( sessionsDir , ".usage-cost-cache.json.lock.12345.tmp" ) ;
756+ await Promise . all (
757+ [ staleLegacyTempPath , staleCurrentTempPath , recentTempPath , lockTempPath ] . map ( ( tempPath ) =>
758+ fs . writeFile ( tempPath , "partial\n" , "utf-8" ) ,
759+ ) ,
760+ ) ;
761+ const staleTime = new Date ( Date . now ( ) - 60_000 ) ;
762+ await Promise . all (
763+ [ staleLegacyTempPath , staleCurrentTempPath , lockTempPath ] . map ( ( tempPath ) =>
764+ fs . utimes ( tempPath , staleTime , staleTime ) ,
765+ ) ,
766+ ) ;
767+
768+ const exists = async ( filePath : string ) : Promise < boolean > =>
769+ await fs . stat ( filePath ) . then (
770+ ( ) => true ,
771+ ( ) => false ,
772+ ) ;
773+
774+ await withStateDir ( root , async ( ) => {
775+ const result = await refreshCostUsageCache ( ) ;
776+ expect ( result ) . toBe ( "refreshed" ) ;
777+
778+ expect ( await exists ( staleLegacyTempPath ) ) . toBe ( false ) ;
779+ expect ( await exists ( staleCurrentTempPath ) ) . toBe ( false ) ;
780+ expect ( await exists ( recentTempPath ) ) . toBe ( true ) ;
781+ expect ( await exists ( lockTempPath ) ) . toBe ( true ) ;
782+
783+ const summary = await loadCostUsageSummaryFromCache ( {
784+ startMs : Date . UTC ( 2026 , 1 , 5 ) ,
785+ endMs : Date . UTC ( 2026 , 1 , 5 ) + 24 * 60 * 60 * 1000 - 1 ,
786+ requestRefresh : false ,
787+ } ) ;
788+ expect ( summary . totals . totalTokens ) . toBe ( 30 ) ;
789+ expect ( summary . cacheStatus ?. status ) . toBe ( "fresh" ) ;
790+ } ) ;
791+ } ) ;
792+
727793 it ( "keeps queued durable aggregate refresh state scoped to the cache path" , async ( ) => {
728794 const firstRoot = await makeSessionCostRoot ( "cost-cache-queued-first" ) ;
729795 const secondRoot = await makeSessionCostRoot ( "cost-cache-queued-second" ) ;
0 commit comments