Skip to content

Commit da346b4

Browse files
authored
Merge pull request #166 from rostilos/1.5.5-rc
1.5.5 rc
2 parents 5165967 + f0c426e commit da346b4

141 files changed

Lines changed: 10698 additions & 4816 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

deployment/.env.sample

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
INTERNAL_API_SECRET=secret-change-me
22
POSTGRES_DB=codecrow_ai
33
POSTGRES_USER=codecrow
4-
POSTGRES_PASSWORD=codecrow_pass
4+
POSTGRES_PASSWORD=codecrow_pass
5+
PGADMIN_DEFAULT_EMAIL=pgadmin@localhost
6+
PGADMIN_DEFAULT_PASSWORD=CHANGE_ME_TO_A_STRONG_PGADMIN_PASSWORD

deployment/ci/server-init.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ POSTGRES_DB=codecrow_ai
119119
POSTGRES_USER=codecrow_user
120120
POSTGRES_PASSWORD=CHANGE_ME_TO_YOUR_ACTUAL_DB_PASSWORD
121121
122+
# pgAdmin (bind to 127.0.0.1 only; expose externally only via authenticated tunnel)
123+
PGADMIN_DEFAULT_EMAIL=pgadmin@localhost
124+
PGADMIN_DEFAULT_PASSWORD=CHANGE_ME_TO_A_STRONG_PGADMIN_PASSWORD
125+
122126
# Internal API secret (service-to-service auth)
123127
INTERNAL_API_SECRET=CHANGE_ME_GENERATE_WITH_openssl_rand_hex_32
124128
SAMPLE

deployment/config/inference-orchestrator/.env.sample

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ SERVICE_SECRET=change-me-to-a-random-secret
6868
# Max cache entries
6969
# RAG_CACHE_MAX_SIZE=100
7070

71+
# === New Relic APM ===
72+
# Path to newrelic.ini inside the container (mounted via docker-compose volume).
73+
# Set this to enable New Relic Python agent instrumentation.
74+
# On production the file is mounted at /app/newrelic.ini from
75+
# /opt/codecrow/config/inference-orchestrator/newrelic.ini on the host.
76+
# Leave commented out to disable New Relic (e.g. local dev).
77+
# NEW_RELIC_CONFIG_FILE=/app/newrelic.ini
78+
7179
# === Prompt Logging (Debug) ===
7280
# Enable prompt logging for debugging
7381
# PROMPT_LOG_ENABLED=false

deployment/docker-compose.prod.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ services:
2424
retries: 5
2525
start_period: 30s
2626

27+
pgadmin:
28+
image: dpage/pgadmin4:9
29+
container_name: codecrow-pgadmin
30+
environment:
31+
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin@localhost}
32+
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:?PGADMIN_DEFAULT_PASSWORD must be set in .env}
33+
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "True"
34+
PGADMIN_DISABLE_POSTFIX: "True"
35+
PGADMIN_LISTEN_PORT: 80
36+
ports:
37+
- "127.0.0.1:5050:80"
38+
depends_on:
39+
postgres:
40+
condition: service_healthy
41+
volumes:
42+
- pgadmin_data:/var/lib/pgadmin
43+
networks:
44+
- codecrow-network
45+
restart: unless-stopped
46+
2747
redis:
2848
image: redis:7-alpine
2949
container_name: codecrow-redis
@@ -272,6 +292,9 @@ volumes:
272292
qdrant_data:
273293
name: qdrant_data
274294
driver: local
295+
pgadmin_data:
296+
name: pgadmin_data
297+
driver: local
275298
web_logs:
276299
name: web_logs
277300
driver: local

