From d5374150d77a80140093fd1727d947ae374846da Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 10 May 2026 03:36:09 -0400 Subject: [PATCH 1/2] fix(wfctl plugin install): preserve cliCommands + buildHooks in plugin.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `writeInstalledManifest` constructed `installedPluginCapabilities` from the registry manifest by hand-copying ModuleTypes / StepTypes / TriggerTypes / IaCProvider — but silently dropped CLICommands and BuildHooks. Result: plugin.json on disk after install had no cliCommands, so BuildCLIRegistry could not discover plugin-provided wfctl subcommands. Workflow-registry's payments manifest had been updated to declare `cliCommands: [{name: payments, …}]`, but every BMW `provision-stripe-issuing-webhook` retrigger still reported `unknown command: payments` because the field never made it from the registry manifest to the on-disk plugin.json. Fix: extend `installedPluginCapabilities` with `CLICommands` + `BuildHooks` fields and propagate them in writeInstalledManifest. The registry-side struct already carries both fields; this just plumbs them through. Tests: TestInstalledManifestPreservesCLICommands as a regression guard — read-back of writeInstalledManifest's output must contain the CLICommands and BuildHooks the registry manifest declared. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/wfctl/plugin_install.go | 13 +++++++ cmd/wfctl/plugin_install_e2e_test.go | 51 ++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/cmd/wfctl/plugin_install.go b/cmd/wfctl/plugin_install.go index 8a3d12c5..176ff9cd 100644 --- a/cmd/wfctl/plugin_install.go +++ b/cmd/wfctl/plugin_install.go @@ -1198,6 +1198,17 @@ type installedPluginCapabilities struct { StepTypes []string `json:"stepTypes,omitempty"` TriggerTypes []string `json:"triggerTypes,omitempty"` IaCProvider *RegistryIaCProvider `json:"iacProvider,omitempty"` + // CLICommands flow through to plugin.json so BuildCLIRegistry can + // discover plugin-provided wfctl subcommands after install. Earlier + // versions of writeInstalledManifest dropped this field, leaving + // the installed manifest stripped of cliCommands even when the + // registry manifest declared them — `wfctl ` then + // reported `unknown command` post-install. + CLICommands []RegistryCLICommand `json:"cliCommands,omitempty"` + // BuildHooks flow through for the same reason: the build-hook + // dispatcher reads them from the installed plugin.json, not the + // registry manifest. + BuildHooks []RegistryBuildHook `json:"buildHooks,omitempty"` } // writeInstalledManifest writes a full plugin.json compatible with the engine's @@ -1220,6 +1231,8 @@ func writeInstalledManifest(path string, m *RegistryManifest) error { StepTypes: m.Capabilities.StepTypes, TriggerTypes: m.Capabilities.TriggerTypes, IaCProvider: m.Capabilities.IaCProvider, + CLICommands: m.Capabilities.CLICommands, + BuildHooks: m.Capabilities.BuildHooks, } } data, err := json.MarshalIndent(pj, "", " ") diff --git a/cmd/wfctl/plugin_install_e2e_test.go b/cmd/wfctl/plugin_install_e2e_test.go index 46020393..5b926344 100644 --- a/cmd/wfctl/plugin_install_e2e_test.go +++ b/cmd/wfctl/plugin_install_e2e_test.go @@ -423,6 +423,57 @@ func TestSafeJoin(t *testing.T) { } } +// TestInstalledManifestPreservesCLICommands is the regression test for the +// post-install plugin-CLI dispatch bug: writeInstalledManifest used to drop +// capabilities.cliCommands, so even when a registry manifest declared them, +// `wfctl ` reported `unknown command` because BuildCLIRegistry +// reads from the on-disk plugin.json (not the registry manifest). +func TestInstalledManifestPreservesCLICommands(t *testing.T) { + rm := &RegistryManifest{ + Name: "workflow-plugin-payments", + Version: "0.3.1", + Author: "tester", + Description: "regression: cliCommands preserved post-install", + Type: "external", + Tier: "core", + License: "Apache-2.0", + Capabilities: &RegistryCapabilities{ + ModuleTypes: []string{"payments.provider"}, + StepTypes: []string{"step.payment_charge"}, + CLICommands: []RegistryCLICommand{ + {Name: "payments", Description: "Payment provider operations"}, + }, + BuildHooks: []RegistryBuildHook{ + {Event: "pre-build", Priority: 10}, + }, + }, + } + + dir := t.TempDir() + path := filepath.Join(dir, "plugin.json") + if err := writeInstalledManifest(path, rm); err != nil { + t.Fatalf("writeInstalledManifest: %v", err) + } + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read installed plugin.json: %v", err) + } + var got installedPluginJSON + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal installed plugin.json: %v", err) + } + if got.Capabilities == nil { + t.Fatal("expected non-nil Capabilities") + } + if len(got.Capabilities.CLICommands) != 1 || got.Capabilities.CLICommands[0].Name != "payments" { + t.Errorf("CLICommands = %+v, want [{Name: payments, …}]", got.Capabilities.CLICommands) + } + if len(got.Capabilities.BuildHooks) != 1 || got.Capabilities.BuildHooks[0].Event != "pre-build" { + t.Errorf("BuildHooks = %+v, want [{Event: pre-build, …}]", got.Capabilities.BuildHooks) + } +} + // TestInstalledManifestEngineValidation verifies that the plugin.json written by // writeInstalledManifest passes the engine's plugin.LoadManifest and Validate. func TestInstalledManifestEngineValidation(t *testing.T) { From de4f6abdceb97525af79ea394cee94e71e883b5c Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 10 May 2026 03:48:03 -0400 Subject: [PATCH 2/2] fix(test): rename + use canonical hook event identifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 2 of #612 Copilot review: - Test name now reflects both behaviors (CLICommands + BuildHooks). - BuildHooks fixture event renamed pre-build → pre_build to match interfaces.HookEvent* underscore convention. Hyphen variant was a placeholder that could mask validation drift. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/wfctl/plugin_install_e2e_test.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/cmd/wfctl/plugin_install_e2e_test.go b/cmd/wfctl/plugin_install_e2e_test.go index 5b926344..63132878 100644 --- a/cmd/wfctl/plugin_install_e2e_test.go +++ b/cmd/wfctl/plugin_install_e2e_test.go @@ -423,17 +423,18 @@ func TestSafeJoin(t *testing.T) { } } -// TestInstalledManifestPreservesCLICommands is the regression test for the -// post-install plugin-CLI dispatch bug: writeInstalledManifest used to drop -// capabilities.cliCommands, so even when a registry manifest declared them, -// `wfctl ` reported `unknown command` because BuildCLIRegistry -// reads from the on-disk plugin.json (not the registry manifest). -func TestInstalledManifestPreservesCLICommands(t *testing.T) { +// TestInstalledManifestPreservesCLICommandsAndBuildHooks is the regression +// test for the post-install dispatch bug: writeInstalledManifest used to drop +// capabilities.cliCommands and capabilities.buildHooks, so even when a +// registry manifest declared them, BuildCLIRegistry / build-hook discovery +// reading the on-disk plugin.json saw an empty list. `wfctl ` +// then reported `unknown command` and build hooks silently no-oped. +func TestInstalledManifestPreservesCLICommandsAndBuildHooks(t *testing.T) { rm := &RegistryManifest{ Name: "workflow-plugin-payments", Version: "0.3.1", Author: "tester", - Description: "regression: cliCommands preserved post-install", + Description: "regression: cliCommands + buildHooks preserved post-install", Type: "external", Tier: "core", License: "Apache-2.0", @@ -443,8 +444,11 @@ func TestInstalledManifestPreservesCLICommands(t *testing.T) { CLICommands: []RegistryCLICommand{ {Name: "payments", Description: "Payment provider operations"}, }, + // Use a canonical event identifier (underscore-separated) per + // interfaces.HookEvent* convention; using a hyphen-separated + // placeholder would mask future validation drift. BuildHooks: []RegistryBuildHook{ - {Event: "pre-build", Priority: 10}, + {Event: "pre_build", Priority: 10}, }, }, } @@ -469,8 +473,8 @@ func TestInstalledManifestPreservesCLICommands(t *testing.T) { if len(got.Capabilities.CLICommands) != 1 || got.Capabilities.CLICommands[0].Name != "payments" { t.Errorf("CLICommands = %+v, want [{Name: payments, …}]", got.Capabilities.CLICommands) } - if len(got.Capabilities.BuildHooks) != 1 || got.Capabilities.BuildHooks[0].Event != "pre-build" { - t.Errorf("BuildHooks = %+v, want [{Event: pre-build, …}]", got.Capabilities.BuildHooks) + if len(got.Capabilities.BuildHooks) != 1 || got.Capabilities.BuildHooks[0].Event != "pre_build" { + t.Errorf("BuildHooks = %+v, want [{Event: pre_build, …}]", got.Capabilities.BuildHooks) } }