Skip to content
Merged
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
35 changes: 35 additions & 0 deletions internal/engine/conflict_extract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package engine

import (
"strings"
"testing"
)

func TestExtractResolvedFileContent(t *testing.T) {
// The exact shape that corrupted a real client build's package.json: the
// model returned conversational preamble + a fenced block + postamble.
chatty := "Resolved. Kept `@playwright/test` (HEAD) — needed by test:e2e.\n\n" +
"Write blocked on permission. File content to apply:\n\n" +
"```json\n{\n \"name\": \"isiqalo-pos-frontend\",\n \"private\": true\n}\n```\n\n" +
"Grant write to apply, or paste yourself."
got := extractResolvedFileContent(chatty)
want := "{\n \"name\": \"isiqalo-pos-frontend\",\n \"private\": true\n}"
if got != want {
t.Fatalf("expected only the fenced JSON, got:\n%q", got)
}
if strings.Contains(got, "Resolved.") || strings.Contains(got, "Grant write") || strings.Contains(got, "```") {
t.Fatalf("extracted content still contains chatter/fences:\n%q", got)
}

// A clean response (no fence) passes through unchanged.
clean := "package main\n\nfunc main() {}\n"
if g := extractResolvedFileContent(clean); strings.TrimSpace(g) != strings.TrimSpace(clean) {
t.Fatalf("clean content altered: %q", g)
}

// A response that is ONLY a fenced block (no chatter) still yields its body.
fenced := "```go\npackage x\n```"
if g := extractResolvedFileContent(fenced); g != "package x" {
t.Fatalf("fenced-only extraction failed: %q", g)
}
}
24 changes: 22 additions & 2 deletions internal/engine/conflict_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ File: %s
return "", err
}

resolved := stripCodeFences(resp.Content)
resolved := extractResolvedFileContent(resp.Content)

// Sanity check: resolved content must not contain conflict markers.
if strings.Contains(resolved, "<<<<<<<") || strings.Contains(resolved, ">>>>>>>") {
Expand Down Expand Up @@ -431,7 +431,7 @@ CRITICAL OUTPUT RULES:
return "", err
}

resolved := stripCodeFences(resp.Content)
resolved := extractResolvedFileContent(resp.Content)

if strings.Contains(resolved, "<<<<<<<") || strings.Contains(resolved, ">>>>>>>") {
return "", fmt.Errorf("tech lead output still contains conflict markers")
Expand Down Expand Up @@ -508,6 +508,26 @@ func truncateConflictContent(content string) string {
}

// stripCodeFences removes leading/trailing markdown code fences from LLM output.
// extractResolvedFileContent pulls the resolved file out of an LLM response.
// Conflict-resolution models sometimes wrap the file in a ```fenced block with
// conversational preamble/postamble ("Resolved. Kept X ... File content to
// apply: ```json {…}``` Grant write to apply."). Writing that whole reply
// verbatim corrupts the file (it broke a real build's package.json into invalid
// JSON). When a fenced block is present return ONLY its contents; otherwise
// fall back to trimming stray fences.
func extractResolvedFileContent(resp string) string {
if i := strings.Index(resp, "```"); i >= 0 {
rest := resp[i+3:]
if nl := strings.IndexByte(rest, '\n'); nl >= 0 {
rest = rest[nl+1:]
}
if j := strings.Index(rest, "```"); j >= 0 {
return strings.TrimSpace(rest[:j])
}
}
return strings.TrimSpace(stripCodeFences(resp))
}

func stripCodeFences(s string) string {
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "```") {
Expand Down
Loading