199 lines
9.7 KiB
Go
199 lines
9.7 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"infinite-canvas/backend/internal/model"
|
|
)
|
|
|
|
func seedCloudAgentCharacter(t *testing.T, db *gorm.DB, canvasID string) map[string]any {
|
|
t.Helper()
|
|
node := map[string]any{
|
|
"id": "character-card", "type": "text", "title": "扶颦",
|
|
"metadata": map[string]any{
|
|
"workflowKind": "character", "characterAssetId": "character-asset",
|
|
"characterVersionPolicy": "current", "characterDefinition": map[string]any{"appearance": "UNTRUSTED_METADATA"},
|
|
},
|
|
}
|
|
for _, item := range []any{
|
|
&model.Project{ID: "character-project", UserID: "user", Name: "短剧"},
|
|
&model.Asset{ID: "character-asset", UserID: "user", Category: model.AssetCategoryCharacter, Kind: "entity", Title: "扶颦", Status: model.AssetVersionStatusConfirmed, PrimaryVersionID: "character-v2"},
|
|
&model.ProjectAssetLink{ID: "character-link", ProjectID: "character-project", AssetID: "character-asset"},
|
|
&model.AssetVersion{ID: "character-v1", AssetID: "character-asset", Version: 1, Status: model.AssetVersionStatusConfirmed, DefinitionJSON: `{"appearance":"第一版红衣"}`},
|
|
&model.AssetVersion{ID: "character-v2", AssetID: "character-asset", Version: 2, Status: model.AssetVersionStatusConfirmed, DefinitionJSON: `{"role":"女主角","appearance":"三岁幼童,乌黑大眼睛","clothing":"黑红小裙","voiceTimbre":"稚嫩童声"}`},
|
|
&model.AssetRepresentation{ID: "character-representation", AssetVersionID: "character-v2", ResourceID: "character-image", MediaType: "image", Role: "turnaround_sheet"},
|
|
&model.Resource{ID: "character-image", UserID: "user", Kind: "image", Status: model.ResourceStatusReady, MimeType: "image/png", Size: 123, Width: 720, Height: 720},
|
|
&model.VoiceProfile{ID: "character-voice", UserID: "user", Name: "扶颦声音", Provider: "openai_compatible", VoiceKey: "test-voice", Status: "active", Timbre: "稚嫩童声", CompatibleModelsJSON: `[]`},
|
|
&model.CharacterVoiceBinding{ID: "character-voice-binding", AssetVersionID: "character-v2", VoiceProfileID: "character-voice", Instructions: "轻声说话"},
|
|
} {
|
|
if err := db.Create(item).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := db.Model(&model.CanvasProject{}).Where("id = ?", canvasID).Update("project_id", "character-project").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return node
|
|
}
|
|
|
|
func TestCloudAgentCharacterReadUsesAuthorizedAssetNotEmptyTextContent(t *testing.T) {
|
|
service, db, _, _ := creationTestService(t)
|
|
if err := db.Create(&model.CanvasProject{ID: "agent-canvas", UserID: "user", PayloadJSON: `{}`}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
node := seedCloudAgentCharacter(t, db, "agent-canvas")
|
|
doc := map[string]any{"nodes": []any{node}, "connections": []any{}}
|
|
view, err := cloudAgentCanvasState(service.repo, "user", "agent-canvas", doc, 0, []string{"character-card"}, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
character := view.(map[string]any)["nodes"].([]any)[0].(map[string]any)["character"].(map[string]any)
|
|
definition := character["definition"].(map[string]any)
|
|
if character["versionId"] != "character-v2" || character["visualStatus"] != "ready" || character["voiceStatus"] != "ready" || definition["appearance"] != "三岁幼童,乌黑大眼睛" {
|
|
t.Fatalf("character read missed project asset: %#v", character)
|
|
}
|
|
if character["imageReference"].(map[string]any)["ready"] != true || character["audioReference"].(map[string]any)["ready"] != true {
|
|
t.Fatalf("character media references are not ready: %#v", character)
|
|
}
|
|
representations := character["representations"].([]map[string]any)
|
|
if len(representations) != 1 || representations[0]["resourceId"] != "character-image" {
|
|
t.Fatalf("character representation details missing: %#v", representations)
|
|
}
|
|
encoded, _ := json.Marshal(view)
|
|
if strings.Contains(string(encoded), "UNTRUSTED_METADATA") {
|
|
t.Fatal("read exposed untrusted canvas metadata")
|
|
}
|
|
meta := node["metadata"].(map[string]any)
|
|
meta["characterVersionPolicy"], meta["characterVersionId"] = "pinned", "character-v1"
|
|
pinned, err := cloudAgentResolveCharacter(service.repo, "user", "character-project", node)
|
|
if err != nil || pinned.Card.VersionID != "character-v1" || pinned.Card.Definition["appearance"] != "第一版红衣" {
|
|
t.Fatalf("pinned read = %#v, %v", pinned, err)
|
|
}
|
|
meta["characterVersionId"] = "foreign-v1"
|
|
if err := db.Create(&model.AssetVersion{ID: "foreign-v1", AssetID: "other-asset", Version: 1, DefinitionJSON: `{}`}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := cloudAgentResolveCharacter(service.repo, "user", "character-project", node); err == nil {
|
|
t.Fatal("cross-asset pinned version accepted")
|
|
}
|
|
meta["characterVersionPolicy"] = "current"
|
|
if _, err := cloudAgentResolveCharacter(service.repo, "other-user", "character-project", node); err == nil {
|
|
t.Fatal("cross-user read accepted")
|
|
}
|
|
}
|
|
|
|
func TestCloudAgentCharacterDraftUsesCanvasRolesAndProviderImageSlots(t *testing.T) {
|
|
service, db, args := agentMediaFixture(t)
|
|
node := seedCloudAgentCharacter(t, db, "agent-canvas")
|
|
canvas, err := service.repo.CanvasProjectForUser("user", "agent-canvas")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doc, err := creationDocument(canvas.PayloadJSON)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doc["nodes"] = append(doc["nodes"].([]any), node)
|
|
raw, _ := json.Marshal(doc)
|
|
if err := db.Model(&model.CanvasProject{}).Where("id = ?", canvas.ID).Update("payload_json", string(raw)).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
args.ReferenceNodeIDs = []string{"character-card"}
|
|
args.Prompt = "Keep the face and clothing consistent with @图片1."
|
|
args.SnapshotHash = cloudAgentCanvasHash(doc)
|
|
run, state := agentMediaRun(t, service, args, "request_approval")
|
|
request, _, err := service.prepareCloudAgentMedia(run, &state, agentMediaCall(args))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.advanceCloudAgentByID("user", run.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stored, err := service.repo.CanvasProjectForUser("user", canvas.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
storedDoc, _ := creationDocument(stored.PayloadJSON)
|
|
nodes, _ := creationObjects(storedDoc["nodes"])
|
|
metadata := nodes[args.NodeID]["metadata"].(map[string]any)
|
|
if !strings.Contains(stringValue(metadata["composerContent"]), "@角色1") || strings.Contains(stringValue(metadata["composerContent"]), "@图片1") {
|
|
t.Fatalf("character draft kept provider slot labels: %#v", metadata["composerContent"])
|
|
}
|
|
if !strings.Contains(request.Prompt, "@图片1") || strings.Contains(request.Prompt, "@角色1") {
|
|
t.Fatalf("provider slot labels changed: %q", request.Prompt)
|
|
}
|
|
approveAgentMediaDraft(t, service, run.ID)
|
|
storedRun, err := service.repo.CloudAgent("user", run.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
approvedState, err := cloudAgentDecode(storedRun)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
task, err := service.repo.TaskForUser("user", approvedState.MediaTaskID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(task.Prompt, "@图片1") || strings.Contains(task.Prompt, "@角色1") {
|
|
t.Fatalf("approved task lost provider image slots: %q", task.Prompt)
|
|
}
|
|
}
|
|
|
|
func TestCloudAgentCharacterReferenceUsesDefinitionAndTurnaround(t *testing.T) {
|
|
service, db, args := agentMediaFixture(t)
|
|
node := seedCloudAgentCharacter(t, db, "agent-canvas")
|
|
canvas, err := service.repo.CanvasProjectForUser("user", "agent-canvas")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doc, err := creationDocument(canvas.PayloadJSON)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doc["nodes"] = append(doc["nodes"].([]any), node)
|
|
raw, _ := json.Marshal(doc)
|
|
if err := db.Model(&model.CanvasProject{}).Where("id = ?", canvas.ID).Update("payload_json", string(raw)).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
args.ReferenceNodeIDs = []string{"character-card"}
|
|
args.SnapshotHash = cloudAgentCanvasHash(doc)
|
|
run, state := agentMediaRun(t, service, args, "request_approval")
|
|
request, plan, err := service.prepareCloudAgentMedia(run, &state, agentMediaCall(args))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
images := creationMaps(request.Input["referenceImages"])
|
|
if len(images) != 1 || images[0]["storageKey"] != "resource:character-image" || images[0]["id"] != "character-card" {
|
|
t.Fatalf("missing real image reference: %#v", images)
|
|
}
|
|
if !strings.Contains(request.Prompt, "三岁幼童,乌黑大眼睛") || strings.Contains(request.Prompt, "UNTRUSTED_METADATA") || plan.Args.CharacterVersions["character-card"] != "character-v2" {
|
|
t.Fatalf("missing real character definition or version: %q %#v", request.Prompt, plan.Args.CharacterVersions)
|
|
}
|
|
if intent, err := service.cloudAgentModelIntent("user", canvas.ID, `{"mode":"video","referenceNodeIds":["character-card"]}`); err != nil || intent == nil {
|
|
t.Fatalf("model list did not recognize character image: %#v, %v", intent, err)
|
|
}
|
|
textOnly := args
|
|
textOnly.ReferenceNodeIDs = nil
|
|
textOnly.SourceNodeID = "character-card"
|
|
textRequest, _, err := service.prepareCloudAgentMedia(run, &state, agentMediaCall(textOnly))
|
|
if err != nil || !strings.Contains(textRequest.Prompt, "三岁幼童,乌黑大眼睛") || len(creationMaps(textRequest.Input["referenceImages"])) != 0 {
|
|
t.Fatalf("text-only character reference = %q, %v", textRequest.Prompt, err)
|
|
}
|
|
prepared := &cloudAgentPreparedMedia{Version: 1, Hash: "test", DependencyHash: "", CharacterVersions: plan.Args.CharacterVersions, Quote: cloudAgentMediaQuote{ExpiresAt: time.Now().Add(time.Minute)}}
|
|
prepared.DependencyHash, err = cloudAgentMediaDependencyHash(doc, plan.Args)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Model(&model.Asset{}).Where("id = ?", "character-asset").Update("primary_version_id", "character-v1").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := validateCloudAgentPreparedAdmission(service.repo, "user", prepared, &model.Task{ProjectID: canvas.ID}, nil, doc, plan.Args); err == nil || !strings.Contains(err.Error(), "角色卡版本已变化") {
|
|
t.Fatalf("approved character version drift not rejected: %v", err)
|
|
}
|
|
}
|