diff --git a/cmd/wfctl/ci_run.go b/cmd/wfctl/ci_run.go index 8f150147..b505de63 100644 --- a/cmd/wfctl/ci_run.go +++ b/cmd/wfctl/ci_run.go @@ -373,6 +373,4 @@ func runPreDeploySteps(ctx context.Context, steps []string, verbose bool) error } // newCommandContext wraps exec.CommandContext so tests can replace it. -var newCommandContext = func(ctx context.Context, name string, args ...string) *exec.Cmd { - return exec.CommandContext(ctx, name, args...) -} +var newCommandContext = exec.CommandContext //nolint:gosec // G204: subprocess args come from validated user config diff --git a/cmd/wfctl/deploy_providers.go b/cmd/wfctl/deploy_providers.go index 543d1a63..a66ee438 100644 --- a/cmd/wfctl/deploy_providers.go +++ b/cmd/wfctl/deploy_providers.go @@ -470,29 +470,6 @@ func injectSecrets(ctx context.Context, cfg *config.WorkflowConfig, envName stri return result, nil } -// injectSecretsLegacy is the pre-multi-store implementation kept for callers -// that only have a SecretsConfig (not a full WorkflowConfig). -func injectSecretsLegacy(ctx context.Context, secretsCfg *config.SecretsConfig) (map[string]string, error) { - if secretsCfg == nil || len(secretsCfg.Entries) == 0 { - return nil, nil - } - - provider, err := newSecretsProvider(secretsCfg.Provider) - if err != nil { - return nil, fmt.Errorf("secrets provider: %w", err) - } - - result := make(map[string]string, len(secretsCfg.Entries)) - for _, entry := range secretsCfg.Entries { - val, err := provider.Get(ctx, entry.Name) - if err != nil { - return nil, fmt.Errorf("fetch secret %q: %w", entry.Name, err) - } - result[entry.Name] = val - } - return result, nil -} - // cmp returns a if non-empty, otherwise b. Mirrors cmp.Or for strings. func cmp(a, b string) string { if a != "" { diff --git a/cmd/wfctl/dev_compose.go b/cmd/wfctl/dev_compose.go index d8725cde..c9a439b1 100644 --- a/cmd/wfctl/dev_compose.go +++ b/cmd/wfctl/dev_compose.go @@ -17,12 +17,12 @@ const devComposeFileName = "docker-compose.dev.yml" // moduleToDockerImage maps workflow module types to Docker images. var moduleToDockerImage = map[string]string{ - "database.postgres": "postgres:16", - "database.workflow": "postgres:16", - "nosql.redis": "redis:7-alpine", - "cache.redis": "redis:7-alpine", - "messaging.nats": "nats:latest", - "messaging.kafka": "confluentinc/cp-kafka:latest", + "database.postgres": "postgres:16", + "database.workflow": "postgres:16", + "nosql.redis": "redis:7-alpine", + "cache.redis": "redis:7-alpine", + "messaging.nats": "nats:latest", + "messaging.kafka": "confluentinc/cp-kafka:latest", "messaging.rabbitmq": "rabbitmq:3-management-alpine", } @@ -58,13 +58,13 @@ var infraModuleVolumeMounts = map[string]string{ // devComposeService represents a docker-compose service entry for dev mode. type devComposeService struct { - Image string `yaml:"image,omitempty"` - Build *devComposeBuild `yaml:"build,omitempty"` - Ports []string `yaml:"ports,omitempty"` - Environment map[string]string `yaml:"environment,omitempty"` - Volumes []string `yaml:"volumes,omitempty"` - DependsOn []string `yaml:"depends_on,omitempty"` - Restart string `yaml:"restart,omitempty"` + Image string `yaml:"image,omitempty"` + Build *devComposeBuild `yaml:"build,omitempty"` + Ports []string `yaml:"ports,omitempty"` + Environment map[string]string `yaml:"environment,omitempty"` + Volumes []string `yaml:"volumes,omitempty"` + DependsOn []string `yaml:"depends_on,omitempty"` + Restart string `yaml:"restart,omitempty"` Healthcheck *devComposeHealthcheck `yaml:"healthcheck,omitempty"` } @@ -254,8 +254,7 @@ func moduleTypeToDNS(modType string) string { parts := strings.Split(modType, ".") last := parts[len(parts)-1] // Shorten well-known names. - switch last { - case "workflow": + if last == "workflow" { return "postgres" } return last @@ -271,7 +270,7 @@ func runDevCompose(cfg *config.WorkflowConfig, cfgPath string, verbose bool) err // Write to the same directory as the config file. outDir := filepath.Dir(cfgPath) outPath := filepath.Join(outDir, devComposeFileName) - if err := os.WriteFile(outPath, []byte(composeYAML), 0o644); err != nil { + if err := os.WriteFile(outPath, []byte(composeYAML), 0o600); err != nil { return fmt.Errorf("write %s: %w", outPath, err) } fmt.Printf("Generated %s\n", outPath) diff --git a/cmd/wfctl/dev_k8s.go b/cmd/wfctl/dev_k8s.go index 87e786a9..8e5abf4c 100644 --- a/cmd/wfctl/dev_k8s.go +++ b/cmd/wfctl/dev_k8s.go @@ -38,7 +38,7 @@ func runDevK8s(cfg *config.WorkflowConfig, verbose bool) error { // 5. Write manifests to a temp file and apply. const manifestFile = "dev-manifests.yaml" - if err := os.WriteFile(manifestFile, []byte(manifests), 0o644); err != nil { + if err := os.WriteFile(manifestFile, []byte(manifests), 0o600); err != nil { return fmt.Errorf("write manifests: %w", err) } defer os.Remove(manifestFile) //nolint:errcheck diff --git a/cmd/wfctl/dev_process.go b/cmd/wfctl/dev_process.go index 1eb3a293..315a9aad 100644 --- a/cmd/wfctl/dev_process.go +++ b/cmd/wfctl/dev_process.go @@ -48,7 +48,7 @@ func runDevProcess(cfg *config.WorkflowConfig, verbose bool) error { return fmt.Errorf("generate infra compose: %w", err) } const infraComposeFile = "docker-compose.dev-infra.yml" - if err := os.WriteFile(infraComposeFile, []byte(composeYAML), 0o644); err != nil { + if err := os.WriteFile(infraComposeFile, []byte(composeYAML), 0o600); err != nil { return fmt.Errorf("write infra compose: %w", err) } @@ -309,7 +309,7 @@ func collectWatchDirs(root string) []string { seen := map[string]bool{} _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { - return nil + return nil //nolint:nilerr // intentionally skip unreadable paths in watch dirs } if !info.IsDir() { return nil diff --git a/cmd/wfctl/infra.go b/cmd/wfctl/infra.go index f8ca3d33..e56eae6f 100644 --- a/cmd/wfctl/infra.go +++ b/cmd/wfctl/infra.go @@ -5,13 +5,13 @@ import ( "encoding/json" "flag" "fmt" - "os" - "path/filepath" - "strings" "github.com/GoCodeAlone/workflow/interfaces" "github.com/GoCodeAlone/workflow/platform" "github.com/GoCodeAlone/workflow/secrets" "gopkg.in/yaml.v3" + "os" + "path/filepath" + "strings" ) func runInfra(args []string) error { @@ -593,46 +593,6 @@ func extractSources(m map[string]any, singular, plural string) string { return "" } -// formatFirewallRules produces a compact summary of firewall rule config (legacy, single-line). -func formatFirewallRules(v any) string { - switch rules := v.(type) { - case []any: - if len(rules) == 0 { - return "" - } - // Summarise first rule. - first, ok := rules[0].(map[string]any) - if !ok { - return fmt.Sprintf("%d rule(s)", len(rules)) - } - proto, _ := first["protocol"].(string) - ports, _ := first["ports"].(string) - src, _ := first["source"].(string) - dst, _ := first["destination"].(string) - var parts []string - if proto != "" { - parts = append(parts, strings.ToUpper(proto)) - } - if ports != "" { - parts = append(parts, ports) - } - if src != "" { - parts = append(parts, "from "+src) - } - if dst != "" { - parts = append(parts, "to "+dst) - } - summary := strings.Join(parts, " ") - if len(rules) > 1 { - summary += fmt.Sprintf(" (+%d more)", len(rules)-1) - } - return summary - case string: - return rules - } - return "" -} - func actionSymbol(action string) string { switch action { case "create": diff --git a/cmd/wfctl/infra_bootstrap.go b/cmd/wfctl/infra_bootstrap.go index 6653da63..adaa9651 100644 --- a/cmd/wfctl/infra_bootstrap.go +++ b/cmd/wfctl/infra_bootstrap.go @@ -56,8 +56,11 @@ func runInfraBootstrap(args []string) error { // backing infrastructure (e.g. a DO Spaces bucket) if it does not already exist. func bootstrapStateBackend(ctx context.Context, cfgFile string) error { iacStates, _, _, err := discoverInfraModules(cfgFile) - if err != nil || len(iacStates) == 0 { - return nil // no state module configured — nothing to bootstrap + if err != nil { + return fmt.Errorf("discover infra modules: %w", err) + } + if len(iacStates) == 0 { + return nil } m := iacStates[0] backend, _ := m.Config["backend"].(string) diff --git a/cmd/wfctl/infra_secrets.go b/cmd/wfctl/infra_secrets.go index 01a818d9..e37f501a 100644 --- a/cmd/wfctl/infra_secrets.go +++ b/cmd/wfctl/infra_secrets.go @@ -74,7 +74,7 @@ func resolveSecretsProvider(cfg *SecretsConfig) (secrets.Provider, error) { repo, _ := c["repo"].(string) tokenVar, _ := c["token_env"].(string) if tokenVar == "" { - tokenVar = "GITHUB_TOKEN" + tokenVar = "GITHUB_TOKEN" //nolint:gosec // G101: this is an env var name, not a credential } return secrets.NewGitHubSecretsProvider(repo, tokenVar) diff --git a/cmd/wfctl/migrate_expressions.go b/cmd/wfctl/migrate_expressions.go index 49de6181..b5483001 100644 --- a/cmd/wfctl/migrate_expressions.go +++ b/cmd/wfctl/migrate_expressions.go @@ -79,7 +79,7 @@ Options: return nil } - if err := os.WriteFile(dest, []byte(converted), 0o644); err != nil { + if err := os.WriteFile(dest, []byte(converted), 0o600); err != nil { return fmt.Errorf("write %s: %w", dest, err) } fmt.Printf("Wrote %s: %d expression(s) converted, %d marked as TODO\n", @@ -191,12 +191,12 @@ func convertGoTemplateExpr(inner string) (string, bool) { // .steps.stepName.field → steps["stepName"]["field"] if m := stepsDotRe.FindStringSubmatch(inner); m != nil { - return fmt.Sprintf(`steps["%s"]["%s"]`, m[1], m[2]), true + return fmt.Sprintf(`steps[%q][%q]`, m[1], m[2]), true } // index .steps "name" "field" → steps["name"]["field"] if m := indexStepsRe.FindStringSubmatch(inner); m != nil { - return fmt.Sprintf(`steps["%s"]["%s"]`, m[1], m[2]), true + return fmt.Sprintf(`steps[%q][%q]`, m[1], m[2]), true } // eq .field "value" → field == "value" diff --git a/cmd/wfctl/plugin_deps_test.go b/cmd/wfctl/plugin_deps_test.go index 9408f5bd..ccd6a7f9 100644 --- a/cmd/wfctl/plugin_deps_test.go +++ b/cmd/wfctl/plugin_deps_test.go @@ -10,16 +10,6 @@ import ( "testing" ) -// makeManifestJSON returns a JSON-encoded RegistryManifest for use in test servers. -func makeManifestJSON(t *testing.T, m RegistryManifest) []byte { - t.Helper() - data, err := json.Marshal(m) - if err != nil { - t.Fatalf("marshal manifest: %v", err) - } - return data -} - // TestCompareSemverConstraints verifies semver comparison used in version constraint checks. func TestCompareSemverConstraints(t *testing.T) { tests := []struct { @@ -47,10 +37,10 @@ func TestCompareSemverConstraints(t *testing.T) { // TestCheckVersionConstraints verifies min/max version enforcement. func TestCheckVersionConstraints(t *testing.T) { tests := []struct { - name string - dep PluginDependency - version string - wantErr bool + name string + dep PluginDependency + version string + wantErr bool errContains string }{ { @@ -287,7 +277,7 @@ func TestPluginInstall_ResolveDependencies(t *testing.T) { Author: "test", Description: "bento stream processor", Type: "external", Tier: "community", License: "MIT", Downloads: []PluginDownload{ - {OS: "linux", Arch: "amd64", URL: ""}, // filled below + {OS: "linux", Arch: "amd64", URL: ""}, // filled below {OS: "darwin", Arch: "amd64", URL: ""}, {OS: "darwin", Arch: "arm64", URL: ""}, }, diff --git a/cmd/wfctl/plugin_infra.go b/cmd/wfctl/plugin_infra.go index b66a97c8..52580f9b 100644 --- a/cmd/wfctl/plugin_infra.go +++ b/cmd/wfctl/plugin_infra.go @@ -60,7 +60,8 @@ func DetectPluginInfraNeeds(cfg *config.WorkflowConfig, manifests map[string]*co var needs []config.InfraRequirement addRequirements := func(reqs []config.InfraRequirement) { - for _, req := range reqs { + for i := range reqs { + req := reqs[i] key := req.Type + ":" + req.Name if seen[key] { continue diff --git a/cmd/wfctl/secrets_detect.go b/cmd/wfctl/secrets_detect.go index be3f94a0..1b694c17 100644 --- a/cmd/wfctl/secrets_detect.go +++ b/cmd/wfctl/secrets_detect.go @@ -273,9 +273,12 @@ func secretStateLabel(state SecretState) string { func loadWorkflowConfigForSecrets(configFile string) (*config.WorkflowConfig, error) { data, err := os.ReadFile(configFile) if err != nil { - return &config.WorkflowConfig{ - Secrets: &config.SecretsConfig{Provider: "env"}, - }, nil + if os.IsNotExist(err) { + return &config.WorkflowConfig{ //nolint:nilerr // gracefully fall back when file is absent + Secrets: &config.SecretsConfig{Provider: "env"}, + }, nil + } + return nil, fmt.Errorf("read config: %w", err) } var cfg config.WorkflowConfig if err := yaml.Unmarshal(data, &cfg); err != nil { @@ -420,8 +423,10 @@ func runSecretsSync(args []string) error { func loadSecretsConfig(configFile string) (*config.SecretsConfig, error) { data, err := os.ReadFile(configFile) if err != nil { - // If the file doesn't exist, return a default env provider config. - return &config.SecretsConfig{Provider: "env"}, nil + if os.IsNotExist(err) { + return &config.SecretsConfig{Provider: "env"}, nil //nolint:nilerr // gracefully fall back when file is absent + } + return nil, fmt.Errorf("read config %q: %w", configFile, err) } var cfg config.WorkflowConfig if err := yaml.Unmarshal(data, &cfg); err != nil { diff --git a/cmd/wfctl/security_cmd.go b/cmd/wfctl/security_cmd.go index c72766c4..775c18f6 100644 --- a/cmd/wfctl/security_cmd.go +++ b/cmd/wfctl/security_cmd.go @@ -207,11 +207,12 @@ func runSecurityGenerateNetworkPolicies(args []string) error { return nil } - if err := os.MkdirAll(*outputDir, 0755); err != nil { + if err := os.MkdirAll(*outputDir, 0750); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } - for name, policy := range policies { + for name := range policies { + policy := policies[name] outPath := filepath.Join(*outputDir, fmt.Sprintf("netpol-%s.yaml", name)) data, err := yaml.Marshal(policy) if err != nil { @@ -229,10 +230,10 @@ func runSecurityGenerateNetworkPolicies(args []string) error { // k8sNetworkPolicy is a minimal Kubernetes NetworkPolicy for YAML generation. type k8sNetworkPolicy struct { - APIVersion string `yaml:"apiVersion"` - Kind string `yaml:"kind"` - Metadata k8sMetadata `yaml:"metadata"` - Spec k8sNetworkPolicySpec `yaml:"spec"` + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata k8sMetadata `yaml:"metadata"` + Spec k8sNetworkPolicySpec `yaml:"spec"` } type k8sMetadata struct { diff --git a/cmd/wfctl/type_registry.go b/cmd/wfctl/type_registry.go index 689e090c..c4456f2a 100644 --- a/cmd/wfctl/type_registry.go +++ b/cmd/wfctl/type_registry.go @@ -686,6 +686,14 @@ func KnownModuleTypes() map[string]ModuleTypeInfo { ConfigKeys: []string{"system", "workers", "handler"}, }, + // mcp plugin + "mcp.registry": { + Type: "mcp.registry", + Plugin: "mcp", + Stateful: false, + ConfigKeys: []string{"log_on_init", "expose_admin_api", "audit_tool_calls"}, + }, + // scanner plugin "security.scanner": { Type: "security.scanner", diff --git a/cmd/wfctl/wizard.go b/cmd/wfctl/wizard.go index d5e4d4e1..5f6c1bb6 100644 --- a/cmd/wfctl/wizard.go +++ b/cmd/wfctl/wizard.go @@ -39,7 +39,7 @@ var ( Foreground(lipgloss.Color("#10B981")) checkboxOffStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#6B7280")) + Foreground(lipgloss.Color("#6B7280")) hintStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#6B7280")). @@ -732,11 +732,12 @@ func (m wizardModel) progressBar() string { current := int(m.screen) var parts []string for i := 0; i < total; i++ { - if i < current { + switch { + case i < current: parts = append(parts, "●") - } else if i == current { + case i == current: parts = append(parts, activeStyle.Render("●")) - } else { + default: parts = append(parts, dimStyle.Render("○")) } } @@ -774,9 +775,9 @@ func (m wizardModel) viewProjectInfo() string { display = dimStyle.Render(f.placeholder) } if focused { - b.WriteString(fmt.Sprintf("%s %s: %s\n", cursor, headerStyle.Render(f.label), inputStyle.Render(display+" "))) + fmt.Fprintf(&b, "%s %s: %s\n", cursor, headerStyle.Render(f.label), inputStyle.Render(display+" ")) } else { - b.WriteString(fmt.Sprintf("%s %s: %s\n", cursor, dimStyle.Render(f.label), display)) + fmt.Fprintf(&b, "%s %s: %s\n", cursor, dimStyle.Render(f.label), display) } } return b.String() @@ -807,7 +808,7 @@ func (m wizardModel) viewInfrastructure() string { if item.checked { box = checkboxOnStyle.Render("[x]") } - b.WriteString(fmt.Sprintf("%s%s %s\n", cursor, box, item.label)) + fmt.Fprintf(&b, "%s%s %s\n", cursor, box, item.label) } return b.String() } @@ -822,7 +823,8 @@ func (m wizardModel) viewInfraResolution() string { b.WriteString("Set how each infrastructure resource is resolved in each environment.\n") b.WriteString(dimStyle.Render("Use ← → to change strategy, ↑ ↓ to move between rows.") + "\n\n") - for i, row := range m.infraResList { + for i := range m.infraResList { + row := &m.infraResList[i] cursor := " " if i == m.infraResCursor { cursor = activeStyle.Render("▶ ") @@ -831,7 +833,7 @@ func (m wizardModel) viewInfraResolution() string { if row.cursor < len(row.strategies) { stratLabel = row.strategies[row.cursor].label } - b.WriteString(fmt.Sprintf("%s%-12s %-10s %s\n", cursor, row.resource, row.env, activeStyle.Render(stratLabel))) + fmt.Fprintf(&b, "%s%-12s %-10s %s\n", cursor, row.resource, row.env, activeStyle.Render(stratLabel)) if i == m.infraResCursor && row.showConn { conn := row.connInput.value if conn == "" { @@ -856,7 +858,7 @@ func (m wizardModel) viewEnvironments() string { if item.checked { box = checkboxOnStyle.Render("[x]") } - b.WriteString(fmt.Sprintf("%s%s %s\n", cursor, box, item.label)) + fmt.Fprintf(&b, "%s%s %s\n", cursor, box, item.label) } return b.String() } @@ -870,7 +872,7 @@ func (m wizardModel) viewDeployment() string { if i == m.deployCursor { cursor = activeStyle.Render("▶ ") } - b.WriteString(fmt.Sprintf("%s%s\n", cursor, item.label)) + fmt.Fprintf(&b, "%s%s\n", cursor, item.label) } return b.String() } @@ -888,7 +890,7 @@ func (m wizardModel) viewSecretStores() string { if row.isDefault { def = activeStyle.Render(" [default]") } - b.WriteString(fmt.Sprintf(" %-12s (%s)%s\n", row.name, row.provider, def)) + fmt.Fprintf(&b, " %-12s (%s)%s\n", row.name, row.provider, def) } b.WriteString("\n") } @@ -900,7 +902,7 @@ func (m wizardModel) viewSecretStores() string { if i == m.storeCursor { cursor = activeStyle.Render("▶ ") } - b.WriteString(fmt.Sprintf("%s%s\n", cursor, item.label)) + fmt.Fprintf(&b, "%s%s\n", cursor, item.label) } b.WriteString("\n" + headerStyle.Render("Store name: ")) display := m.storeNameInput.value @@ -916,7 +918,7 @@ func (m wizardModel) viewSecretStores() string { if i == m.storeCursor { cursor = activeStyle.Render("▶ ") } - b.WriteString(fmt.Sprintf("%s%s\n", cursor, item.label)) + fmt.Fprintf(&b, "%s%s\n", cursor, item.label) } } return b.String() @@ -941,7 +943,7 @@ func (m wizardModel) viewSecretRouting() string { if row.cursor < len(row.storeItems) { storeLabel = activeStyle.Render(row.storeItems[row.cursor].label) } - b.WriteString(fmt.Sprintf("%s%-20s → %s\n", cursor, row.secretName, storeLabel)) + fmt.Fprintf(&b, "%s%-20s → %s\n", cursor, row.secretName, storeLabel) } return b.String() } @@ -968,15 +970,16 @@ func (m wizardModel) viewBulkSecrets() string { } valueDisplay = inputStyle.Render(masked + " ") } else { - if row.autoGen { + switch { + case row.autoGen: valueDisplay = activeStyle.Render("(auto-generated)") - } else if row.value != "" { + case row.value != "": valueDisplay = activeStyle.Render(strings.Repeat("*", len(row.value))) - } else { + default: valueDisplay = dimStyle.Render("(not set)") } } - b.WriteString(fmt.Sprintf("%s%-20s %s\n", cursor, row.name, valueDisplay)) + fmt.Fprintf(&b, "%s%-20s %s\n", cursor, row.name, valueDisplay) } return b.String() } @@ -998,7 +1001,7 @@ func (m wizardModel) viewCICD() string { if i == m.ciPlatformCursor { cursor = activeStyle.Render(" ▶ ") } - b.WriteString(fmt.Sprintf("%s%s\n", cursor, item.label)) + fmt.Fprintf(&b, "%s%s\n", cursor, item.label) } } return b.String() @@ -1055,7 +1058,7 @@ func buildWizardYAML(d *wizardData) string { name = "my-app" } - b.WriteString(fmt.Sprintf("# %s — generated by wfctl wizard\n\n", name)) + fmt.Fprintf(&b, "# %s — generated by wfctl wizard\n\n", name) // Modules. var modules []string @@ -1102,8 +1105,8 @@ func buildWizardYAML(d *wizardData) string { if d.MultiService && len(d.ServiceNames) > 1 { b.WriteString("services:\n") for _, svc := range d.ServiceNames { - b.WriteString(fmt.Sprintf(" %s:\n", svc)) - b.WriteString(fmt.Sprintf(" binary: ./cmd/%s\n", svc)) + fmt.Fprintf(&b, " %s:\n", svc) + fmt.Fprintf(&b, " binary: ./cmd/%s\n", svc) b.WriteString(" expose:\n") b.WriteString(" - port: 8080\n") b.WriteString(" protocol: http\n") @@ -1117,14 +1120,14 @@ func buildWizardYAML(d *wizardData) string { b.WriteString("environments:\n") for _, env := range envNames { provider := wizardEnvProvider(env, d.DeployProvider) - b.WriteString(fmt.Sprintf(" %s:\n", env)) - b.WriteString(fmt.Sprintf(" provider: %s\n", provider)) + fmt.Fprintf(&b, " %s:\n", env) + fmt.Fprintf(&b, " provider: %s\n", provider) if env == "local" { b.WriteString(" exposure:\n") b.WriteString(" method: port-forward\n") } if d.SecretsProvider != "env" && env != "local" { - b.WriteString(fmt.Sprintf(" secretsProvider: %s\n", d.SecretsProvider)) + fmt.Fprintf(&b, " secretsProvider: %s\n", d.SecretsProvider) } } b.WriteString("\n") @@ -1134,8 +1137,8 @@ func buildWizardYAML(d *wizardData) string { if len(d.SecretStores) > 1 || (len(d.SecretStores) == 1 && d.SecretStores[0].Provider != "env") { b.WriteString("secretStores:\n") for _, store := range d.SecretStores { - b.WriteString(fmt.Sprintf(" %s:\n", store.Name)) - b.WriteString(fmt.Sprintf(" provider: %s\n", store.Provider)) + fmt.Fprintf(&b, " %s:\n", store.Name) + fmt.Fprintf(&b, " provider: %s\n", store.Provider) } b.WriteString("\n") } @@ -1144,33 +1147,33 @@ func buildWizardYAML(d *wizardData) string { if d.SecretsProvider != "" { b.WriteString("secrets:\n") if d.DefaultSecretStore != "" && d.DefaultSecretStore != "primary" { - b.WriteString(fmt.Sprintf(" defaultStore: %s\n", d.DefaultSecretStore)) + fmt.Fprintf(&b, " defaultStore: %s\n", d.DefaultSecretStore) } if d.SecretsProvider != "env" { - b.WriteString(fmt.Sprintf(" provider: %s\n", d.SecretsProvider)) + fmt.Fprintf(&b, " provider: %s\n", d.SecretsProvider) } b.WriteString(" entries:\n") if d.HasDatabase { b.WriteString(" - name: DATABASE_URL\n") if store, ok := d.SecretRoutes["DATABASE_URL"]; ok && store != d.DefaultSecretStore { - b.WriteString(fmt.Sprintf(" store: %s\n", store)) + fmt.Fprintf(&b, " store: %s\n", store) } } if d.HasCache { b.WriteString(" - name: REDIS_URL\n") if store, ok := d.SecretRoutes["REDIS_URL"]; ok && store != d.DefaultSecretStore { - b.WriteString(fmt.Sprintf(" store: %s\n", store)) + fmt.Fprintf(&b, " store: %s\n", store) } } if d.HasMQ { b.WriteString(" - name: NATS_URL\n") if store, ok := d.SecretRoutes["NATS_URL"]; ok && store != d.DefaultSecretStore { - b.WriteString(fmt.Sprintf(" store: %s\n", store)) + fmt.Fprintf(&b, " store: %s\n", store) } } b.WriteString(" - name: JWT_SECRET\n") if store, ok := d.SecretRoutes["JWT_SECRET"]; ok && store != d.DefaultSecretStore { - b.WriteString(fmt.Sprintf(" store: %s\n", store)) + fmt.Fprintf(&b, " store: %s\n", store) } b.WriteString("\n") } @@ -1180,7 +1183,7 @@ func buildWizardYAML(d *wizardData) string { b.WriteString("ci:\n") b.WriteString(" build:\n") b.WriteString(" binaries:\n") - b.WriteString(fmt.Sprintf(" - name: %s\n", name)) + fmt.Fprintf(&b, " - name: %s\n", name) b.WriteString(" path: ./cmd/server\n") b.WriteString(" test:\n") b.WriteString(" unit:\n") @@ -1191,8 +1194,8 @@ func buildWizardYAML(d *wizardData) string { if env == "local" { continue } - b.WriteString(fmt.Sprintf(" - name: %s\n", env)) - b.WriteString(fmt.Sprintf(" provider: %s\n", wizardEnvProvider(env, d.DeployProvider))) + fmt.Fprintf(&b, " - name: %s\n", env) + fmt.Fprintf(&b, " provider: %s\n", wizardEnvProvider(env, d.DeployProvider)) } b.WriteString("\n") } diff --git a/cmd/wfctl/wizard_models.go b/cmd/wfctl/wizard_models.go index 39c85706..bac54fe0 100644 --- a/cmd/wfctl/wizard_models.go +++ b/cmd/wfctl/wizard_models.go @@ -48,30 +48,22 @@ type secretStoreEntry struct { IsDefault bool } -// infraResolutionEntry stores per-environment resolution strategy for one resource. -type infraResolutionEntry struct { - ResourceName string - EnvName string - Strategy string // container, provision, existing - Connection string // host:port for existing -} - // screenID identifies a wizard screen. type screenID int const ( - screenProjectInfo screenID = iota - screenServices // 1 - screenInfrastructure // 2 - screenInfraResolution // 3: per-env strategy for each detected infra resource - screenEnvironments // 4 - screenDeployment // 5 - screenSecretStores // 6: define named secret stores + default - screenSecretRouting // 7: per-secret store override - screenBulkSecrets // 8: hidden input for each required secret - screenCICD // 9 - screenReview // 10 - screenDone // 11 + screenProjectInfo screenID = iota + screenServices // 1 + screenInfrastructure // 2 + screenInfraResolution // 3: per-env strategy for each detected infra resource + screenEnvironments // 4 + screenDeployment // 5 + screenSecretStores // 6: define named secret stores + default + screenSecretRouting // 7: per-secret store override + screenBulkSecrets // 8: hidden input for each required secret + screenCICD // 9 + screenReview // 10 + screenDone // 11 ) // inputField holds a named text input. @@ -145,5 +137,4 @@ type wizardBulkRow struct { name string value string autoGen bool // true when the secret was auto-generated - skip bool // true for no-access stores } diff --git a/config/config.go b/config/config.go index 1fd0e1b5..9d43eac2 100644 --- a/config/config.go +++ b/config/config.go @@ -122,26 +122,26 @@ type PluginsConfig struct { // WorkflowConfig represents the overall configuration for the workflow engine type WorkflowConfig struct { - Imports []string `json:"imports,omitempty" yaml:"imports,omitempty"` - Modules []ModuleConfig `json:"modules" yaml:"modules"` - Workflows map[string]any `json:"workflows" yaml:"workflows"` - Triggers map[string]any `json:"triggers" yaml:"triggers"` - Pipelines map[string]any `json:"pipelines,omitempty" yaml:"pipelines,omitempty"` - Platform map[string]any `json:"platform,omitempty" yaml:"platform,omitempty"` - Requires *RequiresConfig `json:"requires,omitempty" yaml:"requires,omitempty"` - Plugins *PluginsConfig `json:"plugins,omitempty" yaml:"plugins,omitempty"` - Sidecars []SidecarConfig `json:"sidecars,omitempty" yaml:"sidecars,omitempty"` - Infrastructure *InfrastructureConfig `json:"infrastructure,omitempty" yaml:"infrastructure,omitempty"` - Engine *EngineConfig `json:"engine,omitempty" yaml:"engine,omitempty"` - CI *CIConfig `json:"ci,omitempty" yaml:"ci,omitempty"` - Environments map[string]*EnvironmentConfig `json:"environments,omitempty" yaml:"environments,omitempty"` - Secrets *SecretsConfig `json:"secrets,omitempty" yaml:"secrets,omitempty"` - SecretStores map[string]*SecretStoreConfig `json:"secretStores,omitempty" yaml:"secretStores,omitempty"` - Services map[string]*ServiceConfig `json:"services,omitempty" yaml:"services,omitempty"` - Mesh *MeshConfig `json:"mesh,omitempty" yaml:"mesh,omitempty"` - Networking *NetworkingConfig `json:"networking,omitempty" yaml:"networking,omitempty"` - Security *SecurityConfig `json:"security,omitempty" yaml:"security,omitempty"` - ConfigDir string `json:"-" yaml:"-"` // directory containing the config file, used for relative path resolution + Imports []string `json:"imports,omitempty" yaml:"imports,omitempty"` + Modules []ModuleConfig `json:"modules" yaml:"modules"` + Workflows map[string]any `json:"workflows" yaml:"workflows"` + Triggers map[string]any `json:"triggers" yaml:"triggers"` + Pipelines map[string]any `json:"pipelines,omitempty" yaml:"pipelines,omitempty"` + Platform map[string]any `json:"platform,omitempty" yaml:"platform,omitempty"` + Requires *RequiresConfig `json:"requires,omitempty" yaml:"requires,omitempty"` + Plugins *PluginsConfig `json:"plugins,omitempty" yaml:"plugins,omitempty"` + Sidecars []SidecarConfig `json:"sidecars,omitempty" yaml:"sidecars,omitempty"` + Infrastructure *InfrastructureConfig `json:"infrastructure,omitempty" yaml:"infrastructure,omitempty"` + Engine *EngineConfig `json:"engine,omitempty" yaml:"engine,omitempty"` + CI *CIConfig `json:"ci,omitempty" yaml:"ci,omitempty"` + Environments map[string]*EnvironmentConfig `json:"environments,omitempty" yaml:"environments,omitempty"` + Secrets *SecretsConfig `json:"secrets,omitempty" yaml:"secrets,omitempty"` + SecretStores map[string]*SecretStoreConfig `json:"secretStores,omitempty" yaml:"secretStores,omitempty"` + Services map[string]*ServiceConfig `json:"services,omitempty" yaml:"services,omitempty"` + Mesh *MeshConfig `json:"mesh,omitempty" yaml:"mesh,omitempty"` + Networking *NetworkingConfig `json:"networking,omitempty" yaml:"networking,omitempty"` + Security *SecurityConfig `json:"security,omitempty" yaml:"security,omitempty"` + ConfigDir string `json:"-" yaml:"-"` // directory containing the config file, used for relative path resolution } // EngineConfig holds engine-level runtime settings. diff --git a/config/environments_config.go b/config/environments_config.go index 9ccf7078..09fea98f 100644 --- a/config/environments_config.go +++ b/config/environments_config.go @@ -2,16 +2,16 @@ package config // EnvironmentConfig defines a deployment environment with its provider and overrides. type EnvironmentConfig struct { - Provider string `json:"provider" yaml:"provider"` - Region string `json:"region,omitempty" yaml:"region,omitempty"` - EnvVars map[string]string `json:"envVars,omitempty" yaml:"envVars,omitempty"` - SecretsProvider string `json:"secretsProvider,omitempty" yaml:"secretsProvider,omitempty"` - SecretsPrefix string `json:"secretsPrefix,omitempty" yaml:"secretsPrefix,omitempty"` + Provider string `json:"provider" yaml:"provider"` + Region string `json:"region,omitempty" yaml:"region,omitempty"` + EnvVars map[string]string `json:"envVars,omitempty" yaml:"envVars,omitempty"` + SecretsProvider string `json:"secretsProvider,omitempty" yaml:"secretsProvider,omitempty"` + SecretsPrefix string `json:"secretsPrefix,omitempty" yaml:"secretsPrefix,omitempty"` // SecretsStoreOverride forces all secrets in this environment to use a specific named store. // Overrides defaultStore but is itself overridden by a per-secret Store field. - SecretsStoreOverride string `json:"secretsStoreOverride,omitempty" yaml:"secretsStoreOverride,omitempty"` - ApprovalRequired bool `json:"approvalRequired,omitempty" yaml:"approvalRequired,omitempty"` - Exposure *ExposureConfig `json:"exposure,omitempty" yaml:"exposure,omitempty"` + SecretsStoreOverride string `json:"secretsStoreOverride,omitempty" yaml:"secretsStoreOverride,omitempty"` + ApprovalRequired bool `json:"approvalRequired,omitempty" yaml:"approvalRequired,omitempty"` + Exposure *ExposureConfig `json:"exposure,omitempty" yaml:"exposure,omitempty"` } // ExposureConfig defines how a service is exposed to the network. diff --git a/config/infra_resolution.go b/config/infra_resolution.go index fc960e36..b1ce59b3 100644 --- a/config/infra_resolution.go +++ b/config/infra_resolution.go @@ -6,17 +6,17 @@ package config // "existing" means connect to an already-running instance. type InfraEnvironmentResolution struct { // Strategy determines how the resource is obtained: container, provision, existing. - Strategy string `json:"strategy" yaml:"strategy"` + Strategy string `json:"strategy" yaml:"strategy"` // DockerImage is used when Strategy is "container". - DockerImage string `json:"dockerImage,omitempty" yaml:"dockerImage,omitempty"` + DockerImage string `json:"dockerImage,omitempty" yaml:"dockerImage,omitempty"` // Port overrides the default service port when Strategy is "container". - Port int `json:"port,omitempty" yaml:"port,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` // Provider names the cloud provider when Strategy is "provision". - Provider string `json:"provider,omitempty" yaml:"provider,omitempty"` + Provider string `json:"provider,omitempty" yaml:"provider,omitempty"` // Config holds provider-specific provisioning options. - Config map[string]any `json:"config,omitempty" yaml:"config,omitempty"` + Config map[string]any `json:"config,omitempty" yaml:"config,omitempty"` // Connection holds connection details when Strategy is "existing". - Connection *InfraConnectionConfig `json:"connection,omitempty" yaml:"connection,omitempty"` + Connection *InfraConnectionConfig `json:"connection,omitempty" yaml:"connection,omitempty"` } // InfraConnectionConfig holds connection details for an existing infrastructure resource. diff --git a/config/secrets_config.go b/config/secrets_config.go index 0ef37f8b..a3878b10 100644 --- a/config/secrets_config.go +++ b/config/secrets_config.go @@ -14,8 +14,8 @@ type SecretsConfig struct { Entries []SecretEntry `json:"entries,omitempty" yaml:"entries,omitempty"` // Provider is the legacy single-store provider name. Kept for backward compatibility. // Prefer secretStores + defaultStore for new configs. - Provider string `json:"provider,omitempty" yaml:"provider,omitempty"` - Config map[string]any `json:"config,omitempty" yaml:"config,omitempty"` + Provider string `json:"provider,omitempty" yaml:"provider,omitempty"` + Config map[string]any `json:"config,omitempty" yaml:"config,omitempty"` Rotation *SecretsRotationConfig `json:"rotation,omitempty" yaml:"rotation,omitempty"` } @@ -28,8 +28,8 @@ type SecretsRotationConfig struct { // SecretEntry declares a single secret the application needs. type SecretEntry struct { - Name string `json:"name" yaml:"name"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` + Name string `json:"name" yaml:"name"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` // Store names the store (from secretStores) this secret lives in. // Overrides defaultStore and environment secretsStoreOverride. Store string `json:"store,omitempty" yaml:"store,omitempty"` diff --git a/lsp/completion.go b/lsp/completion.go index 644a4d33..0f576033 100644 --- a/lsp/completion.go +++ b/lsp/completion.go @@ -613,4 +613,3 @@ func metaFieldCompletions() []protocol.CompletionItem { } return items } - diff --git a/lsp/dsl_reference.go b/lsp/dsl_reference.go index 30181a77..cce4ebf3 100644 --- a/lsp/dsl_reference.go +++ b/lsp/dsl_reference.go @@ -33,16 +33,6 @@ var topLevelKeyToSectionID = map[string]string{ "sidecars": "platform", } -// sectionKindToSectionID maps SectionKind values to DSL section IDs. -var sectionKindToSectionID = map[SectionKind]string{ - SectionModules: "modules", - SectionWorkflow: "workflows", - SectionPipeline: "pipelines", - SectionTriggers: "triggers", - SectionImports: "imports", - SectionRequires: "application", -} - // loadDSLSections locates docs/dsl-reference.md by searching upward from cwd, // parses it, and returns a map of section ID → DSLSectionDoc. // Returns nil (no error) if the file cannot be found — hover will skip DSL docs. diff --git a/lsp/library.go b/lsp/library.go index 244eb79a..e781cf00 100644 --- a/lsp/library.go +++ b/lsp/library.go @@ -81,7 +81,8 @@ func HoverAt(content string, line, col int, pluginDir ...string) *HoverResult { // convertDiagnostics converts protocol diagnostics to library Diagnostic values. func convertDiagnostics(diags []protocol.Diagnostic) []Diagnostic { out := make([]Diagnostic, 0, len(diags)) - for _, d := range diags { + for i := range diags { + d := &diags[i] sev := SeverityWarning if d.Severity != nil { sev = DiagSeverity(*d.Severity) @@ -91,10 +92,10 @@ func convertDiagnostics(diags []protocol.Diagnostic) []Diagnostic { src = *d.Source } out = append(out, Diagnostic{ - Line: int(d.Range.Start.Line), //nolint:gosec // G115: LSP positions are non-negative + Line: int(d.Range.Start.Line), //nolint:gosec // G115: LSP positions are non-negative Col: int(d.Range.Start.Character), //nolint:gosec // G115: LSP positions are non-negative - EndLine: int(d.Range.End.Line), //nolint:gosec // G115: LSP positions are non-negative - EndCol: int(d.Range.End.Character), //nolint:gosec // G115: LSP positions are non-negative + EndLine: int(d.Range.End.Line), //nolint:gosec // G115: LSP positions are non-negative + EndCol: int(d.Range.End.Character), //nolint:gosec // G115: LSP positions are non-negative Message: d.Message, Severity: sev, Source: src, @@ -106,7 +107,8 @@ func convertDiagnostics(diags []protocol.Diagnostic) []Diagnostic { // convertCompletions converts protocol completion items to library CompletionResult values. func convertCompletions(items []protocol.CompletionItem) []CompletionResult { out := make([]CompletionResult, 0, len(items)) - for _, item := range items { + for i := range items { + item := &items[i] kind := "" if item.Kind != nil { kind = completionKindName(*item.Kind) diff --git a/mcp/scaffold_tools.go b/mcp/scaffold_tools.go index c1f21392..6cb63b1d 100644 --- a/mcp/scaffold_tools.go +++ b/mcp/scaffold_tools.go @@ -308,8 +308,8 @@ func (s *Server) handleScaffoldInfra(_ context.Context, req mcp.CallToolRequest) } type infraResource struct { - ModuleType string - ModuleName string + ModuleType string + ModuleName string ResourceType string ServiceName string } @@ -395,7 +395,7 @@ func (s *Server) handleDetectSecrets(_ context.Context, req mcp.CallToolRequest) for field, val := range mod.Config { fieldLower := strings.ToLower(field) for _, secretField := range secretFields { - if fieldLower == strings.ToLower(secretField) { + if strings.EqualFold(fieldLower, secretField) { valStr, _ := val.(string) reason := "field name matches secret pattern" suggested := strings.ToUpper(strings.ReplaceAll(field, ".", "_")) @@ -571,7 +571,8 @@ func (s *Server) handleDetectInfraNeeds(_ context.Context, req mcp.CallToolReque for _, n := range needs { seen[n.ModuleType+":"+n.ModuleName] = true } - for _, req := range pluginReqs { + for i := range pluginReqs { + req := pluginReqs[i] needs = append(needs, infraNeed{ Category: req.Type, ModuleType: req.Type, @@ -633,7 +634,8 @@ func loadPluginInfraNeeds(pluginsDir string, cfg *config.WorkflowConfig) ([]conf if !usedTypes[moduleType] { continue } - for _, req := range spec.Requires { + for i := range spec.Requires { + req := spec.Requires[i] key := req.Type + ":" + req.Name if seen[key] { continue diff --git a/mcp/server.go b/mcp/server.go index d479f485..b5c9ed37 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -77,8 +77,8 @@ type Server struct { mcpServer *server.MCPServer pluginDir string registryDir string - documentationFile string // optional explicit path to DOCUMENTATION.md - engine EngineProvider // optional; enables execution tools when set + documentationFile string // optional explicit path to DOCUMENTATION.md + engine EngineProvider // optional; enables execution tools when set toolHandlers map[string]ToolHandlerFunc // populated by collectToolHandlers } diff --git a/module/http_router.go b/module/http_router.go index 104f9103..756f4dfb 100644 --- a/module/http_router.go +++ b/module/http_router.go @@ -193,6 +193,7 @@ func (r *StandardHTTPRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) slog.Error("panic in HTTP handler", "panic", rec, "stack", string(debug.Stack())) if !rw.written.Load() { http.Error(rw, "Internal Server Error", http.StatusInternalServerError) + return } } }() diff --git a/module/http_server.go b/module/http_server.go index 09788686..b8d1e7b2 100644 --- a/module/http_server.go +++ b/module/http_server.go @@ -31,15 +31,15 @@ type HTTPServerTLSConfig struct { // StandardHTTPServer implements the HTTPServer interface and modular.Module interfaces type StandardHTTPServer struct { - name string - server *http.Server - address string - router HTTPRouter - logger modular.Logger - readTimeout time.Duration - writeTimeout time.Duration - idleTimeout time.Duration - tlsCfg HTTPServerTLSConfig + name string + server *http.Server + address string + router HTTPRouter + logger modular.Logger + readTimeout time.Duration + writeTimeout time.Duration + idleTimeout time.Duration + tlsCfg HTTPServerTLSConfig listenErr chan error acmeChallengeErr chan error } diff --git a/module/nosql_redis.go b/module/nosql_redis.go index 4d4705a3..d6aaec09 100644 --- a/module/nosql_redis.go +++ b/module/nosql_redis.go @@ -15,7 +15,7 @@ import ( // When addr == "memory://" the module falls back to the in-memory backend. type RedisNoSQLConfig struct { Addr string `json:"addr" yaml:"addr" editor:"type=string,description=Redis server address (memory:// for in-memory fallback),default=localhost:6379,label=Address"` // "memory://" => in-memory fallback - Password string `json:"password" yaml:"password" editor:"type=string,sensitive,description=Redis password,label=Password"` //nolint:gosec // G117: config struct field, not a hardcoded secret + Password string `json:"password" yaml:"password" editor:"type=string,sensitive,description=Redis password,label=Password"` //nolint:gosec // G117: config struct field, not a hardcoded secret DB int `json:"db" yaml:"db" editor:"type=number,description=Redis database number,default=0,label=Database"` } diff --git a/module/pipeline_expr.go b/module/pipeline_expr.go index de740fca..34074d7c 100644 --- a/module/pipeline_expr.go +++ b/module/pipeline_expr.go @@ -11,14 +11,3 @@ type ExprEngine = pipeline.ExprEngine // NewExprEngine creates a new ExprEngine. // Delegates to pipeline.NewExprEngine. var NewExprEngine = pipeline.NewExprEngine - -// containsExpr reports whether s contains a ${ } expression block. -func containsExpr(s string) bool { - return pipeline.ContainsExpr(s) -} - -// resolveExprBlocks replaces all ${ ... } blocks in s by evaluating each -// expression against pc. Returns the substituted string or the first error. -func resolveExprBlocks(s string, pc *PipelineContext, ee *ExprEngine) (string, error) { - return pipeline.ResolveExprBlocks(s, pc, ee) -} diff --git a/schema/reflect_test.go b/schema/reflect_test.go index 7d5fd548..190e08d6 100644 --- a/schema/reflect_test.go +++ b/schema/reflect_test.go @@ -7,21 +7,21 @@ import ( // --- test structs --- type allTagsStruct struct { - Driver string `json:"driver" editor:"type=select,options=postgres|mysql|sqlite,required,description=Database driver"` - DSN string `json:"dsn" editor:"type=string,required,sensitive,description=Connection string,placeholder=postgres://user:pass@host/db"` - MaxConns int `json:"maxConns" editor:"type=number,description=Max connections,default=25"` - Enabled bool `json:"enabled" editor:"type=boolean,description=Enable feature,default=true"` - Tags []string `json:"tags" editor:"type=array,arrayItemType=string"` - Meta map[string]string `json:"meta" editor:"type=map,mapValueType=string"` + Driver string `json:"driver" editor:"type=select,options=postgres|mysql|sqlite,required,description=Database driver"` + DSN string `json:"dsn" editor:"type=string,required,sensitive,description=Connection string,placeholder=postgres://user:pass@host/db"` + MaxConns int `json:"maxConns" editor:"type=number,description=Max connections,default=25"` + Enabled bool `json:"enabled" editor:"type=boolean,description=Enable feature,default=true"` + Tags []string `json:"tags" editor:"type=array,arrayItemType=string"` + Meta map[string]string `json:"meta" editor:"type=map,mapValueType=string"` } type inferredTypesStruct struct { - Name string `json:"name" editor:"description=Name"` - Count int `json:"count" editor:"description=Count"` - Ratio float64 `json:"ratio" editor:"description=Ratio"` - Active bool `json:"active" editor:"description=Active"` - Items []string `json:"items" editor:"description=Items"` - Labels map[string]string `json:"labels" editor:"description=Labels"` + Name string `json:"name" editor:"description=Name"` + Count int `json:"count" editor:"description=Count"` + Ratio float64 `json:"ratio" editor:"description=Ratio"` + Active bool `json:"active" editor:"description=Active"` + Items []string `json:"items" editor:"description=Items"` + Labels map[string]string `json:"labels" editor:"description=Labels"` } type noEditorTagsStruct struct { @@ -29,15 +29,6 @@ type noEditorTagsStruct struct { Age int `json:"age"` } -type nestedStruct struct { - Name string `json:"name" editor:"type=string,description=Name"` - Inner innerStruct `json:"inner" editor:"type=string,description=Inner"` -} - -type innerStruct struct { - Value string `json:"value" editor:"type=string"` -} - type sensitiveStruct struct { Password string `json:"password" editor:"type=string,sensitive,required"` } @@ -52,10 +43,10 @@ type groupStruct struct { } type defaultValueStruct struct { - Count int `json:"count" editor:"type=number,default=42"` - Ratio float64 `json:"ratio" editor:"type=number,default=3.14"` - Active bool `json:"active" editor:"type=boolean,default=true"` - Name string `json:"name" editor:"type=string,default=hello"` + Count int `json:"count" editor:"type=number,default=42"` + Ratio float64 `json:"ratio" editor:"type=number,default=3.14"` + Active bool `json:"active" editor:"type=boolean,default=true"` + Name string `json:"name" editor:"type=string,default=hello"` } // --- tests --- diff --git a/wftest/bdd/runner_test.go b/wftest/bdd/runner_test.go index 66175ec6..55139399 100644 --- a/wftest/bdd/runner_test.go +++ b/wftest/bdd/runner_test.go @@ -35,3 +35,28 @@ func TestRunFeatures_State(t *testing.T) { func TestRunFeatures_UndefinedLenient(t *testing.T) { bdd.RunFeatures(t, "testdata/undefined.feature") } + +// Strict-mode variants: verify that no undefined/pending steps exist in the feature files. +func TestRunFeatures_MinimalStrict(t *testing.T) { + bdd.RunFeatures(t, "testdata/minimal.feature", bdd.Strict()) +} + +func TestRunFeatures_MockStrict(t *testing.T) { + bdd.RunFeatures(t, "testdata/mock.feature", bdd.Strict()) +} + +func TestRunFeatures_HTTPStrict(t *testing.T) { + bdd.RunFeatures(t, "testdata/http.feature", bdd.Strict()) +} + +func TestRunFeatures_TriggersStrict(t *testing.T) { + bdd.RunFeatures(t, "testdata/triggers.feature", bdd.Strict()) +} + +func TestRunFeatures_AssertionsStrict(t *testing.T) { + bdd.RunFeatures(t, "testdata/assertions.feature", bdd.Strict()) +} + +func TestRunFeatures_StateStrict(t *testing.T) { + bdd.RunFeatures(t, "testdata/state.feature", bdd.Strict()) +}