a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
310 lines
10 KiB
Go
310 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// runPRsBundleCmd invokes the bundle subcommand with args, capturing its
|
|
// stdout. It mirrors runPRsCmd (prs_test.go) but drives runPRsBundle.
|
|
func runPRsBundleCmd(t *testing.T, args ...string) (string, error) {
|
|
t.Helper()
|
|
cmd := &cobra.Command{Use: "bundle"}
|
|
var buf strings.Builder
|
|
cmd.SetOut(&buf)
|
|
cmd.SetErr(&buf)
|
|
err := runPRsBundle(cmd, args)
|
|
return buf.String(), err
|
|
}
|
|
|
|
// cannedImpact is a get_pr_impact payload (with receipt) returned by the
|
|
// stubbed daemon-tool seam.
|
|
func cannedImpact() json.RawMessage {
|
|
return json.RawMessage(`{
|
|
"number": 7,
|
|
"risk": "HIGH",
|
|
"score": 68.5,
|
|
"review_priorities": [{"axis":"flow","score":72.0,"reason":"wide fan-in"}],
|
|
"changed_files": ["internal/forge/forge.go","internal/forge/ghrest.go"],
|
|
"changed_symbols": [{"id":"internal/forge/forge.go::ListPRs","name":"ListPRs","kind":"function","file":"internal/forge/forge.go"}],
|
|
"communities": ["c1","c2"],
|
|
"blast": {"callers": []},
|
|
"receipt": {"receipt_version":1,"risk_tier":"HIGH","next_safe_action":"add-tests","merge_blocker":false}
|
|
}`)
|
|
}
|
|
|
|
// cannedReviewers is a suggest_reviewers payload returned by the stub.
|
|
func cannedReviewers() json.RawMessage {
|
|
return json.RawMessage(`{
|
|
"reviewers": [{"reviewer":"alice","kind":"person","score":5,"reasons":["codeowner"],"matched_files":["internal/forge/forge.go"]}],
|
|
"total": 1,
|
|
"changed_files": 2,
|
|
"codeowners_found": true
|
|
}`)
|
|
}
|
|
|
|
// stubBundleDaemon wires the daemon-tool seam to return the canned impact /
|
|
// reviewers payloads, recording the tool name and args of each call.
|
|
func stubBundleDaemon(t *testing.T) (calls *[]string, argsByTool map[string]map[string]any) {
|
|
t.Helper()
|
|
seen := []string{}
|
|
argsByTool = map[string]map[string]any{}
|
|
prsDaemonTool = func(_ string, tool string, args map[string]any) (json.RawMessage, error) {
|
|
seen = append(seen, tool)
|
|
argsByTool[tool] = args
|
|
switch tool {
|
|
case "get_pr_impact":
|
|
return cannedImpact(), nil
|
|
case "suggest_reviewers":
|
|
return cannedReviewers(), nil
|
|
default:
|
|
t.Errorf("unexpected daemon tool %q", tool)
|
|
return json.RawMessage(`{}`), nil
|
|
}
|
|
}
|
|
return &seen, argsByTool
|
|
}
|
|
|
|
// TestPRsBundle_WritesWellFormedBundle asserts `prs bundle <N>` writes a JSON
|
|
// bundle carrying the PR number, changed files, the impact slice, and the
|
|
// reviewer suggestions — joined from the stubbed daemon tools, no network.
|
|
func TestPRsBundle_WritesWellFormedBundle(t *testing.T) {
|
|
resetPRsSeams(t)
|
|
forgeAvailable = func(context.Context) bool { return true }
|
|
forgePRFiles = func(_ context.Context, _ string, num int) ([]string, error) {
|
|
if num != 7 {
|
|
t.Errorf("PRFiles called with num=%d, want 7", num)
|
|
}
|
|
return []string{"internal/forge/forge.go", "internal/forge/ghrest.go"}, nil
|
|
}
|
|
calls, argsByTool := stubBundleDaemon(t)
|
|
|
|
dir := t.TempDir()
|
|
out := filepath.Join(dir, "bundle.json")
|
|
prsBundleOut = out
|
|
t.Cleanup(func() { prsBundleOut = "" })
|
|
|
|
stdout, err := runPRsBundleCmd(t, "7")
|
|
if err != nil {
|
|
t.Fatalf("bundle: %v", err)
|
|
}
|
|
if !strings.Contains(stdout, out) || !strings.Contains(stdout, "PR #7") {
|
|
t.Errorf("stdout should report the written path + PR number, got: %q", stdout)
|
|
}
|
|
|
|
// Both daemon tools must have been called, get_pr_impact with receipt:true
|
|
// and the forwarded file set.
|
|
if len(*calls) != 2 {
|
|
t.Fatalf("expected 2 daemon calls (impact + reviewers), got %v", *calls)
|
|
}
|
|
impArgs := argsByTool["get_pr_impact"]
|
|
if impArgs == nil {
|
|
t.Fatal("get_pr_impact was not called")
|
|
}
|
|
if r, _ := impArgs["receipt"].(bool); !r {
|
|
t.Errorf("get_pr_impact must be called with receipt:true, got %v", impArgs["receipt"])
|
|
}
|
|
if n, _ := impArgs["number"].(int); n != 7 {
|
|
t.Errorf("get_pr_impact number = %v, want 7", impArgs["number"])
|
|
}
|
|
filesArg, ok := impArgs["files"].(string)
|
|
if !ok {
|
|
t.Fatalf("files arg type = %T, want JSON-string", impArgs["files"])
|
|
}
|
|
var fwd []string
|
|
if err := json.Unmarshal([]byte(filesArg), &fwd); err != nil || len(fwd) != 2 {
|
|
t.Errorf("forwarded files = %q (%v)", filesArg, err)
|
|
}
|
|
if revArgs := argsByTool["suggest_reviewers"]; revArgs == nil {
|
|
t.Error("suggest_reviewers was not called")
|
|
} else if n, _ := revArgs["number"].(int); n != 7 {
|
|
t.Errorf("suggest_reviewers number = %v, want 7", revArgs["number"])
|
|
}
|
|
|
|
// Assert the bundle file's JSON shape.
|
|
bundle := readBundle(t, out)
|
|
if bundle.BundleVersion != bundleVersion {
|
|
t.Errorf("bundle_version = %d, want %d", bundle.BundleVersion, bundleVersion)
|
|
}
|
|
if bundle.Number != 7 {
|
|
t.Errorf("number = %d, want 7", bundle.Number)
|
|
}
|
|
wantFiles := []string{"internal/forge/forge.go", "internal/forge/ghrest.go"}
|
|
if len(bundle.ChangedFiles) != 2 || bundle.ChangedFiles[0] != wantFiles[0] || bundle.ChangedFiles[1] != wantFiles[1] {
|
|
t.Errorf("changed_files = %v, want %v (sorted)", bundle.ChangedFiles, wantFiles)
|
|
}
|
|
|
|
// Impact slice round-trips with its risk + receipt.
|
|
var imp struct {
|
|
Risk string `json:"risk"`
|
|
Receipt struct {
|
|
RiskTier string `json:"risk_tier"`
|
|
} `json:"receipt"`
|
|
}
|
|
if err := json.Unmarshal(bundle.Impact, &imp); err != nil {
|
|
t.Fatalf("decode impact slice: %v", err)
|
|
}
|
|
if imp.Risk != "HIGH" || imp.Receipt.RiskTier != "HIGH" {
|
|
t.Errorf("impact risk/receipt = %q/%q, want HIGH/HIGH", imp.Risk, imp.Receipt.RiskTier)
|
|
}
|
|
|
|
// Reviewers slice round-trips with the ranked reviewer.
|
|
var rev struct {
|
|
Reviewers []struct {
|
|
Reviewer string `json:"reviewer"`
|
|
} `json:"reviewers"`
|
|
CodeownersFound bool `json:"codeowners_found"`
|
|
}
|
|
if err := json.Unmarshal(bundle.Reviewers, &rev); err != nil {
|
|
t.Fatalf("decode reviewers slice: %v", err)
|
|
}
|
|
if len(rev.Reviewers) != 1 || rev.Reviewers[0].Reviewer != "alice" || !rev.CodeownersFound {
|
|
t.Errorf("reviewers slice = %+v", rev)
|
|
}
|
|
}
|
|
|
|
// TestPRsBundle_DefaultOutPath asserts the bundle file name defaults to
|
|
// pr-<number>-bundle.json when --out is not given.
|
|
func TestPRsBundle_DefaultOutPath(t *testing.T) {
|
|
resetPRsSeams(t)
|
|
forgeAvailable = func(context.Context) bool { return false } // no token: skip PRFiles
|
|
stubBundleDaemon(t)
|
|
|
|
dir := t.TempDir()
|
|
prevWd, _ := os.Getwd()
|
|
if err := os.Chdir(dir); err != nil {
|
|
t.Fatalf("chdir: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chdir(prevWd) })
|
|
|
|
prsBundleOut = ""
|
|
if _, err := runPRsBundleCmd(t, "7"); err != nil {
|
|
t.Fatalf("bundle: %v", err)
|
|
}
|
|
|
|
want := filepath.Join(dir, "pr-7-bundle.json")
|
|
if _, err := os.Stat(want); err != nil {
|
|
t.Fatalf("default bundle path %q not written: %v", want, err)
|
|
}
|
|
// With no token, the impact's changed_files seed the top-level list.
|
|
bundle := readBundle(t, want)
|
|
if len(bundle.ChangedFiles) != 2 {
|
|
t.Errorf("changed_files (from impact) = %v, want 2", bundle.ChangedFiles)
|
|
}
|
|
}
|
|
|
|
// TestPRsBundle_DeterministicForUnchangedPR asserts two runs over an unchanged
|
|
// PR produce byte-identical bundle files (suitable for CI artifact diffing).
|
|
func TestPRsBundle_DeterministicForUnchangedPR(t *testing.T) {
|
|
resetPRsSeams(t)
|
|
forgeAvailable = func(context.Context) bool { return true }
|
|
forgePRFiles = func(_ context.Context, _ string, _ int) ([]string, error) {
|
|
// Deliberately unsorted to prove the writer sorts deterministically.
|
|
return []string{"internal/forge/ghrest.go", "internal/forge/forge.go"}, nil
|
|
}
|
|
stubBundleDaemon(t)
|
|
|
|
dir := t.TempDir()
|
|
out1 := filepath.Join(dir, "a.json")
|
|
out2 := filepath.Join(dir, "b.json")
|
|
|
|
prsBundleOut = out1
|
|
t.Cleanup(func() { prsBundleOut = "" })
|
|
if _, err := runPRsBundleCmd(t, "7"); err != nil {
|
|
t.Fatalf("bundle 1: %v", err)
|
|
}
|
|
prsBundleOut = out2
|
|
if _, err := runPRsBundleCmd(t, "7"); err != nil {
|
|
t.Fatalf("bundle 2: %v", err)
|
|
}
|
|
|
|
a, _ := os.ReadFile(out1)
|
|
b, _ := os.ReadFile(out2)
|
|
if string(a) != string(b) {
|
|
t.Errorf("bundle output is not deterministic:\n--- a ---\n%s\n--- b ---\n%s", a, b)
|
|
}
|
|
}
|
|
|
|
// TestPRsBundle_BadNumber rejects a non-numeric / non-positive PR argument.
|
|
func TestPRsBundle_BadNumber(t *testing.T) {
|
|
resetPRsSeams(t)
|
|
// Guard: the daemon seam must never be reached for a bad number.
|
|
prsDaemonTool = func(_ string, tool string, _ map[string]any) (json.RawMessage, error) {
|
|
t.Errorf("daemon tool %q must not be called for a bad PR number", tool)
|
|
return nil, nil
|
|
}
|
|
for _, bad := range []string{"notanumber", "0", "-3"} {
|
|
if _, err := runPRsBundleCmd(t, bad); err == nil {
|
|
t.Errorf("expected an error for PR number %q", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPRsBundle_ReviewersOptional asserts a suggest_reviewers failure does not
|
|
// sink the bundle: the impact slice is still written and reviewers is omitted.
|
|
func TestPRsBundle_ReviewersOptional(t *testing.T) {
|
|
resetPRsSeams(t)
|
|
forgeAvailable = func(context.Context) bool { return false }
|
|
prsDaemonTool = func(_ string, tool string, _ map[string]any) (json.RawMessage, error) {
|
|
if tool == "suggest_reviewers" {
|
|
return nil, errReviewerUnavailable
|
|
}
|
|
return cannedImpact(), nil
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
out := filepath.Join(dir, "bundle.json")
|
|
prsBundleOut = out
|
|
t.Cleanup(func() { prsBundleOut = "" })
|
|
|
|
if _, err := runPRsBundleCmd(t, "7"); err != nil {
|
|
t.Fatalf("bundle: %v", err)
|
|
}
|
|
bundle := readBundle(t, out)
|
|
if len(bundle.Reviewers) != 0 {
|
|
t.Errorf("reviewers must be omitted when suggest_reviewers fails, got %s", bundle.Reviewers)
|
|
}
|
|
if len(bundle.Impact) == 0 {
|
|
t.Error("impact slice must still be written when reviewers fail")
|
|
}
|
|
}
|
|
|
|
// TestWriteReviewerBundle_RejectsEmptyImpact asserts the writer refuses to
|
|
// produce a bundle without an impact payload.
|
|
func TestWriteReviewerBundle_RejectsEmptyImpact(t *testing.T) {
|
|
dir := t.TempDir()
|
|
out := filepath.Join(dir, "bundle.json")
|
|
if err := writeReviewerBundle(out, 7, []string{"a.go"}, nil, nil); err == nil {
|
|
t.Error("expected an error for an empty impact payload")
|
|
}
|
|
if _, err := os.Stat(out); err == nil {
|
|
t.Error("no file should be written when impact is empty")
|
|
}
|
|
}
|
|
|
|
// readBundle decodes a written bundle file into a reviewerBundle.
|
|
func readBundle(t *testing.T, path string) reviewerBundle {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read bundle %q: %v", path, err)
|
|
}
|
|
var b reviewerBundle
|
|
if err := json.Unmarshal(data, &b); err != nil {
|
|
t.Fatalf("decode bundle %q: %v\n%s", path, err, data)
|
|
}
|
|
return b
|
|
}
|
|
|
|
var errReviewerUnavailable = errBundle("reviewers unavailable")
|
|
|
|
type errBundle string
|
|
|
|
func (e errBundle) Error() string { return string(e) }
|