Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_contract_test.go
2026-10-13 20:54:44 +08:00

551 lines
26 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"yingce/backend/internal/canvas/capability"
"yingce/backend/internal/model"
)
func TestCloudAgentMixedCanvasReadsUnsupportedNodesWithoutGrantingCapabilities(t *testing.T) {
nodes := []map[string]any{{"id": "text", "type": "text", "metadata": map[string]any{"content": "readable"}}}
for _, kind := range []string{"ai-art-critique", "config", "drawing", "future-plugin"} {
nodes = append(nodes, map[string]any{"id": kind, "type": kind, "title": "插件节点", "position": map[string]any{"x": 10.0, "y": 20.0}, "metadata": map[string]any{"content": "PRIVATE_SENTINEL", "apiKey": "PRIVATE_SENTINEL"}})
if _, supported := cloudAgentNodeCapabilityForType(kind); supported {
t.Fatalf("unsupported type gained write capability: %s", kind)
}
if err := validateCreationOps([]CreationCanvasOp{{Type: "add_node", ID: "new", NodeType: kind}}); err == nil {
t.Fatalf("unsupported creation accepted: %s", kind)
}
if _, _, err := cloudAgentReferenceDescriptor(nodes[len(nodes)-1]); err == nil {
t.Fatalf("unsupported media reference accepted: %s", kind)
}
if err := validateCloudAgentConnection(nodes, kind, "text"); err == nil {
t.Fatalf("unsupported connection accepted: %s", kind)
}
}
doc := map[string]any{"nodes": nodes, "connections": []map[string]any{{"id": "edge", "fromNodeId": "drawing", "toNodeId": "text"}}}
raw, _ := json.Marshal(doc)
summary, err := cloudAgentCanvasSummary(&model.CanvasProject{PayloadJSON: string(raw)})
if err != nil || strings.Contains(summary, "PRIVATE_SENTINEL") || !strings.Contains(summary, `"agentSupported":false`) {
t.Fatalf("mixed summary failed or leaked metadata: %v", err)
}
for _, ids := range [][]string{nil, {"drawing", "config"}} {
view, err := cloudAgentCanvasState(nil, "user", "agent-canvas", doc, 0, ids, 0)
if err != nil {
t.Fatal(err)
}
result := view.(map[string]any)
encoded, _ := json.Marshal(result)
if strings.Contains(string(encoded), "PRIVATE_SENTINEL") || result["snapshotHash"] != cloudAgentCanvasHash(doc) || len(result["connections"].([]any)) != 1 {
t.Fatal("unsafe projection or lost snapshot/connection")
}
want := len(nodes)
if ids != nil {
want = len(ids)
}
if len(result["nodes"].([]any)) != want {
t.Fatal("nodes silently omitted")
}
}
}
func TestCloudAgentCanvasSummaryIsACatalogNotNodeBodies(t *testing.T) {
body := strings.Repeat("镜", 4000)
nodes := make([]map[string]any, 0, 200)
for index := 0; index < 200; index++ {
nodes = append(nodes, map[string]any{
"id": fmt.Sprintf("node-%d", index),
"type": "text",
"title": fmt.Sprintf("镜头 %d %s", index, strings.Repeat("标题", 40)),
"metadata": map[string]any{
"content": body, "prompt": "PRIVATE_PROMPT", "apiKey": "PRIVATE_SENTINEL",
},
})
}
raw, err := json.Marshal(map[string]any{"nodes": nodes})
if err != nil {
t.Fatal(err)
}
summary, err := cloudAgentCanvasSummary(&model.CanvasProject{Title: "大画布", PayloadJSON: string(raw)})
if err != nil {
t.Fatalf("large canvas catalog rejected: %v", err)
}
if strings.Contains(summary, body[:40]) || strings.Contains(summary, "PRIVATE_PROMPT") || strings.Contains(summary, "PRIVATE_SENTINEL") || strings.Contains(summary, `"content"`) {
t.Fatal("catalog leaked node bodies or metadata")
}
var parsed map[string]any
if err := json.Unmarshal([]byte(summary), &parsed); err != nil {
t.Fatal(err)
}
if parsed["kind"] != "node_catalog" || parsed["totalNodes"] != float64(200) {
t.Fatalf("catalog identity = %+v", parsed)
}
included, _ := parsed["includedNodes"].(float64)
omitted, _ := parsed["omittedNodes"].(float64)
if included <= 0 || omitted <= 0 || int(included+omitted) != 200 || included > float64(cloudAgentCanvasSummaryMaxNodes) {
t.Fatalf("expected a bounded catalog, included=%v omitted=%v", included, omitted)
}
if len(summary) > cloudAgentCanvasSummaryBudgetBytes+4096 {
t.Fatalf("catalog still carries canvas-sized payload: %d", len(summary))
}
}
func TestCloudAgentCanvasSummaryUsesSelectedNodeNeighborhood(t *testing.T) {
doc := map[string]any{
"nodes": []map[string]any{
{"id": "focus", "type": "text", "title": "主体"},
{"id": "neighbor", "type": "image", "title": "关联素材"},
{"id": "unrelated", "type": "text", "title": "无关内容"},
},
"connections": []map[string]any{{"fromNodeId": "focus", "toNodeId": "neighbor"}},
}
raw, err := json.Marshal(doc)
if err != nil {
t.Fatal(err)
}
summary, err := cloudAgentCanvasSummary(&model.CanvasProject{PayloadJSON: string(raw)}, "focus")
if err != nil {
t.Fatal(err)
}
var result map[string]any
if err := json.Unmarshal([]byte(summary), &result); err != nil {
t.Fatal(err)
}
encoded, _ := json.Marshal(result["nodes"])
if strings.Contains(string(encoded), "unrelated") || !strings.Contains(string(encoded), "focus") || !strings.Contains(string(encoded), "neighbor") {
t.Fatalf("focused catalog did not include exactly the local graph neighborhood: %s", encoded)
}
selection := result["selection"].(map[string]any)
if selection["includedNeighbors"] != float64(1) || selection["nextReadDepth"] != float64(1) {
t.Fatalf("focused catalog metadata is incorrect: %+v", selection)
}
if _, err := cloudAgentCanvasSummary(&model.CanvasProject{PayloadJSON: string(raw)}, "deleted"); err == nil {
t.Fatal("stale selected node was accepted")
}
}
func TestCloudAgentToolsFollowCanvasCapabilityRegistry(t *testing.T) {
req := agentTestRequest()
req.PermissionMode = "auto"
req.ContextScope = []string{"canvas"}
req.Budget.MaxGenerationTasks = 1
functions := map[string]map[string]any{}
for _, tool := range cloudAgentTools(req) {
function := tool["function"].(map[string]any)
functions[function["name"].(string)] = function["parameters"].(map[string]any)
}
apply := functions["canvas_apply_ops"]
if apply == nil {
t.Fatal("canvas write tool is missing")
}
ops := apply["properties"].(map[string]any)["ops"].(map[string]any)["items"].(map[string]any)["properties"].(map[string]any)
if got := ops["nodeType"].(map[string]any)["enum"]; !reflect.DeepEqual(got, canvasCapabilityRegistry.Types()) {
t.Fatalf("node types differ from registry: %v", got)
}
patch := ops["patch"].(map[string]any)["properties"].(map[string]any)
nodeTypes := cloudAgentNodeTypes()["nodes"].([]map[string]any)
nodeTypeDefinitions := map[string]map[string]any{}
for _, item := range nodeTypes {
nodeTypeDefinitions[item["type"].(string)] = item
}
for _, descriptor := range canvasCapabilityRegistry.List() {
if !descriptor.CanUpdate && len(descriptor.PatchFields) != 0 {
t.Fatalf("non-updateable capability %s declares dead patch fields", descriptor.Type)
}
for key, field := range descriptor.PatchFields {
property, ok := patch[key].(map[string]any)
if !ok || property["type"] != field.Kind {
t.Fatalf("patch field %s (%s) missing from tool schema", key, descriptor.Type)
}
definitions, _ := nodeTypeDefinitions[descriptor.Type]["updateFields"].(map[string]any)
definition, _ := definitions[key].(map[string]any)
if field.Label == "" || definition["label"] != field.Label {
t.Fatalf("patch field %s (%s) has no stable user-facing label", key, descriptor.Type)
}
}
}
script := nodeTypeDefinitions["script"]
if script == nil || script["canSource"] != true || script["canTarget"] != true || script["canReference"] != false || script["inputKind"] != "text" {
t.Fatalf("script capability is incomplete: %#v", script)
}
updateFields, _ := script["updateFields"].(map[string]any)
for _, field := range []string{"title", "x", "y"} {
if _, ok := updateFields[field]; !ok {
t.Fatalf("script update field %s is missing: %#v", field, script)
}
}
if accepted, _ := script["acceptedInputKinds"].([]string); !reflect.DeepEqual(accepted, []string{"audio", "character", "image", "text", "video"}) {
t.Fatalf("script accepted input kinds are incomplete: %#v", script["acceptedInputKinds"])
}
if got := functions["generate_media"]["properties"].(map[string]any)["mode"].(map[string]any)["enum"]; !reflect.DeepEqual(got, cloudAgentGenerationModeNames()) {
t.Fatalf("generation modes differ from implemented adapters: %v", got)
}
sizeDescription, _ := functions["generate_media"]["properties"].(map[string]any)["size"].(map[string]any)["description"].(string)
if !strings.Contains(sizeDescription, "imageSize/image.size.parameter") || !strings.Contains(sizeDescription, "allowCustom=true") {
t.Fatalf("generation size schema does not explain model-specific parameters: %q", sizeDescription)
}
splitProperties := functions["image_layer_split"]["properties"].(map[string]any)
if _, ok := splitProperties["size"]; !ok {
t.Fatal("image_layer_split is missing the shared image size field")
}
if got := CloudAgentCapabilitySetInfo(); got.Hash != canvasCapabilityRegistry.Hash() || got.Version != capability.SetVersion || !reflect.DeepEqual(got.Nodes, canvasCapabilityRegistry.Types()) {
t.Fatalf("capability endpoint info differs from registry: %+v", got)
}
}
func TestCloudAgentImageEditingToolsRespectPermissionBoundaries(t *testing.T) {
readOnly := agentTestRequest()
readOnly.PermissionMode = "read_only"
readOnly.ContextScope = []string{"canvas"}
readNames := map[string]bool{}
for _, tool := range cloudAgentTools(readOnly) {
readNames[tool["function"].(map[string]any)["name"].(string)] = true
}
for _, name := range []string{"image_text_detect", "image_annotation_render"} {
if !readNames[name] {
t.Fatalf("read-only tool %s missing", name)
}
}
if readNames["image_layer_split"] {
t.Fatal("image_layer_split must not be exposed in read-only mode")
}
write := readOnly
write.PermissionMode = "auto"
write.Budget.MaxGenerationTasks = 1
writeNames := map[string]bool{}
for _, tool := range cloudAgentTools(write) {
writeNames[tool["function"].(map[string]any)["name"].(string)] = true
}
if !writeNames["image_layer_split"] || !cloudAgentWrite("image_layer_split") {
t.Fatal("image_layer_split must be an approved write tool")
}
if got := cloudAgentMediaCall(cloudAgentCall{Function: struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}{Name: "image_layer_split", Arguments: `{"prompt":"split"}`}}); got.Function.Name != "image_layer_split" || !strings.Contains(got.Function.Arguments, `"mode":"image"`) {
t.Fatalf("image layer split was not normalized to image media: %+v", got.Function)
}
}
func TestCloudAgentAnnotationRenderFeedsControlledTransientReference(t *testing.T) {
s, db, _, _ := creationTestService(t)
canvas := &model.CanvasProject{ID: "annotation-canvas", UserID: "user", PayloadJSON: `{"nodes":[{"id":"image-1","type":"image","title":"原图","width":640,"height":480,"metadata":{"content":"saved"}}],"connections":[]}`}
if err := db.Create(canvas).Error; err != nil {
t.Fatal(err)
}
state := &cloudAgentRuntime{RuntimeRunID: "annotation-run", Request: CloudAgentRequest{CanvasID: canvas.ID}, TransientReferences: map[string]cloudAgentTransientReference{}}
call := cloudAgentCall{ID: "call-annotation"}
call.Function.Name = "image_annotation_render"
call.Function.Arguments = `{"nodeId":"image-1","annotations":[{"label":"主体","x":0.5,"y":0.5}]}`
result, err := cloudAgentReadTool(s.repo, "user", state, call, s)
if err != nil {
t.Fatal(err)
}
view := result.(map[string]any)
refID := stringValue(view["referenceTransientId"])
ref, ok := state.TransientReferences[refID]
if !ok || ref.ResourceID == "" || ref.ExpiresAt.IsZero() {
t.Fatalf("annotation transient reference missing or unsafe: %#v", state.TransientReferences)
}
resultJSON, _ := json.Marshal(result)
if !strings.Contains(string(resultJSON), "image/png") {
t.Fatalf("annotation result must advertise PNG reference: %s", resultJSON)
}
doc, err := creationDocument(canvas.PayloadJSON)
if err != nil {
t.Fatal(err)
}
args := cloudAgentMediaArgs{Mode: "image", Prompt: "按标注编辑", SnapshotHash: cloudAgentMediaContentHash(doc), NodeID: "result-image", Title: "编辑结果", Size: "1:1", ReferenceTransientIDs: []string{refID}}
_, _, refs, err := cloudAgentMediaDocument(s.repo, "user", canvas.ID, args, state.TransientReferences)
if err != nil {
t.Fatal(err)
}
images, ok := refs["referenceImages"].([]any)
if !ok || len(images) != 1 || stringValue(images[0].(map[string]any)["id"]) != refID {
t.Fatalf("controlled transient reference was not forwarded: %#v", refs)
}
}
func TestCloudAgentPolicyPublishesSkillManifestWithoutInliningSkillBody(t *testing.T) {
skill := cloudAgentSkill{ID: "skill-1", Name: "任务技能", Description: "当用户要写短剧剧本时调用", Version: "v1", Hash: agentProfileHash("skill"), Instruction: "PRIVATE_SKILL_BODY", Files: map[string]string{"references/a.md": "A"}}
text, _, err := compileCloudAgentPolicies(agentTestRequest(), defaultCanvasAppearance().AgentName, []cloudAgentSkill{skill}, "", cloudAgentProfileSnapshot{Revision: agentProfileRevision(nil), Hash: agentProfileHash("")})
if err != nil {
t.Fatal(err)
}
if strings.Contains(text, skill.Instruction) || !strings.Contains(text, `"entryPath":"SKILL.md"`) || !strings.Contains(text, `"files":["SKILL.md","references/a.md"]`) || !strings.Contains(text, `"description":"当用户要写短剧剧本时调用"`) {
t.Fatalf("compiled policy did not publish a safe on-demand skill manifest: %s", text)
}
}
func TestCloudAgentPolicyTruncatesOversizedSkillDescription(t *testing.T) {
long := strings.Repeat("描", 600)
skill := cloudAgentSkill{ID: "skill-2", Name: "长描述技能", Description: long, Version: "v1", Hash: agentProfileHash("skill2")}
text, _, err := compileCloudAgentPolicies(agentTestRequest(), defaultCanvasAppearance().AgentName, []cloudAgentSkill{skill}, "", cloudAgentProfileSnapshot{Revision: agentProfileRevision(nil), Hash: agentProfileHash("")})
if err != nil {
t.Fatal(err)
}
if strings.Contains(text, long) || !strings.Contains(text, strings.Repeat("描", 500)+"…") {
t.Fatalf("oversized skill description was not capped at 500 runes: %s", text)
}
}
func TestCloudAgentPolicyPublishesCapabilityRoutingGuide(t *testing.T) {
text, _, err := compileCloudAgentPolicies(agentTestRequest(), defaultCanvasAppearance().AgentName, nil, "", cloudAgentProfileSnapshot{Revision: agentProfileRevision(nil), Hash: agentProfileHash("")})
if err != nil {
t.Fatal(err)
}
for _, expected := range []string{
"节点能力速查",
"由服务端能力注册表生成",
"分镜脚本(script)",
"多镜头",
"逐镜审查",
"后续维护",
"单画面、一次性说明或快速试验优先轻量节点",
"普通文本或 Markdown 不能伪装成结构化分镜",
"不为形式强制使用任何节点",
} {
if !strings.Contains(text, expected) {
t.Fatalf("compiled policy omitted capability routing guidance %q: %s", expected, text)
}
}
}
func TestCloudAgentGenerationAdapterRejectsUnimplementedMode(t *testing.T) {
original := canvasCapabilityRegistry
defer func() { canvasCapabilityRegistry = original }()
descriptors := original.List()
descriptors = append(descriptors, capability.Descriptor{
Type: "table", Version: "1", Label: "多维表格", DefaultWidth: 640, DefaultHeight: 400,
InputKind: "table_data", GenerationMode: "table-render",
})
registry, err := capability.NewRegistry(descriptors)
if err != nil {
t.Fatal(err)
}
canvasCapabilityRegistry = registry
if descriptor, ok := cloudAgentNodeCapabilityForGenerationMode("table-render"); !ok || descriptor.Type != "table" {
t.Fatal("hypothetical new mode did not resolve its canvas descriptor")
}
if cloudAgentGenerationModeSupported("table-render") || cloudAgentMediaOperation("table-render", nil) != "" {
t.Fatal("unimplemented mode may create a billable task")
}
for _, mode := range cloudAgentGenerationModeNames() {
if mode == "table-render" {
t.Fatal("unimplemented billing mode appeared in the tool schema")
}
}
if err := validateCloudAgentMediaReferences("table-render", nil); err == nil {
t.Fatal("unimplemented mode accepted media references")
}
for _, mode := range cloudAgentGenerationModeNames() {
if _, ok := cloudAgentNodeCapabilityForGenerationMode(mode); !ok || cloudAgentMediaOperation(mode, nil) == "" {
t.Fatalf("exposed mode %s lacks a node or task adapter", mode)
}
}
}
func TestCloudAgentProfileProjectScopeDoesNotSilentlyDiscardCanvas(t *testing.T) {
if err := validateAgentProfileScope(AgentProfileRequest{Scope: "project", ProjectID: "p1", CanvasID: "c1"}); err == nil {
t.Fatal("project profile accepted a canvas ID that would be silently discarded")
}
}
func TestCloudAgentProjectionRejectsMissingAdapterAndOpaqueMetadata(t *testing.T) {
node := map[string]any{"id": "n1", "type": "text", "title": "镜头"}
meta := map[string]any{"content": "公开正文", "storageKey": "resource:private", "url": "https://private.example/test", "status": "idle"}
descriptor, _ := cloudAgentNodeCapabilityForType("text")
projected, err := cloudAgentProjectNodeFields(node, meta, descriptor, descriptor.DetailFields, 16000, true, 0)
if err != nil || projected["content"] != "公开正文" || projected["storageKey"] != nil || projected["url"] != nil {
t.Fatalf("unsafe or incomplete projection: %v, %v", projected, err)
}
descriptor.ProjectionField = "storyboard"
descriptor.ProjectionKind = "unregistered-projector"
descriptor.DetailFields = []string{"storyboard"}
meta["storyboard"] = map[string]any{"rows": []any{}}
if _, err := cloudAgentProjectNodeFields(node, meta, descriptor, descriptor.DetailFields, 16000, true, 0); err == nil {
t.Fatal("unregistered structured projector must fail closed")
}
}
func TestCloudAgentTextResourceProjectionHidesLocator(t *testing.T) {
node := map[string]any{"id": "novel", "type": "text", "title": "小说原文"}
meta := map[string]any{"content": "/api/resources/novel-res/file", "storageKey": "resource:novel-res", "mimeType": "text/plain"}
descriptor, _ := cloudAgentNodeCapabilityForType("text")
projected, err := cloudAgentProjectNodeFields(node, meta, descriptor, descriptor.DetailFields, 16000, true, 0)
if err != nil || projected["content"] != nil || projected["contentAvailable"] != true || projected["contentSource"] != "resource" {
t.Fatalf("resource locator leaked or content availability missing: %v, %v", projected, err)
}
}
func TestCloudAgentReadTextLoadsOwnedResource(t *testing.T) {
s, db, _, _ := creationTestService(t)
content := "第一章\n林逸在石殿中醒来。"
objectKey := "users/user-1/file/novel.txt"
resource := model.Resource{ID: "novel-res", UserID: "user", Kind: "file", Status: model.ResourceStatusReady, Provider: "local", ObjectKey: objectKey, MimeType: "text/plain", Size: int64(len(content))}
if err := db.Create(&resource).Error; err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(s.dataDir, "resources", filepath.Dir(objectKey)), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(s.dataDir, "resources", objectKey), []byte(content), 0600); err != nil {
t.Fatal(err)
}
doc := map[string]any{"nodes": []map[string]any{{"id": "novel", "type": "text", "title": "小说原文", "metadata": map[string]any{"storageKey": "resource:novel-res", "content": "/api/resources/novel-res/file", "mimeType": "text/plain"}}}, "connections": []any{}}
raw, _ := json.Marshal(doc)
if err := db.Create(&model.CanvasProject{ID: "agent-canvas", UserID: "user", PayloadJSON: string(raw)}).Error; err != nil {
t.Fatal(err)
}
state := &cloudAgentRuntime{Request: CloudAgentRequest{CanvasID: "agent-canvas"}}
call := cloudAgentCall{ID: "read-text"}
call.Function.Name = "canvas_read_text"
call.Function.Arguments = `{"nodeId":"novel"}`
result, err := cloudAgentReadTool(s.repo, "user", state, call, s)
if err != nil {
t.Fatal(err)
}
view, ok := result.(map[string]any)
if !ok || view["content"] != content || view["hasMore"] != false {
t.Fatalf("unexpected text resource result: %#v", result)
}
}
func TestCloudAgentCanvasStateBoundsDefaultBodyProjection(t *testing.T) {
body := strings.Repeat("长正文 ", 999) + "长正文"
doc := map[string]any{"nodes": []map[string]any{{"id": "markdown", "type": "markdown", "title": "长文档", "metadata": map[string]any{"content": body}}}, "connections": []map[string]any{}}
view, err := cloudAgentCanvasState(nil, "user", "agent-canvas", doc, 0, nil, 0)
if err != nil {
t.Fatal(err)
}
item := view.(map[string]any)["nodes"].([]any)[0].(map[string]any)
content := stringValue(item["content"])
if len([]rune(content)) > 323 || item["contentTruncated"] != true {
t.Fatalf("default canvas read was not bounded: len=%d item=%v", len([]rune(content)), item)
}
precise, err := cloudAgentCanvasState(nil, "user", "agent-canvas", doc, 0, []string{"markdown"}, 0)
if err != nil {
t.Fatal(err)
}
preciseItem := precise.(map[string]any)["nodes"].([]any)[0].(map[string]any)
if stringValue(preciseItem["content"]) != body || preciseItem["contentTruncated"] != nil {
t.Fatalf("precise canvas read lost full content: got=%d want=%d item=%v", len([]rune(stringValue(preciseItem["content"]))), len([]rune(body)), preciseItem)
}
}
func TestCloudAgentCanvasStateDoesNotForwardUnknownObjectFields(t *testing.T) {
doc := map[string]any{"nodes": []map[string]any{{
"id": "safe", "type": "text", "title": "镜头",
"position": map[string]any{"x": 10.0, "y": 20.0, "storageKey": "resource:secret"},
"width": 200.0, "metadata": map[string]any{"status": map[string]any{"url": "https://secret.invalid"}, "content": "画面内容"},
}}}
view, err := cloudAgentCanvasState(nil, "user", "agent-canvas", doc, 0, nil, 0)
if err != nil {
t.Fatal(err)
}
item := view.(map[string]any)["nodes"].([]any)[0].(map[string]any)
if item["status"] != nil || item["position"].(map[string]any)["storageKey"] != nil || item["content"] != "画面内容" {
t.Fatalf("unsafe object fields leaked into model context: %v", item)
}
}
func TestCloudAgentDurablePolicySnapshotRejectsMissingOrUnsupportedContracts(t *testing.T) {
_, snapshot, err := compileCloudAgentPolicies(agentTestRequest(), defaultCanvasAppearance().AgentName, nil, "", cloudAgentProfileSnapshot{Revision: agentProfileRevision(nil), Hash: agentProfileHash("")})
if err != nil || validateCloudAgentPolicySnapshot(snapshot) != nil {
t.Fatalf("valid policy snapshot rejected: %v", err)
}
tests := []struct {
name string
mutate func(*cloudAgentPolicySnapshot)
}{
{"missing compiler", func(p *cloudAgentPolicySnapshot) { p.CompilerVersion = "" }},
{"unsupported compiler", func(p *cloudAgentPolicySnapshot) { p.CompilerVersion = "future" }},
{"missing system id", func(p *cloudAgentPolicySnapshot) { p.SystemPolicyID = "" }},
{"missing system hash", func(p *cloudAgentPolicySnapshot) { p.SystemPolicyHash = "" }},
{"missing media id", func(p *cloudAgentPolicySnapshot) { p.MediaPolicyID = "" }},
{"missing media hash", func(p *cloudAgentPolicySnapshot) { p.MediaPolicyHash = "" }},
{"missing capability version", func(p *cloudAgentPolicySnapshot) { p.CapabilitySetVersion = "" }},
{"missing capability hash", func(p *cloudAgentPolicySnapshot) { p.CapabilitySetHash = "" }},
{"invalid reasoning", func(p *cloudAgentPolicySnapshot) { p.ReasoningMode = "enabled" }},
{"missing profile revision", func(p *cloudAgentPolicySnapshot) { p.ProfileRevision = "" }},
{"invalid profile hash", func(p *cloudAgentPolicySnapshot) { p.ProfileHash = "not-sha256" }},
{"changed system contents", func(p *cloudAgentPolicySnapshot) { p.SystemPolicyHash = agentProfileHash("different system") }},
{"changed media contents", func(p *cloudAgentPolicySnapshot) { p.MediaPolicyHash = agentProfileHash("different media") }},
{"changed canvas contract", func(p *cloudAgentPolicySnapshot) { p.CapabilitySetHash = agentProfileHash("different capabilities") }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
corrupt := snapshot
test.mutate(&corrupt)
if err := validateCloudAgentPolicySnapshot(corrupt); err == nil {
t.Fatal("corrupted durable policy snapshot was accepted")
}
})
}
}
func TestCloudAgentCanvasStateIncludesBoundedRelatedComponent(t *testing.T) {
doc := map[string]any{
"nodes": []map[string]any{
{"id": "upstream", "type": "text", "title": "上游"},
{"id": "focus", "type": "text", "title": "焦点"},
{"id": "downstream", "type": "text", "title": "下游"},
{"id": "unrelated", "type": "text", "title": "无关"},
},
"connections": []map[string]any{
{"id": "e1", "fromNodeId": "upstream", "toNodeId": "focus"},
{"id": "e2", "fromNodeId": "focus", "toNodeId": "downstream"},
},
}
view, err := cloudAgentCanvasStateWithRelated(nil, "user", "canvas", doc, 0, []string{"focus"}, 0)
if err != nil {
t.Fatal(err)
}
result := view.(map[string]any)
selection := result["selection"].(map[string]any)
if selection["relationMode"] != "all" || selection["truncated"] != false || selection["selectedNodes"] != 3.0 && selection["selectedNodes"] != 3 {
t.Fatalf("unexpected related selection: %#v", selection)
}
ids := map[string]bool{}
for _, raw := range result["nodes"].([]any) {
ids[raw.(map[string]any)["id"].(string)] = true
}
if ids["focus"] == false || ids["upstream"] == false || ids["downstream"] == false || ids["unrelated"] {
t.Fatalf("related component selected wrong nodes: %#v", ids)
}
if len(result["connections"].([]any)) != 2 {
t.Fatalf("related component lost edges: %#v", result["connections"])
}
}
func TestCloudAgentCanvasStateRelatedComponentIsBounded(t *testing.T) {
nodes := make([]map[string]any, 0, cloudAgentRelatedNodeLimit+32)
edges := make([]map[string]any, 0, cloudAgentRelatedNodeLimit+32)
nodes = append(nodes, map[string]any{"id": "focus", "type": "text", "title": "焦点"})
for index := 0; index < cloudAgentRelatedNodeLimit+32; index++ {
id := fmt.Sprintf("child-%d", index)
nodes = append(nodes, map[string]any{"id": id, "type": "text", "title": id})
edges = append(edges, map[string]any{"id": fmt.Sprintf("edge-%d", index), "fromNodeId": "focus", "toNodeId": id})
}
view, err := cloudAgentCanvasStateWithRelated(nil, "user", "canvas", map[string]any{"nodes": nodes, "connections": edges}, 0, []string{"focus"}, 0)
if err != nil {
t.Fatal(err)
}
result := view.(map[string]any)
selection := result["selection"].(map[string]any)
if selection["truncated"] != true || selection["selectedNodes"] != float64(cloudAgentRelatedNodeLimit) && selection["selectedNodes"] != cloudAgentRelatedNodeLimit {
t.Fatalf("related component should be truncated at %d: %#v", cloudAgentRelatedNodeLimit, selection)
}
if len(result["nodes"].([]any)) != cloudAgentRelatedNodeLimit {
t.Fatalf("related component returned too many nodes: %d", len(result["nodes"].([]any)))
}
}