Files
tasks/common/payload/payload_test.go
Alan WuandGitHub 9416158cb3 Remove nil search attributes from history events / mutable state / ch… (#9091)
## What changed?
Remove nil search attributes from history events / mutable state / chasm
node

## Why?
Search attributes can be explicitly set as null on the following events:

* Workflow Start

* Upsert Search Attributes

* Continue-as-new

Starting a workflow or continuing a workflow as new with a nil search
attribute records the `{key: nil}` mapping in mutable state and the
history event.

Upserting search attributes erases any current search attribute value
from persistence (mutable state and visibility record).

Upserting the search attribute as nil in the SDK retains the mapping in
the WorkflowInfo as a {key: nil} mapping. On a continue-as-new event,
SDK sends this mapping to Server, and the search attribute gets
repopulated with the null search attribute in the history event and
mutable state.

This last scenario can cause inconsistency between the expected search
attributes and what is recorded in history. After upserting a nil search
attribute, the mutable state and visibility record no longer shows this
search attribute. But, once continuing as new, the search attribute
reappears, leading to unexpected NDE if the search attribute is also
deregistered.

## CHASM change
This also removes nil search attributes from CHASM nodes on replacement.
This mirrors the change to Workflow History Events and Mutable State.

## How did you test it?
- [X] built
- [X] run locally and tested manually
- [X] covered by existing tests
- [X] added new unit test(s)
- [X] added new functional test(s)
2026-04-01 15:21:16 -04:00

281 lines
6.8 KiB
Go

package payload
import (
"testing"
"github.com/stretchr/testify/assert"
commonpb "go.temporal.io/api/common/v1"
)
type testStruct struct {
Int int
String string
Bytes []byte
}
func TestToString(t *testing.T) {
s := assert.New(t)
var result string
p := EncodeString("str")
result = ToString(p)
s.Equal(`"str"`, result)
p, err := Encode(10)
s.NoError(err)
result = ToString(p)
s.Equal("10", result)
p, err = Encode([]byte{41, 42, 43})
s.NoError(err)
result = ToString(p)
s.Equal("KSor", result)
p, err = Encode(&testStruct{
Int: 10,
String: "str",
Bytes: []byte{51, 52, 53},
})
s.NoError(err)
result = ToString(p)
s.Equal(`{"Int":10,"String":"str","Bytes":"MzQ1"}`, result)
p, err = Encode(nil)
s.NoError(err)
result = ToString(p)
s.Equal("nil", result)
result = ToString(nil)
s.Equal("", result)
}
func TestEncodeDecode(t *testing.T) {
s := assert.New(t)
var p *commonpb.Payload
var err error
var decodedValueStr string
valueStr := "asdf"
p, err = Encode(valueStr)
s.NoError(err)
err = Decode(p, &decodedValueStr)
s.NoError(err)
s.Equal(valueStr, decodedValueStr)
var decodedValueInt int64
valueInt := int64(1234)
p, err = Encode(valueInt)
s.NoError(err)
err = Decode(p, &decodedValueInt)
s.NoError(err)
s.Equal(valueInt, decodedValueInt)
var decodedValueBool bool
valueBool := true
p, err = Encode(valueBool)
s.NoError(err)
err = Decode(p, &decodedValueBool)
s.NoError(err)
s.Equal(valueBool, decodedValueBool)
var decodedValueAny any
var valueAny any = 123.45
p, err = Encode(valueAny)
s.NoError(err)
err = Decode(p, &decodedValueAny)
s.NoError(err)
s.Equal(valueAny, decodedValueAny)
}
func TestMergeMapOfPayload(t *testing.T) {
s := assert.New(t)
var currentMap map[string]*commonpb.Payload
var newMap map[string]*commonpb.Payload
resultMap := MergeMapOfPayload(currentMap, newMap)
s.Equal(newMap, resultMap)
newMap = make(map[string]*commonpb.Payload)
resultMap = MergeMapOfPayload(currentMap, newMap)
s.Equal(newMap, resultMap)
newMap = map[string]*commonpb.Payload{"key": EncodeString("val")}
resultMap = MergeMapOfPayload(currentMap, newMap)
s.Equal(newMap, resultMap)
newMap = map[string]*commonpb.Payload{
"key": EncodeString("val"),
"nil": nilPayload,
"emptyArray": emptySlicePayload,
}
resultMap = MergeMapOfPayload(currentMap, newMap)
s.Equal(map[string]*commonpb.Payload{"key": EncodeString("val")}, resultMap)
currentMap = map[string]*commonpb.Payload{"number": EncodeString("1")}
resultMap = MergeMapOfPayload(currentMap, newMap)
s.Equal(
map[string]*commonpb.Payload{"number": EncodeString("1"), "key": EncodeString("val")},
resultMap,
)
newValue, _ := Encode(nil)
newMap = map[string]*commonpb.Payload{"number": newValue}
resultMap = MergeMapOfPayload(currentMap, newMap)
s.Equal(0, len(resultMap))
newValue, _ = Encode([]int{})
newValue.Metadata["key"] = []byte("foo")
newMap = map[string]*commonpb.Payload{"number": newValue}
resultMap = MergeMapOfPayload(currentMap, newMap)
s.Equal(0, len(resultMap))
}
func TestIsEqual(t *testing.T) {
s := assert.New(t)
a, _ := Encode(nil)
b, _ := Encode(nil)
s.True(isEqual(a, b))
a, _ = Encode([]string{})
b, _ = Encode([]string{})
s.True(isEqual(a, b))
a.Metadata["key"] = []byte("foo")
b.Metadata["key"] = []byte("bar")
s.True(isEqual(a, b))
a, _ = Encode(nil)
b, _ = Encode([]string{})
s.False(isEqual(a, b))
a, _ = Encode([]string{})
b, _ = Encode("foo")
s.False(isEqual(a, b))
}
func TestFilterNilSearchAttributes(t *testing.T) {
s := assert.New(t)
// nil input returns nil
result := FilterNilSearchAttributes(nil)
s.Nil(result)
// empty SearchAttributes returns nil
emptySA := &commonpb.SearchAttributes{IndexedFields: map[string]*commonpb.Payload{}}
result = FilterNilSearchAttributes(emptySA)
s.Nil(result)
// SearchAttributes with only valid values returns filtered copy
validPayload := EncodeString("value")
saNonNil := &commonpb.SearchAttributes{
IndexedFields: map[string]*commonpb.Payload{
"key1": validPayload,
},
}
result = FilterNilSearchAttributes(saNonNil)
s.NotNil(result)
s.Len(result.IndexedFields, 1)
s.Equal(validPayload, result.IndexedFields["key1"])
// SearchAttributes with nil values filters them out
nilPayloadVal, _ := Encode(nil)
saMixed := &commonpb.SearchAttributes{
IndexedFields: map[string]*commonpb.Payload{
"valid": validPayload,
"nilVal": nilPayloadVal,
},
}
result = FilterNilSearchAttributes(saMixed)
s.NotNil(result)
s.Len(result.IndexedFields, 1)
s.Equal(validPayload, result.IndexedFields["valid"])
s.Nil(result.IndexedFields["nilVal"])
// SearchAttributes with empty slice values filters them out
emptySlicePayloadVal, _ := Encode([]string{})
saEmptySlice := &commonpb.SearchAttributes{
IndexedFields: map[string]*commonpb.Payload{
"valid": validPayload,
"emptySlice": emptySlicePayloadVal,
},
}
result = FilterNilSearchAttributes(saEmptySlice)
s.NotNil(result)
s.Len(result.IndexedFields, 1)
s.Equal(validPayload, result.IndexedFields["valid"])
// SearchAttributes with all nil/empty values returns nil
saAllNil := &commonpb.SearchAttributes{
IndexedFields: map[string]*commonpb.Payload{
"nil1": nilPayloadVal,
"nil2": emptySlicePayloadVal,
},
}
result = FilterNilSearchAttributes(saAllNil)
s.Nil(result)
}
func TestFilterNilMemo(t *testing.T) {
s := assert.New(t)
// nil input returns nil
result := FilterNilMemo(nil)
s.Nil(result)
// empty Memo returns nil
emptyMemo := &commonpb.Memo{Fields: map[string]*commonpb.Payload{}}
result = FilterNilMemo(emptyMemo)
s.Nil(result)
// Memo with only valid values returns filtered copy
validPayload := EncodeString("value")
memoNonNil := &commonpb.Memo{
Fields: map[string]*commonpb.Payload{
"key1": validPayload,
},
}
result = FilterNilMemo(memoNonNil)
s.NotNil(result)
s.Len(result.Fields, 1)
s.Equal(validPayload, result.Fields["key1"])
// Memo with nil values filters them out
nilPayloadVal, _ := Encode(nil)
memoMixed := &commonpb.Memo{
Fields: map[string]*commonpb.Payload{
"valid": validPayload,
"nilVal": nilPayloadVal,
},
}
result = FilterNilMemo(memoMixed)
s.NotNil(result)
s.Len(result.Fields, 1)
s.Equal(validPayload, result.Fields["valid"])
s.Nil(result.Fields["nilVal"])
// Memo with empty slice values filters them out
emptySlicePayloadVal, _ := Encode([]string{})
memoEmptySlice := &commonpb.Memo{
Fields: map[string]*commonpb.Payload{
"valid": validPayload,
"emptySlice": emptySlicePayloadVal,
},
}
result = FilterNilMemo(memoEmptySlice)
s.NotNil(result)
s.Len(result.Fields, 1)
s.Equal(validPayload, result.Fields["valid"])
// Memo with all nil/empty values returns nil
memoAllNil := &commonpb.Memo{
Fields: map[string]*commonpb.Payload{
"nil1": nilPayloadVal,
"nil2": emptySlicePayloadVal,
},
}
result = FilterNilMemo(memoAllNil)
s.Nil(result)
}