Fix bd-17d5: conflict marker false positives on JSON-encoded content

- import.go: Check raw bytes before JSON decoding using bytes.HasPrefix
- validate.go: Use bytes.Split and bytes.HasPrefix on raw data
- Added regression test TestAutoImportConflictMarkerFalsePositive
- Verified with vc-85 issue that triggered the bug

Amp-Thread-ID: https://ampcode.com/threads/T-3f81e22a-14b9-435b-8932-5641aadb7d31
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-11-08 13:09:26 -08:00
parent 53c9e9bf89
commit 0b28bfec7a
3 changed files with 95 additions and 9 deletions

View File

@@ -2,6 +2,7 @@ package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
@@ -96,15 +97,20 @@ NOTE: Import requires direct database access and does not work with daemon mode.
for scanner.Scan() {
lineNum++
line := scanner.Text()
rawLine := scanner.Bytes()
line := string(rawLine)
// Skip empty lines
if line == "" {
continue
}
// Detect git conflict markers and attempt automatic 3-way merge
if strings.Contains(line, "<<<<<<<") || strings.Contains(line, "=======") || strings.Contains(line, ">>>>>>>") {
// Detect git conflict markers in raw bytes (before JSON decoding)
// This prevents false positives when issue content contains these strings
trimmed := bytes.TrimSpace(rawLine)
if bytes.HasPrefix(trimmed, []byte("<<<<<<< ")) ||
bytes.Equal(trimmed, []byte("=======")) ||
bytes.HasPrefix(trimmed, []byte(">>>>>>> ")) {
fmt.Fprintf(os.Stderr, "Git conflict markers detected in JSONL file (line %d)\n", lineNum)
fmt.Fprintf(os.Stderr, "→ Attempting automatic 3-way merge...\n\n")

View File

@@ -5,6 +5,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
@@ -1067,6 +1068,83 @@ func TestAutoImportMergeConflict(t *testing.T) {
}
}
// TestAutoImportConflictMarkerFalsePositive tests that conflict marker detection
// doesn't trigger on JSON-encoded conflict markers in issue content (bd-17d5)
func TestAutoImportConflictMarkerFalsePositive(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "bd-test-false-positive-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
dbPath = filepath.Join(tmpDir, "test.db")
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
testStore := newTestStore(t, dbPath)
store = testStore
storeMutex.Lock()
storeActive = true
storeMutex.Unlock()
defer func() {
storeMutex.Lock()
storeActive = false
storeMutex.Unlock()
testStore.Close()
}()
ctx := context.Background()
// Create a JSONL file with an issue that has conflict markers in the description
// The conflict markers are JSON-encoded (as \u003c\u003c\u003c...) which should NOT trigger detection
now := time.Now().Format(time.RFC3339Nano)
jsonlContent := fmt.Sprintf(`{"id":"test-fp-1","title":"Test false positive","description":"This issue documents git conflict markers:\n\u003c\u003c\u003c\u003c\u003c\u003c\u003c HEAD\n=======\n\u003e\u003e\u003e\u003e\u003e\u003e\u003e branch","status":"open","priority":1,"issue_type":"task","created_at":"%s","updated_at":"%s"}`, now, now)
if err := os.WriteFile(jsonlPath, []byte(jsonlContent+"\n"), 0644); err != nil {
t.Fatalf("Failed to create JSONL: %v", err)
}
// Verify the JSONL contains JSON-encoded conflict markers (not literal ones)
jsonlData, err := os.ReadFile(jsonlPath)
if err != nil {
t.Fatalf("Failed to read JSONL: %v", err)
}
jsonlStr := string(jsonlData)
if !strings.Contains(jsonlStr, `\u003c\u003c\u003c`) {
t.Logf("JSONL content: %s", jsonlStr)
t.Fatalf("Expected JSON-encoded conflict markers in JSONL")
}
// Capture stderr
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
// Run auto-import - should succeed without conflict detection
autoImportIfNewer()
w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
io.Copy(&buf, r)
stderrOutput := buf.String()
// Verify NO conflict was detected
if strings.Contains(stderrOutput, "conflict") {
t.Errorf("False positive: conflict detection triggered on JSON-encoded markers. stderr: %s", stderrOutput)
}
// Verify the issue was successfully imported
result, err := testStore.GetIssue(ctx, "test-fp-1")
if err != nil {
t.Fatalf("Failed to get issue (import failed): %v", err)
}
expectedDesc := "This issue documents git conflict markers:\n<<<<<<< HEAD\n=======\n>>>>>>> branch"
if result.Description != expectedDesc {
t.Errorf("Expected description with conflict markers, got: %s", result.Description)
}
}
// TestAutoImportClosedAtInvariant tests that auto-import enforces status/closed_at invariant
func TestAutoImportClosedAtInvariant(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "bd-test-invariant-*")

View File

@@ -1,5 +1,6 @@
package main
import (
"bytes"
"context"
"fmt"
"os"
@@ -324,14 +325,15 @@ func validateGitConflicts(_ context.Context, fix bool) checkResult {
result.err = fmt.Errorf("failed to read JSONL: %w", err)
return result
}
// Look for git conflict markers
lines := strings.Split(string(data), "\n")
// Look for git conflict markers in raw bytes (before JSON decoding)
// This prevents false positives when issue content contains these strings
lines := bytes.Split(data, []byte("\n"))
var conflictLines []int
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "<<<<<<< ") ||
trimmed == "=======" ||
strings.HasPrefix(trimmed, ">>>>>>> ") {
trimmed := bytes.TrimSpace(line)
if bytes.HasPrefix(trimmed, []byte("<<<<<<< ")) ||
bytes.Equal(trimmed, []byte("=======")) ||
bytes.HasPrefix(trimmed, []byte(">>>>>>> ")) {
conflictLines = append(conflictLines, i+1)
}
}