mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2026-08-27 02:27:45 +00:00
4e62d158cb
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)
115 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|