@@ -13,37 +13,56 @@ import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo
13
13
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
14
14
import com.github.tomakehurst.wiremock.http.Body
15
15
import com.github.tomakehurst.wiremock.junit.WireMockRule
16
+ import com.intellij.openapi.application.ApplicationManager
16
17
import com.intellij.openapi.project.Project
18
+ import com.intellij.testFramework.DisposableRule
19
+ import com.intellij.testFramework.replaceService
20
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
21
+ import kotlinx.coroutines.TimeoutCancellationException
22
+ import kotlinx.coroutines.test.StandardTestDispatcher
17
23
import kotlinx.coroutines.test.TestScope
24
+ import kotlinx.coroutines.test.advanceUntilIdle
18
25
import kotlinx.coroutines.test.runTest
26
+ import kotlinx.coroutines.withContext
19
27
import org.assertj.core.api.Assertions.assertThat
20
28
import org.junit.Before
21
29
import org.junit.Rule
22
30
import org.junit.jupiter.api.assertThrows
23
31
import org.mockito.kotlin.any
24
32
import org.mockito.kotlin.doReturn
33
+ import org.mockito.kotlin.mock
25
34
import org.mockito.kotlin.spy
26
35
import org.mockito.kotlin.stub
27
36
import org.mockito.kotlin.times
28
37
import org.mockito.kotlin.verify
29
38
import org.mockito.kotlin.whenever
39
+ import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
30
40
import software.aws.toolkits.jetbrains.services.amazonq.project.EncoderServer
31
41
import software.aws.toolkits.jetbrains.services.amazonq.project.IndexRequest
42
+ import software.aws.toolkits.jetbrains.services.amazonq.project.IndexUpdateMode
43
+ import software.aws.toolkits.jetbrains.services.amazonq.project.InlineBm25Chunk
32
44
import software.aws.toolkits.jetbrains.services.amazonq.project.LspMessage
33
45
import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextProvider
34
46
import software.aws.toolkits.jetbrains.services.amazonq.project.QueryChatRequest
47
+ import software.aws.toolkits.jetbrains.services.amazonq.project.QueryInlineCompletionRequest
35
48
import software.aws.toolkits.jetbrains.services.amazonq.project.RelevantDocument
36
49
import software.aws.toolkits.jetbrains.services.amazonq.project.UpdateIndexRequest
50
+ import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
37
51
import software.aws.toolkits.jetbrains.utils.rules.CodeInsightTestFixtureRule
38
52
import software.aws.toolkits.jetbrains.utils.rules.JavaCodeInsightTestFixtureRule
39
53
import java.net.ConnectException
40
54
import kotlin.test.Test
41
55
56
+ @OptIn(ExperimentalCoroutinesApi ::class )
42
57
class ProjectContextProviderTest {
43
58
@Rule
44
59
@JvmField
45
60
val projectRule: CodeInsightTestFixtureRule = JavaCodeInsightTestFixtureRule ()
46
61
62
+ @Rule
63
+ @JvmField
64
+ val disposableRule: DisposableRule = DisposableRule ()
65
+
47
66
@Rule
48
67
@JvmField
49
68
val wireMock: WireMockRule = createMockServer()
@@ -56,21 +75,23 @@ class ProjectContextProviderTest {
56
75
57
76
private val mapper = jacksonObjectMapper()
58
77
78
+ private val dispatcher = StandardTestDispatcher ()
79
+
59
80
@Before
60
81
fun setup () {
61
82
encoderServer = spy(EncoderServer (project))
62
83
encoderServer.stub { on { port } doReturn wireMock.port() }
63
84
64
- sut = ProjectContextProvider (project, encoderServer, TestScope ())
85
+ sut = ProjectContextProvider (project, encoderServer, TestScope (context = dispatcher ))
65
86
66
87
// initialization
67
88
stubFor(any(urlPathEqualTo(" /initialize" )).willReturn(aResponse().withStatus(200 ).withResponseBody(Body (" initialize response" ))))
68
89
69
90
// build index
70
- stubFor(any(urlPathEqualTo(" /indexFiles " )).willReturn(aResponse().withStatus(200 ).withResponseBody(Body (" initialize response" ))))
91
+ stubFor(any(urlPathEqualTo(" /buildIndex " )).willReturn(aResponse().withStatus(200 ).withResponseBody(Body (" initialize response" ))))
71
92
72
93
// update index
73
- stubFor(any(urlPathEqualTo(" /updateIndex " )).willReturn(aResponse().withStatus(200 ).withResponseBody(Body (" initialize response" ))))
94
+ stubFor(any(urlPathEqualTo(" /updateIndexV2 " )).willReturn(aResponse().withStatus(200 ).withResponseBody(Body (" initialize response" ))))
74
95
75
96
// query
76
97
stubFor(
@@ -80,6 +101,15 @@ class ProjectContextProviderTest {
80
101
.withResponseBody(Body (validQueryChatResponse))
81
102
)
82
103
)
104
+ stubFor(
105
+ any(urlPathEqualTo(" /queryInlineProjectContext" )).willReturn(
106
+ aResponse()
107
+ .withStatus(200 )
108
+ .withResponseBody(
109
+ Body (validQueryInlineResponse)
110
+ )
111
+ )
112
+ )
83
113
84
114
stubFor(
85
115
any(urlPathEqualTo(" /getUsage" ))
@@ -92,32 +122,73 @@ class ProjectContextProviderTest {
92
122
}
93
123
94
124
@Test
95
- fun `Lsp endpoint are correct ` () {
125
+ fun `Lsp endpoint correctness ` () {
96
126
assertThat(LspMessage .Initialize .endpoint).isEqualTo(" initialize" )
97
- assertThat(LspMessage .Index .endpoint).isEqualTo(" indexFiles" )
127
+ assertThat(LspMessage .Index .endpoint).isEqualTo(" buildIndex" )
128
+ assertThat(LspMessage .UpdateIndex .endpoint).isEqualTo(" updateIndexV2" )
98
129
assertThat(LspMessage .QueryChat .endpoint).isEqualTo(" query" )
130
+ assertThat(LspMessage .QueryInlineCompletion .endpoint).isEqualTo(" queryInlineProjectContext" )
99
131
assertThat(LspMessage .GetUsageMetrics .endpoint).isEqualTo(" getUsage" )
100
132
}
101
133
102
134
@Test
103
- fun `index should send files within the project to lsp` () {
135
+ fun `index should send files within the project to lsp - vector index enabled` () {
136
+ ApplicationManager .getApplication().replaceService(
137
+ CodeWhispererSettings ::class .java,
138
+ mock { on { isProjectContextEnabled() } doReturn true },
139
+ disposableRule.disposable
140
+ )
141
+
142
+ projectRule.fixture.addFileToProject(" Foo.java" , " foo" )
143
+ projectRule.fixture.addFileToProject(" Bar.java" , " bar" )
144
+ projectRule.fixture.addFileToProject(" Baz.java" , " baz" )
145
+
146
+ sut.index()
147
+
148
+ val request = IndexRequest (listOf (" /src/Foo.java" , " /src/Bar.java" , " /src/Baz.java" ), " /src" , " all" , " " )
149
+ assertThat(request.filePaths).hasSize(3 )
150
+ assertThat(request.filePaths).satisfies({
151
+ it.contains(" /src/Foo.java" ) &&
152
+ it.contains(" /src/Baz.java" ) &&
153
+ it.contains(" /src/Bar.java" )
154
+ })
155
+ assertThat(request.config).isEqualTo(" all" )
156
+
157
+ wireMock.verify(
158
+ 1 ,
159
+ postRequestedFor(urlPathEqualTo(" /buildIndex" ))
160
+ .withHeader(" Content-Type" , equalTo(" text/plain" ))
161
+ // comment it out because order matters and will cause json string different
162
+ // .withRequestBody(equalTo(encryptedRequest))
163
+ )
164
+ }
165
+
166
+ @Test
167
+ fun `index should send files within the project to lsp - vector index disabled` () {
168
+ ApplicationManager .getApplication().replaceService(
169
+ CodeWhispererSettings ::class .java,
170
+ mock { on { isProjectContextEnabled() } doReturn false },
171
+ disposableRule.disposable
172
+ )
173
+
104
174
projectRule.fixture.addFileToProject(" Foo.java" , " foo" )
105
175
projectRule.fixture.addFileToProject(" Bar.java" , " bar" )
106
176
projectRule.fixture.addFileToProject(" Baz.java" , " baz" )
107
177
108
178
sut.index()
109
179
110
- val request = IndexRequest (listOf (" /src/Foo.java" , " /src/Bar.java" , " /src/Baz.java" ), " /src" , false )
180
+ val request = IndexRequest (listOf (" /src/Foo.java" , " /src/Bar.java" , " /src/Baz.java" ), " /src" , " default " , " " )
111
181
assertThat(request.filePaths).hasSize(3 )
112
182
assertThat(request.filePaths).satisfies({
113
183
it.contains(" /src/Foo.java" ) &&
114
184
it.contains(" /src/Baz.java" ) &&
115
185
it.contains(" /src/Bar.java" )
116
186
})
187
+ assertThat(request.config).isEqualTo(" default" )
117
188
118
189
wireMock.verify(
119
190
1 ,
120
- postRequestedFor(urlPathEqualTo(" /indexFiles " ))
191
+ postRequestedFor(urlPathEqualTo(" /buildIndex " ))
121
192
.withHeader(" Content-Type" , equalTo(" text/plain" ))
122
193
// comment it out because order matters and will cause json string different
123
194
// .withRequestBody(equalTo(encryptedRequest))
@@ -126,17 +197,17 @@ class ProjectContextProviderTest {
126
197
127
198
@Test
128
199
fun `updateIndex should send correct encrypted request to lsp` () {
129
- sut.updateIndex(" foo.java" )
130
- val request = UpdateIndexRequest (" foo.java" )
200
+ sut.updateIndex(listOf ( " foo.java" ), IndexUpdateMode . UPDATE )
201
+ val request = UpdateIndexRequest (listOf ( " foo.java" ), IndexUpdateMode . UPDATE .command )
131
202
val requestJson = mapper.writeValueAsString(request)
132
203
133
- assertThat(mapper.readTree(requestJson)).isEqualTo(mapper.readTree(""" { "filePath ": "foo.java" }""" ))
204
+ assertThat(mapper.readTree(requestJson)).isEqualTo(mapper.readTree(""" { "filePaths ": [ "foo.java"], "mode": "update " }""" ))
134
205
135
206
val encryptedRequest = encoderServer.encrypt(requestJson)
136
207
137
208
wireMock.verify(
138
209
1 ,
139
- postRequestedFor(urlPathEqualTo(" /updateIndex " ))
210
+ postRequestedFor(urlPathEqualTo(" /updateIndexV2 " ))
140
211
.withHeader(" Content-Type" , equalTo(" text/plain" ))
141
212
.withRequestBody(equalTo(encryptedRequest))
142
213
)
@@ -161,6 +232,26 @@ class ProjectContextProviderTest {
161
232
)
162
233
}
163
234
235
+ @Test
236
+ fun `queryInline should send correct encrypted request to lsp` () = runTest {
237
+ sut = ProjectContextProvider (project, encoderServer, this )
238
+ sut.queryInline(" foo" , " Foo.java" )
239
+ advanceUntilIdle()
240
+
241
+ val request = QueryInlineCompletionRequest (" foo" , " Foo.java" )
242
+ val requestJson = mapper.writeValueAsString(request)
243
+
244
+ assertThat(mapper.readTree(requestJson)).isEqualTo(mapper.readTree(""" { "query": "foo", "filePath": "Foo.java" }""" ))
245
+
246
+ val encryptedRequest = encoderServer.encrypt(requestJson)
247
+ wireMock.verify(
248
+ 1 ,
249
+ postRequestedFor(urlPathEqualTo(" /queryInlineProjectContext" ))
250
+ .withHeader(" Content-Type" , equalTo(" text/plain" ))
251
+ .withRequestBody(equalTo(encryptedRequest))
252
+ )
253
+ }
254
+
164
255
@Test
165
256
fun `query chat should return empty if result set non deserializable` () = runTest {
166
257
stubFor(
@@ -200,12 +291,92 @@ class ProjectContextProviderTest {
200
291
)
201
292
}
202
293
294
+ @Test
295
+ fun `query inline should throw if resultset not deserializable` () {
296
+ assertThrows<Exception > {
297
+ runTest {
298
+ sut = ProjectContextProvider (project, encoderServer, this )
299
+ stubFor(
300
+ any(urlPathEqualTo(" /queryInlineProjectContext" )).willReturn(
301
+ aResponse().withStatus(200 ).withResponseBody(
302
+ Body (
303
+ """
304
+ [
305
+ "foo", "bar"
306
+ ]
307
+ """ .trimIndent()
308
+ )
309
+ )
310
+ )
311
+ )
312
+
313
+ assertThrows<Exception > {
314
+ sut.queryInline(" foo" , " filepath" )
315
+ advanceUntilIdle()
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ @Test
322
+ fun `query inline should return deserialized bm25 chunks` () = runTest {
323
+ sut = ProjectContextProvider (project, encoderServer, this )
324
+ advanceUntilIdle()
325
+ val r = sut.queryInline(" foo" , " filepath" )
326
+ assertThat(r).hasSize(3 )
327
+ assertThat(r[0 ]).isEqualTo(
328
+ InlineBm25Chunk (
329
+ " content1" ,
330
+ " file1" ,
331
+ 0.1
332
+ )
333
+ )
334
+ assertThat(r[1 ]).isEqualTo(
335
+ InlineBm25Chunk (
336
+ " content2" ,
337
+ " file2" ,
338
+ 0.2
339
+ )
340
+ )
341
+ assertThat(r[2 ]).isEqualTo(
342
+ InlineBm25Chunk (
343
+ " content3" ,
344
+ " file3" ,
345
+ 0.3
346
+ )
347
+ )
348
+ }
349
+
203
350
@Test
204
351
fun `get usage should return memory, cpu usage` () = runTest {
205
352
val r = sut.getUsage()
206
353
assertThat(r).isEqualTo(ProjectContextProvider .Usage (123 , 456 ))
207
354
}
208
355
356
+ @Test
357
+ fun `queryInline should throw if time elapsed is greater than 50ms` () = runTest {
358
+ assertThrows<TimeoutCancellationException > {
359
+ sut = ProjectContextProvider (project, encoderServer, this )
360
+ stubFor(
361
+ any(urlPathEqualTo(" /queryInlineProjectContext" )).willReturn(
362
+ aResponse()
363
+ .withStatus(200 )
364
+ .withResponseBody(
365
+ Body (validQueryInlineResponse)
366
+ )
367
+ .withFixedDelay(51 ) // 10 sec
368
+ )
369
+ )
370
+
371
+ // it won't throw if it's executed within TestDispatcher context
372
+ withContext(getCoroutineBgContext()) {
373
+ sut.queryInline(" foo" , " bar" )
374
+ }
375
+
376
+ advanceUntilIdle()
377
+ }
378
+ }
379
+
209
380
@Test
210
381
fun `test index payload is encrypted` () = runTest {
211
382
whenever(encoderServer.port).thenReturn(3000 )
@@ -231,6 +402,27 @@ class ProjectContextProviderTest {
231
402
private fun createMockServer () = WireMockRule (wireMockConfig().dynamicPort())
232
403
}
233
404
405
+ // language=JSON
406
+ val validQueryInlineResponse = """
407
+ [
408
+ {
409
+ "content": "content1",
410
+ "filePath": "file1",
411
+ "score": 0.1
412
+ },
413
+ {
414
+ "content": "content2",
415
+ "filePath": "file2",
416
+ "score": 0.2
417
+ },
418
+ {
419
+ "content": "content3",
420
+ "filePath": "file3",
421
+ "score": 0.3
422
+ }
423
+ ]
424
+ """ .trimIndent()
425
+
234
426
// language=JSON
235
427
val validQueryChatResponse = """
236
428
[
0 commit comments