Files
MCP/operation/readme_test.go
T
Lunny Xiao 4e62d158cb docs(image): document Docker image naming mismatch
Add OCI title/description labels to the Dockerfile, note the
docker.gitea.com/gitea-mcp-server vs gitea-mcp naming mismatch in each
README with a link to a new docs/adr note recording the maintainers'
preferred gitea/mcp-server direction from issue #181, and add a test
that keeps the README image references consistent.

Co-Authored-By: Codet <codet@commitgo.dev> (GPT-5-Codex)
2026-08-23 23:28:23 -07:00

115 lines
4.1 KiB
Go

package operation
import (
"maps"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"testing"
)
// toolTableRow matches a row of the "Available Tools" table in the README
// files, capturing the tool name, the scope cell and the access cell, e.g.
// "| get_me | user | Read | Get the current authenticated user |".
var toolTableRow = regexp.MustCompile(`^\|\s*([a-z_]+)\s*\|\s*([a-z_]+)\s*\|\s*(\S+)\s*\|`)
// readmeAccessLabels maps each README to the access-column labels it uses.
var readmeAccessLabels = map[string]map[string]string{
"../README.md": {"Read": "read", "Write": "write"},
"../README.zh-cn.md": {"读取": "read", "写入": "write"},
"../README.zh-tw.md": {"讀取": "read", "寫入": "write"},
}
// publishedDockerImage is the image name published for this project. See
// docs/adr/docker-image-naming.md for why it differs from the repository name.
const publishedDockerImage = "docker.gitea.com/gitea-mcp-server"
// dockerImageReference matches any docker.gitea.com/<name> image reference so
// stray typos or partial renames in the README files can be caught.
var dockerImageReference = regexp.MustCompile(`docker\.gitea\.com/[a-zA-Z0-9._-]+`)
// TestReadmeDockerImageReferencesAreConsistent ensures every README mentions
// the same, currently published Docker image name. A partial rename, where
// one example is updated but another is missed, would otherwise leave users
// copying a command that pulls a nonexistent image.
func TestReadmeDockerImageReferencesAreConsistent(t *testing.T) {
for _, path := range []string{"../README.md", "../README.zh-cn.md", "../README.zh-tw.md"} {
t.Run(filepath.Base(path), func(t *testing.T) {
content, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
references := dockerImageReference.FindAllString(string(content), -1)
if len(references) == 0 {
t.Fatalf("no docker.gitea.com image reference found in %s", path)
}
for _, ref := range references {
if ref != publishedDockerImage {
t.Errorf("found image reference %q in %s, want %q", ref, path, publishedDockerImage)
}
}
})
}
}
// toolInfo is what TestReadmeToolTables tracks per tool, both as registered
// in code and as documented in a README, so the two can be compared.
type toolInfo struct {
scope string
access string
}
// TestReadmeToolTables ensures the tool tables in the README files stay in sync
// with the registered tools, in both directions and for every translation.
// The tables listed tools that no longer existed for several releases before
// anyone noticed.
// The scope names in the README are the canonical, lowercase snake_case names
// returned by (*tool.Tool).Scope(), so no translation is needed to compare them.
func TestReadmeToolTables(t *testing.T) {
registered := map[string]toolInfo{}
for _, d := range domainTools {
scope := d.Scope()
for _, st := range d.ReadTools() {
registered[st.Tool.Name] = toolInfo{scope: scope, access: "read"}
}
for _, st := range d.WriteTools() {
registered[st.Tool.Name] = toolInfo{scope: scope, access: "write"}
}
}
for path, labels := range readmeAccessLabels {
t.Run(filepath.Base(path), func(t *testing.T) {
content, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
documented := map[string]toolInfo{}
for line := range strings.SplitSeq(string(content), "\n") {
if match := toolTableRow.FindStringSubmatch(line); match != nil {
documented[match[1]] = toolInfo{scope: match[2], access: labels[match[3]]}
}
}
for _, name := range slices.Sorted(maps.Keys(registered)) {
got, ok := documented[name]
want := registered[name]
switch {
case !ok:
t.Errorf("tool %q is registered but missing from the tool table", name)
case got.access != want.access:
t.Errorf("tool %q is documented with %q access, want %q", name, got.access, want.access)
case got.scope != want.scope:
t.Errorf("tool %q is documented with scope %q, want %q", name, got.scope, want.scope)
}
}
for _, name := range slices.Sorted(maps.Keys(documented)) {
if _, ok := registered[name]; !ok {
t.Errorf("tool %q is in the tool table but is not registered", name)
}
}
})
}
}