Files
zzet--gortex/internal/mcp/tools_review_post_test.go
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

201 lines
7.3 KiB
Go

package mcp
import (
"context"
"encoding/json"
"strings"
"testing"
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/forge"
"github.com/zzet/gortex/internal/review"
)
// callPostReview invokes the post_review handler directly with the given args.
func callPostReview(t *testing.T, srv *Server, args map[string]any) *mcplib.CallToolResult {
t.Helper()
req := mcplib.CallToolRequest{}
req.Params.Name = "post_review"
req.Params.Arguments = args
res, err := srv.handlePostReview(context.Background(), req)
require.NoError(t, err)
return res
}
type postReviewOut struct {
Posted int `json:"posted"`
Skipped int `json:"skipped"`
Redacted int `json:"redacted"`
Total int `json:"total"`
DryRun bool `json:"dry_run"`
ReviewURL string `json:"review_url"`
Payloads []map[string]any `json:"payloads"`
Error string `json:"error"`
}
func decodePostReview(t *testing.T, res *mcplib.CallToolResult) postReviewOut {
t.Helper()
require.False(t, res.IsError, "errored: %v", res)
var out postReviewOut
require.NoError(t, json.Unmarshal([]byte(res.Content[0].(mcplib.TextContent).Text), &out))
return out
}
func findingsJSON(t *testing.T, findings []review.Finding) string {
t.Helper()
b, err := json.Marshal(findings)
require.NoError(t, err)
return string(b)
}
// TestPostReview_DryRunReturnsRedactedPayload drives the real review.PostFindings
// (no forge seam swap) on a dry run: the planted secret must be absent from the
// returned payload and no network call happens (dry run never reaches the forge).
func TestPostReview_DryRunReturnsRedactedPayload(t *testing.T) {
dir, _ := reviewGitRepo(t)
srv := indexedSiblingServer(t, dir)
const secret = "AKIA0123456789ABCDEF"
findings := []review.Finding{
{Rule: "secret", Severity: review.SevError, Category: "security", File: "pkg/a.go", Line: 5,
Message: "hard-coded credential", Body: "leaked key inline: " + secret},
{Rule: "nil-deref", Severity: review.SevWarning, Category: "correctness", File: "pkg/b.go", Line: 9,
Message: "nil deref", Body: "x may be nil"},
}
out := decodePostReview(t, callPostReview(t, srv, map[string]any{
"number": 1,
"findings": findingsJSON(t, findings),
"dry_run": true,
"refuse_on_secret": false,
}))
require.True(t, out.DryRun)
require.Equal(t, 1, out.Redacted, "the secret-bearing finding must be redacted")
require.Equal(t, 2, out.Posted, "both findings build into the would-post payload")
require.Len(t, out.Payloads, 2)
for _, p := range out.Payloads {
body, _ := p["body"].(string)
require.NotContains(t, body, secret, "secret leaked into dry-run payload")
require.Equal(t, "RIGHT", p["side"])
}
}
// TestPostReview_RealPathPostsViaStubbedForgeSeam swaps the post-seam and asserts
// the real (non-dry-run) path reaches review.PostFindings, which maps the
// findings to RIGHT-side comments — no network.
func TestPostReview_RealPathPostsViaStubbedForgeSeam(t *testing.T) {
dir, _ := reviewGitRepo(t)
srv := indexedSiblingServer(t, dir)
var got []forge.ReviewComment
var gotNum int
orig := postReviewFindings
postReviewFindings = func(_ context.Context, _ string, target review.PostTarget, findings []review.Finding, opts review.PostOptions) (review.PostResult, error) {
require.False(t, opts.DryRun, "live path must not be a dry run")
gotNum = target.PRNumber
// Exercise the real mapping + redaction without networking: build the
// would-post comments exactly as the live post would.
got = nil
for _, f := range findings {
clean, _ := review.RedactSecrets(review.RenderCommentBody(f))
end := f.EndLine
if end == 0 {
end = f.Line
}
start := f.StartLine
c := forge.ReviewComment{Path: f.File, Line: end, Side: "RIGHT", Body: clean}
if start > 0 && start < end {
c.StartLine = start
}
got = append(got, c)
}
return review.PostResult{Posted: len(got)}, nil
}
t.Cleanup(func() { postReviewFindings = orig })
findings := []review.Finding{
{Rule: "nil-deref", Severity: review.SevError, File: "pkg/a.go", Line: 14, Body: "deref of nil"},
{Rule: "race", Severity: review.SevWarning, File: "pkg/b.go", StartLine: 20, EndLine: 25, Body: "data race"},
}
out := decodePostReview(t, callPostReview(t, srv, map[string]any{
"number": 42,
"findings": findingsJSON(t, findings),
}))
require.Equal(t, 42, gotNum)
require.False(t, out.DryRun)
require.Equal(t, 2, out.Posted)
require.Len(t, got, 2)
for _, c := range got {
require.Equal(t, "RIGHT", c.Side)
require.NotEmpty(t, c.Path)
require.Greater(t, c.Line, 0)
}
// Multi-line finding carries start_line < line.
require.Equal(t, 20, got[1].StartLine)
require.Equal(t, 25, got[1].Line)
}
// TestPostReview_PublicRepoRefusedWithoutAllowPublic asserts a public target is
// refused (structured error, nothing posted) unless confirm_public is set. The
// seam runs the REAL review.PostFindings gate, so the refusal is the library's,
// proving the tool threads public + confirm_public through correctly.
func TestPostReview_PublicRepoRefusedWithoutAllowPublic(t *testing.T) {
dir, _ := reviewGitRepo(t)
srv := indexedSiblingServer(t, dir)
sawAllowPublic := false
orig := postReviewFindings
postReviewFindings = func(ctx context.Context, repoDir string, target review.PostTarget, findings []review.Finding, opts review.PostOptions) (review.PostResult, error) {
sawAllowPublic = opts.AllowPublic
require.True(t, target.Public, "tool must thread public:true into the target")
// Run the real gate, but with a no-op network call so a permitted post
// does not reach a forge. The gate runs before any network on the dry-run
// path, so a dry run exercises the public/fork refusal without networking.
opts.DryRun = true
return review.PostFindings(ctx, repoDir, target, findings, opts)
}
t.Cleanup(func() { postReviewFindings = orig })
findings := []review.Finding{{Rule: "x", Severity: review.SevWarning, File: "a.go", Line: 1, Body: "f"}}
// Public, no confirm_public → refused by the real gate.
out := decodePostReview(t, callPostReview(t, srv, map[string]any{
"number": 3,
"findings": findingsJSON(t, findings),
"public": true,
}))
require.False(t, sawAllowPublic, "confirm_public not set → AllowPublic must be false")
require.Contains(t, strings.ToLower(out.Error), "public")
require.Equal(t, 0, out.Posted)
// Public + confirm_public → AllowPublic threaded through, gate permits.
out = decodePostReview(t, callPostReview(t, srv, map[string]any{
"number": 3,
"findings": findingsJSON(t, findings),
"public": true,
"confirm_public": true,
}))
require.True(t, sawAllowPublic, "confirm_public:true must set AllowPublic")
require.Empty(t, out.Error)
require.Equal(t, 1, out.Posted)
}
// TestPostReview_RegisteredEagerly asserts post_review is in the eager (hot) set.
func TestPostReview_RegisteredEagerly(t *testing.T) {
require.True(t, hotEagerTools["post_review"],
"post_review must be eagerly registered (hot), not deferred")
t.Setenv("GORTEX_LAZY_TOOLS", "1")
srv, _ := setupTestServer(t)
live := srv.mcpServer.ListTools()
require.Contains(t, live, "post_review",
"eager post_review tool must appear in tools/list without tools_search expansion")
require.False(t, srv.lazy.IsDeferred("post_review"),
"post_review must not be deferred")
}