@@ -8,7 +8,12 @@ import {
88 spyOn ,
99} from 'bun:test'
1010
11- import { trimMessagesToFitTokenLimit , messagesWithSystem } from '../messages'
11+ import { logger } from '../logger'
12+ import {
13+ trimMessagesToFitTokenLimit ,
14+ messagesWithSystem ,
15+ getPreviouslyReadFiles ,
16+ } from '../messages'
1217import * as tokenCounter from '../token-counter'
1318
1419import type { Message } from '@codebuff/common/types/messages/codebuff-message'
@@ -417,3 +422,383 @@ describe('trimMessagesToFitTokenLimit', () => {
417422 } )
418423 } )
419424} )
425+
426+ describe ( 'getPreviouslyReadFiles' , ( ) => {
427+ it ( 'returns empty array when no messages provided' , ( ) => {
428+ const result = getPreviouslyReadFiles ( [ ] )
429+ expect ( result ) . toEqual ( [ ] )
430+ } )
431+
432+ it ( 'returns empty array when no tool messages with relevant tool names' , ( ) => {
433+ const messages : Message [ ] = [
434+ { role : 'user' , content : 'hello' } ,
435+ { role : 'assistant' , content : 'hi' } ,
436+ {
437+ role : 'tool' ,
438+ content : {
439+ type : 'tool-result' ,
440+ toolName : 'write_file' ,
441+ toolCallId : 'test-id' ,
442+ output : [ { type : 'json' , value : { file : 'test.ts' } } ] ,
443+ } ,
444+ } ,
445+ ]
446+
447+ const result = getPreviouslyReadFiles ( messages )
448+ expect ( result ) . toEqual ( [ ] )
449+ } )
450+
451+ it ( 'extracts files from read_files tool messages' , ( ) => {
452+ const messages : Message [ ] = [
453+ {
454+ role : 'tool' ,
455+ content : {
456+ type : 'tool-result' ,
457+ toolName : 'read_files' ,
458+ toolCallId : 'test-id' ,
459+ output : [
460+ {
461+ type : 'json' ,
462+ value : [
463+ {
464+ path : 'src/test.ts' ,
465+ content : 'export function test() {}' ,
466+ referencedBy : { 'main.ts' : [ 'line 10' ] } ,
467+ } ,
468+ {
469+ path : 'src/utils.ts' ,
470+ content : 'export const utils = {}' ,
471+ } ,
472+ ] ,
473+ } ,
474+ ] ,
475+ } ,
476+ } ,
477+ ]
478+
479+ const result = getPreviouslyReadFiles ( messages )
480+ expect ( result ) . toEqual ( [
481+ {
482+ path : 'src/test.ts' ,
483+ content : 'export function test() {}' ,
484+ referencedBy : { 'main.ts' : [ 'line 10' ] } ,
485+ } ,
486+ {
487+ path : 'src/utils.ts' ,
488+ content : 'export const utils = {}' ,
489+ } ,
490+ ] )
491+ } )
492+
493+ it ( 'extracts files from find_files tool messages' , ( ) => {
494+ const messages : Message [ ] = [
495+ {
496+ role : 'tool' ,
497+ content : {
498+ type : 'tool-result' ,
499+ toolName : 'find_files' ,
500+ toolCallId : 'test-id' ,
501+ output : [
502+ {
503+ type : 'json' ,
504+ value : [
505+ {
506+ path : 'components/Button.tsx' ,
507+ content : 'export const Button = () => {}' ,
508+ } ,
509+ ] ,
510+ } ,
511+ ] ,
512+ } ,
513+ } ,
514+ ]
515+
516+ const result = getPreviouslyReadFiles ( messages )
517+ expect ( result ) . toEqual ( [
518+ {
519+ path : 'components/Button.tsx' ,
520+ content : 'export const Button = () => {}' ,
521+ } ,
522+ ] )
523+ } )
524+
525+ it ( 'extracts files from file_updates tool messages' , ( ) => {
526+ const messages : Message [ ] = [
527+ {
528+ role : 'tool' ,
529+ content : {
530+ type : 'tool-result' ,
531+ toolName : 'file_updates' ,
532+ toolCallId : 'test-id' ,
533+ output : [
534+ {
535+ type : 'json' ,
536+ value : [
537+ {
538+ path : 'config/database.ts' ,
539+ content : 'export const dbConfig = {}' ,
540+ referencedBy : { 'app.ts' : [ 'line 5' , 'line 20' ] } ,
541+ } ,
542+ ] ,
543+ } ,
544+ ] ,
545+ } ,
546+ } ,
547+ ]
548+
549+ const result = getPreviouslyReadFiles ( messages )
550+ expect ( result ) . toEqual ( [
551+ {
552+ path : 'config/database.ts' ,
553+ content : 'export const dbConfig = {}' ,
554+ referencedBy : { 'app.ts' : [ 'line 5' , 'line 20' ] } ,
555+ } ,
556+ ] )
557+ } )
558+
559+ it ( 'combines files from multiple tool messages' , ( ) => {
560+ const messages : Message [ ] = [
561+ {
562+ role : 'tool' ,
563+ content : {
564+ type : 'tool-result' ,
565+ toolName : 'read_files' ,
566+ toolCallId : 'test-id-1' ,
567+ output : [
568+ {
569+ type : 'json' ,
570+ value : [
571+ {
572+ path : 'file1.ts' ,
573+ content : 'content 1' ,
574+ } ,
575+ ] ,
576+ } ,
577+ ] ,
578+ } ,
579+ } ,
580+ {
581+ role : 'tool' ,
582+ content : {
583+ type : 'tool-result' ,
584+ toolName : 'find_files' ,
585+ toolCallId : 'test-id-2' ,
586+ output : [
587+ {
588+ type : 'json' ,
589+ value : [
590+ {
591+ path : 'file2.ts' ,
592+ content : 'content 2' ,
593+ } ,
594+ ] ,
595+ } ,
596+ ] ,
597+ } ,
598+ } ,
599+ {
600+ role : 'user' ,
601+ content : 'Some user message' ,
602+ } ,
603+ {
604+ role : 'tool' ,
605+ content : {
606+ type : 'tool-result' ,
607+ toolName : 'file_updates' ,
608+ toolCallId : 'test-id-3' ,
609+ output : [
610+ {
611+ type : 'json' ,
612+ value : [
613+ {
614+ path : 'file3.ts' ,
615+ content : 'content 3' ,
616+ } ,
617+ ] ,
618+ } ,
619+ ] ,
620+ } ,
621+ } ,
622+ ]
623+
624+ const result = getPreviouslyReadFiles ( messages )
625+ expect ( result ) . toEqual ( [
626+ { path : 'file1.ts' , content : 'content 1' } ,
627+ { path : 'file2.ts' , content : 'content 2' } ,
628+ { path : 'file3.ts' , content : 'content 3' } ,
629+ ] )
630+ } )
631+
632+ it ( 'handles contentOmittedForLength files by filtering them out' , ( ) => {
633+ const messages : Message [ ] = [
634+ {
635+ role : 'tool' ,
636+ content : {
637+ type : 'tool-result' ,
638+ toolName : 'read_files' ,
639+ toolCallId : 'test-id' ,
640+ output : [
641+ {
642+ type : 'json' ,
643+ value : [
644+ {
645+ path : 'small-file.ts' ,
646+ content : 'small content' ,
647+ } ,
648+ {
649+ path : 'large-file.ts' ,
650+ contentOmittedForLength : true ,
651+ } ,
652+ {
653+ path : 'another-small-file.ts' ,
654+ content : 'another small content' ,
655+ } ,
656+ ] ,
657+ } ,
658+ ] ,
659+ } ,
660+ } ,
661+ ]
662+
663+ const result = getPreviouslyReadFiles ( messages )
664+ expect ( result ) . toEqual ( [
665+ { path : 'small-file.ts' , content : 'small content' } ,
666+ { path : 'another-small-file.ts' , content : 'another small content' } ,
667+ ] )
668+ } )
669+
670+ it ( 'handles malformed tool message output gracefully' , ( ) => {
671+ const mockLoggerError = spyOn ( logger , 'error' ) . mockImplementation ( ( ) => { } )
672+
673+ const messages : Message [ ] = [
674+ {
675+ role : 'tool' ,
676+ content : {
677+ type : 'tool-result' ,
678+ toolName : 'read_files' ,
679+ toolCallId : 'test-id' ,
680+ output : null , // Invalid output
681+ } as any ,
682+ } ,
683+ ]
684+
685+ const result = getPreviouslyReadFiles ( messages )
686+ expect ( result ) . toEqual ( [ ] )
687+ expect ( mockLoggerError ) . toHaveBeenCalled ( )
688+
689+ mockLoggerError . mockRestore ( )
690+ } )
691+
692+ it ( 'handles find_files tool messages with error message instead of files' , ( ) => {
693+ const messages : Message [ ] = [
694+ {
695+ role : 'tool' ,
696+ content : {
697+ type : 'tool-result' ,
698+ toolName : 'find_files' ,
699+ toolCallId : 'test-id' ,
700+ output : [
701+ {
702+ type : 'json' ,
703+ value : {
704+ message : 'No files found matching the criteria' ,
705+ } ,
706+ } ,
707+ ] ,
708+ } ,
709+ } ,
710+ ]
711+
712+ const result = getPreviouslyReadFiles ( messages )
713+ expect ( result ) . toEqual ( [ ] )
714+ } )
715+
716+ it ( 'ignores non-tool messages' , ( ) => {
717+ const messages : Message [ ] = [
718+ { role : 'user' , content : 'hello' } ,
719+ { role : 'assistant' , content : 'hi there' } ,
720+ { role : 'system' , content : 'system message' } ,
721+ {
722+ role : 'tool' ,
723+ content : {
724+ type : 'tool-result' ,
725+ toolName : 'read_files' ,
726+ toolCallId : 'test-id' ,
727+ output : [
728+ {
729+ type : 'json' ,
730+ value : [
731+ {
732+ path : 'test.ts' ,
733+ content : 'test content' ,
734+ } ,
735+ ] ,
736+ } ,
737+ ] ,
738+ } ,
739+ } ,
740+ ]
741+
742+ const result = getPreviouslyReadFiles ( messages )
743+ expect ( result ) . toEqual ( [ { path : 'test.ts' , content : 'test content' } ] )
744+ } )
745+
746+ it ( 'handles empty file arrays in tool output' , ( ) => {
747+ const messages : Message [ ] = [
748+ {
749+ role : 'tool' ,
750+ content : {
751+ type : 'tool-result' ,
752+ toolName : 'read_files' ,
753+ toolCallId : 'test-id' ,
754+ output : [
755+ {
756+ type : 'json' ,
757+ value : [ ] , // Empty array
758+ } ,
759+ ] ,
760+ } ,
761+ } ,
762+ ]
763+
764+ const result = getPreviouslyReadFiles ( messages )
765+ expect ( result ) . toEqual ( [ ] )
766+ } )
767+
768+ it ( 'handles multiple outputs in single tool message' , ( ) => {
769+ const messages : Message [ ] = [
770+ {
771+ role : 'tool' ,
772+ content : {
773+ type : 'tool-result' ,
774+ toolName : 'read_files' ,
775+ toolCallId : 'test-id' ,
776+ output : [
777+ {
778+ type : 'json' ,
779+ value : [
780+ {
781+ path : 'file1.ts' ,
782+ content : 'content 1' ,
783+ } ,
784+ ] ,
785+ } ,
786+ {
787+ type : 'json' ,
788+ value : [
789+ {
790+ path : 'file2.ts' ,
791+ content : 'content 2' ,
792+ } ,
793+ ] ,
794+ } ,
795+ ] ,
796+ } ,
797+ } ,
798+ ]
799+
800+ const result = getPreviouslyReadFiles ( messages )
801+ // Function uses output[0], so only first output is processed
802+ expect ( result ) . toEqual ( [ { path : 'file1.ts' , content : 'content 1' } ] )
803+ } )
804+ } )
0 commit comments