mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-08-20 14:14:30 +00:00
* refactor(background): treat --background and --background-file as mutually exclusive with file precedence Replace mergeBackground with selectBackground: when both flags are provided, --background-file wins and --background is ignored (with a stderr warning). The commit-message fallback now fires only when neither entry point was used. This fixes the inconsistency where review and delegate produced different backgrounds for equivalent input (issue #1013), and makes the effective background deterministic regardless of which command is invoked. Closes #1013 * refactor(background): extract resolveBackground helper and fix tests Address reviewer feedback: - Extract resolveBackground() so review and delegate share one call site, making future drift impossible. - Rewrite tests to call resolveBackground directly instead of duplicating the if/else-if logic (which could never fail). - Fix stale comment on TestBackgroundFilePrecedenceOverCommit.
271 lines
8.6 KiB
Go
271 lines
8.6 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright 2026 alibaba/open-code-review Contributors
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/alibaba/open-code-review/internal/delegate"
|
|
)
|
|
|
|
func captureDelegateStdout(t *testing.T, fn func()) []byte {
|
|
t.Helper()
|
|
orig := os.Stdout
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("create stdout pipe: %v", err)
|
|
}
|
|
os.Stdout = w
|
|
defer func() { os.Stdout = orig }()
|
|
|
|
// Drain while fn runs. Reading only after fn returns caps the capture at
|
|
// whatever the pipe buffer holds: 64 KiB on Linux, far less on a Windows
|
|
// anonymous pipe, and a payload past that blocks the writer forever.
|
|
var out []byte
|
|
var readErr error
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
out, readErr = io.ReadAll(r)
|
|
}()
|
|
fn()
|
|
if err := w.Close(); err != nil {
|
|
t.Fatalf("close stdout writer: %v", err)
|
|
}
|
|
<-done
|
|
if readErr != nil {
|
|
t.Fatalf("read stdout: %v", readErr)
|
|
}
|
|
_ = r.Close()
|
|
return out
|
|
}
|
|
|
|
// gitCommitFile writes a file and commits it, returning after the commit lands.
|
|
func gitCommitFile(t *testing.T, dir, name, content, msg string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", name, err)
|
|
}
|
|
for _, args := range [][]string{{"add", "."}, {"commit", "-m", msg}} {
|
|
cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v: %v: %s", args, err, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// silenceStdout redirects os.Stdout to /dev/null for the duration of fn so the
|
|
// delegate commands' Printf output does not clutter test logs.
|
|
func silenceStdout(t *testing.T, fn func()) {
|
|
t.Helper()
|
|
orig := os.Stdout
|
|
devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|
if err != nil {
|
|
t.Fatalf("open devnull: %v", err)
|
|
}
|
|
os.Stdout = devnull
|
|
defer func() {
|
|
os.Stdout = orig
|
|
_ = devnull.Close()
|
|
}()
|
|
fn()
|
|
}
|
|
|
|
// freshOCRHome points the OCR home at a temp dir so a test can assert on what a
|
|
// command wrote there. It also neutralizes global git config: git resolves that
|
|
// via XDG_CONFIG_HOME as well, so overriding HOME alone would still pick up the
|
|
// developer's settings (e.g. commit.gpgsign, which fails without their keyring).
|
|
func freshOCRHome(t *testing.T) string {
|
|
t.Helper()
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("GIT_CONFIG_GLOBAL", os.DevNull)
|
|
t.Setenv("GIT_CONFIG_SYSTEM", os.DevNull)
|
|
return home
|
|
}
|
|
|
|
// assertNoSessionStore fails if the session store was created under home.
|
|
// Preview neither runs nor finalizes a review, so it must not open persistence
|
|
// at all; session.New creates this directory before writing its JSONL file.
|
|
func assertNoSessionStore(t *testing.T, home string) {
|
|
t.Helper()
|
|
store := filepath.Join(home, ".opencodereview", "sessions")
|
|
if _, err := os.Stat(store); !os.IsNotExist(err) {
|
|
t.Errorf("preview created the session store at %s (stat err = %v)", store, err)
|
|
}
|
|
}
|
|
|
|
func TestExecuteDelegatePreview_Workspace(t *testing.T) {
|
|
dir := initTestGitRepo(t)
|
|
// Uncommitted change so the workspace preview has at least one entry.
|
|
if err := os.WriteFile(filepath.Join(dir, "app.go"), []byte("package app\n"), 0o644); err != nil {
|
|
t.Fatalf("write app.go: %v", err)
|
|
}
|
|
silenceStdout(t, func() {
|
|
if err := executeDelegatePreview(delegateOptions{repoDir: dir}); err != nil {
|
|
t.Fatalf("executeDelegatePreview(workspace) error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestExecuteDelegatePreview_Range(t *testing.T) {
|
|
dir := initTestGitRepo(t)
|
|
gitCommitFile(t, dir, "b.go", "package b\n", "second commit")
|
|
silenceStdout(t, func() {
|
|
err := executeDelegatePreview(delegateOptions{repoDir: dir, from: "HEAD~1", to: "HEAD"})
|
|
if err != nil {
|
|
t.Fatalf("executeDelegatePreview(range) error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestExecuteDelegatePreview_Commit(t *testing.T) {
|
|
dir := initTestGitRepo(t)
|
|
gitCommitFile(t, dir, "c.go", "package c\n", "add c")
|
|
silenceStdout(t, func() {
|
|
// commit mode auto-fills background from the commit message.
|
|
err := executeDelegatePreview(delegateOptions{repoDir: dir, commit: "HEAD"})
|
|
if err != nil {
|
|
t.Fatalf("executeDelegatePreview(commit) error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestExecuteDelegatePreviewCreatesNoSession covers the third preview entry
|
|
// point, which built its agent the same leaky way as review and scan.
|
|
func TestExecuteDelegatePreviewCreatesNoSession(t *testing.T) {
|
|
home := freshOCRHome(t)
|
|
|
|
dir := initTestGitRepo(t)
|
|
gitCommitFile(t, dir, "d.go", "package d\n", "add d")
|
|
silenceStdout(t, func() {
|
|
if err := executeDelegatePreview(delegateOptions{repoDir: dir, commit: "HEAD"}); err != nil {
|
|
t.Fatalf("executeDelegatePreview error: %v", err)
|
|
}
|
|
})
|
|
|
|
assertNoSessionStore(t, home)
|
|
}
|
|
|
|
func TestExecuteDelegateRule(t *testing.T) {
|
|
dir := initTestGitRepo(t)
|
|
silenceStdout(t, func() {
|
|
err := executeDelegateRule(delegateOptions{repoDir: dir}, []string{"README.md"})
|
|
if err != nil {
|
|
t.Fatalf("executeDelegateRule error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestExecuteDelegatePreviewJSON(t *testing.T) {
|
|
dir := initTestGitRepo(t)
|
|
if err := os.WriteFile(filepath.Join(dir, "app.go"), []byte("package app\n"), 0o644); err != nil {
|
|
t.Fatalf("write app.go: %v", err)
|
|
}
|
|
out := captureDelegateStdout(t, func() {
|
|
if err := executeDelegatePreview(delegateOptions{repoDir: dir, format: "json"}); err != nil {
|
|
t.Fatalf("executeDelegatePreview(json) error: %v", err)
|
|
}
|
|
})
|
|
var got delegatePreviewJSON
|
|
if err := json.Unmarshal(out, &got); err != nil {
|
|
t.Fatalf("decode preview JSON: %v\n%s", err, out)
|
|
}
|
|
if got.SchemaVersion != delegateSchemaVersion || got.Mode != "workspace" {
|
|
t.Fatalf("unexpected envelope: %#v", got)
|
|
}
|
|
if len(got.ReviewableFiles) != 1 || got.ReviewableFiles[0].Path != "app.go" {
|
|
t.Fatalf("reviewable_files = %#v", got.ReviewableFiles)
|
|
}
|
|
if got.ReviewableFiles == nil || got.ExcludedFiles == nil {
|
|
t.Fatal("JSON arrays must not be null")
|
|
}
|
|
}
|
|
|
|
func TestExecuteDelegateRuleJSON(t *testing.T) {
|
|
dir := initTestGitRepo(t)
|
|
out := captureDelegateStdout(t, func() {
|
|
if err := executeDelegateRule(delegateOptions{repoDir: dir, format: "json"}, []string{"README.md"}); err != nil {
|
|
t.Fatalf("executeDelegateRule(json) error: %v", err)
|
|
}
|
|
})
|
|
var got delegateRulesJSON
|
|
if err := json.Unmarshal(out, &got); err != nil {
|
|
t.Fatalf("decode rules JSON: %v\n%s", err, out)
|
|
}
|
|
if got.SchemaVersion != delegateSchemaVersion || len(got.Groups) != 1 {
|
|
t.Fatalf("unexpected rules envelope: %#v", got)
|
|
}
|
|
if len(got.Groups[0].Files) != 1 || got.Groups[0].Files[0] != "README.md" || got.Groups[0].Rule == "" {
|
|
t.Fatalf("unexpected rule group: %#v", got.Groups[0])
|
|
}
|
|
}
|
|
|
|
func TestRuleGroupsJSONEmptyFiles(t *testing.T) {
|
|
groups := ruleGroupsJSON([]delegate.RuleGroup{{ID: 1}})
|
|
if len(groups) != 1 {
|
|
t.Fatalf("groups = %#v", groups)
|
|
}
|
|
if groups[0].Files == nil || len(groups[0].Files) != 0 {
|
|
t.Fatalf("files must be an empty, non-nil slice: %#v", groups[0].Files)
|
|
}
|
|
payload, err := json.Marshal(groups[0])
|
|
if err != nil {
|
|
t.Fatalf("marshal rule group: %v", err)
|
|
}
|
|
if string(payload) == "" || !json.Valid(payload) {
|
|
t.Fatalf("invalid JSON: %s", payload)
|
|
}
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
|
t.Fatalf("decode rule group: %v", err)
|
|
}
|
|
if files, ok := decoded["files"].([]any); !ok || len(files) != 0 {
|
|
t.Fatalf("files JSON must be []: %s", payload)
|
|
}
|
|
}
|
|
|
|
func TestLoadDelegateContext_BackgroundFile(t *testing.T) {
|
|
dir := initTestGitRepo(t)
|
|
bgPath := filepath.Join(dir, "bg.txt")
|
|
if err := os.WriteFile(bgPath, []byte("extra background"), 0o644); err != nil {
|
|
t.Fatalf("write bg: %v", err)
|
|
}
|
|
dc, err := loadDelegateContext(delegateOptions{repoDir: dir, backgroundFile: "bg.txt", background: "base"})
|
|
if err != nil {
|
|
t.Fatalf("loadDelegateContext error: %v", err)
|
|
}
|
|
if !strings.Contains(dc.opts.background, "extra background") {
|
|
t.Errorf("expected file content to win, got %q", dc.opts.background)
|
|
}
|
|
if strings.Contains(dc.opts.background, "base") {
|
|
t.Errorf("inline --background should be ignored when --background-file is set, got %q", dc.opts.background)
|
|
}
|
|
}
|
|
|
|
func TestLoadDelegateContext_NotGitRepo(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if _, err := loadDelegateContext(delegateOptions{repoDir: dir}); err == nil {
|
|
t.Fatal("expected error for non-git dir")
|
|
}
|
|
}
|
|
|
|
func TestDelegateContextMergeBase_Range(t *testing.T) {
|
|
dir := initTestGitRepo(t)
|
|
gitCommitFile(t, dir, "d.go", "package d\n", "add d")
|
|
dc, err := loadDelegateContext(delegateOptions{repoDir: dir, from: "HEAD~1", to: "HEAD"})
|
|
if err != nil {
|
|
t.Fatalf("loadDelegateContext error: %v", err)
|
|
}
|
|
if got := dc.mergeBase(context.Background()); got == "" {
|
|
t.Error("expected non-empty merge base for range mode")
|
|
}
|
|
}
|