Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend
Original file line number Diff line number Diff line change
Expand Up @@ -100,23 +100,20 @@ default boolean isMultiBranchEnabled(Project project) {
}

/**
* Check if a branch should have indexed context based on project configuration.
* Branch indexes are created for branches that match branchPushPatterns in BranchAnalysisConfig.
*
* Check if a branch is explicitly configured for a retained RAG index.
* Branch analysis configuration is a separate concern and never grants RAG
* snapshot ownership.
*
* @param project The project to check
* @param branchName The branch name to evaluate
* @return true if branch should have indexed context
* @return true if the branch is explicitly configured for retained indexed context
*/
default boolean shouldHaveBranchIndex(Project project, String branchName) {
var config = project.getConfiguration();
if (config == null || config.ragConfig() == null) {
return false;
}
// Get branch push patterns from branch analysis config
var branchPushPatterns = config.branchAnalysis() != null
? config.branchAnalysis().branchPushPatterns()
: null;
return config.ragConfig().shouldHaveBranchIndex(branchName, branchPushPatterns);
return config.ragConfig().shouldHaveBranchIndex(branchName);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,7 @@ private void performIncrementalRagUpdate(BranchProcessRequest request, Project p
"RAG module not deployed — skipping incremental update");
return;
}
if (commitDiff == null || commitDiff.isBlank()) {
if (scopedOnly && (commitDiff == null || commitDiff.isBlank())) {
log.info("Skipping RAG incremental update - no scoped files require an update");
EventNotificationEmitter.emitStatus(consumer, "rag_skipped",
"No scoped files require a RAG update");
Expand All @@ -966,19 +966,19 @@ private void performIncrementalRagUpdate(BranchProcessRequest request, Project p
return;
}

String targetBranch = request.getTargetBranchName();
String baseBranch = ragOperationsService.getBaseBranch(project);
String targetBranch = request.getTargetBranchName();
String baseBranch = ragOperationsService.getBaseBranch(project);

if (!targetBranch.equals(baseBranch)
&& !ragOperationsService.shouldHaveBranchIndex(project, targetBranch)) {
log.info("Skipping RAG update for non-retained branch: project={}, branch={}",
project.getId(), targetBranch);
EventNotificationEmitter.emitStatus(consumer, "rag_skipped",
"Branch is analyzed but is not configured as a retained RAG branch");
return;
}
if (!targetBranch.equals(baseBranch)
&& !ragOperationsService.shouldHaveBranchIndex(project, targetBranch)) {
log.info("Skipping RAG update for non-retained branch: project={}, branch={}",
project.getId(), targetBranch);
EventNotificationEmitter.emitStatus(consumer, "rag_skipped",
"Branch is analyzed but is not configured as a retained RAG branch");
return;
}

// Health check: verify RAG pipeline is reachable before starting
// Health check: verify RAG pipeline is reachable before starting
if (!ragOperationsService.isRagPipelineHealthy()) {
log.warn("RAG pipeline is not reachable — skipping incremental update for project={}",
project.getId());
Expand Down Expand Up @@ -1013,10 +1013,11 @@ private void performIncrementalRagUpdate(BranchProcessRequest request, Project p
}
}

log.info("RAG update completed for project={}, branch={}, commit={}",
// RagOperationsService owns the precise terminal event. A boolean true
// also covers an already-current revision, so translating it into a
// generic "updated" event here would be a false success report.
log.info("RAG reconciliation completed for project={}, branch={}, commit={}",
project.getId(), targetBranch, request.getCommitHash());
EventNotificationEmitter.emitStatus(consumer, "rag_update_complete",
"RAG index updated successfully for branch: " + targetBranch);
} catch (Exception e) {
log.warn("RAG incremental update failed (non-critical): {}", e.getMessage());
EventNotificationEmitter.emitStatus(consumer, "rag_update_failed",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,67 @@ void shouldNotEmitRagSuccessAfterIncrementalFailure() {
assertThat(event).containsEntry("state", "rag_update_complete"));
}

@Test
@DisplayName("should let RAG service report the precise successful outcome")
void shouldNotSynthesizeRagUpdatedFromBooleanSuccess() {
BranchProcessRequest request = createRequest();
request.commitHash = "current-commit";
request.targetBranchName = "main";
String rawDiff = "diff --git a/f.java b/f.java\n+x\n";
List<Map<String, Object>> events = new ArrayList<>();

when(project.getId()).thenReturn(1L);
when(ragOperationsService.isRagEnabled(project)).thenReturn(true);
when(ragOperationsService.isRagIndexReady(project)).thenReturn(true);
when(ragOperationsService.isRagPipelineHealthy()).thenReturn(true);
when(ragOperationsService.getBaseBranch(project)).thenReturn("main");
when(ragOperationsService.triggerIncrementalUpdate(
eq(project), eq("main"), eq("current-commit"), eq(rawDiff), any()))
.thenReturn(true);

ReflectionTestUtils.invokeMethod(
processor,
"performIncrementalRagUpdate",
request,
project,
rawDiff,
(Consumer<Map<String, Object>>) events::add,
false);

assertThat(events)
.noneSatisfy(event ->
assertThat(event).containsEntry("state", "rag_update_complete"));
}

@Test
@DisplayName("should reconcile an empty base-branch diff through the durable RAG operation")
void shouldDelegateEmptyBaseBranchDiff() {
BranchProcessRequest request = createRequest();
request.commitHash = "empty-range-commit";
request.targetBranchName = "main";

when(project.getId()).thenReturn(1L);
when(ragOperationsService.isRagEnabled(project)).thenReturn(true);
when(ragOperationsService.isRagIndexReady(project)).thenReturn(true);
when(ragOperationsService.isRagPipelineHealthy()).thenReturn(true);
when(ragOperationsService.getBaseBranch(project)).thenReturn("main");
when(ragOperationsService.triggerIncrementalUpdate(
eq(project), eq("main"), eq("empty-range-commit"), eq(""), any()))
.thenReturn(true);

ReflectionTestUtils.invokeMethod(
processor,
"performIncrementalRagUpdate",
request,
project,
"",
(Consumer<Map<String, Object>>) ignored -> { },
false);

verify(ragOperationsService).triggerIncrementalUpdate(
eq(project), eq("main"), eq("empty-range-commit"), eq(""), any());
}

@Test
@DisplayName("should call updateBranchIndex for non-main branch RAG update")
void shouldCallUpdateBranchIndexForNonMainBranch() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* source changes come from the exact PR overlay rather than a second branch.
* - branchRetentionDays: how long to keep branch index metadata before auto-cleanup (default: 90 days)
* - indexedBranches: explicit non-primary branches whose complete snapshots are retained.
* A null/empty value preserves the legacy branchPushPatterns interpretation.
* Branch analysis patterns do not implicitly retain RAG snapshots.
* - transientBranchIndexesEnabled: whether an analyzed PR target that is not retained
* may receive a revision-pinned temporary snapshot.
*/
Expand Down Expand Up @@ -58,8 +58,9 @@ public RagConfig(boolean enabled, String branch, List<String> includePatterns, L
}

/**
* Backward-compatible constructor for configurations written before explicit
* retained and transient branch ownership was introduced.
* Compatibility constructor for configurations written before explicit
* retained and transient branch ownership was introduced. Such configurations
* retain no non-primary RAG branches until they are selected explicitly.
*/
public RagConfig(
boolean enabled,
Expand Down Expand Up @@ -88,11 +89,6 @@ public int getEffectiveBranchRetentionDays() {
return branchRetentionDays != null ? branchRetentionDays : DEFAULT_BRANCH_RETENTION_DAYS;
}

public boolean hasExplicitIndexedBranches() {
return indexedBranches != null && indexedBranches.stream()
.anyMatch(value -> value != null && !value.isBlank());
}

@JsonIgnore
public List<String> getEffectiveIndexedBranches() {
if (indexedBranches == null) {
Expand All @@ -111,36 +107,16 @@ public boolean isTransientBranchIndexesEnabled() {
}

/**
* Check if a branch should have indexed context based on branchPushPatterns.
* @param branchName the branch to check
* @param branchPushPatterns patterns from BranchAnalysisConfig
* @return true if branch matches any pattern and multi-branch is enabled
* Check whether a branch is explicitly configured for a retained RAG index.
* Branch-analysis patterns intentionally have no effect on this decision.
*
* @param branchName the exact branch name to check
* @return true if the branch is explicitly retained and multi-branch indexing is enabled
*/
public boolean shouldHaveBranchIndex(String branchName, List<String> branchPushPatterns) {
public boolean shouldHaveBranchIndex(String branchName) {
if (!isMultiBranchEnabled() || branchName == null || branchName.isBlank()) {
return false;
}
if (hasExplicitIndexedBranches()) {
return getEffectiveIndexedBranches().contains(branchName.trim());
}
if (branchPushPatterns == null || branchPushPatterns.isEmpty()) {
return false;
}
return branchPushPatterns.stream()
.anyMatch(pattern -> matchesBranchPattern(branchName, pattern));
}

/**
* Match a branch name against a glob pattern.
*/
public static boolean matchesBranchPattern(String branchName, String pattern) {
if (pattern == null || branchName == null) return false;
// Convert glob pattern to regex
String regex = pattern
.replace(".", "\\.")
.replace("**", "§§") // Temp placeholder for **
.replace("*", "[^/]*")
.replace("§§", ".*");
return branchName.matches(regex);
return getEffectiveIndexedBranches().contains(branchName.trim());
}
}
Loading
Loading