mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-07-09 17:28:58 +00:00
* feat: add tool_calls field to JSON output for tool usage statistics Track per-tool invocation counts in llmloop.Runner and expose them through the ResultProvider interface so both review and scan modes report tool call statistics in --format json output. * fix: correct tool_calls counting and ensure stable JSON schema Move recordToolCall after lookupTool nil check so only actually-executed tool calls are counted. Always emit tool_calls field in JSON output for a consistent schema, initializing by_tool to empty map when nil. * fix: remove omitempty from tool_calls to ensure stable JSON schema Drop omitempty from the tool_calls struct tag and initialize ToolCalls in outputJSONNoFiles so the field is always present in JSON output regardless of execution path.
135 lines
3.6 KiB
Go
135 lines
3.6 KiB
Go
package scan
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/open-code-review/open-code-review/internal/model"
|
|
)
|
|
|
|
func itemList(paths ...string) []model.ScanItem {
|
|
out := make([]model.ScanItem, len(paths))
|
|
for i, p := range paths {
|
|
out[i] = model.ScanItem{Path: p}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func batchPaths(b [][]model.ScanItem) [][]string {
|
|
out := make([][]string, len(b))
|
|
for i, batch := range b {
|
|
ps := make([]string, len(batch))
|
|
for j, it := range batch {
|
|
ps[j] = it.Path
|
|
}
|
|
out[i] = ps
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestParseBatchStrategy(t *testing.T) {
|
|
cases := map[string]BatchStrategy{
|
|
"": BatchNone,
|
|
" ": BatchNone,
|
|
"by-language": BatchByLanguage,
|
|
"BY-LANGUAGE": BatchByLanguage,
|
|
"by-directory": BatchByDirectory,
|
|
"none": BatchNone,
|
|
"by-author": BatchNone, // unknown → safe default
|
|
" by-language ": BatchByLanguage,
|
|
}
|
|
for in, want := range cases {
|
|
if got := parseBatchStrategy(in); got != want {
|
|
t.Errorf("parseBatchStrategy(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGroupBatches_ByLanguage(t *testing.T) {
|
|
items := itemList(
|
|
"cmd/main.go",
|
|
"internal/scan/agent.go",
|
|
"docs/README.md",
|
|
"scripts/build.sh",
|
|
"internal/scan/preview.go",
|
|
"docs/intro.md",
|
|
)
|
|
got := batchPaths(groupBatches(items, BatchByLanguage, 0))
|
|
// Batches are emitted in lexicographic key order: .go < .md < .sh.
|
|
// Within a batch, input order is preserved.
|
|
want := [][]string{
|
|
{"cmd/main.go", "internal/scan/agent.go", "internal/scan/preview.go"}, // .go
|
|
{"docs/README.md", "docs/intro.md"}, // .md
|
|
{"scripts/build.sh"}, // .sh
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("got %v\nwant %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestGroupBatches_ByDirectory(t *testing.T) {
|
|
items := itemList(
|
|
"README.md", // <root>
|
|
"cmd/main.go", // cmd
|
|
"internal/a/x.go", // internal
|
|
"internal/b/y.go", // internal
|
|
"cmd/scan.go", // cmd
|
|
"LICENSE", // <root>
|
|
)
|
|
got := batchPaths(groupBatches(items, BatchByDirectory, 0))
|
|
want := [][]string{
|
|
{"README.md", "LICENSE"}, // <root>
|
|
{"cmd/main.go", "cmd/scan.go"}, // cmd
|
|
{"internal/a/x.go", "internal/b/y.go"}, // internal
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("got %v\nwant %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestGroupBatches_None(t *testing.T) {
|
|
items := itemList("a.go", "b.go", "c.py")
|
|
got := batchPaths(groupBatches(items, BatchNone, 0))
|
|
want := [][]string{{"a.go"}, {"b.go"}, {"c.py"}}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("got %v\nwant %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestGroupBatches_BatchSizeCap(t *testing.T) {
|
|
items := itemList("a.go", "b.go", "c.go", "d.go", "e.go")
|
|
// All .go in one natural group; size=2 → 3 chunks of 2,2,1
|
|
got := batchPaths(groupBatches(items, BatchByLanguage, 2))
|
|
want := [][]string{
|
|
{"a.go", "b.go"},
|
|
{"c.go", "d.go"},
|
|
{"e.go"},
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("got %v\nwant %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestGroupBatches_Empty(t *testing.T) {
|
|
if got := groupBatches(nil, BatchByLanguage, 0); got != nil {
|
|
t.Errorf("expected nil for empty input, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestLanguageKey_ExtensionlessAndDotfiles(t *testing.T) {
|
|
cases := map[string]string{
|
|
"Makefile": "<no-ext>",
|
|
"src/Dockerfile": "<no-ext>",
|
|
".gitignore": "<no-ext>", // dotfile w/o extension
|
|
".github/CODEOWNERS": "<no-ext>",
|
|
"cmd/main.go": ".go",
|
|
"docs/README.MD": ".md",
|
|
"a/b/c.Test.go": ".go",
|
|
}
|
|
for path, want := range cases {
|
|
got := languageKey(model.ScanItem{Path: path})
|
|
if got != want {
|
|
t.Errorf("languageKey(%q) = %q, want %q", path, got, want)
|
|
}
|
|
}
|
|
}
|