4d48917e36
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 25s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m1s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m0s
The test constructs github.Client directly (matching the Gitea integration test pattern), so setting VCS_TYPE does not affect the code under test. Remove the setenv call to avoid implying routing is being exercised.
245 lines
7.7 KiB
Go
245 lines
7.7 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.weiker.me/rodin/review-bot/gitea"
|
|
"gitea.weiker.me/rodin/review-bot/github"
|
|
"gitea.weiker.me/rodin/review-bot/llm"
|
|
"gitea.weiker.me/rodin/review-bot/review"
|
|
)
|
|
|
|
// Integration test requires a running Gitea instance and LLM endpoint.
|
|
// Set environment variables:
|
|
//
|
|
// INTEGRATION_VCS_URL - VCS base URL
|
|
// INTEGRATION_GITEA_TOKEN - Gitea API token with repo access
|
|
// INTEGRATION_GITEA_REPO - owner/repo with an open PR
|
|
// INTEGRATION_PR_NUMBER - PR number to test against
|
|
// INTEGRATION_LLM_BASE_URL - LLM API base URL
|
|
// INTEGRATION_LLM_API_KEY - LLM API key
|
|
// INTEGRATION_LLM_MODEL - Model name
|
|
func TestIntegration_FullReviewFlow(t *testing.T) {
|
|
giteaURL := os.Getenv("INTEGRATION_VCS_URL")
|
|
giteaToken := os.Getenv("INTEGRATION_GITEA_TOKEN")
|
|
giteaRepo := os.Getenv("INTEGRATION_GITEA_REPO")
|
|
prNumStr := os.Getenv("INTEGRATION_PR_NUMBER")
|
|
llmBaseURL := os.Getenv("INTEGRATION_LLM_BASE_URL")
|
|
llmAPIKey := os.Getenv("INTEGRATION_LLM_API_KEY")
|
|
llmModel := os.Getenv("INTEGRATION_LLM_MODEL")
|
|
|
|
if giteaURL == "" || giteaToken == "" || giteaRepo == "" || prNumStr == "" ||
|
|
llmBaseURL == "" || llmAPIKey == "" || llmModel == "" {
|
|
t.Skip("Integration test env vars not set, skipping")
|
|
}
|
|
|
|
prNumber, err := strconv.Atoi(prNumStr)
|
|
if err != nil {
|
|
t.Fatalf("Invalid PR number %q: %v", prNumStr, err)
|
|
}
|
|
|
|
// Parse owner/repo
|
|
parts := strings.SplitN(giteaRepo, "/", 2)
|
|
if len(parts) != 2 {
|
|
t.Fatalf("Invalid repo format %q", giteaRepo)
|
|
}
|
|
owner, repoName := parts[0], parts[1]
|
|
if owner == "" || repoName == "" {
|
|
t.Fatalf("Invalid repo format %q", giteaRepo)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Step 1: Fetch PR
|
|
giteaClient := gitea.NewClient(giteaURL, giteaToken)
|
|
pr, err := giteaClient.GetPullRequest(ctx, owner, repoName, prNumber)
|
|
if err != nil {
|
|
t.Fatalf("GetPullRequest: %v", err)
|
|
}
|
|
t.Logf("PR: %s (sha: %s)", pr.Title, pr.Head.Sha)
|
|
|
|
// Step 2: Fetch diff
|
|
diff, err := giteaClient.GetPullRequestDiff(ctx, owner, repoName, prNumber)
|
|
if err != nil {
|
|
t.Fatalf("GetPullRequestDiff: %v", err)
|
|
}
|
|
if diff == "" {
|
|
t.Fatal("diff is empty")
|
|
}
|
|
t.Logf("Diff size: %d bytes", len(diff))
|
|
|
|
// Step 3: Build prompts
|
|
systemPrompt := review.BuildSystemPrompt("", "")
|
|
userPrompt := review.BuildUserPrompt(pr.Title, pr.Body, diff, "", true, "")
|
|
|
|
// Step 4: Call LLM
|
|
llmClient := llm.NewClient(llmBaseURL, llmAPIKey, llmModel)
|
|
response, err := llmClient.Complete(ctx, []llm.Message{
|
|
{Role: "system", Content: systemPrompt},
|
|
{Role: "user", Content: userPrompt},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LLM Complete: %v", err)
|
|
}
|
|
t.Logf("LLM response: %d bytes", len(response))
|
|
|
|
// Step 5: Parse response
|
|
result, err := review.ParseResponse(response)
|
|
if err != nil {
|
|
t.Fatalf("ParseResponse: %v", err)
|
|
}
|
|
t.Logf("Verdict: %s, Findings: %d", result.Verdict, len(result.Findings))
|
|
|
|
// Step 6: Format (dry-run validation)
|
|
body := review.FormatMarkdown(result, "integration-test")
|
|
if body == "" {
|
|
t.Fatal("formatted review body is empty")
|
|
}
|
|
t.Logf("Review body:\n%s", body)
|
|
}
|
|
|
|
func TestIntegration_PostAndCleanup(t *testing.T) {
|
|
giteaURL := os.Getenv("INTEGRATION_VCS_URL")
|
|
giteaToken := os.Getenv("INTEGRATION_GITEA_TOKEN")
|
|
giteaRepo := os.Getenv("INTEGRATION_GITEA_REPO")
|
|
prNumStr := os.Getenv("INTEGRATION_PR_NUMBER")
|
|
|
|
if giteaURL == "" || giteaToken == "" || giteaRepo == "" || prNumStr == "" {
|
|
t.Skip("Integration test env vars not set, skipping")
|
|
}
|
|
|
|
prNumber, err := strconv.Atoi(prNumStr)
|
|
if err != nil {
|
|
t.Fatalf("Invalid PR number %q: %v", prNumStr, err)
|
|
}
|
|
|
|
parts := strings.SplitN(giteaRepo, "/", 2)
|
|
if len(parts) != 2 {
|
|
t.Fatalf("Invalid repo format %q", giteaRepo)
|
|
}
|
|
owner, repoName := parts[0], parts[1]
|
|
|
|
ctx := context.Background()
|
|
giteaClient := gitea.NewClient(giteaURL, giteaToken)
|
|
|
|
// Post a test review
|
|
sentinel := "<!-- review-bot:integration-test -->"
|
|
testBody := "# Integration Test Review\n\nThis is a test review.\n\n" + sentinel
|
|
posted, err := giteaClient.PostReview(ctx, owner, repoName, prNumber, "COMMENT", testBody, "", nil)
|
|
if err != nil {
|
|
t.Fatalf("PostReview: %v", err)
|
|
}
|
|
t.Logf("Posted review ID: %d", posted.ID)
|
|
|
|
// Verify it appears in listing
|
|
reviews, err := giteaClient.ListReviews(ctx, owner, repoName, prNumber)
|
|
if err != nil {
|
|
t.Fatalf("ListReviews: %v", err)
|
|
}
|
|
|
|
found := false
|
|
for _, r := range reviews {
|
|
if r.ID == posted.ID && strings.Contains(r.Body, sentinel) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("posted review not found in listing")
|
|
}
|
|
|
|
// Cleanup: delete the test review
|
|
err = giteaClient.DeleteReview(ctx, owner, repoName, prNumber, posted.ID)
|
|
if err != nil {
|
|
t.Logf("Warning: could not delete test review %d: %v", posted.ID, err)
|
|
}
|
|
}
|
|
|
|
// TestIntegration_GitHub_PostAndVerifyReview exercises the full VCS routing path
|
|
// for GitHub when INTEGRATION_GITHUB_TOKEN and INTEGRATION_GITHUB_REPO are set.
|
|
// It verifies that the GitHub adapter is selected via VCS_TYPE=github and that
|
|
// PostReview succeeds against a real GitHub PR.
|
|
//
|
|
// Required environment variables:
|
|
//
|
|
// INTEGRATION_GITHUB_TOKEN - GitHub personal access token with repo access
|
|
// INTEGRATION_GITHUB_REPO - owner/repo with an open PR (e.g. Rodin-AI/review-bot)
|
|
// INTEGRATION_GITHUB_PR - PR number to test against
|
|
//
|
|
// The test skips gracefully when these variables are absent.
|
|
func TestIntegration_GitHub_PostAndVerifyReview(t *testing.T) {
|
|
githubToken := os.Getenv("INTEGRATION_GITHUB_TOKEN")
|
|
githubRepo := os.Getenv("INTEGRATION_GITHUB_REPO")
|
|
prNumStr := os.Getenv("INTEGRATION_GITHUB_PR")
|
|
|
|
if githubToken == "" || githubRepo == "" || prNumStr == "" {
|
|
t.Skip("INTEGRATION_GITHUB_TOKEN, INTEGRATION_GITHUB_REPO, and INTEGRATION_GITHUB_PR not set, skipping")
|
|
}
|
|
|
|
prNumber, err := strconv.Atoi(prNumStr)
|
|
if err != nil {
|
|
t.Fatalf("Invalid PR number %q: %v", prNumStr, err)
|
|
}
|
|
|
|
parts := strings.SplitN(githubRepo, "/", 2)
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
t.Fatalf("Invalid repo format %q, expected owner/repo", githubRepo)
|
|
}
|
|
owner, repoName := parts[0], parts[1]
|
|
|
|
ctx := context.Background()
|
|
ghClient := github.NewClient(githubToken, "https://api.github.com")
|
|
|
|
// Verify adapter selection: GetAuthenticatedUser must succeed.
|
|
user, err := ghClient.GetAuthenticatedUser(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetAuthenticatedUser: %v — check INTEGRATION_GITHUB_TOKEN", err)
|
|
}
|
|
t.Logf("Authenticated as: %s", user)
|
|
|
|
// Verify PR is accessible via GitHub adapter.
|
|
pr, err := ghClient.GetPullRequest(ctx, owner, repoName, prNumber)
|
|
if err != nil {
|
|
t.Fatalf("GetPullRequest: %v", err)
|
|
}
|
|
t.Logf("PR: %s (sha: %s)", pr.Title, pr.Head.Sha)
|
|
|
|
// Post a COMMENT review — does not require PR approval permissions.
|
|
sentinel := "<!-- review-bot:integration-test -->"
|
|
testBody := "# Integration Test Review (GitHub)\n\nThis is an automated integration test.\n\n" + sentinel
|
|
posted, err := ghClient.PostReview(ctx, owner, repoName, prNumber, "COMMENT", testBody, "", nil)
|
|
if err != nil {
|
|
t.Fatalf("PostReview: %v", err)
|
|
}
|
|
t.Logf("Posted review ID: %d", posted.ID)
|
|
|
|
// Verify the review appears in ListReviews.
|
|
reviews, err := ghClient.ListReviews(ctx, owner, repoName, prNumber)
|
|
if err != nil {
|
|
t.Fatalf("ListReviews: %v", err)
|
|
}
|
|
found := false
|
|
for _, r := range reviews {
|
|
if r.ID == posted.ID && strings.Contains(r.Body, sentinel) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("posted review ID %d not found in ListReviews output", posted.ID)
|
|
}
|
|
|
|
// Attempt cleanup — GitHub does not allow deleting submitted reviews,
|
|
// so this is expected to fail with ErrCannotDeleteSubmittedReview (422).
|
|
// Log it as informational only.
|
|
if err := ghClient.DeleteReview(ctx, owner, repoName, prNumber, posted.ID); err != nil {
|
|
t.Logf("Note: DeleteReview returned (expected for submitted GitHub reviews): %v", err)
|
|
}
|
|
}
|