228 lines
6.3 KiB
Go
228 lines
6.3 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",
|
|
"update.Run",
|
|
"manifest.LoadDefaultOrEmbedded",
|
|
"bootstrap.Run",
|
|
"os.Executable()",
|
|
"errors.Is(err, os.ErrNotExist)",
|
|
`var embeddedManifest = `,
|
|
"ManifestSource",
|
|
"ManifestCheck: r.manifestDoctorCheck()",
|
|
} {
|
|
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"`,
|
|
`go install "${MODULE_PATH}/cmd/${BINARY_NAME}@latest"`,
|
|
"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)
|
|
}
|
|
}
|