mcp-framework/scaffold/scaffold_test.go

243 lines
7 KiB
Go

package scaffold
import (
"errors"
"go/parser"
"go/token"
"os"
"path/filepath"
"slices"
"strings"
"testing"
)
func TestGenerateCreatesRecommendedSkeleton(t *testing.T) {
target := filepath.Join(t.TempDir(), "my-mcp")
result, err := Generate(Options{
TargetDir: target,
ModulePath: "example.com/acme/my-mcp",
BinaryName: "my-mcp",
Description: "Client MCP interne",
DefaultProfile: "prod",
Profiles: []string{"dev", "prod"},
})
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
if result.Root != target {
t.Fatalf("result root = %q, want %q", result.Root, target)
}
wantFiles := []string{
".gitignore",
"README.md",
"cmd/my-mcp/main.go",
"go.mod",
"install.sh",
"internal/app/app.go",
"mcp.toml",
}
if !slices.Equal(result.Files, wantFiles) {
t.Fatalf("result files = %v, want %v", result.Files, wantFiles)
}
for _, path := range wantFiles {
if _, err := os.Stat(filepath.Join(target, filepath.FromSlash(path))); err != nil {
t.Fatalf("generated file %q missing: %v", path, err)
}
}
mainGo, err := os.ReadFile(filepath.Join(target, "cmd", "my-mcp", "main.go"))
if err != nil {
t.Fatalf("ReadFile main.go: %v", err)
}
if !strings.Contains(string(mainGo), "\"example.com/acme/my-mcp/internal/app\"") {
t.Fatalf("main.go does not import internal app package")
}
if _, err := parser.ParseFile(token.NewFileSet(), "main.go", mainGo, parser.AllErrors); err != nil {
t.Fatalf("generated main.go is invalid Go: %v", err)
}
appGo, err := os.ReadFile(filepath.Join(target, "internal", "app", "app.go"))
if err != nil {
t.Fatalf("ReadFile app.go: %v", err)
}
for _, snippet := range []string{
"config.NewStore[Profile]",
"secretstore.Open(secretstore.Options",
"secretstore.EnsureBitwardenSessionEnv",
"func (r Runtime) ensureBitwardenSession() error {",
"\\033[31m",
"secretstore.LoginBitwarden",
"update.Run",
"manifest.LoadDefaultOrEmbedded",
"bootstrap.Run",
"os.Executable()",
"errors.Is(err, os.ErrNotExist)",
`var embeddedManifest = `,
"ManifestSource",
"ManifestCheck: r.manifestDoctorCheck()",
"SecretBackendPolicy: r.activeBackendPolicy()",
"Login: r.runLogin,",
"func (r Runtime) runLogin(_ context.Context, inv bootstrap.Invocation) error {",
"secretStoreFactory := memoizeSecretStoreFactory(r.openSecretStore)",
"func memoizeSecretStoreFactory(factory func() (secretstore.Store, error)) func() (secretstore.Store, error) {",
"cli.WriteSetupSecretVerified",
} {
if !strings.Contains(string(appGo), snippet) {
t.Fatalf("app.go missing snippet %q", snippet)
}
}
if _, err := parser.ParseFile(token.NewFileSet(), "app.go", appGo, parser.AllErrors); err != nil {
t.Fatalf("generated app.go is invalid Go: %v", err)
}
manifestContent, err := os.ReadFile(filepath.Join(target, "mcp.toml"))
if err != nil {
t.Fatalf("ReadFile mcp.toml: %v", err)
}
for _, snippet := range []string{
"binary_name = \"my-mcp\"",
"[update]",
"checksum_required = true",
"signature_asset_name = \"{asset}.sig\"",
"signature_required = false",
"[secret_store]",
"[environment]",
"[profiles]",
"backend_policy = \"auto\"",
} {
if !strings.Contains(string(manifestContent), snippet) {
t.Fatalf("mcp.toml missing snippet %q", snippet)
}
}
readme, err := os.ReadFile(filepath.Join(target, "README.md"))
if err != nil {
t.Fatalf("ReadFile README.md: %v", err)
}
for _, snippet := range []string{
"Arborescence générée",
"go run ./cmd/my-mcp setup",
"curl -fsSL https://<forge>/<org>/<repo>/raw/branch/main/install.sh | bash",
"internal/app/app.go",
} {
if !strings.Contains(string(readme), snippet) {
t.Fatalf("README missing snippet %q", snippet)
}
}
installScriptPath := filepath.Join(target, "install.sh")
installScript, err := os.ReadFile(installScriptPath)
if err != nil {
t.Fatalf("ReadFile install.sh: %v", err)
}
for _, snippet := range []string{
"#!/usr/bin/env bash",
`MODULE_PATH="example.com/acme/my-mcp"`,
`DEFAULT_RELEASE_REPOSITORY="org/my-mcp"`,
`load_release_config_from_manifest`,
`resolve_latest_release_url()`,
`curl_download "$release_url" "$release_json" "json"`,
`asset_name="$(resolve_asset_name "$goos" "$goarch")"`,
`Reinstaller depuis la derniere release ? (y/N)`,
"MCP Install Wizard",
`menu_select() {`,
`Utilise ↑/↓ puis Entrée.`,
`Configurer le MCP maintenant ?`,
`claude mcp add \`,
`--transport stdio \`,
`--scope "$claude_scope" \`,
`--env "${PROFILE_ENV}=${PROFILE_VALUE}" \`,
`codex mcp add \`,
`Dossier projet cible pour .codex/config.toml`,
`[mcp_servers.%s]`,
`"${PROFILE_ENV}=${PROFILE_VALUE}"`,
} {
if !strings.Contains(string(installScript), snippet) {
t.Fatalf("install.sh missing snippet %q", snippet)
}
}
info, err := os.Stat(installScriptPath)
if err != nil {
t.Fatalf("Stat install.sh: %v", err)
}
if info.Mode().Perm() != 0o755 {
t.Fatalf("install.sh mode = %o, want 755", info.Mode().Perm())
}
}
func TestGenerateUsesDefaultsFromTargetDirectory(t *testing.T) {
target := filepath.Join(t.TempDir(), "super-agent-mcp")
_, err := Generate(Options{TargetDir: target})
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
goModContent, err := os.ReadFile(filepath.Join(target, "go.mod"))
if err != nil {
t.Fatalf("ReadFile go.mod: %v", err)
}
if !strings.Contains(string(goModContent), "module example.com/super-agent-mcp") {
t.Fatalf("go.mod should contain default module path")
}
manifestContent, err := os.ReadFile(filepath.Join(target, "mcp.toml"))
if err != nil {
t.Fatalf("ReadFile mcp.toml: %v", err)
}
for _, snippet := range []string{
"binary_name = \"super-agent-mcp\"",
"SUPER_AGENT_MCP_PROFILE",
"SUPER_AGENT_MCP_API_TOKEN",
} {
if !strings.Contains(string(manifestContent), snippet) {
t.Fatalf("mcp.toml missing snippet %q", snippet)
}
}
}
func TestGenerateFailsWhenFileAlreadyExistsWithoutOverwrite(t *testing.T) {
target := t.TempDir()
readmePath := filepath.Join(target, "README.md")
if err := os.WriteFile(readmePath, []byte("pre-existing"), 0o644); err != nil {
t.Fatalf("WriteFile README.md: %v", err)
}
_, err := Generate(Options{TargetDir: target})
if !errors.Is(err, ErrFileExists) {
t.Fatalf("Generate error = %v, want ErrFileExists", err)
}
}
func TestGenerateOverwritesExistingFilesWhenRequested(t *testing.T) {
target := t.TempDir()
readmePath := filepath.Join(target, "README.md")
if err := os.WriteFile(readmePath, []byte("pre-existing"), 0o644); err != nil {
t.Fatalf("WriteFile README.md: %v", err)
}
_, err := Generate(Options{TargetDir: target, Overwrite: true})
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
readmeContent, err := os.ReadFile(readmePath)
if err != nil {
t.Fatalf("ReadFile README.md: %v", err)
}
if !strings.Contains(string(readmeContent), "Démarrage rapide") {
t.Fatalf("README should be overwritten with scaffold content")
}
}
func TestGenerateRequiresTargetDirectory(t *testing.T) {
_, err := Generate(Options{})
if !errors.Is(err, ErrTargetDirRequired) {
t.Fatalf("Generate error = %v, want ErrTargetDirRequired", err)
}
}