feat(plugins): 插件「管理面板声明」库——声明模型 + 闭集校验 + catalog(design/09 §5.2/§5.2.1)
纯库、零接线:不碰 gateway/frontend/Docs/.env*(本片刻意不接线,避免与在途切片争用文件面)。 - pkg/plugins/sdk:面板声明类型(JSON tag 与 design/09 §5.2 示例逐字同形); 字段类型与 section 类型都是**闭集**;When 三态 + **严格 UnmarshalJSON**(未知条件键报错, 不静默丢) - internal/capability/paneldecl:闭集校验(47 条拒绝用例,断言的是**分类哨兵 + JSON 路径**, 不是错误字符串)· catalog(并发安全、返回拷贝、按 (group, order, 登记序号) 的**全序**稳定排序)· when 求值(上下文由 Resolver 注入,本包不读环境变量/文件)· 角色过滤(viewer 只留 status/log/table 且摘掉 rowActions,纯写面板整体隐藏;未知角色 fail-closed) - 拒绝粒度:插件 id 非法 ⇒ 整插件拒绝;单个面板非法 ⇒ 只拒该面板 + 聚合显式报错(理由见报告) 【我的验收】隔离 worktree(19 文件)build/vet/linux/darwin 全过、69 包全绿; 我亲手打红变异:把同组同序的回退比较 反成 ⇒ TestCatalog_StableOrder FAIL。 (记录一条观察:比较器已靠 seq 构成全序,故 sort.SliceStable 的稳定性是冗余保护 —— 我第一次用「SliceStable→Slice」打不红,正是因为全序与算法无关;这不是测试虚护。)
This commit is contained in:
1 parent
6e28b38b08
commit
c9144c1096
19 files changed
+3053
No files matched your search
@@ -0,0 +1,108 @@
|
||||
package paneldecl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||||
)
|
||||
|
||||
// 本文件是测试夹具:一份**逐字对齐 design/09 §5.2 示例**的合法声明。
|
||||
//
|
||||
// 为什么所有非法用例都从它派生:只有这样才能保证「被拒的是那一处改动,而不是碰巧别处也不
|
||||
// 合法」—— 否则一个用例变红,无法区分是目标规则生效还是夹具本身有毛病。
|
||||
|
||||
// boolPtr 是 &true / &false 的简写(sdk.When.Enabled 是三态指针)。
|
||||
func boolPtr(v bool) *bool { return &v }
|
||||
|
||||
// floatPtr 是 &x 的简写(sdk.Field.Min / Max)。
|
||||
func floatPtr(v float64) *float64 { return &v }
|
||||
|
||||
// samplePanel 返回 §5.2 示例里的面板(web-search 的 providers 页):form + table 两段,
|
||||
// 覆盖 secret / select(optsFrom) / text 三种字段与两个行内动作。
|
||||
func samplePanel() sdk.Panel {
|
||||
return sdk.Panel{
|
||||
ID: "providers",
|
||||
Title: "搜索提供商",
|
||||
Group: sdk.GroupPlugins,
|
||||
Order: 20,
|
||||
Icon: "search",
|
||||
When: &sdk.When{Enabled: boolPtr(true)},
|
||||
Sections: []sdk.Section{
|
||||
{
|
||||
Type: sdk.SectionForm,
|
||||
ID: "add",
|
||||
Title: "添加提供商",
|
||||
Fields: []sdk.Field{
|
||||
{Key: "provider", Label: "提供商", Type: sdk.FieldSelect,
|
||||
OptionsFrom: "plugin:providers", Required: true},
|
||||
{Key: "apiKey", Label: "API Key", Type: sdk.FieldSecret, Required: true},
|
||||
{Key: "endpoint", Label: "接口地址", Type: sdk.FieldText, Placeholder: "https://..."},
|
||||
},
|
||||
Submit: &sdk.Submit{Action: "plugin.saveProvider"},
|
||||
},
|
||||
{
|
||||
Type: sdk.SectionTable,
|
||||
ID: "list",
|
||||
Title: "已配置的提供商",
|
||||
Source: &sdk.Source{Action: "plugin.listProviders"},
|
||||
Columns: []sdk.Column{
|
||||
{Key: "provider", Label: "提供商"},
|
||||
{Key: "state", Label: "状态"},
|
||||
},
|
||||
RowActions: []sdk.RowAction{
|
||||
{ID: "test", Label: "测试连通", Action: "plugin.testProvider"},
|
||||
{ID: "reauth", Label: "重配凭据", Action: "plugin.reauthProvider"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// statusPanel 是只读事实面夹具(§5.2.1 section.status)。
|
||||
func statusPanel() sdk.Panel {
|
||||
return sdk.Panel{
|
||||
ID: "chain",
|
||||
Title: "降级链",
|
||||
Group: sdk.GroupPlugins,
|
||||
Order: 10,
|
||||
Sections: []sdk.Section{{
|
||||
Type: sdk.SectionStatus,
|
||||
ID: "chain",
|
||||
Title: "当前生效的降级链",
|
||||
Fields: []sdk.Field{
|
||||
{Key: "active", Label: "实际应答者", Type: sdk.FieldText},
|
||||
},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// logPanel 是只读日志卡夹具(§5.2.1 section.log)。
|
||||
func logPanel() sdk.Panel {
|
||||
return sdk.Panel{
|
||||
ID: "trace",
|
||||
Title: "最近调用",
|
||||
Group: sdk.GroupPlugins,
|
||||
Order: 30,
|
||||
Sections: []sdk.Section{{
|
||||
Type: sdk.SectionLog,
|
||||
ID: "recent",
|
||||
Title: "最近 N 条",
|
||||
Source: &sdk.Source{Action: "plugin.recentCalls"},
|
||||
Limit: 50,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// mustPanelIDs 抽取目录快照里的面板 id 序列(顺序敏感,用于稳定序断言)。
|
||||
func mustPanelIDs(t *testing.T, entries []Entry) []string {
|
||||
t.Helper()
|
||||
ids := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
ids = append(ids, e.Panel.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// alwaysEnabled / neverEnabled 是两种最简 Resolver 桩。
|
||||
func alwaysEnabled(string) Context { return Context{Enabled: true} }
|
||||
func neverEnabled(string) Context { return Context{} }
|
||||
Reference in new issue
Block a user