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:
@@ -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")
|
||||
|
||||
|
||||
@@ -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-*")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user