test(#139): improve cmd/review-bot coverage from 44.6% to 49.3% #140

Merged
rodin merged 1 commits from issue-139 into main 2026-05-15 04:35:54 +00:00
2 changed files with 180 additions and 0 deletions
+123
View File
@@ -1383,3 +1383,126 @@ func TestFetchPatterns_MultipleRepos(t *testing.T) {
t.Errorf("expected Elixir pipes content, got: %q", got)
}
}
// TestMainSubprocess_MissingLLMBaseURL confirms that --llm-base-url is required
// when provider=openai (the default).
func TestMainSubprocess_MissingLLMBaseURL(t *testing.T) {
if os.Getenv("TEST_SUBPROCESS_MAIN") == "1" {
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
os.Args = []string{"review-bot",
"--vcs-url", "https://gitea.example.com",
"--repo", "owner/repo",
"--pr", "1",
"--reviewer-token", "tok",
"--llm-model", "gpt-4",
// --llm-base-url and --llm-api-key intentionally omitted
}
main()
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestMainSubprocess_MissingLLMBaseURL")
cmd.Env = append(cleanEnv(), "TEST_SUBPROCESS_MAIN=1")
out, err := cmd.CombinedOutput()
if err == nil {
t.Fatal("expected non-zero exit when llm-base-url is missing")
}
if !strings.Contains(string(out), "llm-base-url") {
t.Errorf("expected error mentioning llm-base-url, got: %s", out)
}
}
// TestMainSubprocess_MissingAICoreCredentials confirms that aicore-specific credentials
// are required when provider=aicore.
func TestMainSubprocess_MissingAICoreCredentials(t *testing.T) {
if os.Getenv("TEST_SUBPROCESS_MAIN") == "1" {
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
os.Args = []string{"review-bot",
"--vcs-url", "https://gitea.example.com",
"--repo", "owner/repo",
"--pr", "1",
"--reviewer-token", "tok",
"--llm-model", "gpt-4",
"--llm-provider", "aicore",
// aicore-client-id, aicore-client-secret, aicore-auth-url, aicore-api-url omitted
}
main()
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestMainSubprocess_MissingAICoreCredentials")
cmd.Env = append(cleanEnv(), "TEST_SUBPROCESS_MAIN=1")
out, err := cmd.CombinedOutput()
if err == nil {
t.Fatal("expected non-zero exit when aicore credentials are missing")
}
if !strings.Contains(string(out), "AI Core credentials") {
t.Errorf("expected error about AI Core credentials, got: %s", out)
}
}
// TestMainSubprocess_ConflictingPersonaFlags confirms that --persona and --persona-file
// cannot be used together.
func TestMainSubprocess_ConflictingPersonaFlags(t *testing.T) {
if os.Getenv("TEST_SUBPROCESS_MAIN") == "1" {
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
os.Args = []string{"review-bot",
"--vcs-url", "https://gitea.example.com",
"--repo", "owner/repo",
"--pr", "1",
"--reviewer-token", "tok",
"--llm-base-url", "https://api.example.com",
"--llm-api-key", "key",
"--llm-model", "gpt-4",
"--persona", "security",
"--persona-file", "custom.json",
}
main()
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestMainSubprocess_ConflictingPersonaFlags")
cmd.Env = append(cleanEnv(), "TEST_SUBPROCESS_MAIN=1")
out, err := cmd.CombinedOutput()
if err == nil {
t.Fatal("expected non-zero exit with both --persona and --persona-file set")
}
if !strings.Contains(string(out), "mutually exclusive") {
t.Errorf("expected error about mutually exclusive flags, got: %s", out)
}
}
// TestMainSubprocess_DeprecatedGiteaURLEnv confirms that GITEA_URL env var still works
// as a deprecated fallback for VCS_URL.
func TestMainSubprocess_DeprecatedGiteaURLEnv(t *testing.T) {
if os.Getenv("TEST_SUBPROCESS_MAIN") == "1" {
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
// Set required flags but omit --vcs-url; GITEA_URL should be picked up.
// The test will exit with an error after VCS init (no PR to fetch), but
// the deprecation warning must appear.
os.Args = []string{"review-bot",
// No --vcs-url: should fall back to GITEA_URL env var
"--repo", "owner/repo",
"--pr", "1",
"--reviewer-token", "tok",
Review

[MINOR] TestMainSubprocess_DeprecatedGiteaURLEnv ignores the error return from cmd.CombinedOutput() with out, _ := cmd.CombinedOutput(). All other subprocess tests in this file capture out, err := cmd.CombinedOutput() and check err (or intentionally assert on it). The deprecation test explicitly doesn't need a non-zero exit assertion, but discarding the error silently is inconsistent with the rest of the file. Consider out, _ := cmd.CombinedOutput() with a comment, or just accept and ignore the error explicitly: //nolint or a comment explaining why it's intentionally ignored.

**[MINOR]** TestMainSubprocess_DeprecatedGiteaURLEnv ignores the error return from cmd.CombinedOutput() with `out, _ := cmd.CombinedOutput()`. All other subprocess tests in this file capture `out, err := cmd.CombinedOutput()` and check err (or intentionally assert on it). The deprecation test explicitly doesn't need a non-zero exit assertion, but discarding the error silently is inconsistent with the rest of the file. Consider `out, _ := cmd.CombinedOutput()` with a comment, or just accept and ignore the error explicitly: `//nolint` or a comment explaining why it's intentionally ignored.
"--llm-base-url", "https://api.example.com",
"--llm-api-key", "key",
"--llm-model", "gpt-4",
}
main()
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestMainSubprocess_DeprecatedGiteaURLEnv")
// Inject GITEA_URL but NOT VCS_URL.
env := append(cleanEnv(),
"TEST_SUBPROCESS_MAIN=1",
"GITEA_URL=https://gitea.example.com",
)
cmd.Env = env
out, _ := cmd.CombinedOutput()
// The process will fail (no real server), but the deprecation warning must appear.
if !strings.Contains(string(out), "deprecated") {
t.Errorf("expected deprecation warning for GITEA_URL, got: %s", out)
}
}
+57
View File
@@ -125,3 +125,60 @@ func TestRunValidateURL_WithCapture(t *testing.T) {
t.Errorf("expected error about https in stderr, got %q", errBuf.String())
}
}
// TestIsValidateError_Nil confirms that isValidateError returns false for a nil error.
func TestIsValidateError_Nil(t *testing.T) {
var ve *validateError
if isValidateError(nil, &ve) {
t.Error("isValidateError(nil, ...) should return false")
}
}
// TestValidateURL_EmptyHost confirms that a URL with no hostname returns a code-2 error.
func TestValidateURL_EmptyHost(t *testing.T) {
// "https://" parses fine but has no hostname.
err := validateURL("https://")
if err == nil {
t.Fatal("expected error for URL with no host, got nil")
}
var ve *validateError
if !isValidateError(err, &ve) {
t.Fatalf("expected *validateError, got %T: %v", err, err)
}
if ve.code != 2 {
t.Errorf("expected code 2, got %d (msg=%s)", ve.code, ve.message)
}
if !strings.Contains(ve.message, "no host") {
t.Errorf("expected 'no host' in error message, got %q", ve.message)
}
}
// TestRunValidateURL_Success confirms that a resolvable public URL prints "OK" and returns 0.
Review

[MINOR] TestRunValidateURL_Success calls validateURL directly as a pre-check, which means the actual DNS resolution happens twice (once in the pre-check, once in runValidateURL). This is not a bug, but it's slightly inconsistent with the pattern in TestValidateURL_BlockedPrivateIP which calls validateURL once and inspects the result. More critically, if validateURL passes the pre-check but the network state changes between the two calls (unlikely in practice), the test could produce a confusing failure. Using t.Skip based on a simple net.LookupHost check might be slightly cleaner, but the current approach is not wrong.

**[MINOR]** TestRunValidateURL_Success calls validateURL directly as a pre-check, which means the actual DNS resolution happens twice (once in the pre-check, once in runValidateURL). This is not a bug, but it's slightly inconsistent with the pattern in TestValidateURL_BlockedPrivateIP which calls validateURL once and inspects the result. More critically, if validateURL passes the pre-check but the network state changes between the two calls (unlikely in practice), the test could produce a confusing failure. Using t.Skip based on a simple net.LookupHost check might be slightly cleaner, but the current approach is not wrong.
// This test requires external DNS; it is skipped in environments without network access.
func TestRunValidateURL_Success(t *testing.T) {
// Pre-check: validate that DNS is available before exercising the success path.
err := validateURL("https://example.com/")
if err != nil {
t.Skipf("skipping success-path test: DNS unavailable or example.com blocked (%v)", err)
}
var outBuf, errBuf bytes.Buffer
origOut, origErr := outWriter, errWriter
outWriter = &outBuf
errWriter = &errBuf
defer func() {
outWriter = origOut
errWriter = origErr
}()
code := runValidateURL([]string{"https://example.com/"})
if code != 0 {
t.Errorf("expected exit code 0 for safe URL, got %d (stderr: %s)", code, errBuf.String())
}
if !strings.Contains(outBuf.String(), "OK:") {
t.Errorf("expected 'OK:' in stdout, got %q", outBuf.String())
}
if errBuf.Len() != 0 {
t.Errorf("expected no stderr for safe URL, got %q", errBuf.String())
}
}