mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-10-03 23:54:06 +00:00
fix(plugin): /api/ui-inject 的 stages 漏掉 chain_step + 忽略临时构建目录
部署到线上时用隔离实例(独立端口 18099 + 独立 config/runtime/adapter 目录) 发真实请求验证,抓到的第三个 bug。 ## bug:discovery 文档漏掉新 stage handlePluginUI 的响应里 stages 是**字面写死的三个**。加 chain_step 时只改了 lua.AllStages,没改这里,于是插件作者读 GET /api/ui-inject 会看到 ["request_start","routed","request_end"],**合理地得出结论:没有 chain_step 这个 stage**。stage 本身是注册好的、也确实在触发,只是没被声明。 改成从 lua.AllStages 派生——AllStages 是唯一定义顺序的地方,让它保持唯一。 ★ 而我原来的测试断言 `len(view.Stages) != 3`,**断言本身是 bug 的保护伞**: 它把"三个"固化成了期望值,于是新增第四个 stage 时测试全绿、bug 上线。 现在断言改为「与 AllStages 等长且逐项相同」,并显式要求 chain_step 在其中。 新增 stage 而忘了声明,这类问题会立刻红。 ## 顺带:.gitignore 补上临时构建目录 .probe/ 和 .build-work/ 是我调试时当 GOTMPDIR 和临时二进制用的,之前每轮 手工删,这轮差点提交进去 9.5MB 的二进制。 ## 部署验证留档 隔离实例跑真实 chat(158 prompt / 13 completion / 128 cache_hit),计费插件 算出 0.000688,与手算 (158-128)*1e-5 + 128*1e-5*0.1 + 13*2e-5 **逐位吻合**。 ★ 第一次手算我按全价算成 0.00184,一度以为插件算错了——查审计记录才看到 cache_hit_tokens。**算钱不对时先查输入再怀疑实现**,而我忘的恰是刚修的折扣。 ## 验证 351 个测试全绿;变异(stages 改回硬编码三个)被 TestUIInjectServesPluginUI 抓住,3 条断言同时红。
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@ -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/
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
Reference in New Issue
Block a user