deployment/setup.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,22 @@ main() {
160160
local DB_PASSWORD
161161
DB_PASSWORD=$(generate_base64)
162162

163+
# pgAdmin credentials — auto-generated, stored only in .env
164+
local PGADMIN_PASSWORD
165+
PGADMIN_PASSWORD=$(generate_base64)
166+
163167
# Write all shared secrets to .env (docker-compose.yml reads this automatically)
164168
cat > "$ROOT_ENV" <<EOF
165169
INTERNAL_API_SECRET=${INTERNAL_SECRET}
166170
POSTGRES_DB=codecrow_ai
167171
POSTGRES_USER=codecrow_user
168172
POSTGRES_PASSWORD=${DB_PASSWORD}
173+
PGADMIN_DEFAULT_EMAIL=pgadmin@localhost
174+
PGADMIN_DEFAULT_PASSWORD=${PGADMIN_PASSWORD}
169175
EOF
170176
success "Internal API secret (synced: application.properties + .env)"
171177
success "Database credentials (auto-generated in .env)"
178+
success "pgAdmin credentials (auto-generated in .env)"
172179

173180
# Service / RAG secret (inference-orchestrator <-> rag-pipeline)
174181
local SERVICE_SECRET

java-ecosystem/libs/analysis-api/src/main/java/org/rostilos/codecrow/analysisapi/rag/RagOperationsService.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ void triggerIncrementalUpdate(
6161
Consumer<Map<String, Object>> eventConsumer
6262
);
6363

64+
// ==========================================================================
65+
// PR-SPECIFIC RAG OPERATIONS
66+
// ==========================================================================
67+
68+
/**
69+
* Delete all RAG-indexed points for a specific PR from the project's collection.
70+
* Called after PR analysis completes or when a PR is closed/merged.
71+
* This operation is idempotent — safe to call even if no points exist for this PR.
72+
*
73+
* @param project The project
74+
* @param prNumber The PR number whose indexed data should be cleaned up
75+
* @return true if cleanup succeeded or nothing to clean, false on error
76+
*/
77+
default boolean deletePrFiles(Project project, int prNumber) {
78+
return true; // Default: no-op
79+
}
80+
6481
// ==========================================================================
6582
// MULTI-BRANCH INDEX OPERATIONS
6683
// ==========================================================================

java-ecosystem/libs/analysis-engine/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@
129129
<artifactId>spring-test</artifactId>
130130
<scope>test</scope>
131131
</dependency>
132+
133+
<!-- Test Support -->
134+
<dependency>
135+
<groupId>org.rostilos.codecrow</groupId>
136+
<artifactId>codecrow-test-support</artifactId>
137+
<scope>test</scope>
138+
</dependency>
132139
</dependencies>
133140

134141
<build>
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package org.rostilos.codecrow.analysisengine;
2+
3+
import okhttp3.mockwebserver.MockResponse;
4+
import okhttp3.mockwebserver.MockWebServer;
5+
import okhttp3.mockwebserver.RecordedRequest;
6+
import org.junit.jupiter.api.*;
7+
8+
import java.io.IOException;
9+
import java.util.concurrent.TimeUnit;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
/**
14+
* Integration test for AI inference HTTP client behavior using MockWebServer.
15+
* Verifies request/response handling, timeouts, retries, and error scenarios.
16+
*/
17+
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
18+
class AiClientIT {
19+
20+
private MockWebServer mockServer;
21+
22+
@BeforeEach
23+
void setup() throws IOException {
24+
mockServer = new MockWebServer();
25+
mockServer.start();
26+
}
27+
28+
@AfterEach
29+
void teardown() throws IOException {
30+
mockServer.shutdown();
31+
}
32+
33+
@Test
34+
@Order(1)
35+
@DisplayName("Should send correct request format to AI provider")
36+
void shouldSendCorrectRequestFormat() throws Exception {
37+
mockServer.enqueue(new MockResponse()
38+
.setBody("{\"choices\":[{\"message\":{\"content\":\"test response\"}}]}")
39+
.addHeader("Content-Type", "application/json")
40+
.setResponseCode(200));
41+
42+
String baseUrl = mockServer.url("/v1/chat/completions").toString();
43+
44+
// Simulate HTTP client call
45+
okhttp3.OkHttpClient client = new okhttp3.OkHttpClient();
46+
okhttp3.RequestBody body = okhttp3.RequestBody.create(
47+
"{\"model\":\"gpt-4\",\"messages\":[{\"role\":\"user\",\"content\":\"analyze code\"}]}",
48+
okhttp3.MediaType.parse("application/json"));
49+
okhttp3.Request request = new okhttp3.Request.Builder()
50+
.url(baseUrl)
51+
.post(body)
52+
.addHeader("Authorization", "Bearer test-key")
53+
.build();
54+
55+
try (okhttp3.Response response = client.newCall(request).execute()) {
56+
assertThat(response.isSuccessful()).isTrue();
57+
String responseBody = response.body().string();
58+
assertThat(responseBody).contains("test response");
59+
}
60+
61+
RecordedRequest recorded = mockServer.takeRequest(5, TimeUnit.SECONDS);
62+
assertThat(recorded).isNotNull();
63+
assertThat(recorded.getHeader("Authorization")).isEqualTo("Bearer test-key");
64+
assertThat(recorded.getBody().readUtf8()).contains("gpt-4");
65+
}
66+
67+
@Test
68+
@Order(2)
69+
@DisplayName("Should handle 429 rate limit response")
70+
void shouldHandleRateLimitResponse() throws Exception {
71+
mockServer.enqueue(new MockResponse()
72+
.setResponseCode(429)
73+
.addHeader("Retry-After", "5")
74+
.setBody("{\"error\":{\"message\":\"Rate limited\"}}"));
75+
76+
String baseUrl = mockServer.url("/v1/chat/completions").toString();
77+
78+
okhttp3.OkHttpClient client = new okhttp3.OkHttpClient();
79+
okhttp3.Request request = new okhttp3.Request.Builder()
80+
.url(baseUrl)
81+
.post(okhttp3.RequestBody.create("{}", okhttp3.MediaType.parse("application/json")))
82+
.build();
83+
84+
try (okhttp3.Response response = client.newCall(request).execute()) {
85+
assertThat(response.code()).isEqualTo(429);
86+
}
87+
}
88+
89+
@Test
90+
@Order(3)
91+
@DisplayName("Should handle 500 server error")
92+
void shouldHandleServerError() throws Exception {
93+
mockServer.enqueue(new MockResponse()
94+
.setResponseCode(500)
95+
.setBody("{\"error\":{\"message\":\"Internal server error\"}}"));
96+
97+
String baseUrl = mockServer.url("/v1/chat/completions").toString();
98+
99+
okhttp3.OkHttpClient client = new okhttp3.OkHttpClient();
100+
okhttp3.Request request = new okhttp3.Request.Builder()
101+
.url(baseUrl)
102+
.post(okhttp3.RequestBody.create("{}", okhttp3.MediaType.parse("application/json")))
103+
.build();
104+
105+
try (okhttp3.Response response = client.newCall(request).execute()) {
106+
assertThat(response.code()).isEqualTo(500);
107+
}
108+
}
109+
110+
@Test
111+
@Order(4)
112+
@DisplayName("Should handle empty response body gracefully")
113+
void shouldHandleEmptyResponseBody() throws Exception {
114+
mockServer.enqueue(new MockResponse()
115+
.setResponseCode(200)
116+
.setBody("")
117+
.addHeader("Content-Type", "application/json"));
118+
119+
String baseUrl = mockServer.url("/v1/chat/completions").toString();
120+
121+
okhttp3.OkHttpClient client = new okhttp3.OkHttpClient();
122+
okhttp3.Request request = new okhttp3.Request.Builder()
123+
.url(baseUrl)
124+
.post(okhttp3.RequestBody.create("{}", okhttp3.MediaType.parse("application/json")))
125+
.build();
126+
127+
try (okhttp3.Response response = client.newCall(request).execute()) {
128+
assertThat(response.isSuccessful()).isTrue();
129+
assertThat(response.body().string()).isEmpty();
130+
}
131+
}
132+
133+
@Test
134+
@Order(5)
135+
@DisplayName("Should handle large response payload")
136+
void shouldHandleLargeResponsePayload() throws Exception {
137+
String largeContent = "x".repeat(500_000);
138+
mockServer.enqueue(new MockResponse()
139+
.setResponseCode(200)
140+
.setBody("{\"choices\":[{\"message\":{\"content\":\"" + largeContent + "\"}}]}")
141+
.addHeader("Content-Type", "application/json"));
142+
143+
String baseUrl = mockServer.url("/v1/chat/completions").toString();
144+
145+
okhttp3.OkHttpClient client = new okhttp3.OkHttpClient();
146+
okhttp3.Request request = new okhttp3.Request.Builder()
147+
.url(baseUrl)
148+
.post(okhttp3.RequestBody.create("{}", okhttp3.MediaType.parse("application/json")))
149+
.build();
150+
151+
try (okhttp3.Response response = client.newCall(request).execute()) {
152+
assertThat(response.isSuccessful()).isTrue();
153+
assertThat(response.body().string()).contains(largeContent);
154+
}
155+
}
156+
}

java-ecosystem/libs/analysis-engine/src/main/java/org/rostilos/codecrow/analysisengine/aiclient/AiAnalysisClient.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
44
import org.rostilos.codecrow.analysisengine.dto.request.ai.AiAnalysisRequest;
5+
import org.rostilos.codecrow.analysisengine.dto.request.ai.AiAnalysisRequestImpl;
56
import org.slf4j.Logger;
67
import org.slf4j.LoggerFactory;
78
import org.springframework.beans.factory.annotation.Qualifier;
@@ -11,6 +12,7 @@
1112

1213
import java.io.IOException;
1314
import java.security.GeneralSecurityException;
15+
import java.util.LinkedHashMap;
1416
import java.util.List;
1517
import java.util.Map;
1618
import java.util.UUID;
@@ -61,7 +63,7 @@ public Map<String, Object> performAnalysis(AiAnalysisRequest request,
6163
// Wrap the request with the jobId
6264
Map<String, Object> jobPayload = Map.of(
6365
"job_id", jobId,
64-
"request", request);
66+
"request", buildSerializableRequestPayload(request));
6567

6668
String jsonPayload = objectMapper.writeValueAsString(jobPayload);
6769

@@ -142,6 +144,45 @@ public Map<String, Object> performAnalysis(AiAnalysisRequest request,
142144
}
143145
}
144146

