288 lines
11 KiB
Go
288 lines
11 KiB
Go
package capability
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func testDescriptor() Descriptor {
|
|
return Descriptor{
|
|
Type: " custom ", Version: "1", Label: "自定义", DefaultWidth: 100, DefaultHeight: 80,
|
|
InputKind: " TEXT ", GenerationMode: " IMAGE ",
|
|
Connection: ConnectionPolicy{CanSource: true, CanTarget: true, CanReference: true, AcceptedInputKinds: []string{"image", " TEXT "}, RejectedInputKinds: []string{"audio"}, MaxInputCount: 2},
|
|
CanUpdate: true, SummaryFields: []string{" prompt ", "prompt", "content"}, DetailFields: []string{"content"},
|
|
PatchFields: map[string]PatchField{
|
|
"title": {Path: " title ", Kind: " STRING ", Label: " 节点名称 ", Order: 10, Description: " 节点标题 ", MaxRunes: 20},
|
|
"enabled": {Path: "metadata.enabled", Kind: "boolean", Label: "启用状态", Order: 20},
|
|
"weight": {Path: "metadata.weight", Kind: "number", Label: "权重", Order: 30},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestRegistryNormalizesAndProtectsDescriptors(t *testing.T) {
|
|
input := testDescriptor()
|
|
registry, err := NewRegistry([]Descriptor{input})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input.Connection.AcceptedInputKinds[0] = "video"
|
|
input.PatchFields["title"] = PatchField{Path: "unsafe", Kind: "number"}
|
|
|
|
descriptor, ok := registry.Resolve(" CUSTOM ")
|
|
if !ok {
|
|
t.Fatal("normalized type was not resolved")
|
|
}
|
|
if descriptor.Type != "custom" || descriptor.InputKind != "text" || descriptor.GenerationMode != "image" {
|
|
t.Fatalf("descriptor was not normalized: %#v", descriptor)
|
|
}
|
|
if descriptor.Connection.AcceptedInputKinds[0] != "image" {
|
|
t.Fatalf("registry retained caller mutation: %#v", descriptor.Connection.AcceptedInputKinds)
|
|
}
|
|
if descriptor.PatchFields["title"].Path != "title" || descriptor.PatchFields["title"].Kind != "string" || descriptor.PatchFields["title"].Label != "节点名称" || descriptor.PatchFields["title"].Description != "节点标题" {
|
|
t.Fatalf("registry retained caller mutation: %#v", descriptor.PatchFields)
|
|
}
|
|
|
|
descriptor.Connection.AcceptedInputKinds[0] = "video"
|
|
descriptor.PatchFields["title"] = PatchField{Path: "changed", Kind: "number"}
|
|
fresh, _ := registry.Resolve("custom")
|
|
if fresh.Connection.AcceptedInputKinds[0] != "image" || fresh.PatchFields["title"].Path != "title" {
|
|
t.Fatal("Resolve returned mutable registry state")
|
|
}
|
|
}
|
|
|
|
func TestRegistryAcceptsExtensibleIdentifiers(t *testing.T) {
|
|
descriptor := testDescriptor()
|
|
descriptor.InputKind = "table_data"
|
|
descriptor.GenerationMode = "table-render"
|
|
descriptor.Connection.AcceptedInputKinds = []string{"table_data", "text"}
|
|
descriptor.Connection.RejectedInputKinds = nil
|
|
registry, err := NewRegistry([]Descriptor{descriptor})
|
|
if err != nil {
|
|
t.Fatalf("extensible kind or mode rejected: %v", err)
|
|
}
|
|
resolved, ok := registry.ResolveGenerationMode(" TABLE-RENDER ")
|
|
if !ok || resolved.Type != "custom" || resolved.InputKind != "table_data" {
|
|
t.Fatalf("generation mode did not resolve descriptor: %#v, %v", resolved, ok)
|
|
}
|
|
}
|
|
|
|
func TestRegistryHashIsStableAndTracksDeclarativeContract(t *testing.T) {
|
|
first := testDescriptor()
|
|
second := testDescriptor()
|
|
second.Connection.AcceptedInputKinds = []string{"text", "image"}
|
|
second.Connection.RejectedInputKinds = []string{"audio"}
|
|
first.CreateMetadata = func(string) map[string]any { return map[string]any{"one": true} }
|
|
second.CreateMetadata = func(string) map[string]any { return map[string]any{"two": true} }
|
|
|
|
left, err := NewRegistry([]Descriptor{first})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
right, err := NewRegistry([]Descriptor{second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if left.Hash() != right.Hash() {
|
|
t.Fatal("factory function changed the capability contract hash")
|
|
}
|
|
|
|
reordered, err := NewRegistry([]Descriptor{second, Descriptor{Type: "z", Version: "1", Label: "Z", DefaultWidth: 1, DefaultHeight: 1}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
otherOrder, err := NewRegistry([]Descriptor{{Type: "z", Version: "1", Label: "Z", DefaultWidth: 1, DefaultHeight: 1}, second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reordered.Hash() != otherOrder.Hash() {
|
|
t.Fatal("descriptor registration order changed the contract hash")
|
|
}
|
|
|
|
projected := second
|
|
projected.ProjectionKind = "storyboard"
|
|
projected.ProjectionField = "storyboard"
|
|
withProjection, err := NewRegistry([]Descriptor{projected})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if withProjection.Hash() == right.Hash() {
|
|
t.Fatal("projection declaration did not change capability contract hash")
|
|
}
|
|
|
|
described := second
|
|
field := described.PatchFields["title"]
|
|
field.Description = "不同的字段语义"
|
|
described.PatchFields["title"] = field
|
|
withDescription, err := NewRegistry([]Descriptor{described})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if withDescription.Hash() == right.Hash() {
|
|
t.Fatal("patch field description did not change capability contract hash")
|
|
}
|
|
}
|
|
|
|
func TestRegistryRejectsInvalidContracts(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
edit func(*Descriptor)
|
|
}{
|
|
{"invalid input kind", func(d *Descriptor) { d.InputKind = "9table" }},
|
|
{"invalid generation mode", func(d *Descriptor) { d.GenerationMode = "table mode" }},
|
|
{"duplicate generation mode", func(d *Descriptor) {}},
|
|
{"negative max input", func(d *Descriptor) { d.Connection.MaxInputCount = -1 }},
|
|
{"accepted and rejected conflict", func(d *Descriptor) { d.Connection.RejectedInputKinds = []string{"image"} }},
|
|
{"source without kind", func(d *Descriptor) { d.InputKind = "" }},
|
|
{"projection missing field", func(d *Descriptor) { d.ProjectionKind = "storyboard" }},
|
|
{"projection missing kind", func(d *Descriptor) { d.ProjectionField = "storyboard" }},
|
|
{"invalid projection kind", func(d *Descriptor) { d.ProjectionKind, d.ProjectionField = "bad kind", "storyboard" }},
|
|
{"unsafe projection field", func(d *Descriptor) { d.ProjectionKind, d.ProjectionField = "storyboard", "metadata.__proto__" }},
|
|
{"empty patch path", func(d *Descriptor) { d.PatchFields["title"] = PatchField{Kind: "string"} }},
|
|
{"unsafe patch path", func(d *Descriptor) {
|
|
d.PatchFields["title"] = PatchField{Path: "metadata.__proto__.value", Kind: "string"}
|
|
}},
|
|
{"unknown patch kind", func(d *Descriptor) { d.PatchFields["title"] = PatchField{Path: "title", Kind: "object"} }},
|
|
{"negative max runes", func(d *Descriptor) { d.PatchFields["title"] = PatchField{Path: "title", Kind: "string", MaxRunes: -1} }},
|
|
{"missing patch label", func(d *Descriptor) {
|
|
field := d.PatchFields["title"]
|
|
field.Label = ""
|
|
d.PatchFields["title"] = field
|
|
}},
|
|
{"missing patch order", func(d *Descriptor) {
|
|
field := d.PatchFields["title"]
|
|
field.Order = 0
|
|
d.PatchFields["title"] = field
|
|
}},
|
|
{"patch fields without update", func(d *Descriptor) { d.CanUpdate = false }},
|
|
{"update without patch fields", func(d *Descriptor) { d.PatchFields = nil }},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
descriptor := testDescriptor()
|
|
test.edit(&descriptor)
|
|
descriptors := []Descriptor{descriptor}
|
|
if test.name == "duplicate generation mode" {
|
|
other := testDescriptor()
|
|
other.Type = "other"
|
|
descriptors = append(descriptors, other)
|
|
}
|
|
if _, err := NewRegistry(descriptors); err == nil {
|
|
t.Fatal("invalid descriptor accepted")
|
|
}
|
|
})
|
|
}
|
|
if _, err := NewRegistry([]Descriptor{testDescriptor(), testDescriptor()}); err == nil {
|
|
t.Fatal("duplicate descriptor accepted")
|
|
}
|
|
}
|
|
|
|
func TestDescriptorConnectionAndPatchContract(t *testing.T) {
|
|
registry, err := NewRegistry([]Descriptor{testDescriptor()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
descriptor, _ := registry.Resolve("custom")
|
|
if !descriptor.AllowsInput("IMAGE") || !descriptor.AllowsInput("text") || descriptor.AllowsInput("audio") || descriptor.AllowsInput("video") {
|
|
t.Fatalf("input admission contract is wrong: %#v", descriptor.Connection)
|
|
}
|
|
if descriptor.Connection.MaxInputCount != 2 {
|
|
t.Fatalf("max input count = %d", descriptor.Connection.MaxInputCount)
|
|
}
|
|
for name, patch := range map[string]map[string]any{
|
|
"unknown": {"missing": "value"},
|
|
"string type": {"title": true},
|
|
"number type": {"weight": "1"},
|
|
"boolean type": {"enabled": 1.0},
|
|
"rune limit": {"title": strings.Repeat("界", 21)},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if err := descriptor.ValidatePatch(patch); err == nil {
|
|
t.Fatal("invalid patch accepted")
|
|
}
|
|
})
|
|
}
|
|
node := map[string]any{"metadata": map[string]any{"untouched": true}}
|
|
patch := map[string]any{"title": "标题", "enabled": true, "weight": 1.5}
|
|
if err := descriptor.ApplyPatch(node, patch); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
metadata := node["metadata"].(map[string]any)
|
|
if node["title"] != "标题" || metadata["enabled"] != true || metadata["weight"] != 1.5 || metadata["untouched"] != true {
|
|
t.Fatalf("patch was not applied by declared paths: %#v", node)
|
|
}
|
|
}
|
|
|
|
func TestRegistryGenerationModesAreStableAndResolveUniquely(t *testing.T) {
|
|
image := testDescriptor()
|
|
image.Type, image.GenerationMode = "image-node", "image"
|
|
video := testDescriptor()
|
|
video.Type, video.GenerationMode = "video-node", "video"
|
|
registry, err := NewRegistry([]Descriptor{video, image})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := strings.Join(registry.GenerationModeNames(), ","); got != "image,video" {
|
|
t.Fatalf("generation modes = %s", got)
|
|
}
|
|
if !registry.SupportsGenerationMode(" VIDEO ") || registry.SupportsGenerationMode("audio") {
|
|
t.Fatal("generation mode support is wrong")
|
|
}
|
|
resolved, ok := registry.ResolveGenerationMode("image")
|
|
if !ok || resolved.Type != "image-node" {
|
|
t.Fatalf("resolved generation descriptor = %#v, %v", resolved, ok)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinMediaCapabilitiesUpdateDraftWithoutOverwritingGeneratedAssets(t *testing.T) {
|
|
registry := BuiltinRegistry()
|
|
for _, nodeType := range []string{"image", "video", "audio"} {
|
|
t.Run(nodeType, func(t *testing.T) {
|
|
descriptor, ok := registry.Resolve(nodeType)
|
|
if !ok || !descriptor.CanUpdate {
|
|
t.Fatalf("media capability is not updateable: %#v", descriptor)
|
|
}
|
|
if descriptor.PatchFields["content"].Path != "metadata.composerContent" || descriptor.PatchFields["title"].Path != "title" {
|
|
t.Fatalf("unsafe media update paths: %#v", descriptor.PatchFields)
|
|
}
|
|
|
|
node := map[string]any{
|
|
"title": "旧标题",
|
|
"metadata": map[string]any{
|
|
"content": "resource:generated-result",
|
|
"prompt": "已提交提示词",
|
|
"composerContent": "旧草稿",
|
|
"status": "success",
|
|
},
|
|
}
|
|
if err := descriptor.ApplyPatch(node, map[string]any{"title": "新标题", "content": "下一版提示词"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
metadata := node["metadata"].(map[string]any)
|
|
if node["title"] != "新标题" || metadata["composerContent"] != "下一版提示词" {
|
|
t.Fatalf("media draft was not updated: %#v", node)
|
|
}
|
|
if metadata["content"] != "resource:generated-result" || metadata["prompt"] != "已提交提示词" || metadata["status"] != "success" {
|
|
t.Fatalf("media result or submission snapshot was overwritten: %#v", metadata)
|
|
}
|
|
|
|
created := descriptor.Metadata("初始草稿")
|
|
if created["content"] != "" || created["prompt"] != "初始草稿" || created["composerContent"] != "初始草稿" || created["status"] != "idle" {
|
|
t.Fatalf("media draft metadata is incomplete: %#v", created)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegistryResolveUnknownTypeFailsClosed(t *testing.T) {
|
|
registry, err := NewRegistry([]Descriptor{testDescriptor()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := registry.Resolve("table"); ok {
|
|
t.Fatal("unknown capability resolved")
|
|
}
|
|
if _, ok := registry.ResolveGenerationMode("table"); ok {
|
|
t.Fatal("unknown generation mode resolved")
|
|
}
|
|
}
|