fix(design): preserve existing canvas nodes when compressed snapshot omits them

SaveCanvas treated a nil Nodes/Connections slice from a compressed snapshot
as an empty canvas, wiping existing content. Distinguish nil (field omitted,
keep the project's current nodes/connections) from an explicit empty slice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 01:10:07 +08:00
parent ffd8d78df7
commit 9a07f863ea
2 changed files with 64 additions and 2 deletions
@@ -182,6 +182,60 @@ func TestSaveCanvasAcceptsCompressedSnapshotPayload(t *testing.T) {
}
}
func TestSaveCanvasPreservesNodesWhenCompressedSnapshotOmitsThem(t *testing.T) {
ctx := context.Background()
repo := memory.NewProjectRepository()
service := NewDesignService(repo, nil, nil, nil, nil)
service.now = func() time.Time {
return time.Unix(101, 0).UTC()
}
project := design.Project{
ID: "project-null-snapshot-nodes",
Title: "Null snapshot nodes",
Status: design.StatusReady,
UpdatedAt: time.Unix(100, 0).UTC(),
Version: "100000",
SnapshotID: "snapshot-old",
Viewport: design.Viewport{X: 1, Y: 2, K: 1},
Nodes: []design.Node{
{ID: "existing-image", Type: design.NodeTypeImage, Title: "Existing", Content: "http://localhost:19000/canvas-assets/existing.png", Status: "success"},
},
Connections: []design.Connection{
{ID: "existing-connection", FromNodeID: "existing-image", ToNodeID: "existing-image"},
},
}
if err := repo.Save(ctx, project); err != nil {
t.Fatal(err)
}
snapshotProject := project
snapshotProject.SnapshotID = "snapshot-client"
snapshotProject.Viewport = design.Viewport{X: 12, Y: 34, K: 0.8}
snapshotProject.Nodes = nil
snapshotProject.Connections = nil
canvas, err := design.EncodeCanvasSnapshot(snapshotProject)
if err != nil {
t.Fatal(err)
}
saved, err := service.SaveCanvas(ctx, project.ID, design.Viewport{}, nil, nil, "", "", canvas)
if err != nil {
t.Fatal(err)
}
if saved.SnapshotID != "snapshot-client" {
t.Fatalf("expected client snapshot id, got %s", saved.SnapshotID)
}
if saved.Viewport != snapshotProject.Viewport {
t.Fatalf("unexpected viewport: %#v", saved.Viewport)
}
if len(saved.Nodes) != 1 || saved.Nodes[0].ID != "existing-image" {
t.Fatalf("expected existing nodes to be preserved, got %#v", saved.Nodes)
}
if len(saved.Connections) != 1 || saved.Connections[0].ID != "existing-connection" {
t.Fatalf("expected existing connections to be preserved, got %#v", saved.Connections)
}
}
func TestSaveCanvasDoesNotDeleteImageAssetReferencedByAgentMessage(t *testing.T) {
ctx := context.Background()
repo := memory.NewProjectRepository()
+10 -2
View File
@@ -1082,10 +1082,18 @@ func (s *DesignService) SaveCanvas(ctx context.Context, projectID string, viewpo
return design.Project{}, fmt.Errorf("%w: invalid canvas snapshot", design.ErrInvalidInput)
}
if len(nodes) == 0 {
nodes = snapshot.Nodes
if snapshot.Nodes != nil {
nodes = snapshot.Nodes
} else {
nodes = append([]design.Node(nil), project.Nodes...)
}
}
if len(connections) == 0 {
connections = snapshot.Connections
if snapshot.Connections != nil {
connections = snapshot.Connections
} else {
connections = append([]design.Connection(nil), project.Connections...)
}
}
if viewport.K == 0 {
viewport = snapshot.Viewport