| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- 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])
- })
- }
- }
|