Files
3x-ui/bot_context_test.go
T
Sanaei 58669f6146 refactor(ci): replace the in-house review lanes with the official code-review skill
The four pull_request_target review jobs in claude-bot.yml (Senior
Developer / QA / Tester / Arbiter and their shared rubric) are replaced by
a single review job running the official code-review plugin - the same
skill behind Anthropic's hosted Code Review and the review workflow
/install-github-app generates. The hosted service needs a Team/Enterprise
organisation, so the plugin runs in CI on the maintainer's subscription
instead: inline findings on PR open and ready-for-review, plus manual
(re-)review when the owner or a collaborator comments "@claude review".

The official example triggers on pull_request, but GitHub withholds
secrets from fork runs and essentially every 3x-ui pull request is from a
fork, so the job keeps the lanes' pull_request_target posture: the
workspace is the base revision and nothing from the pull request is
checked out or executed.

What the lanes uniquely knew is distilled into REVIEW.md, handed to the
skill via --append-system-prompt and pinned by bot_context_test.go the way
repo-context.md is: the runtime.Runtime dispatch rule, migration and
upgrade safety, the four-step route contract chain including the unchecked
docs copy, the i18n rule, the three link implementations, and the
wire-format verification bar. The mention job now ignores "@claude
review" comments on pull requests so the review trigger does not also
wake the generic bot, and the lane-only rubric file goes with the lanes.

The remaining prompts also lose their tone micro-rules (no emoji, no
exclamation marks, no filler) and the workflow's comment banners are
removed.
2026-08-20 15:09:48 +02:00

156 lines
4.8 KiB
Go

package main
// The Claude bot prompts in .github/workflows/claude-bot.yml no longer restate
// repository facts; they read .github/claude/repo-context.md instead. A stale
// claim in that file is invisible until it produces a wrong review, so every
// claim a machine can check is pinned here.
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
const (
botContextPath = ".github/claude/repo-context.md"
reviewPath = "REVIEW.md"
ciWorkflowPath = ".github/workflows/ci.yml"
)
func readRepoFile(t *testing.T, path string) string {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
return string(b)
}
// section returns the text between two markers, so a table is matched only
// inside the heading that owns it.
func section(t *testing.T, doc, from, to string) string {
t.Helper()
i := strings.Index(doc, from)
if i < 0 {
t.Fatalf("%s no longer contains the heading %q", botContextPath, from)
}
rest := doc[i+len(from):]
if j := strings.Index(rest, to); j >= 0 {
return rest[:j]
}
return rest
}
func TestBotContextLocaleFileCount(t *testing.T) {
doc := readRepoFile(t, botContextPath)
m := regexp.MustCompile("`internal/web/translation/` \\((\\d+) files\\)").FindStringSubmatch(doc)
if m == nil {
t.Fatalf("%s no longer states the locale file count as \"`internal/web/translation/` (N files)\"", botContextPath)
}
files, err := filepath.Glob("internal/web/translation/*.json")
if err != nil {
t.Fatalf("glob locales: %v", err)
}
if got := len(files); m[1] != itoa(got) {
t.Errorf("%s claims %s locale files, internal/web/translation/ holds %d; update the claim and every prompt that relies on it", botContextPath, m[1], got)
}
}
func itoa(n int) string {
if n == 0 {
return "0"
}
var b []byte
for n > 0 {
b = append([]byte{byte('0' + n%10)}, b...)
n /= 10
}
return string(b)
}
func TestBotContextNamesRealCIJobs(t *testing.T) {
doc := readRepoFile(t, botContextPath)
ci := readRepoFile(t, ciWorkflowPath)
table := section(t, doc, "## What CI runs", "**What CI does NOT prove.**")
rows := regexp.MustCompile("(?m)^\\| `([a-z0-9-]+)` \\|").FindAllStringSubmatch(table, -1)
if len(rows) < 5 {
t.Fatalf("expected the CI table in %s to list at least 5 jobs, found %d", botContextPath, len(rows))
}
for _, r := range rows {
t.Run(r[1], func(t *testing.T) {
if !strings.Contains(ci, "\n "+r[1]+":\n") {
t.Errorf("%s describes a CI job %q that %s does not define", botContextPath, r[1], ciWorkflowPath)
}
})
}
}
func TestBotContextNamesRealPaths(t *testing.T) {
// REVIEW.md briefs the review job the way repo-context.md briefs the
// issue bot, so both get their paths pinned.
// internal/web/dist and frontend/node_modules are build output: absent from a
// fresh clone, created by `make dist-stub` and `npm ci`.
generated := map[string]bool{
"internal/web/dist/": true,
"frontend/node_modules": true,
"frontend/src/generated/": true,
}
seen := map[string]bool{}
counts := map[string]int{}
for _, src := range []string{botContextPath, reviewPath} {
for _, m := range regexp.MustCompile("`([^`]+)`").FindAllStringSubmatch(readRepoFile(t, src), -1) {
p := m[1]
if !regexp.MustCompile(`^(internal|frontend|docs|tools|\.github)/`).MatchString(p) ||
strings.ContainsAny(p, "*{ ") || generated[p] || seen[p] {
continue
}
seen[p] = true
counts[src]++
t.Run(p, func(t *testing.T) {
if _, err := os.Stat(strings.TrimSuffix(p, "/")); err != nil {
t.Errorf("%s names %q, which does not exist; the bot prompts trust this file", src, p)
}
})
}
}
if counts[botContextPath] < 20 {
t.Errorf("expected the bot context to name at least 20 repository paths, found %d - has the file been gutted?", counts[botContextPath])
}
}
func TestBotContextSkipGatesExist(t *testing.T) {
doc := readRepoFile(t, botContextPath)
table := section(t, doc, "**What CI does NOT prove.**", "Mutation testing")
// [A-Z0-9_] and not [A-Z_]: XRAY_E2E_BINARY carries a digit, and excluding it
// silently dropped that gate from the check instead of failing.
gates := regexp.MustCompile("`((?:XUI|XRAY)_[A-Z0-9_]+)`").FindAllStringSubmatch(table, -1)
if len(gates) < 5 {
t.Fatalf("expected at least 5 skip-gate variables in %s, found %d", botContextPath, len(gates))
}
var sources []string
err := filepath.WalkDir("internal", func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && strings.HasSuffix(path, ".go") {
sources = append(sources, path)
}
return nil
})
if err != nil {
t.Fatalf("walk internal: %v", err)
}
for _, g := range gates {
t.Run(g[1], func(t *testing.T) {
for _, f := range sources {
if strings.Contains(readRepoFile(t, f), g[1]) {
return
}
}
t.Errorf("%s lists %s as a test skip gate, but no .go file under internal/ reads it", botContextPath, g[1])
})
}
}