package generate import ( "errors" "os" "os/exec" "path/filepath" "slices" "strings" "testing" ) func TestGenerateCreatesManifestLoader(t *testing.T) { projectDir := newProject(t, ` binary_name = "demo-mcp" docs_url = "https://docs.example.com/demo" [bootstrap] description = "Demo MCP" `) result, err := Generate(Options{ProjectDir: projectDir}) if err != nil { t.Fatalf("Generate returned error: %v", err) } if !slices.Equal(result.Files, defaultGeneratedFiles("mcpgen")) { t.Fatalf("result files = %v", result.Files) } generatedPath := filepath.Join(projectDir, "mcpgen", "manifest.go") content, err := os.ReadFile(generatedPath) if err != nil { t.Fatalf("ReadFile generated manifest: %v", err) } for _, snippet := range []string{ "// Code generated by mcp-framework generate. DO NOT EDIT.", "package mcpgen", "import fwmanifest \"forge.lclr.dev/AI/mcp-framework/manifest\"", "const embeddedManifest = ", "func LoadManifest(startDir string) (fwmanifest.File, string, error) {", "return fwmanifest.LoadDefaultOrEmbedded(startDir, embeddedManifest)", `binary_name = \"demo-mcp\"`, } { if !strings.Contains(string(content), snippet) { t.Fatalf("generated manifest.go missing snippet %q:\n%s", snippet, content) } } } func TestGenerateCreatesP1Helpers(t *testing.T) { projectDir := newProject(t, ` binary_name = "demo-mcp" docs_url = "https://docs.example.com/demo" [update] driver = "gitea" repository = "org/demo-mcp" base_url = "https://gitea.example.com" asset_name_template = "{binary}-{os}-{arch}{ext}" [secret_store] backend_policy = "env-only" [bootstrap] description = "Demo MCP" `) result, err := Generate(Options{ProjectDir: projectDir}) if err != nil { t.Fatalf("Generate returned error: %v", err) } wantFiles := []string{ filepath.Join("mcpgen", "manifest.go"), filepath.Join("mcpgen", "metadata.go"), filepath.Join("mcpgen", "secretstore.go"), filepath.Join("mcpgen", "update.go"), } if !slices.Equal(result.Files, wantFiles) { t.Fatalf("result files = %v, want %v", result.Files, wantFiles) } metadata, err := os.ReadFile(filepath.Join(projectDir, "mcpgen", "metadata.go")) if err != nil { t.Fatalf("ReadFile metadata.go: %v", err) } for _, snippet := range []string{ `const BinaryName = "demo-mcp"`, `const DefaultDescription = "Demo MCP"`, `const DocsURL = "https://docs.example.com/demo"`, "func BootstrapInfo(startDir string) (fwmanifest.BootstrapMetadata, string, error) {", "func ScaffoldInfo(startDir string) (fwmanifest.ScaffoldMetadata, string, error) {", } { if !strings.Contains(string(metadata), snippet) { t.Fatalf("metadata.go missing snippet %q:\n%s", snippet, metadata) } } update, err := os.ReadFile(filepath.Join(projectDir, "mcpgen", "update.go")) if err != nil { t.Fatalf("ReadFile update.go: %v", err) } for _, snippet := range []string{ "func UpdateOptions(version string, stdout io.Writer) (fwupdate.Options, error) {", "func UpdateOptionsFrom(startDir string, version string, stdout io.Writer) (fwupdate.Options, error) {", "func RunUpdate(ctx context.Context, args []string, version string, stdout io.Writer) error {", "ReleaseSource:", } { if !strings.Contains(string(update), snippet) { t.Fatalf("update.go missing snippet %q:\n%s", snippet, update) } } secretstore, err := os.ReadFile(filepath.Join(projectDir, "mcpgen", "secretstore.go")) if err != nil { t.Fatalf("ReadFile secretstore.go: %v", err) } for _, snippet := range []string{ "type SecretStoreOptions struct {", "func OpenSecretStore(options SecretStoreOptions) (fwsecretstore.Store, error) {", "func DescribeSecretRuntime(options SecretStoreOptions) (fwsecretstore.RuntimeDescription, error) {", "func PreflightSecretStore(options SecretStoreOptions) (fwsecretstore.PreflightReport, error) {", "ManifestLoader:", } { if !strings.Contains(string(secretstore), snippet) { t.Fatalf("secretstore.go missing snippet %q:\n%s", snippet, secretstore) } } } func TestGenerateCreatesConfigHelpersFromManifestFields(t *testing.T) { projectDir := newProject(t, ` binary_name = "demo-mcp" [[config.fields]] name = "base_url" flag = "base-url" env = "BASE_URL" config_key = "base_url" type = "url" label = "Graylog URL" required = true sources = ["flag", "env", "config"] [[config.fields]] name = "api_token" flag = "api-token" env = "API_TOKEN" secret_key_template = "profile/{profile}/api-token" type = "secret" label = "API token" required = true sources = ["flag", "env", "secret"] `) result, err := Generate(Options{ProjectDir: projectDir}) if err != nil { t.Fatalf("Generate returned error: %v", err) } wantFiles := generatedFilesWithConfig("mcpgen") if !slices.Equal(result.Files, wantFiles) { t.Fatalf("result files = %v, want %v", result.Files, wantFiles) } config, err := os.ReadFile(filepath.Join(projectDir, "mcpgen", "config.go")) if err != nil { t.Fatalf("ReadFile config.go: %v", err) } for _, snippet := range []string{ "type ConfigFlags struct {", "func AddConfigFlags(fs *flag.FlagSet) ConfigFlags {", "func ConfigFlagValues(flags ConfigFlags) map[string]string {", "func ResolveFieldSpecs(profile string) []fwcli.FieldSpec {", "func SetupFields(existing map[string]string) []fwcli.SetupField {", `fs.String("base-url", "", "Graylog URL")`, `SecretKey: replaceProfile("profile/{profile}/api-token", profile)`, "fwcli.SetupFieldURL", "fwcli.SetupFieldSecret", } { if !strings.Contains(string(config), snippet) { t.Fatalf("config.go missing snippet %q:\n%s", snippet, config) } } } func TestGenerateIsIdempotentAndCheckDetectsDrift(t *testing.T) { projectDir := newProject(t, `binary_name = "demo-mcp"`) if _, err := Generate(Options{ProjectDir: projectDir}); err != nil { t.Fatalf("first Generate returned error: %v", err) } generatedPath := filepath.Join(projectDir, "mcpgen", "manifest.go") first, err := os.ReadFile(generatedPath) if err != nil { t.Fatalf("ReadFile first generated file: %v", err) } if _, err := Generate(Options{ProjectDir: projectDir}); err != nil { t.Fatalf("second Generate returned error: %v", err) } second, err := os.ReadFile(generatedPath) if err != nil { t.Fatalf("ReadFile second generated file: %v", err) } if string(second) != string(first) { t.Fatalf("second generation changed content") } if _, err := Generate(Options{ProjectDir: projectDir, Check: true}); err != nil { t.Fatalf("check after generation returned error: %v", err) } if err := os.WriteFile(generatedPath, append(second, []byte("// drift\n")...), 0o600); err != nil { t.Fatalf("WriteFile drift: %v", err) } _, err = Generate(Options{ProjectDir: projectDir, Check: true}) if !errors.Is(err, ErrGeneratedFilesOutdated) { t.Fatalf("check error = %v, want ErrGeneratedFilesOutdated", err) } } func TestGenerateSupportsManifestAndPackageFlags(t *testing.T) { projectDir := t.TempDir() manifestPath := filepath.Join(projectDir, "config", "custom.toml") if err := os.MkdirAll(filepath.Dir(manifestPath), 0o755); err != nil { t.Fatalf("MkdirAll manifest dir: %v", err) } if err := os.WriteFile(manifestPath, []byte(`binary_name = "demo-mcp"`), 0o600); err != nil { t.Fatalf("WriteFile manifest: %v", err) } result, err := Generate(Options{ ProjectDir: projectDir, ManifestPath: manifestPath, PackageDir: "internal/generated", PackageName: "generated", }) if err != nil { t.Fatalf("Generate returned error: %v", err) } if !slices.Equal(result.Files, defaultGeneratedFiles(filepath.Join("internal", "generated"))) { t.Fatalf("result files = %v", result.Files) } content, err := os.ReadFile(filepath.Join(projectDir, "internal", "generated", "manifest.go")) if err != nil { t.Fatalf("ReadFile generated manifest: %v", err) } if !strings.Contains(string(content), "package generated") { t.Fatalf("generated file should use package name: %s", content) } } func TestGenerateRejectsInvalidManifest(t *testing.T) { projectDir := newProject(t, "[bootstrap\n") _, err := Generate(Options{ProjectDir: projectDir}) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "parse manifest") { t.Fatalf("error = %v", err) } } func TestGeneratedLoaderFallsBackToEmbeddedManifest(t *testing.T) { projectDir := newProject(t, ` binary_name = "embedded-demo" docs_url = "https://docs.example.com/embedded" [update] driver = "gitea" repository = "org/embedded-demo" base_url = "https://gitea.example.com" [secret_store] backend_policy = "env-only" [bootstrap] description = "Embedded Demo" [[config.fields]] name = "base_url" flag = "base-url" env = "BASE_URL" config_key = "base_url" type = "url" label = "Base URL" required = true sources = ["flag", "env", "config"] [[config.fields]] name = "api_token" flag = "api-token" env = "API_TOKEN" secret_key_template = "profile/{profile}/api-token" type = "secret" label = "API token" required = true sources = ["flag", "env", "secret"] `) writeModule(t, projectDir) if _, err := Generate(Options{ProjectDir: projectDir}); err != nil { t.Fatalf("Generate returned error: %v", err) } if err := os.Remove(filepath.Join(projectDir, "mcp.toml")); err != nil { t.Fatalf("Remove runtime manifest: %v", err) } cmd := exec.Command("go", "test", "-mod=mod", "./...") cmd.Dir = projectDir output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("go test generated project: %v\n%s", err, output) } } func TestGenerateSecretStoreIncludesBitwardenCacheOption(t *testing.T) { projectDir := newProject(t, ` binary_name = "demo-mcp" [secret_store] backend_policy = "bitwarden-cli" `) if _, err := Generate(Options{ProjectDir: projectDir}); err != nil { t.Fatalf("Generate returned error: %v", err) } content, err := os.ReadFile(filepath.Join(projectDir, "mcpgen", "secretstore.go")) if err != nil { t.Fatalf("ReadFile generated secretstore: %v", err) } text := string(content) for _, snippet := range []string{ "DisableBitwardenCache bool", "DisableBitwardenCache: options.DisableBitwardenCache,", } { if !strings.Contains(text, snippet) { t.Fatalf("generated secretstore.go missing %q:\n%s", snippet, text) } } } func newProject(t *testing.T, manifest string) string { t.Helper() projectDir := t.TempDir() if err := os.WriteFile(filepath.Join(projectDir, "mcp.toml"), []byte(manifest), 0o600); err != nil { t.Fatalf("WriteFile manifest: %v", err) } return projectDir } func defaultGeneratedFiles(packageDir string) []string { return []string{ filepath.Join(packageDir, "manifest.go"), filepath.Join(packageDir, "metadata.go"), filepath.Join(packageDir, "secretstore.go"), filepath.Join(packageDir, "update.go"), } } func generatedFilesWithConfig(packageDir string) []string { return []string{ filepath.Join(packageDir, "config.go"), filepath.Join(packageDir, "manifest.go"), filepath.Join(packageDir, "metadata.go"), filepath.Join(packageDir, "secretstore.go"), filepath.Join(packageDir, "update.go"), } } func writeModule(t *testing.T, projectDir string) { t.Helper() repoRoot, err := filepath.Abs("..") if err != nil { t.Fatalf("Abs repo root: %v", err) } goMod := "module example.com/generated-demo\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/99designs/keyring v1.2.2\n\tgithub.com/BurntSushi/toml v1.6.0\n\tforge.lclr.dev/AI/mcp-framework v0.0.0\n)\n\nreplace forge.lclr.dev/AI/mcp-framework => " + filepath.ToSlash(repoRoot) + "\n" if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte(goMod), 0o600); err != nil { t.Fatalf("WriteFile go.mod: %v", err) } goSum, err := os.ReadFile(filepath.Join(repoRoot, "go.sum")) if err != nil { t.Fatalf("ReadFile go.sum: %v", err) } if err := os.WriteFile(filepath.Join(projectDir, "go.sum"), goSum, 0o600); err != nil { t.Fatalf("WriteFile go.sum: %v", err) } testFile := `package main import ( "flag" "io" "testing" fwcli "forge.lclr.dev/AI/mcp-framework/cli" fwsecretstore "forge.lclr.dev/AI/mcp-framework/secretstore" "example.com/generated-demo/mcpgen" fwmanifest "forge.lclr.dev/AI/mcp-framework/manifest" ) func TestGeneratedHelpersUseEmbeddedManifest(t *testing.T) { file, source, err := mcpgen.LoadManifest(".") if err != nil { t.Fatalf("LoadManifest returned error: %v", err) } if source != fwmanifest.EmbeddedSource { t.Fatalf("source = %q, want %q", source, fwmanifest.EmbeddedSource) } if file.BinaryName != "embedded-demo" { t.Fatalf("binary name = %q", file.BinaryName) } info, source, err := mcpgen.BootstrapInfo(".") if err != nil { t.Fatalf("BootstrapInfo returned error: %v", err) } if source != fwmanifest.EmbeddedSource { t.Fatalf("bootstrap source = %q, want %q", source, fwmanifest.EmbeddedSource) } if info.Description != "Embedded Demo" { t.Fatalf("description = %q", info.Description) } updateOptions, err := mcpgen.UpdateOptions("1.2.3", io.Discard) if err != nil { t.Fatalf("UpdateOptions returned error: %v", err) } if updateOptions.CurrentVersion != "1.2.3" { t.Fatalf("current version = %q", updateOptions.CurrentVersion) } if updateOptions.BinaryName != "embedded-demo" { t.Fatalf("update binary name = %q", updateOptions.BinaryName) } if updateOptions.ReleaseSource.Repository != "org/embedded-demo" { t.Fatalf("release repository = %q", updateOptions.ReleaseSource.Repository) } store, err := mcpgen.OpenSecretStore(mcpgen.SecretStoreOptions{ LookupEnv: func(name string) (string, bool) { return "secret-from-env", true }, }) if err != nil { t.Fatalf("OpenSecretStore returned error: %v", err) } value, err := store.GetSecret("profile/default/api-token") if err != nil { t.Fatalf("GetSecret returned error: %v", err) } if value != "secret-from-env" { t.Fatalf("secret value = %q", value) } if fwsecretstore.EffectiveBackendPolicy(store) != fwsecretstore.BackendEnvOnly { t.Fatalf("effective backend = %q", fwsecretstore.EffectiveBackendPolicy(store)) } flags := mcpgen.AddConfigFlags(flag.NewFlagSet("test", flag.ContinueOnError)) if len(mcpgen.ConfigFlagValues(flags)) != 0 { t.Fatalf("empty flags should not return values") } specs := mcpgen.ResolveFieldSpecs("default") if len(specs) != 2 { t.Fatalf("field specs = %d, want 2", len(specs)) } if specs[1].SecretKey != "profile/default/api-token" { t.Fatalf("secret key = %q", specs[1].SecretKey) } setupFields := mcpgen.SetupFields(map[string]string{"api_token": "stored"}) if len(setupFields) != 2 { t.Fatalf("setup fields = %d, want 2", len(setupFields)) } if setupFields[0].Type != fwcli.SetupFieldURL { t.Fatalf("first setup field type = %q", setupFields[0].Type) } if setupFields[1].ExistingSecret != "stored" { t.Fatalf("existing secret = %q", setupFields[1].ExistingSecret) } } ` if err := os.WriteFile(filepath.Join(projectDir, "main_test.go"), []byte(testFile), 0o600); err != nil { t.Fatalf("WriteFile main_test.go: %v", err) } }