147+
private Map<String, Object> buildSerializableRequestPayload(AiAnalysisRequest request) {
148+
Map<String, Object> payload = new LinkedHashMap<>();
149+
payload.put("projectId", request.getProjectId());
150+
payload.put("projectWorkspace", request.getProjectWorkspace());
151+
payload.put("projectNamespace", request.getProjectNamespace());
152+
payload.put("projectVcsWorkspace", request.getProjectVcsWorkspace());
153+
payload.put("projectVcsRepoSlug", request.getProjectVcsRepoSlug());
154+
payload.put("aiProvider", request.getAiProvider());
155+
payload.put("aiModel", request.getAiModel());
156+
payload.put("aiApiKey", request.getAiApiKey());
157+
payload.put("pullRequestId", request.getPullRequestId());
158+
payload.put("oAuthClient", request.getOAuthClient());
159+
payload.put("oAuthSecret", request.getOAuthSecret());
160+
payload.put("accessToken", request.getAccessToken());
161+
payload.put("maxAllowedTokens", request.getMaxAllowedTokens());
162+
payload.put("useLocalMcp", request.getUseLocalMcp());
163+
payload.put("useMcpTools", request.getUseMcpTools());
164+
payload.put("analysisType", request.getAnalysisType());
165+
payload.put("vcsProvider", request.getVcsProvider());
166+
payload.put("prTitle", request.getPrTitle());
167+
payload.put("prDescription", request.getPrDescription());
168+
payload.put("changedFiles", request.getChangedFiles());
169+
payload.put("deletedFiles", request.getDeletedFiles());
170+
payload.put("diffSnippets", request.getDiffSnippets());
171+
payload.put("targetBranchName", request.getTargetBranchName());
172+
payload.put("sourceBranchName", request.getSourceBranchName());
173+
payload.put("rawDiff", request.getRawDiff());
174+
payload.put("analysisMode", request.getAnalysisMode());
175+
payload.put("deltaDiff", request.getDeltaDiff());
176+
payload.put("previousCommitHash", request.getPreviousCommitHash());
177+
payload.put("currentCommitHash", request.getCurrentCommitHash());
178+
payload.put("reconciliationFileContents", request.getReconciliationFileContents());
179+
if (request instanceof AiAnalysisRequestImpl impl) {
180+
payload.put("enrichmentData", impl.getEnrichmentData());
181+
payload.put("projectRules", impl.getProjectRules());
182+
}
183+
return payload;
184+
}
185+
145186
private Map<String, Object> extractAndValidateAnalysisData(Map<String, Object> result) throws IOException {
146187
try {
147188
if (result == null) {

java-ecosystem/libs/analysis-engine/src/main/java/org/rostilos/codecrow/analysisengine/dto/request/ai/AiAnalysisRequest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
public interface AiAnalysisRequest {
1010
Long getProjectId();
1111

12+
default String getProjectWorkspace() { return null; }
13+
14+
default String getProjectNamespace() { return null; }
15+
1216
String getProjectVcsWorkspace();
1317

1418
String getProjectVcsRepoSlug();
@@ -47,6 +51,8 @@ public interface AiAnalysisRequest {
4751

4852
List<String> getDiffSnippets();
4953

54+
default String getTargetBranchName() { return null; }
55+
5056
String getRawDiff();
5157

5258
AnalysisMode getAnalysisMode();
@@ -64,4 +70,10 @@ public interface AiAnalysisRequest {
6470
* fetch files via VCS tool calls.
6571
*/
6672
default Map<String, String> getReconciliationFileContents() { return null; }
73+
74+
/**
75+
* The source branch name of the PR (the feature branch it comes FROM).
76+
* E.g., "feature/my-change".
77+
*/
78+
default String getSourceBranchName() { return null; }
6779
}

0 commit comments

Comments
 (0)