// 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") } }