Skip to content
Open
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
38 changes: 38 additions & 0 deletions cmd/opencodereview/emit_run_result_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,44 @@ func TestEmitRunResult_JSONWithComments(t *testing.T) {
}
}

func TestEmitRunResult_DoesNotResolveAmbiguousCommentToFirstMatch(t *testing.T) {
ag := &mockResultProvider{
filesReviewed: 1,
diffs: []model.Diff{{
NewPath: "main.go",
Diff: `@@ -1,6 +1,8 @@
func first() {
+ target()
}
func second() {
+ target()
}
`,
}},
}
comments := []model.LlmComment{{
Path: "main.go",
Content: "ambiguous repeated code",
ExistingCode: "target()",
}}

got := captureStdout(t, func() {
if err := emitRunResult(context.Background(), ag, comments, time.Now(), "json", "developer", nil, nil, nil); err != nil {
t.Fatalf("emitRunResult: %v", err)
}
})
var out jsonOutput
if err := json.Unmarshal([]byte(got), &out); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(out.Comments) != 1 {
t.Fatalf("comments = %d, want 1", len(out.Comments))
}
if out.Comments[0].StartLine != 0 || out.Comments[0].EndLine != 0 {
t.Fatalf("comment lines = %d-%d, want unresolved 0-0", out.Comments[0].StartLine, out.Comments[0].EndLine)
}
}

func TestEmitRunResult_JSONWithResumeInfo(t *testing.T) {
ag := &mockResultProvider{
filesReviewed: 2,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
You are a code location assistant. Given a review comment and candidate code locations, choose the single candidate that the comment targets. /no_think
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
A review comment's existing_code matched multiple locations. Choose the one location the comment actually targets.

Rules:
1. Use the review comment, suggestion, optional reasoning, and candidate context to decide.
2. Return ONLY one JSON object, no Markdown and no explanation.
3. If no candidate is clearly correct, return {"candidate_id":null}.
4. Candidates are provided as a JSON array. Use only a candidate_id value from that array.

Output schema:
{"candidate_id":1}

Examples:

Input candidates:
[{"candidate_id":"1"},{"candidate_id":"2"}]
Correct output when candidate 2 is the target:
{"candidate_id":2}

Input candidates:
[{"candidate_id":"1"},{"candidate_id":"2"}]
Correct output when neither candidate is clearly the target:
{"candidate_id":null}

Review comment:
{suggestion_content}

Original existing_code:
```
{existing_code}
```

Suggestion:
```
{suggestion_code}
```

Reviewer reasoning:
{thinking}

Candidates:
{candidates}
6 changes: 6 additions & 0 deletions internal/config/template/task_template.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
{ "role": "user", "prompt_file": "re_location_task_user.md" }
]
},
"CANDIDATE_RE_LOCATION_TASK": {
"messages": [
{ "role": "system", "prompt_file": "candidate_re_location_task_system.md" },
{ "role": "user", "prompt_file": "candidate_re_location_task_user.md" }
]
},
"MAX_TOOL_REQUEST_TIMES": 30,
"PLAN_MODE_LINE_THRESHOLD": 50,
"MAX_TOKENS": 58888
Expand Down
31 changes: 18 additions & 13 deletions internal/config/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ type Template struct {
MaxTokens int `json:"MAX_TOKENS"`
// MaxCompletionTokens is a runtime-only output cap. When zero, callers
// retain the template's historical MaxTokens behavior.
MaxCompletionTokens int `json:"-"`
MaxToolRequestTimes int `json:"MAX_TOOL_REQUEST_TIMES"`
PlanModeLineThreshold int `json:"PLAN_MODE_LINE_THRESHOLD"`
ReLocationTask *LlmConversation `json:"RE_LOCATION_TASK,omitempty"`
ReviewFilterTask *LlmConversation `json:"REVIEW_FILTER_TASK,omitempty"`
MaxCompletionTokens int `json:"-"`
MaxToolRequestTimes int `json:"MAX_TOOL_REQUEST_TIMES"`
PlanModeLineThreshold int `json:"PLAN_MODE_LINE_THRESHOLD"`
ReLocationTask *LlmConversation `json:"RE_LOCATION_TASK,omitempty"`
CandidateReLocationTask *LlmConversation `json:"CANDIDATE_RE_LOCATION_TASK,omitempty"`
ReviewFilterTask *LlmConversation `json:"REVIEW_FILTER_TASK,omitempty"`
}

