Skip to content

Commit 224de55

Browse files
Merge pull request #78 from ServiceNow/scratch/fix-validate-filter-list-task
Do not assume = is the only operator in filter conditions
2 parents f851500 + 1df827f commit 224de55

File tree

2 files changed

+159
-8
lines changed

2 files changed

+159
-8
lines changed

src/browsergym/workarena/tasks/list.py

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@
101101

102102

103103
class ServiceNowListTask(AbstractServiceNowTask):
104+
OPERATOR_EQUALS = "="
105+
OPERATOR_NOT_EQUALS = "!="
106+
OPERATOR_STARTSWITH = "STARTSWITH"
107+
OPERATOR_ISEMPTY = "ISEMPTY"
108+
OPERATOR_EMPTYSTRING = "EMPTYSTRING"
104109

105110
@classmethod
106111
def all_configs(cls) -> List[dict]:
@@ -777,6 +782,9 @@ def validate(
777782
list_info = self._extract_list_info(page)
778783
current_query = list_info["query"]
779784

785+
if not current_query:
786+
return 0, False, "", {"message": "There are no filters yet."}
787+
780788
# Replace "new query" statements with the standard OR separator
781789
current_query = current_query.replace("^NQ", "^OR")
782790

@@ -789,24 +797,74 @@ def validate(
789797
current_sep = "^"
790798

791799
if current_kind != self.filter_kind:
792-
return 0, False, "", {"message": "The kind of filter used is incorrect."}
800+
return (
801+
0,
802+
False,
803+
"",
804+
{"message": f"The kind of filter used is incorrect: {current_query}."},
805+
)
793806

794807
# Extract the query pieces for validation
795808
current_query = current_query.split(current_sep)
796809

797810
# Validate query length is ok
798811
if len(current_query) != self.filter_len:
799-
return 0, False, "", {"message": "Incorrect number of filter conditions."}
812+
return (
813+
0,
814+
False,
815+
"",
816+
{"message": f"Incorrect number of filter conditions: {current_query}."},
817+
)
818+
819+
# Parse column names, operators, and values
820+
current_columns, current_operators, current_values = [], [], []
821+
822+
# Note that this is not exhaustive. If/when other operators are added, this will have to be updated.
823+
for predicate in current_query:
824+
if self.OPERATOR_EMPTYSTRING in predicate:
825+
current_columns.append(predicate.replace(self.OPERATOR_EMPTYSTRING, "").strip())
826+
current_operators.append("=")
827+
current_values.append("")
828+
elif self.OPERATOR_ISEMPTY in predicate:
829+
current_columns.append(predicate.replace(self.OPERATOR_ISEMPTY, "").strip())
830+
current_operators.append("=")
831+
current_values.append("")
832+
elif any(
833+
unsupported_operator in predicate
834+
for unsupported_operator in [self.OPERATOR_NOT_EQUALS, self.OPERATOR_STARTSWITH]
835+
):
836+
return (
837+
0,
838+
False,
839+
"",
840+
{"message": f"Unexpected operator in filter condition: {current_query}."},
841+
)
842+
elif self.OPERATOR_EQUALS in predicate:
843+
col, val = predicate.split(self.OPERATOR_EQUALS, 1)
844+
current_columns.append(col.strip())
845+
current_operators.append("=")
846+
current_values.append(val.strip())
847+
else:
848+
return (
849+
0,
850+
False,
851+
"",
852+
{"message": f"Unexpected operator in filter condition: {current_query}."},
853+
)
800854

801-
# Validate query columns are ok
802-
current_columns = [x.split("=")[0] for x in current_query]
803855
if set(current_columns) != set(self.filter_columns):
804-
return 0, False, "", {"message": "Incorrect filter columns."}
856+
return (
857+
0,
858+
False,
859+
"",
860+
{
861+
"message": f"Incorrect filter columns: {set(current_columns)}. Expected: {set(self.filter_columns)}."
862+
},
863+
)
805864

806865
# Validate query values are ok
807866
# This is the tricky part because we need to expand the values to their display values
808867
# We also need to handle the case where the value is a reference
809-
current_values = [x.split("=")[1] for x in current_query]
810868

811869
# Handle filtering across multiple rows
812870
if len(set(current_columns)) < len(current_columns):
@@ -856,9 +914,21 @@ def validate(
856914

857915
# Validate the values
858916
if set(current_values) != set(self.filter_values):
859-
return 0, False, "", {"message": "Incorrect filter values."}
917+
return (
918+
0,
919+
False,
920+
"",
921+
{
922+
"message": f"Incorrect filter values {set(current_values)}. Expected: {set(self.filter_values)}."
923+
},
924+
)
860925

861-
return 1, True, "Nice work, thank you!", {"message": "Correct filter."}
926+
return (
927+
1,
928+
True,
929+
"Nice work, thank you!",
930+
{"message": f"Correct filter: {list_info['query']}."},
931+
)
862932

863933

864934
class ExtractListInfoTask(ServiceNowListTask):

tests/test_filter_list_task.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import pytest
2+
from playwright.sync_api import Page
3+
from browsergym.workarena.tasks.list import FilterIncidentListTask
4+
5+
6+
# These are all the same ways to express the same filter query (empty string) in ServiceNow.
7+
@pytest.mark.parametrize(
8+
"query",
9+
[
10+
"assigned_toEMPTYSTRING^short_descriptionISEMPTY^description=This is a beautiful incident",
11+
"assigned_toISEMPTY^short_descriptionEMPTYSTRING^description=This is a beautiful incident^",
12+
"assigned_toISEMPTY^short_descriptionISEMPTY^description=This is a beautiful incident",
13+
"assigned_toEMPTYSTRING^short_descriptionEMPTYSTRING^description=This is a beautiful incident^",
14+
"assigned_to=^short_description=^description=This is a beautiful incident",
15+
],
16+
)
17+
@pytest.mark.slow
18+
def test_validate_filter_list_task(page: Page, query):
19+
fixed_config = {
20+
"filter_columns": [
21+
"short_description",
22+
"assigned_to",
23+
"description",
24+
],
25+
"filter_kind": "AND",
26+
"filter_values": [
27+
"",
28+
"",
29+
"This is a beautiful incident",
30+
],
31+
}
32+
task = FilterIncidentListTask(seed=1, fixed_config=fixed_config)
33+
_, _ = task.setup(page=page)
34+
query = query.replace("^", r"%5E").replace("=", r"%3D")
35+
task.page.goto(
36+
task.instance.snow_url
37+
+ rf"/now/nav/ui/classic/params/target/incident_list.do?sysparm_query={query}"
38+
)
39+
reward, done, _, info = task.validate(page, [])
40+
task.teardown()
41+
assert done is True and reward == 1.0 and "Correct filter" in info["message"]
42+
43+
44+
# Different ways in which the filter is wrong
45+
@pytest.mark.parametrize(
46+
"query, expected_message",
47+
[
48+
("", "There are no filters yet"),
49+
("assignment_groupEMPTYSTRING", "Incorrect number of filter conditions"),
50+
(
51+
"assigned_toEMPTYSTRING^short_description!=Description",
52+
"Unexpected operator in filter condition",
53+
),
54+
("assigned_toEMPTYSTRING^short_description=Description", "Incorrect filter columns"),
55+
("assigned_toISEMPTY^description=My Description", "Incorrect filter values"),
56+
],
57+
)
58+
@pytest.mark.slow
59+
def test_invalid_filter_list_task(page: Page, query, expected_message):
60+
fixed_config = {
61+
"filter_columns": [
62+
"assigned_to",
63+
"description",
64+
],
65+
"filter_kind": "AND",
66+
"filter_values": [
67+
"",
68+
"Description",
69+
],
70+
}
71+
task = FilterIncidentListTask(seed=1, fixed_config=fixed_config)
72+
_, _ = task.setup(page=page)
73+
query = query.replace("^", r"%5E").replace("=", r"%3D")
74+
task.page.goto(
75+
task.instance.snow_url
76+
+ f"/now/nav/ui/classic/params/target/incident_list.do?sysparm_query={query}"
77+
)
78+
reward, done, _, info = task.validate(page, [])
79+
task.teardown()
80+
print(info["message"])
81+
assert done is False and reward == 0.0 and expected_message in info["message"]

0 commit comments

Comments
 (0)