Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_media_test.go
2026-09-14 01:11:56 +08:00

539 lines
23 KiB
Go
Raw 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 (
"context"
"encoding/json"
"strings"
"testing"
"gorm.io/gorm"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
)
func agentMediaFixture(t *testing.T) (*Service, *gorm.DB, cloudAgentMediaArgs) {
t.Helper()
s, db, _, _ := creationTestService(t)
capability := DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceVolcengineArkVideo), "seedance-test")
capability.Video.References.PromptMaxChars = 16000
for _, row := range []any{
&model.ChannelModel{ID: "video-cm", ChannelID: "channel", ModelKey: "seedance-test", DisplayName: "视频测试", Capability: "video", Protocol: model.ChannelInterfaceVolcengineArkVideo, CapabilityConfigJSON: mustEncodeModelCapabilityConfig(t, capability), BillingMode: "per_second", PriceConfigured: true, Enabled: true},
&model.ChannelModelPriceTier{ID: "video-tier", ChannelModelID: "video-cm", SelectorKey: "{}", SelectorJSON: "{}", BillingMode: "per_second", UnitPriceMicrocredits: 1, PriceConfigured: true, Enabled: true},
&model.Resource{ID: "ref-one", UserID: "user", Kind: "image", Status: "ready", MimeType: "image/png", Width: 640, Height: 480, Size: 50},
&model.Resource{ID: "ref-two", UserID: "user", Kind: "image", Status: "ready", MimeType: "image/png", Width: 640, Height: 480, Size: 50},
&model.Resource{ID: "private", UserID: "other", Kind: "image", Status: "ready", MimeType: "image/png"},
} {
if err := db.Create(row).Error; err != nil {
t.Fatal(err)
}
}
doc := map[string]any{"nodes": []map[string]any{
{"id": "shot-1", "type": "text", "title": "镜头1", "position": map[string]any{"x": 10, "y": 10}, "width": 300, "metadata": map[string]any{"content": strings.Repeat("镜头完整指令", 500)}},
{"id": "cat", "type": "image", "title": "叮当猫在飞", "position": map[string]any{"x": 10, "y": 300}, "width": 300, "metadata": map[string]any{"status": "success", "storageKey": "resource:ref-one", "content": "https://must-not-expose.invalid/one.png"}},
{"id": "hero", "type": "image", "title": "古风奥特曼", "position": map[string]any{"x": 400, "y": 300}, "width": 300, "metadata": map[string]any{"status": "success", "storageKey": "resource:ref-two"}},
}, "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)
}
audio := true
a := cloudAgentMediaArgs{Mode: "video", Prompt: "镜头1,参考图1是英雄,参考图2是叮当猫", ChannelID: "channel", ChannelModelKey: "seedance-test", Duration: 12, Size: "9:16", VideoGenerateAudio: &audio, SnapshotHash: creationHash(doc), NodeID: "video-shot-1", Title: "镜头1视频", SourceNodeID: "shot-1", ReferenceNodeIDs: []string{"hero", "cat"}}
return s, db, a
}
func agentMediaCall(a cloudAgentMediaArgs) cloudAgentCall {
raw, _ := json.Marshal(a)
call := cloudAgentCall{ID: "media-call"}
call.Function.Name, call.Function.Arguments = "generate_media", string(raw)
return call
}
func agentMediaRun(t *testing.T, s *Service, a cloudAgentMediaArgs, permission string) (*model.CloudAgentExecution, cloudAgentRuntime) {
t.Helper()
req := agentTestRequest()
req.PermissionMode = permission
req.Budget.MaxGenerationTasks = 2
req.Budget.MaxVideoSeconds = 24
root, err := s.CreateCloudAgentRun("user", req, "")
if err != nil {
t.Fatal(err)
}
run, err := s.repo.CloudAgent("user", root.ID)
if err != nil {
t.Fatal(err)
}
state, err := cloudAgentDecode(run)
if err != nil {
t.Fatal(err)
}
state.ActiveTaskID = ""
state.Calls = []cloudAgentCall{agentMediaCall(a)}
if err = s.repo.MutateCloudAgent("user", run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
return cloudAgentSave(current, &state)
}); err != nil {
t.Fatal(err)
}
run, _ = s.repo.CloudAgent("user", run.ID)
return run, state
}
func approveAgentMediaDraft(t *testing.T, s *Service, runID string) {
t.Helper()
run, err := s.CloudAgentRun("user", runID)
if err != nil || run.Approval == nil || run.Status != "waiting_approval" {
t.Fatalf("media must wait for approval even in auto mode: %v", err)
}
if err := s.DecideCloudAgentApproval("user", runID, run.Approval.ID, "approve", ""); err != nil {
t.Fatal(err)
}
if err := s.advanceCloudAgentByID("user", runID); err != nil {
t.Fatal(err)
}
}
func TestCloudAgentMediaApprovalCreatesNodeReferencesAndResult(t *testing.T) {
s, db, a := agentMediaFixture(t)
catalog, err := s.cloudAgentModelList()
if err != nil {
t.Fatal(err)
}
encoded, _ := json.Marshal(catalog)
if !strings.Contains(string(encoded), `"channelModelKey":"seedance-test"`) || strings.Contains(string(encoded), "logicalModelId") {
t.Fatalf("wrong catalog: %s", encoded)
}
run, state := agentMediaRun(t, s, a, "request_approval")
if err = s.advanceCloudAgentTool(run, &state); err != nil {
t.Fatal(err)
}
var count int64
db.Model(&model.Task{}).Where("type = ?", "canvas_video").Count(&count)
if count != 0 {
t.Fatal("charged/submitted before approval")
}
waiting, err := s.CloudAgentRun("user", run.ID)
if err != nil || waiting.Approval == nil {
t.Fatalf("missing approval: %v", err)
}
if err = s.DecideCloudAgentApproval("user", run.ID, waiting.Approval.ID, "approve", ""); err != nil {
t.Fatal(err)
}
if err = s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
run, _ = s.repo.CloudAgent("user", run.ID)
state, _ = cloudAgentDecode(run)
if state.MediaTaskID == "" {
t.Fatalf("no media task: %s", run.StateJSON)
}
task, err := s.repo.TaskForUser("user", state.MediaTaskID)
if err != nil {
t.Fatal(err)
}
var input canvasGenerationInput
if err = json.Unmarshal([]byte(task.InputJSON), &input); err != nil {
t.Fatal(err)
}
if input.Config.Size != "9:16" || input.Config.VideoSeconds != "12" || input.Config.VideoGenerateAudio != "true" || len(input.ReferenceImages) != 2 || input.ReferenceImages[0].ID != "hero" || input.ReferenceImages[1].StorageKey != "resource:ref-one" || input.ReferenceImages[0].URL != "" {
t.Fatalf("lost generation contract: %+v", input)
}
canvas, _ := s.repo.CanvasProjectForUser("user", "agent-canvas")
doc, _ := creationDocument(canvas.PayloadJSON)
nodes, _ := creationObjects(doc["nodes"])
node := nodes[a.NodeID]
if node == nil || node["type"] != "video" || node["metadata"].(map[string]any)["taskId"] != task.ID || len(creationMaps(doc["connections"])) != 3 {
t.Fatalf("no node/edges: %s", canvas.PayloadJSON)
}
if err = s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
db.Model(&model.Task{}).Where("type = ?", "canvas_video").Count(&count)
if count != 1 {
t.Fatal("replay duplicated task")
}
if err = db.Create(&model.Resource{ID: "output", UserID: "user", Kind: "video", Status: "ready", MimeType: "video/mp4", Width: 720, Height: 1280}).Error; err != nil {
t.Fatal(err)
}
if err = db.Model(&model.Task{}).Where("id = ?", task.ID).Updates(map[string]any{"status": model.TaskStatusSucceeded, "result_json": `{"mode":"video","video":{"storageKey":"resource:output"}}`}).Error; err != nil {
t.Fatal(err)
}
if err = s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
canvas, _ = s.repo.CanvasProjectForUser("user", "agent-canvas")
doc, _ = creationDocument(canvas.PayloadJSON)
nodes, _ = creationObjects(doc["nodes"])
meta := nodes[a.NodeID]["metadata"].(map[string]any)
if meta["status"] != "success" || meta["storageKey"] != "resource:output" {
t.Fatalf("result not persisted: %+v", meta)
}
run, _ = s.repo.CloudAgent("user", run.ID)
state, _ = cloudAgentDecode(run)
found := false
for _, event := range state.Events {
if event.Type == "canvas_updated" {
found = true
}
}
if !found || state.MediaTaskID != "" {
t.Fatal("completion event/checkpoint missing")
}
}
func TestCloudAgentMediaReferenceAndSnapshotGuards(t *testing.T) {
s, db, a := agentMediaFixture(t)
run, state := agentMediaRun(t, s, a, "auto")
for _, test := range []struct {
name string
change func(*cloudAgentMediaArgs)
}{
{"stale snapshot", func(v *cloudAgentMediaArgs) { v.SnapshotHash = "old" }},
{"missing reference", func(v *cloudAgentMediaArgs) { v.ReferenceNodeIDs = []string{"missing"} }},
{"text as media", func(v *cloudAgentMediaArgs) { v.ReferenceNodeIDs = []string{"shot-1"} }},
{"duplicate reference", func(v *cloudAgentMediaArgs) { v.ReferenceNodeIDs = []string{"cat", "cat"} }},
{"existing node", func(v *cloudAgentMediaArgs) { v.NodeID = "cat" }},
{"mixed selection", func(v *cloudAgentMediaArgs) { v.LogicalModelID = "made-up" }},
{"duration budget", func(v *cloudAgentMediaArgs) { v.Duration = 25 }},
{"missing aspect ratio", func(v *cloudAgentMediaArgs) { v.Size = "" }},
} {
t.Run(test.name, func(t *testing.T) {
bad := a
test.change(&bad)
if _, _, err := s.prepareCloudAgentMedia(run, &state, agentMediaCall(bad)); err == nil {
t.Fatal("invalid request accepted")
}
})
}
if err := db.Model(&model.Resource{}).Where("id = ?", "ref-one").Update("status", "uploading").Error; err != nil {
t.Fatal(err)
}
if _, _, err := s.prepareCloudAgentMedia(run, &state, agentMediaCall(a)); err == nil {
t.Fatal("unready asset accepted")
}
if err := db.Model(&model.Resource{}).Where("id = ?", "ref-one").Update("user_id", "other").Error; err != nil {
t.Fatal(err)
}
if _, _, err := s.prepareCloudAgentMedia(run, &state, agentMediaCall(a)); err == nil {
t.Fatal("cross-user asset accepted")
}
}
func TestCloudAgentImageCreatesReferencedNode(t *testing.T) {
s, db, a := agentMediaFixture(t)
capability := DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceGrokImage), "grok-image")
for _, row := range []any{
&model.ChannelModel{ID: "image-cm", ChannelID: "channel", ModelKey: "grok-image", Capability: "image", Protocol: model.ChannelInterfaceGrokImage, CapabilityConfigJSON: mustEncodeModelCapabilityConfig(t, capability), BillingMode: "fixed_request", PriceConfigured: true, Enabled: true},
&model.ChannelModelPriceTier{ID: "image-tier", ChannelModelID: "image-cm", SelectorKey: "{}", SelectorJSON: "{}", BillingMode: "fixed_request", UnitPriceMicrocredits: 1, PriceConfigured: true, Enabled: true},
} {
if err := db.Create(row).Error; err != nil {
t.Fatal(err)
}
}
a.Mode, a.ChannelModelKey, a.Duration, a.VideoGenerateAudio = "image", "grok-image", 0, nil
a.Size, a.Quality, a.NodeID = "1:1", "2k", "image-shot-1"
a.ReferenceNodeIDs = []string{"cat"}
run, _ := agentMediaRun(t, s, a, "auto")
if err := s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
approveAgentMediaDraft(t, s, run.ID)
run, _ = s.repo.CloudAgent("user", run.ID)
state, _ := cloudAgentDecode(run)
if state.MediaTaskID == "" {
t.Fatalf("image admission failed: %+v", state.Events[len(state.Events)-1])
}
task, err := s.repo.TaskForUser("user", state.MediaTaskID)
if err != nil {
t.Fatal(err)
}
var input canvasGenerationInput
if err = json.Unmarshal([]byte(task.InputJSON), &input); err != nil {
t.Fatal(err)
}
if task.Operation != "image_to_image" || len(input.ReferenceImages) != 1 || input.Config.Count != "1" || input.Config.Quality != "2k" {
t.Fatal("image generation contract was lost")
}
canvas, _ := s.repo.CanvasProjectForUser("user", "agent-canvas")
doc, _ := creationDocument(canvas.PayloadJSON)
nodes, _ := creationObjects(doc["nodes"])
if nodes[a.NodeID]["type"] != "image" || len(creationMaps(doc["connections"])) != 2 {
t.Fatal("image node/references missing")
}
}
func TestCloudAgentCanvasReadsFullPromptAssetsAndConnections(t *testing.T) {
s, _, _ := agentMediaFixture(t)
canvas, _ := s.repo.CanvasProjectForUser("user", "agent-canvas")
doc, _ := creationDocument(canvas.PayloadJSON)
result, err := cloudAgentCanvasState(s.repo, "user", doc, 0, []string{"shot-1", "cat"}, 0)
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(result)
if strings.Contains(string(raw), "must-not-expose") || !strings.Contains(string(raw), `"referenceReady":true`) || !strings.Contains(string(raw), strings.Repeat("镜头完整指令", 500)) {
t.Fatalf("incomplete/unsafe context: %.200s", raw)
}
}
func TestCloudAgentMediaRejectAndInvalidModelDoNotCreateTasks(t *testing.T) {
for _, scenario := range []string{"reject", "invalid-model", "stale-after-approval"} {
t.Run(scenario, func(t *testing.T) {
s, db, a := agentMediaFixture(t)
if scenario == "invalid-model" {
a.ChannelModelKey = "removed-model"
}
run, _ := agentMediaRun(t, s, a, "request_approval")
if err := s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
run, _ = s.repo.CloudAgent("user", run.ID)
state, _ := cloudAgentDecode(run)
if scenario == "invalid-model" {
var count int64
db.Model(&model.Task{}).Where("type = ?", "canvas_video").Count(&count)
if state.Approval != nil || count != 0 || state.CallIndex != 1 {
t.Fatal("invalid model created approval or task")
}
return
}
decision := "approve"
if scenario == "reject" {
decision = "reject"
}
if err := s.DecideCloudAgentApproval("user", run.ID, state.Approval.ID, decision, ""); err != nil {
t.Fatal(err)
}
if scenario == "stale-after-approval" {
if err := db.Model(&model.CanvasProject{}).Where("id = ?", "agent-canvas").Update("payload_json", `{"nodes":[],"connections":[]}`).Error; err != nil {
t.Fatal(err)
}
}
if err := s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
run, _ = s.repo.CloudAgent("user", run.ID)
state, _ = cloudAgentDecode(run)
var count int64
db.Model(&model.Task{}).Where("type = ?", "canvas_video").Count(&count)
if count != 0 || state.CallIndex != 1 || state.Generations != 0 || run.Status == "failed" {
t.Fatalf("unsafe submission or nonrecoverable tool failure: count=%d index=%d status=%s", count, state.CallIndex, run.Status)
}
})
}
}
func TestCloudAgentMediaChangedCanvasRollsBackAdmission(t *testing.T) {
s, db, a := agentMediaFixture(t)
run, state := agentMediaRun(t, s, a, "auto")
req, plan, err := s.prepareCloudAgentMedia(run, &state, agentMediaCall(a))
if err != nil {
t.Fatal(err)
}
ordersBefore, err := s.repo.BillingOrdersByTaskIDs("user", state.TaskIDs)
if err != nil {
t.Fatal(err)
}
if err := db.Model(&model.CanvasProject{}).Where("id = ?", "agent-canvas").Update("payload_json", `{"nodes":[],"connections":[]}`).Error; err != nil {
t.Fatal(err)
}
if err := s.enqueueCloudAgentTask(run, &state, req, plan); err != nil {
t.Fatal(err)
}
run, _ = s.repo.CloudAgent("user", run.ID)
state, _ = cloudAgentDecode(run)
ordersAfter, _ := s.repo.BillingOrdersByTaskIDs("user", state.TaskIDs)
var tasks, orders int64
db.Model(&model.Task{}).Where("type = ?", "canvas_video").Count(&tasks)
db.Model(&model.BillingOrder{}).Count(&orders)
if tasks != 0 || orders != int64(len(ordersBefore)) || len(ordersAfter) != len(ordersBefore) || state.Generations != 0 || state.MediaTaskID != "" || state.CallIndex != 1 {
t.Fatal("failed canvas CAS left a media task, charge or consumed budget")
}
}
func TestCloudAgentMediaFailedTaskUpdatesNode(t *testing.T) {
s, db, a := agentMediaFixture(t)
run, _ := agentMediaRun(t, s, a, "auto")
if err := s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
approveAgentMediaDraft(t, s, run.ID)
run, _ = s.repo.CloudAgent("user", run.ID)
state, _ := cloudAgentDecode(run)
if state.MediaTaskID == "" {
t.Fatal("media task missing")
}
taskID := state.MediaTaskID
if err := db.Model(&model.Task{}).Where("id = ?", taskID).Updates(map[string]any{"status": model.TaskStatusFailed, "error": "上游拒绝该生成规格"}).Error; err != nil {
t.Fatal(err)
}
if err := s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
canvas, _ := s.repo.CanvasProjectForUser("user", "agent-canvas")
doc, _ := creationDocument(canvas.PayloadJSON)
nodes, _ := creationObjects(doc["nodes"])
if nodes[a.NodeID]["metadata"].(map[string]any)["status"] != "error" {
t.Fatal("failed generation remained loading")
}
run, _ = s.repo.CloudAgent("user", run.ID)
state, _ = cloudAgentDecode(run)
last := state.Events[len(state.Events)-1]
result, _ := last.Payload["result"].(map[string]any)
if last.Type != "tool_failed" || result["taskId"] != taskID || result["nodeId"] != a.NodeID || !strings.Contains(stringValue(result["error"]), "上游拒绝该生成规格") {
t.Fatalf("failure lost diagnostic details: %+v", last)
}
}
func TestCloudAgentAutoMediaDraftRequiresExplicitApproval(t *testing.T) {
for _, decision := range []string{"approve", "reject", "cancel"} {
t.Run(decision, func(t *testing.T) {
s, db, a := agentMediaFixture(t)
run, _ := agentMediaRun(t, s, a, "auto")
var ordersBefore int64
db.Model(&model.BillingOrder{}).Count(&ordersBefore)
if err := s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
run, _ = s.repo.CloudAgent("user", run.ID)
state, _ := cloudAgentDecode(run)
if run.Status != "waiting_approval" || state.Approval == nil || state.Approval.ModelName != "视频测试" || state.MediaTaskID != "" {
t.Fatal("auto mode bypassed approval or omitted selected model")
}
canvas, _ := s.repo.CanvasProjectForUser("user", "agent-canvas")
doc, _ := creationDocument(canvas.PayloadJSON)
nodes, _ := creationObjects(doc["nodes"])
meta, _ := nodes[a.NodeID]["metadata"].(map[string]any)
if meta["status"] != "idle" || meta["taskId"] != nil || meta["size"] != "9:16" || meta["agentDraftRunId"] != run.ID || len(creationMaps(doc["connections"])) != 3 {
t.Fatalf("missing uncharged draft with references: %+v", meta)
}
// Re-entering the tool while awaiting a decision must remain a no-op.
if err := s.advanceCloudAgentTool(run, &state); err != nil {
t.Fatal(err)
}
var count, orders int64
db.Model(&model.Task{}).Where("type = ?", "canvas_video").Count(&count)
db.Model(&model.BillingOrder{}).Count(&orders)
if count != 0 || orders != ordersBefore {
t.Fatal("draft created a task or charge")
}
// Another run may not take over this draft, even with the latest snapshot.
foreign := a
foreign.DraftRunID, foreign.SnapshotHash = "another-run", cloudAgentCanvasHash(doc)
if _, _, _, err := cloudAgentMediaDocument(s.repo, "user", "agent-canvas", foreign); err == nil {
t.Fatal("foreign draft overwrite accepted")
}
if decision == "cancel" {
if err := s.CancelCloudAgent(context.Background(), "user", run.ID); err != nil {
t.Fatal(err)
}
} else {
// A local focus/zoom autosave cannot change the approved content.
doc["viewport"], doc["updatedAt"] = map[string]any{"x": -900, "y": -500, "k": 0.8}, "later"
raw, _ := json.Marshal(doc)
if err := db.Model(&model.CanvasProject{}).Where("id = ?", "agent-canvas").Update("payload_json", string(raw)).Error; err != nil {
t.Fatal(err)
}
for range 2 {
if err := s.DecideCloudAgentApproval("user", run.ID, state.Approval.ID, decision, ""); err != nil {
t.Fatal(err)
}
}
if err := s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
}
db.Model(&model.Task{}).Where("type = ?", "canvas_video").Count(&count)
db.Model(&model.BillingOrder{}).Count(&orders)
if decision == "approve" {
if count != 1 {
t.Fatal("explicit approval did not create exactly one task")
}
} else if count != 0 || orders != ordersBefore {
t.Fatal("reject/cancel submitted generation")
}
})
}
}
func TestCloudAgentMediaPromptCountsUnicodeCharacters(t *testing.T) {
s, _, a := agentMediaFixture(t)
run, state := agentMediaRun(t, s, a, "auto")
a.Prompt = strings.Repeat("镜", 16000)
if _, _, err := s.prepareCloudAgentMedia(run, &state, agentMediaCall(a)); err != nil {
t.Fatal(err)
}
a.Prompt += "头"
if _, _, err := s.prepareCloudAgentMedia(run, &state, agentMediaCall(a)); err == nil || !strings.Contains(err.Error(), "16001") {
t.Fatal("missing exact character count")
}
}
func TestCloudAgentMediaCancellationUpdatesNode(t *testing.T) {
s, _, a := agentMediaFixture(t)
run, _ := agentMediaRun(t, s, a, "auto")
if err := s.advanceCloudAgentByID("user", run.ID); err != nil {
t.Fatal(err)
}
approveAgentMediaDraft(t, s, run.ID)
if err := s.CancelCloudAgent(context.Background(), "user", run.ID); err != nil {
t.Fatal(err)
}
canvas, _ := s.repo.CanvasProjectForUser("user", "agent-canvas")
doc, _ := creationDocument(canvas.PayloadJSON)
nodes, _ := creationObjects(doc["nodes"])
if nodes[a.NodeID]["metadata"].(map[string]any)["status"] != "error" {
t.Fatal("cancelled generation remained loading")
}
run, _ = s.repo.CloudAgent("user", run.ID)
state, _ := cloudAgentDecode(run)
if run.Status != "cancelled" || state.MediaTaskID != "" {
t.Fatal("cancellation did not checkpoint media completion")
}
}
func TestCloudAgentStoryboardPrecisionAndPermission(t *testing.T) {
storyboard := map[string]any{"rows": []any{
map[string]any{"id": "shot-1", "shotNumber": float64(1), "videoMotionPrompt": strings.Repeat("切镜指令", 500), "assetBindings": []any{map[string]any{"nodeId": "hero", "role": "character", "url": "do-not-expose"}}, "private": "do-not-expose"},
map[string]any{"id": "shot-2", "videoMotionPrompt": "下一镜头"},
}}
result := cloudAgentStoryboardState(storyboard, 0, true)
raw, _ := json.Marshal(result)
if !strings.Contains(string(raw), strings.Repeat("切镜指令", 500)) || strings.Contains(string(raw), "do-not-expose") || result["nextOffset"] != 1 {
t.Fatal("storyboard context missing full prompt, pagination or privacy boundary")
}
next := cloudAgentStoryboardState(storyboard, 1, true)
if next["hasMore"] != false || next["rows"].([]any)[0].(map[string]any)["id"] != "shot-2" {
t.Fatal("storyboard pagination failed")
}
req := agentTestRequest()
req.PermissionMode, req.ContextScope, req.Budget.MaxGenerationTasks = "auto", nil, 1
if cloudAgentToolAllowed(req, "generate_media") || cloudAgentToolAllowed(req, "canvas_apply_ops") {
t.Fatal("canvas writes exposed without context permission")
}
}
func TestCloudAgentCanvasCreatesTypedNodesAndEdges(t *testing.T) {
s, _, a := agentMediaFixture(t)
policy, err := s.RuntimePolicy()
if err != nil {
t.Fatal(err)
}
call := cloudAgentCall{}
args := map[string]any{"snapshotHash": a.SnapshotHash, "ops": []map[string]any{
{"type": "add_node", "id": "blank-video", "nodeType": "video", "title": "镜头占位", "content": "生成提示词"},
{"type": "add_node", "id": "group", "nodeType": "frame", "title": "镜头组"},
{"type": "connect_nodes", "id": "ref-edge", "fromNodeId": "cat", "toNodeId": "blank-video"},
}}
raw, _ := json.Marshal(args)
call.Function.Arguments = string(raw)
if _, err = applyCloudAgentCanvas(s.repo, "user", "agent-canvas", call, policy); err != nil {
t.Fatal(err)
}
canvas, _ := s.repo.CanvasProjectForUser("user", "agent-canvas")
doc, _ := creationDocument(canvas.PayloadJSON)
nodes, _ := creationObjects(doc["nodes"])
meta := nodes["blank-video"]["metadata"].(map[string]any)
if meta["content"] != "" || meta["prompt"] != "生成提示词" || len(creationMaps(doc["connections"])) != 1 {
t.Fatal("typed nodes/edges missing")
}
}