@@ -2,6 +2,7 @@ import { RooCodeEventName, ProviderSettings, TokenUsage, ToolUsage } from "@roo-
22
33import { Task } from "../Task"
44import { ClineProvider } from "../../webview/ClineProvider"
5+ import { hasToolUsageChanged , hasTokenUsageChanged } from "../../../shared/getApiMetrics"
56
67// Mock dependencies
78vi . mock ( "../../webview/ClineProvider" )
@@ -381,4 +382,238 @@ describe("Task token usage throttling", () => {
381382 // Should not have emitted again since token usage didn't change
382383 expect ( secondEmitCount ) . toBe ( firstEmitCount )
383384 } )
385+
386+ test ( "should emit when tool usage changes even if token usage is the same" , async ( ) => {
387+ const { taskMetadata } = await import ( "../../task-persistence" )
388+
389+ // Mock taskMetadata to return same token usage
390+ const constantTokenUsage : TokenUsage = {
391+ totalTokensIn : 100 ,
392+ totalTokensOut : 50 ,
393+ totalCost : 0.01 ,
394+ contextTokens : 150 ,
395+ totalCacheWrites : 0 ,
396+ totalCacheReads : 0 ,
397+ }
398+
399+ vi . mocked ( taskMetadata ) . mockResolvedValue ( {
400+ historyItem : {
401+ id : "test-task-id" ,
402+ number : 1 ,
403+ task : "Test task" ,
404+ ts : Date . now ( ) ,
405+ totalCost : 0.01 ,
406+ tokensIn : 100 ,
407+ tokensOut : 50 ,
408+ } ,
409+ tokenUsage : constantTokenUsage ,
410+ } )
411+
412+ const emitSpy = vi . spyOn ( task , "emit" )
413+
414+ // Add first message - should emit
415+ await ( task as any ) . addToClineMessages ( {
416+ ts : Date . now ( ) ,
417+ type : "say" ,
418+ say : "text" ,
419+ text : "Message 1" ,
420+ } )
421+
422+ const firstEmitCount = emitSpy . mock . calls . filter (
423+ ( call ) => call [ 0 ] === RooCodeEventName . TaskTokenUsageUpdated ,
424+ ) . length
425+
426+ // Wait for throttle period
427+ vi . advanceTimersByTime ( 2100 )
428+
429+ // Change tool usage (token usage stays the same)
430+ task . toolUsage = {
431+ read_file : { attempts : 5 , failures : 1 } ,
432+ }
433+
434+ // Add another message
435+ await ( task as any ) . addToClineMessages ( {
436+ ts : Date . now ( ) ,
437+ type : "say" ,
438+ say : "text" ,
439+ text : "Message 2" ,
440+ } )
441+
442+ const secondEmitCount = emitSpy . mock . calls . filter (
443+ ( call ) => call [ 0 ] === RooCodeEventName . TaskTokenUsageUpdated ,
444+ ) . length
445+
446+ // Should have emitted because tool usage changed even though token usage didn't
447+ expect ( secondEmitCount ) . toBeGreaterThan ( firstEmitCount )
448+ } )
449+
450+ test ( "should update toolUsageSnapshot when emission occurs" , async ( ) => {
451+ // Add initial message
452+ await ( task as any ) . addToClineMessages ( {
453+ ts : Date . now ( ) ,
454+ type : "say" ,
455+ say : "text" ,
456+ text : "Message 1" ,
457+ } )
458+
459+ // Initially toolUsageSnapshot should be set to current toolUsage (empty object)
460+ const initialSnapshot = ( task as any ) . toolUsageSnapshot
461+ expect ( initialSnapshot ) . toBeDefined ( )
462+ expect ( Object . keys ( initialSnapshot ) ) . toHaveLength ( 0 )
463+
464+ // Wait for throttle period
465+ vi . advanceTimersByTime ( 2100 )
466+
467+ // Update tool usage
468+ task . toolUsage = {
469+ read_file : { attempts : 3 , failures : 0 } ,
470+ write_to_file : { attempts : 2 , failures : 1 } ,
471+ }
472+
473+ // Add another message
474+ await ( task as any ) . addToClineMessages ( {
475+ ts : Date . now ( ) ,
476+ type : "say" ,
477+ say : "text" ,
478+ text : "Message 2" ,
479+ } )
480+
481+ // Snapshot should be updated to match the new toolUsage
482+ const newSnapshot = ( task as any ) . toolUsageSnapshot
483+ expect ( newSnapshot ) . not . toBe ( initialSnapshot )
484+ expect ( newSnapshot . read_file ) . toEqual ( { attempts : 3 , failures : 0 } )
485+ expect ( newSnapshot . write_to_file ) . toEqual ( { attempts : 2 , failures : 1 } )
486+ } )
487+
488+ test ( "emitFinalTokenUsageUpdate should emit on tool usage change alone" , async ( ) => {
489+ const emitSpy = vi . spyOn ( task , "emit" )
490+
491+ // Set initial tool usage and simulate previous emission
492+ ; ( task as any ) . tokenUsageSnapshot = task . getTokenUsage ( )
493+ ; ( task as any ) . toolUsageSnapshot = { }
494+
495+ // Change tool usage
496+ task . toolUsage = {
497+ execute_command : { attempts : 1 , failures : 0 } ,
498+ }
499+
500+ // Call emitFinalTokenUsageUpdate
501+ task . emitFinalTokenUsageUpdate ( )
502+
503+ // Should emit due to tool usage change
504+ expect ( emitSpy ) . toHaveBeenCalledWith (
505+ RooCodeEventName . TaskTokenUsageUpdated ,
506+ task . taskId ,
507+ expect . any ( Object ) ,
508+ task . toolUsage ,
509+ )
510+ } )
511+ } )
512+
513+ describe ( "hasToolUsageChanged" , ( ) => {
514+ test ( "should return true when snapshot is undefined and current has data" , ( ) => {
515+ const current : ToolUsage = {
516+ read_file : { attempts : 1 , failures : 0 } ,
517+ }
518+ expect ( hasToolUsageChanged ( current , undefined ) ) . toBe ( true )
519+ } )
520+
521+ test ( "should return false when both are empty" , ( ) => {
522+ expect ( hasToolUsageChanged ( { } , { } ) ) . toBe ( false )
523+ } )
524+
525+ test ( "should return false when snapshot is undefined and current is empty" , ( ) => {
526+ expect ( hasToolUsageChanged ( { } , undefined ) ) . toBe ( false )
527+ } )
528+
529+ test ( "should return true when a new tool is added" , ( ) => {
530+ const current : ToolUsage = {
531+ read_file : { attempts : 1 , failures : 0 } ,
532+ write_to_file : { attempts : 1 , failures : 0 } ,
533+ }
534+ const snapshot : ToolUsage = {
535+ read_file : { attempts : 1 , failures : 0 } ,
536+ }
537+ expect ( hasToolUsageChanged ( current , snapshot ) ) . toBe ( true )
538+ } )
539+
540+ test ( "should return true when attempts change" , ( ) => {
541+ const current : ToolUsage = {
542+ read_file : { attempts : 2 , failures : 0 } ,
543+ }
544+ const snapshot : ToolUsage = {
545+ read_file : { attempts : 1 , failures : 0 } ,
546+ }
547+ expect ( hasToolUsageChanged ( current , snapshot ) ) . toBe ( true )
548+ } )
549+
550+ test ( "should return true when failures change" , ( ) => {
551+ const current : ToolUsage = {
552+ read_file : { attempts : 1 , failures : 1 } ,
553+ }
554+ const snapshot : ToolUsage = {
555+ read_file : { attempts : 1 , failures : 0 } ,
556+ }
557+ expect ( hasToolUsageChanged ( current , snapshot ) ) . toBe ( true )
558+ } )
559+
560+ test ( "should return false when nothing changed" , ( ) => {
561+ const current : ToolUsage = {
562+ read_file : { attempts : 3 , failures : 1 } ,
563+ write_to_file : { attempts : 2 , failures : 0 } ,
564+ }
565+ const snapshot : ToolUsage = {
566+ read_file : { attempts : 3 , failures : 1 } ,
567+ write_to_file : { attempts : 2 , failures : 0 } ,
568+ }
569+ expect ( hasToolUsageChanged ( current , snapshot ) ) . toBe ( false )
570+ } )
571+ } )
572+
573+ describe ( "hasTokenUsageChanged" , ( ) => {
574+ test ( "should return true when snapshot is undefined" , ( ) => {
575+ const current : TokenUsage = {
576+ totalTokensIn : 100 ,
577+ totalTokensOut : 50 ,
578+ totalCost : 0.01 ,
579+ contextTokens : 150 ,
580+ }
581+ expect ( hasTokenUsageChanged ( current , undefined ) ) . toBe ( true )
582+ } )
583+
584+ test ( "should return true when totalTokensIn changes" , ( ) => {
585+ const current : TokenUsage = {
586+ totalTokensIn : 200 ,
587+ totalTokensOut : 50 ,
588+ totalCost : 0.01 ,
589+ contextTokens : 150 ,
590+ }
591+ const snapshot : TokenUsage = {
592+ totalTokensIn : 100 ,
593+ totalTokensOut : 50 ,
594+ totalCost : 0.01 ,
595+ contextTokens : 150 ,
596+ }
597+ expect ( hasTokenUsageChanged ( current , snapshot ) ) . toBe ( true )
598+ } )
599+
600+ test ( "should return false when nothing changed" , ( ) => {
601+ const current : TokenUsage = {
602+ totalTokensIn : 100 ,
603+ totalTokensOut : 50 ,
604+ totalCost : 0.01 ,
605+ contextTokens : 150 ,
606+ totalCacheWrites : 10 ,
607+ totalCacheReads : 5 ,
608+ }
609+ const snapshot : TokenUsage = {
610+ totalTokensIn : 100 ,
611+ totalTokensOut : 50 ,
612+ totalCost : 0.01 ,
613+ contextTokens : 150 ,
614+ totalCacheWrites : 10 ,
615+ totalCacheReads : 5 ,
616+ }
617+ expect ( hasTokenUsageChanged ( current , snapshot ) ) . toBe ( false )
618+ } )
384619} )
0 commit comments