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