@@ -1188,18 +1188,16 @@ async def test_schedule_create_limited_actions_validation(
11881188 assert "are remaining actions set" in str (err .value )
11891189
11901190
1191- async def test_schedule_search_attribute_update (
1191+ async def test_schedule_workflow_search_attribute_update (
11921192 client : Client , env : WorkflowEnvironment
11931193):
11941194 if env .supports_time_skipping :
11951195 pytest .skip ("Java test server doesn't support schedules" )
11961196 await assert_no_schedules (client )
11971197
11981198 # Put search attribute on server
1199- text_attr_key = SearchAttributeKey .for_text (f"python-test-schedule-text" )
1200- untyped_keyword_key = SearchAttributeKey .for_keyword (
1201- f"python-test-schedule-keyword"
1202- )
1199+ text_attr_key = SearchAttributeKey .for_text ("python-test-schedule-text" )
1200+ untyped_keyword_key = SearchAttributeKey .for_keyword ("python-test-schedule-keyword" )
12031201 await ensure_search_attributes_present (client , text_attr_key , untyped_keyword_key )
12041202
12051203 # Create a schedule with search attributes on the schedule and on the
@@ -1273,6 +1271,7 @@ def update_schedule_typed_attrs(
12731271 # Check that it changed
12741272 desc = await handle .describe ()
12751273 assert isinstance (desc .schedule .action , ScheduleActionStartWorkflow )
1274+ # Check that the workflow search attributes were changed
12761275 # This assertion has changed since server 1.24. Now, even untyped search
12771276 # attributes are given a type server side
12781277 assert (
@@ -1283,6 +1282,146 @@ def update_schedule_typed_attrs(
12831282 and desc .schedule .action .typed_search_attributes [untyped_keyword_key ]
12841283 == "some-untyped-attr1"
12851284 )
1285+ # Check that the schedule search attributes were not changed
1286+ assert desc .search_attributes [text_attr_key .name ] == ["some-schedule-attr1" ]
1287+ assert desc .typed_search_attributes [text_attr_key ] == "some-schedule-attr1"
1288+
1289+
1290+ @pytest .mark .parametrize (
1291+ "test_case" ,
1292+ [
1293+ "none-is-noop" ,
1294+ "empty-but-non-none-clears" ,
1295+ "all-new-values-overwrites" ,
1296+ "partial-new-values-overwrites-and-drops" ,
1297+ ],
1298+ )
1299+ async def test_schedule_search_attribute_update (
1300+ client : Client , env : WorkflowEnvironment , test_case : str
1301+ ):
1302+ if env .supports_time_skipping :
1303+ pytest .skip ("Java test server doesn't support schedules" )
1304+ await assert_no_schedules (client )
1305+
1306+ # Put search attributes on server
1307+ key_1 = SearchAttributeKey .for_text ("python-test-schedule-key-1" )
1308+ key_2 = SearchAttributeKey .for_text ("python-test-schedule-key-2" )
1309+ await ensure_search_attributes_present (client , key_1 , key_2 )
1310+ val_1 = "val-1"
1311+ val_2 = "val-2"
1312+
1313+ # Create a schedule with search attributes
1314+ create_action = ScheduleActionStartWorkflow (
1315+ "some workflow" ,
1316+ [],
1317+ id = f"workflow-{ uuid .uuid4 ()} " ,
1318+ task_queue = f"tq-{ uuid .uuid4 ()} " ,
1319+ )
1320+ handle = await client .create_schedule (
1321+ f"schedule-{ uuid .uuid4 ()} " ,
1322+ Schedule (action = create_action , spec = ScheduleSpec ()),
1323+ search_attributes = TypedSearchAttributes (
1324+ [
1325+ SearchAttributePair (key_1 , val_1 ),
1326+ SearchAttributePair (key_2 , val_2 ),
1327+ ]
1328+ ),
1329+ )
1330+
1331+ def update_search_attributes (
1332+ input : ScheduleUpdateInput ,
1333+ ) -> Optional [ScheduleUpdate ]:
1334+ # Make sure the initial search attributes are present
1335+ assert input .description .search_attributes [key_1 .name ] == [val_1 ]
1336+ assert input .description .search_attributes [key_2 .name ] == [val_2 ]
1337+ assert input .description .typed_search_attributes [key_1 ] == val_1
1338+ assert input .description .typed_search_attributes [key_2 ] == val_2
1339+
1340+ if test_case == "none-is-noop" :
1341+ print ("none-is-noop" )
1342+ # Passing None makes no changes
1343+ return ScheduleUpdate (input .description .schedule , search_attributes = None )
1344+ elif test_case == "empty-but-non-none-clears" :
1345+ print ("empty-but-non-none-clears" )
1346+ # Pass empty but non-None to clear all attributes
1347+ return ScheduleUpdate (
1348+ input .description .schedule ,
1349+ search_attributes = TypedSearchAttributes .empty ,
1350+ )
1351+ elif test_case == "all-new-values-overwrites" :
1352+ print ("all-new-values-overwrites" )
1353+ # Pass all new values to overwrite existing
1354+ return ScheduleUpdate (
1355+ input .description .schedule ,
1356+ search_attributes = input .description .typed_search_attributes .updated (
1357+ SearchAttributePair (key_1 , val_1 + "-new" ),
1358+ SearchAttributePair (key_2 , val_2 + "-new" ),
1359+ ),
1360+ )
1361+ elif test_case == "partial-new-values-overwrites-and-drops" :
1362+ print ("partial-new-values-overwrites-and-drops" )
1363+ # Only update key_1, which should drop key_2
1364+ return ScheduleUpdate (
1365+ input .description .schedule ,
1366+ search_attributes = TypedSearchAttributes (
1367+ [
1368+ SearchAttributePair (key_1 , val_1 + "-new" ),
1369+ ]
1370+ ),
1371+ )
1372+ else :
1373+ raise ValueError (f"Invalid test case: { test_case } " )
1374+
1375+ await handle .update (update_search_attributes )
1376+
1377+ if test_case == "none-is-noop" : # 🟢
1378+
1379+ async def expectation () -> bool :
1380+ desc = await handle .describe ()
1381+ return (
1382+ desc .search_attributes [key_1 .name ] == [val_1 ]
1383+ and desc .search_attributes [key_2 .name ] == [val_2 ]
1384+ and desc .typed_search_attributes [key_1 ] == val_1
1385+ and desc .typed_search_attributes [key_2 ] == val_2
1386+ )
1387+
1388+ await assert_eq_eventually (True , expectation )
1389+ elif test_case == "empty-but-non-none-clears" : # 🔴
1390+
1391+ async def expectation () -> bool :
1392+ desc = await handle .describe ()
1393+ return (
1394+ len (desc .typed_search_attributes ) == 0
1395+ and len (desc .search_attributes ) == 0
1396+ )
1397+
1398+ await assert_eq_eventually (True , expectation )
1399+ elif test_case == "all-new-values-overwrites" : # 🟢
1400+
1401+ async def expectation () -> bool :
1402+ desc = await handle .describe ()
1403+ return (
1404+ desc .search_attributes [key_1 .name ] == [val_1 + "-new" ]
1405+ and desc .search_attributes [key_2 .name ] == [val_2 + "-new" ]
1406+ and desc .typed_search_attributes [key_1 ] == val_1 + "-new"
1407+ and desc .typed_search_attributes [key_2 ] == val_2 + "-new"
1408+ )
1409+
1410+ await assert_eq_eventually (True , expectation )
1411+ elif test_case == "partial-new-values-overwrites-and-drops" : # 🟢
1412+
1413+ async def expectation () -> bool :
1414+ desc = await handle .describe ()
1415+ return (
1416+ desc .search_attributes [key_1 .name ] == [val_1 + "-new" ]
1417+ and desc .typed_search_attributes [key_1 ] == val_1 + "-new"
1418+ and key_2 .name not in desc .search_attributes
1419+ and key_2 not in desc .typed_search_attributes
1420+ )
1421+
1422+ await assert_eq_eventually (True , expectation )
1423+ else :
1424+ raise ValueError (f"Invalid test case: { test_case } " )
12861425
12871426
12881427async def assert_no_schedules (client : Client ) -> None :
0 commit comments