@@ -10,6 +10,7 @@ import { DocPrepareCodeGenState } from '../../../amazonqDoc'
1010import { createMockSessionStateAction } from '../../amazonq/utils'
1111
1212import { createTestContext , setupTestHooks } from '../../amazonq/session/testSetup'
13+ const filesModule = require ( '../../../amazonq/util/files' )
1314
1415describe ( 'sessionStateDoc' , ( ) => {
1516 const context = createTestContext ( )
@@ -26,4 +27,206 @@ describe('sessionStateDoc', () => {
2627 } )
2728 } )
2829 } )
30+
31+ describe ( 'CodeGenBase generateCode log file handling' , ( ) => {
32+ const RunCommandLogFileName = '.amazonq/dev/run_command.log'
33+
34+ // Minimal stub for registerNewFiles to return newFileContents without the log file entry
35+ const registerNewFilesStub = sinon . stub ( )
36+ registerNewFilesStub . callsFake ( ( fs : any , newFileContents : any [ ] ) => {
37+ // In our test scenario, assume registerNewFiles returns the files as is (log file already spliced out)
38+ return newFileContents
39+ } )
40+ const getDeletedFileInfosStub = sinon . stub ( )
41+ getDeletedFileInfosStub . callsFake ( ( fs : any , deletedFiles : any [ ] ) => {
42+ // In our test scenario, assume getDeletedFileInfos returns the files as is
43+ return [ ]
44+ } )
45+
46+ // Fake CodeGenBase subclass for testing
47+ class TestCodeGen extends ( require ( '../../../amazonq/session/sessionState' ) as any ) . CodeGenBase {
48+ public generatedFiles : any [ ] = [ ]
49+ constructor ( config : any , tabID : string ) {
50+ super ( config , tabID )
51+ }
52+ protected handleProgress ( messenger : any , action : any , detail ?: string ) : void {
53+ // no-op
54+ }
55+ protected getScheme ( ) : string {
56+ return 'file'
57+ }
58+ protected getTimeoutErrorCode ( ) : string {
59+ return 'test_timeout'
60+ }
61+ protected handleGenerationComplete ( messenger : any , newFileInfo : any [ ] , action : any ) : void {
62+ this . generatedFiles = newFileInfo
63+ }
64+ protected handleError ( messenger : any , codegenResult : any ) : Error {
65+ throw new Error ( 'handleError called' )
66+ }
67+ }
68+
69+ // Common objects for testing
70+ let testConfig : any
71+ let fakeProxyClient : any
72+ let fsMock : any
73+ let telemetryMock : any
74+ let messengerMock : any
75+ let testAction : any
76+
77+ beforeEach ( ( ) => {
78+ fakeProxyClient = {
79+ getCodeGeneration : sinon . stub ( ) . resolves ( {
80+ codeGenerationStatus : { status : 'Complete' } ,
81+ codeGenerationRemainingIterationCount : 0 ,
82+ codeGenerationTotalIterationCount : 1 ,
83+ } ) ,
84+ exportResultArchive : sinon . stub ( ) ,
85+ }
86+
87+ testConfig = {
88+ conversationId : 'conv1' ,
89+ uploadId : 'upload1' ,
90+ workspaceRoots : [ '/workspace' ] ,
91+ proxyClient : fakeProxyClient ,
92+ }
93+
94+ fsMock = {
95+ stat : sinon . stub ( ) ,
96+ readFile : sinon . stub ( ) ,
97+ writeFile : sinon . stub ( ) ,
98+ }
99+
100+ telemetryMock = {
101+ setCodeGenerationResult : sinon . spy ( ) ,
102+ setNumberOfFilesGenerated : sinon . spy ( ) ,
103+ setAmazonqNumberOfReferences : sinon . spy ( ) ,
104+ setGenerateCodeIteration : sinon . spy ( ) ,
105+ setGenerateCodeLastInvocationTime : sinon . spy ( ) ,
106+ recordUserCodeGenerationTelemetry : sinon . spy ( ) ,
107+ }
108+
109+ messengerMock = {
110+ sendAnswer : sinon . spy ( ) ,
111+ }
112+
113+ testAction = {
114+ telemetry : telemetryMock ,
115+ fs : fsMock ,
116+ messenger : messengerMock ,
117+ uploadHistory : { } ,
118+ tokenSource : { token : { isCancellationRequested : false , onCancellationRequested : ( ) => { } } } ,
119+ }
120+ } )
121+
122+ afterEach ( ( ) => {
123+ sinon . restore ( )
124+ } )
125+
126+ it ( 'should append existing log file content to new log content and remove the log file entry when file exists' , async ( ) => {
127+ // Prepare exportResultArchive to return a log file and another file
128+ const logFileInfo = {
129+ name : RunCommandLogFileName ,
130+ content : 'newLog' ,
131+ }
132+ const otherFile = { name : 'other.ts' , content : 'other' }
133+
134+ fakeProxyClient . exportResultArchive . resolves ( {
135+ newFileContents : [ logFileInfo , otherFile ] ,
136+ deletedFiles : [ ] ,
137+ references : [ ] ,
138+ } )
139+
140+ // Stub fs.stat to simulate that file exists
141+ fsMock . stat . resolves ( { } )
142+ // Stub fs.readFile to simulate existing file content
143+ fsMock . readFile . resolves ( Buffer . from ( 'existing' ) )
144+
145+ // Stub registerNewFiles function used inside generateCode
146+ // const registerNewFilesOriginal = filesModule.registerNewFiles
147+ // filesModule.registerNewFiles = registerNewFilesStub
148+ sinon . stub ( filesModule , 'registerNewFiles' ) . callsFake ( registerNewFilesStub )
149+ sinon . stub ( filesModule , 'getDeletedFileInfos' ) . callsFake ( getDeletedFileInfosStub )
150+
151+ const testCodeGen = new TestCodeGen ( testConfig , 'tab1' )
152+ // Force a single polling iteration by simulating COMPLETE
153+
154+ // const result =
155+ await testCodeGen . generateCode ( {
156+ messenger : messengerMock ,
157+ fs : fsMock ,
158+ codeGenerationId : 'codegen1' ,
159+ telemetry : telemetryMock ,
160+ workspaceFolders : { } ,
161+ action : testAction ,
162+ } )
163+
164+ // Check that fs.stat and fs.readFile were called with the expected fileUri
165+ const expectedFilePath = `${ testConfig . workspaceRoots [ 0 ] } /${ RunCommandLogFileName } `
166+ const fileUri = vscode . Uri . file ( expectedFilePath )
167+ sinon . assert . calledWith ( fsMock . stat , fileUri )
168+ sinon . assert . calledWith ( fsMock . readFile , fileUri )
169+
170+ // Verify that the log file content was updated correctly: "existing" + "newLog"
171+ // And that the log file info was spliced out so that registerNewFiles gets only the other file.
172+ assert . strictEqual ( logFileInfo . content , 'existingnewLog' )
173+ assert . deepStrictEqual ( testCodeGen . generatedFiles , [ otherFile ] )
174+
175+ // Restore registerNewFiles to its original implementation
176+ sinon . restore ( )
177+ } )
178+
179+ it ( 'should create a new log file when log file does not exist' , async ( ) => {
180+ // Prepare exportResultArchive to return a log file and another file
181+ const logFileInfo = {
182+ name : RunCommandLogFileName ,
183+ content : 'newLog' ,
184+ }
185+ const otherFile = { name : 'other.ts' , content : 'other' }
186+
187+ fakeProxyClient . exportResultArchive . resolves ( {
188+ newFileContents : [ logFileInfo , otherFile ] ,
189+ deletedFiles : [ ] ,
190+ references : [ ] ,
191+ } )
192+
193+ // Stub fs.stat to simulate that file does not exist
194+ fsMock . stat . rejects ( new Error ( 'Not found' ) )
195+ // fs.writeFile should be called in this case
196+ fsMock . writeFile . resolves ( )
197+
198+ // Stub registerNewFiles function
199+ // sinon.replace(filesModule, 'registerNewFiles', registerNewFilesStub)
200+ sinon . stub ( filesModule , 'registerNewFiles' ) . callsFake ( registerNewFilesStub )
201+ sinon . stub ( filesModule , 'getDeletedFileInfos' ) . callsFake ( getDeletedFileInfosStub )
202+
203+ const testCodeGen = new TestCodeGen ( testConfig , 'tab1' )
204+
205+ // const result =
206+ await testCodeGen . generateCode ( {
207+ messenger : messengerMock ,
208+ fs : fsMock ,
209+ codeGenerationId : 'codegen2' ,
210+ telemetry : telemetryMock ,
211+ workspaceFolders : { } ,
212+ action : testAction ,
213+ } )
214+
215+ // Verify that fs.stat was called and then fs.writeFile was invoked
216+ const expectedFilePath = `${ testConfig . workspaceRoots [ 0 ] } /${ RunCommandLogFileName } `
217+ const fileUri = vscode . Uri . file ( expectedFilePath )
218+ sinon . assert . calledWith ( fsMock . stat , fileUri )
219+ sinon . assert . calledWith ( fsMock . writeFile , fileUri , new TextEncoder ( ) . encode ( 'newLog' ) , {
220+ create : true ,
221+ overwrite : true ,
222+ } )
223+ // fs.readFile should not be called
224+ sinon . assert . notCalled ( fsMock . readFile )
225+
226+ // Verify the log file has been removed from new file contents
227+ assert . deepStrictEqual ( testCodeGen . generatedFiles , [ otherFile ] )
228+
229+ sinon . restore ( )
230+ } )
231+ } )
29232} )
0 commit comments