diff --git a/.gitignore b/.gitignore index 131c027..0bfbbea 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ cmd/gui/dist/ # local design/working notes (not part of the shipped repo) /plan.md + +# scratch build/probe dirs used while debugging (GOTMPDIR, throwaway binaries) +.build-work/ +.probe/ diff --git a/internal/gateway/plugins_api.go b/internal/gateway/plugins_api.go index 6b6953b..e7f4dc1 100644 --- a/internal/gateway/plugins_api.go +++ b/internal/gateway/plugins_api.go @@ -51,16 +51,27 @@ func (g *Gateway) handlePluginUI(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]interface{}{"ui": lua.UIExtension{}}) return } + // AllStages, not a hand-written list: an earlier version enumerated the + // three stages literally here, so when chain_step was added it was silently + // missing from this response — a plugin author reading the discovery + // document would have believed the stage did not exist. Deriving it from + // the one place that defines the order is the whole point of having it. writeJSON(w, http.StatusOK, map[string]interface{}{ - "ui": ps.UI(), - "stages": []string{ - string(lua.StageRequestStart), - string(lua.StageRouted), - string(lua.StageRequestEnd), - }, + "ui": ps.UI(), + "stages": pipelineStageNames(), }) } +// stageNames returns the pipeline stage names in firing order, for the +// discovery payload and for tests. +func pipelineStageNames() []string { + out := make([]string, 0, len(lua.AllStages)) + for _, s := range lua.AllStages { + out = append(out, string(s)) + } + return out +} + func (g *Gateway) handlePluginsAPI(w http.ResponseWriter, r *http.Request) { ps := g.core.Plugins() if ps == nil { diff --git a/internal/gateway/plugins_api_test.go b/internal/gateway/plugins_api_test.go index fd56b7b..b9fbffc 100644 --- a/internal/gateway/plugins_api_test.go +++ b/internal/gateway/plugins_api_test.go @@ -97,8 +97,19 @@ func TestUIInjectServesPluginUI(t *testing.T) { t.Errorf("element target = %q, want \"status\"", e.Target) } } - if len(view.Stages) != 3 { - t.Errorf("stages = %v, want the three pipeline stages", view.Stages) + // Derived from AllStages, not hardcoded: the previous assertion of "3" + // is exactly what let chain_step go missing from this payload unnoticed. + if len(view.Stages) != len(lua.AllStages) { + t.Errorf("stages = %v, want %d (one per AllStages entry)", view.Stages, len(lua.AllStages)) + } + for i, st := range lua.AllStages { + if i >= len(view.Stages) || view.Stages[i] != string(st) { + t.Errorf("stages[%d] = %v, want %q", i, view.Stages, string(st)) + } + } + if !containsStr(view.Stages, string(lua.StageChainStep)) { + t.Error("the discovery payload does not advertise chain_step; a plugin " + + "author would conclude the stage does not exist") } } @@ -116,6 +127,16 @@ func TestUIInjectIsEmptyWithoutPlugins(t *testing.T) { } } +// containsStr reports whether list has s. +func containsStr(list []string, s string) bool { + for _, x := range list { + if x == s { + return true + } + } + return false +} + // newRecorderFor pushes a request through the full handler chain. func newRecorderFor(t *testing.T, g *Gateway, req *http.Request) *httptest.ResponseRecorder { t.Helper()