Files
Cyrene_v2/internal/capability/paneldecl/validate_section_test.go
T
AskaEth c9144c1096
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
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」打不红,正是因为全序与算法无关;这不是测试虚护。)
2026-10-06 08:06:20 +08:00

175 lines
5.6 KiB
Go

package paneldecl
import (
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
)
// 本文件是区块层的拒绝用例:类型闭集、id/标题必填、以及 §5.2.1 接入点表的形状矩阵
// (哪些键必须出现、哪些不许出现、哪些是只读区块上的写面)。
// rejectCasesSectionCommon 是区块的通用必填与唯一性。
func rejectCasesSectionCommon() []rejectCase {
return []rejectCase{
{
name: "区块类型不在闭集",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[0].Type = "chart" })
},
want: ErrUnknownSectionType, wantPath: "adminPanels[0].sections[0].type",
},
{
name: "区块 id 缺失",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[0].ID = "" })
},
want: ErrMissingField, wantPath: "adminPanels[0].sections[0].id",
},
{
name: "区块标题缺失",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[0].Title = "" })
},
want: ErrMissingField, wantPath: "adminPanels[0].sections[0].title",
},
{
name: "区块 id 在面板内重复",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[1].ID = "add" })
},
want: ErrDuplicateID, wantPath: "adminPanels[0].sections[1].id",
},
}
}
// rejectCasesFormShape 是 form 区块的形状(§5.2.1 section.form 只消费 fields[] + submit.action)。
func rejectCasesFormShape() []rejectCase {
return []rejectCase{
{
name: "form 缺 submit",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[0].Submit = nil })
},
want: ErrMissingField, wantPath: "adminPanels[0].sections[0].submit",
},
{
name: "form 缺 fields",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[0].Fields = nil })
},
want: ErrMissingField, wantPath: "adminPanels[0].sections[0].fields",
},
{
name: "form 声明 source(形状错误)",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) {
p.Sections[0].Source = &sdk.Source{Action: "plugin.listProviders"}
})
},
want: ErrSectionShape, wantPath: "adminPanels[0].sections[0].source",
},
{
name: "form 声明 columns(形状错误)",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) {
p.Sections[0].Columns = []sdk.Column{{Key: "k"}}
})
},
want: ErrSectionShape, wantPath: "adminPanels[0].sections[0].columns",
},
}
}
// rejectCasesTableShape 是 table 区块的形状(§5.2.1 section.table 消费 columns[]/rowActions[]/source)。
func rejectCasesTableShape() []rejectCase {
return []rejectCase{
{
name: "table 缺 columns",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[1].Columns = nil })
},
want: ErrMissingField, wantPath: "adminPanels[0].sections[1].columns",
},
{
name: "table 缺 source",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[1].Source = nil })
},
want: ErrMissingField, wantPath: "adminPanels[0].sections[1].source",
},
{
name: "table 上出现写面 submit",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) {
p.Sections[1].Submit = &sdk.Submit{Action: "plugin.saveProvider"}
})
},
want: ErrReadOnlySection, wantPath: "adminPanels[0].sections[1].submit",
},
{
name: "table 声明字段(形状错误)",
panels: func() []sdk.Panel {
return panelsOf(samplePanel(), func(p *sdk.Panel) {
p.Sections[1].Fields = []sdk.Field{{Key: "state", Type: sdk.FieldText}}
})
},
want: ErrSectionShape, wantPath: "adminPanels[0].sections[1].fields",
},
}
}
// rejectCasesReadOnlySections 是只读区块(status / log)的写面与形状。
func rejectCasesReadOnlySections() []rejectCase {
return []rejectCase{
{
name: "status 上出现写面 submit",
panels: func() []sdk.Panel {
return panelsOf(statusPanel(), func(p *sdk.Panel) {
p.Sections[0].Submit = &sdk.Submit{Action: "plugin.saveProvider"}
})
},
want: ErrReadOnlySection, wantPath: "adminPanels[0].sections[0].submit",
},
{
name: "status 上出现行内动作",
panels: func() []sdk.Panel {
return panelsOf(statusPanel(), func(p *sdk.Panel) {
p.Sections[0].RowActions = []sdk.RowAction{{ID: "test", Label: "测试", Action: "plugin.t"}}
})
},
want: ErrReadOnlySection, wantPath: "adminPanels[0].sections[0].rowActions",
},
{
name: "status 声明 source(形状错误)",
panels: func() []sdk.Panel {
return panelsOf(statusPanel(), func(p *sdk.Panel) {
p.Sections[0].Source = &sdk.Source{Action: "plugin.chain"}
})
},
want: ErrSectionShape, wantPath: "adminPanels[0].sections[0].source",
},
{
name: "log 缺 source",
panels: func() []sdk.Panel {
return panelsOf(logPanel(), func(p *sdk.Panel) { p.Sections[0].Source = nil })
},
want: ErrMissingField, wantPath: "adminPanels[0].sections[0].source",
},
{
name: "log 的 limit 越界",
panels: func() []sdk.Panel {
return panelsOf(logPanel(), func(p *sdk.Panel) { p.Sections[0].Limit = 5000 })
},
want: ErrInvalidLimit, wantPath: "adminPanels[0].sections[0].limit",
},
{
name: "log 声明 fields(形状错误)",
panels: func() []sdk.Panel {
return panelsOf(logPanel(), func(p *sdk.Panel) {
p.Sections[0].Fields = []sdk.Field{{Key: "line", Type: sdk.FieldText}}
})
},
want: ErrSectionShape, wantPath: "adminPanels[0].sections[0].fields",
},
}
}