// ScanTemplate holds the full-file scan task template configuration loaded
Expand Down Expand Up @@ -82,14 +83,15 @@ type manifestConversation struct {
}

type templateManifest struct {
MainTask manifestConversation `json:"MAIN_TASK"`
PlanTask *manifestConversation `json:"PLAN_TASK,omitempty"`
MemoryCompressionTask manifestConversation `json:"MEMORY_COMPRESSION_TASK"`
MaxTokens int `json:"MAX_TOKENS"`
MaxToolRequestTimes int `json:"MAX_TOOL_REQUEST_TIMES"`
PlanModeLineThreshold int `json:"PLAN_MODE_LINE_THRESHOLD"`
ReLocationTask *manifestConversation `json:"RE_LOCATION_TASK,omitempty"`
ReviewFilterTask *manifestConversation `json:"REVIEW_FILTER_TASK,omitempty"`
MainTask manifestConversation `json:"MAIN_TASK"`
PlanTask *manifestConversation `json:"PLAN_TASK,omitempty"`
MemoryCompressionTask manifestConversation `json:"MEMORY_COMPRESSION_TASK"`
MaxTokens int `json:"MAX_TOKENS"`
MaxToolRequestTimes int `json:"MAX_TOOL_REQUEST_TIMES"`
PlanModeLineThreshold int `json:"PLAN_MODE_LINE_THRESHOLD"`
ReLocationTask *manifestConversation `json:"RE_LOCATION_TASK,omitempty"`
CandidateReLocationTask *manifestConversation `json:"CANDIDATE_RE_LOCATION_TASK,omitempty"`
ReviewFilterTask *manifestConversation `json:"REVIEW_FILTER_TASK,omitempty"`
}

func resolveConversation(m manifestConversation) (LlmConversation, error) {
Expand Down Expand Up @@ -147,6 +149,9 @@ func LoadDefault() (*Template, error) {
if tpl.ReLocationTask, err = resolveOptionalConversation(m.ReLocationTask, "RE_LOCATION_TASK"); err != nil {
return nil, err
}
if tpl.CandidateReLocationTask, err = resolveOptionalConversation(m.CandidateReLocationTask, "CANDIDATE_RE_LOCATION_TASK"); err != nil {
return nil, err
}
if tpl.ReviewFilterTask, err = resolveOptionalConversation(m.ReviewFilterTask, "REVIEW_FILTER_TASK"); err != nil {
return nil, err
}
Expand Down
8 changes: 8 additions & 0 deletions internal/config/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ func TestLoadDefault_FieldsPopulated(t *testing.T) {
if tpl.ReLocationTask == nil {
t.Fatal("ReLocationTask is nil, expected non-nil")
}
if tpl.CandidateReLocationTask == nil {
t.Fatal("CandidateReLocationTask is nil, expected non-nil")
}
if tpl.ReviewFilterTask == nil {
t.Fatal("ReviewFilterTask is nil, expected non-nil")
}
Expand Down Expand Up @@ -124,6 +127,11 @@ func TestLoadDefault_PlaceholdersPresent(t *testing.T) {
{"MemoryCompression user has context", tpl.MemoryCompressionTask.Messages[1].Content, "{{context}}"},
{"ReviewFilter user has comments", tpl.ReviewFilterTask.Messages[1].Content, "{{comments}}"},
{"ReLocation user has diff (single brace)", tpl.ReLocationTask.Messages[1].Content, "{diff}"},
{"CandidateReLocation user has suggestion content", tpl.CandidateReLocationTask.Messages[1].Content, "{suggestion_content}"},
{"CandidateReLocation user has candidates", tpl.CandidateReLocationTask.Messages[1].Content, "{candidates}"},
{"CandidateReLocation user has existing_code", tpl.CandidateReLocationTask.Messages[1].Content, "{existing_code}"},
{"CandidateReLocation user has suggestion_code", tpl.CandidateReLocationTask.Messages[1].Content, "{suggestion_code}"},
{"CandidateReLocation user has thinking", tpl.CandidateReLocationTask.Messages[1].Content, "{thinking}"},
}

for _, tt := range tests {
Expand Down
Loading