browsergym-workarena 0.5.2__py3-none-any.whl → 0.5.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- __version__ = "0.5.2"
1
+ __version__ = "0.5.3"
2
2
 
3
3
  # Check playwright version early to avoid cryptic errors
4
4
  import importlib.metadata
@@ -8,7 +8,11 @@ import re
8
8
  import tenacity
9
9
 
10
10
  from datetime import datetime
11
- from playwright.sync_api import sync_playwright
11
+ from playwright.sync_api import (
12
+ sync_playwright,
13
+ TimeoutError as PlaywrightTimeoutError,
14
+ Error as PlaywrightError,
15
+ )
12
16
  from tenacity import retry, stop_after_attempt, retry_if_exception_type
13
17
  from requests import HTTPError
14
18
  from time import sleep
@@ -55,6 +59,63 @@ from .instance import SNowInstance as _BaseSNowInstance
55
59
  from .utils import url_login
56
60
 
57
61
 
62
+ # Common retry decorator for setup steps - retries on transient errors
63
+ RETRYABLE_ERRORS = (ConnectionError, TimeoutError, OSError, PlaywrightTimeoutError, PlaywrightError)
64
+
65
+
66
+ def retry_on_transient_error(func):
67
+ """Decorator that retries a function up to 5 times on transient errors (network, timeouts, etc.)."""
68
+ return retry(
69
+ stop=stop_after_attempt(5),
70
+ retry=retry_if_exception_type(RETRYABLE_ERRORS),
71
+ reraise=True,
72
+ before_sleep=lambda retry_state: logging.info(
73
+ f"Transient error in {func.__name__}. Retrying (attempt {retry_state.attempt_number + 1}/5)..."
74
+ ),
75
+ )(func)
76
+
77
+
78
+ # Installation progress tracking
79
+ INSTALLATION_PROGRESS_PROPERTY = "workarena.installation.progress"
80
+
81
+
82
+ def get_installation_progress() -> dict:
83
+ """Get the current installation progress from the instance."""
84
+ try:
85
+ progress_json = get_sys_property(SNowInstance(), INSTALLATION_PROGRESS_PROPERTY)
86
+ return json.loads(progress_json) if progress_json else {}
87
+ except:
88
+ return {}
89
+
90
+
91
+ def save_installation_progress(progress: dict):
92
+ """Save the installation progress to the instance."""
93
+ set_sys_property(SNowInstance(), INSTALLATION_PROGRESS_PROPERTY, json.dumps(progress))
94
+
95
+
96
+ def mark_step_completed(step_name: str):
97
+ """Mark a step as completed in the installation progress."""
98
+ progress = get_installation_progress()
99
+ progress[step_name] = {"completed": True, "timestamp": datetime.now().isoformat()}
100
+ save_installation_progress(progress)
101
+ logging.info(f"Step '{step_name}' marked as completed.")
102
+
103
+
104
+ def is_step_completed(step_name: str) -> bool:
105
+ """Check if a step is already completed."""
106
+ progress = get_installation_progress()
107
+ return progress.get(step_name, {}).get("completed", False)
108
+
109
+
110
+ def clear_installation_progress():
111
+ """Clear all installation progress to start fresh."""
112
+ try:
113
+ set_sys_property(SNowInstance(), INSTALLATION_PROGRESS_PROPERTY, "{}")
114
+ logging.info("Installation progress cleared.")
115
+ except:
116
+ pass # Property might not exist yet
117
+
118
+
58
119
  _CLI_INSTANCE_URL: str | None = None
59
120
  _CLI_INSTANCE_PASSWORD: str | None = None
60
121
 
@@ -346,6 +407,7 @@ def create_knowledge_base(
346
407
  )
347
408
 
348
409
 
410
+ @retry_on_transient_error
349
411
  def setup_knowledge_bases():
350
412
  """
351
413
  Verify that the knowledge base is installed correctly in the instance.
@@ -399,6 +461,7 @@ def setup_knowledge_bases():
399
461
  logging.info(f"Knowledge base {kb_name} is already installed.")
400
462
 
401
463
 
464
+ @retry_on_transient_error
402
465
  def setup_workflows():
403
466
  """
404
467
  Verify that workflows are correctly installed.
@@ -521,6 +584,7 @@ def display_all_expected_columns(
521
584
  logging.info(f"...... Done.")
522
585
 
523
586
 
587
+ @retry_on_transient_error
524
588
  def check_all_columns_displayed(
525
589
  instance: SNowInstance, url: str, expected_columns: list[str]
526
590
  ) -> bool:
@@ -566,6 +630,7 @@ def check_all_columns_displayed(
566
630
  return True
567
631
 
568
632
 
633
+ @retry_on_transient_error
569
634
  def setup_list_columns():
570
635
  """
571
636
  Setup the list view to display the expected number of columns.
@@ -611,12 +676,23 @@ def setup_list_columns():
611
676
  },
612
677
  }
613
678
 
679
+ # Check which lists still need to be set up
680
+ lists_to_setup = {
681
+ k: v for k, v in list_mappings.items() if not is_step_completed(f"list_columns_{k}")
682
+ }
683
+
684
+ if not lists_to_setup:
685
+ logging.info("All list columns already set up.")
686
+ return
687
+
688
+ logging.info(f"... {len(lists_to_setup)} list(s) to set up: {list(lists_to_setup.keys())}")
614
689
  logging.info("... Creating a new user account to validate list columns")
615
690
  admin_instance = SNowInstance()
616
691
  username, password, usysid = create_user(instance=admin_instance)
617
692
  user_instance = SNowInstance(snow_credentials=(username, password))
618
693
 
619
- for task, task_info in list_mappings.items():
694
+ for task, task_info in lists_to_setup.items():
695
+ logging.info(f"... Setting up list: {task}")
620
696
  expected_columns_path = task_info["expected_columns_path"]
621
697
  with open(expected_columns_path, "r") as f:
622
698
  expected_columns = list(json.load(f))
@@ -629,16 +705,21 @@ def setup_list_columns():
629
705
  user_instance, task_info["url"], expected_columns=expected_columns
630
706
  ), f"Error setting up list columns at {task_info['url']}"
631
707
 
708
+ # Mark this list as completed
709
+ mark_step_completed(f"list_columns_{task}")
710
+
632
711
  # Delete the user account
633
712
  logging.info("... Deleting the test user account")
634
713
  table_api_call(instance=admin_instance, table=f"sys_user/{usysid}", method="DELETE")
635
714
 
636
715
 
637
716
  @retry(
638
- stop=stop_after_attempt(3),
639
- retry=retry_if_exception_type(TimeoutError),
717
+ stop=stop_after_attempt(5),
718
+ retry=retry_if_exception_type(RETRYABLE_ERRORS),
640
719
  reraise=True,
641
- before_sleep=lambda _: logging.info("Retrying due to a TimeoutError..."),
720
+ before_sleep=lambda retry_state: logging.info(
721
+ f"Transient error in process_form_fields. Retrying (attempt {retry_state.attempt_number + 1}/5)..."
722
+ ),
642
723
  )
643
724
  def process_form_fields(instance: SNowInstance, url: str, expected_fields: list[str], action: str):
644
725
  """Process form fields based on the given action."""
@@ -689,6 +770,7 @@ def process_form_fields(instance: SNowInstance, url: str, expected_fields: list[
689
770
  return True
690
771
 
691
772
 
773
+ @retry_on_transient_error
692
774
  def setup_form_fields():
693
775
  task_mapping = {
694
776
  "create_change_request": {
@@ -717,12 +799,22 @@ def setup_form_fields():
717
799
  },
718
800
  }
719
801
 
802
+ # Check which forms still need to be set up
803
+ forms_to_setup = {
804
+ k: v for k, v in task_mapping.items() if not is_step_completed(f"form_fields_{k}")
805
+ }
806
+
807
+ if not forms_to_setup:
808
+ logging.info("All form fields already set up.")
809
+ return
810
+
811
+ logging.info(f"... {len(forms_to_setup)} form(s) to set up: {list(forms_to_setup.keys())}")
720
812
  logging.info("... Creating a new user account to validate form fields")
721
813
  admin_instance = SNowInstance()
722
814
  username, password, usysid = create_user(instance=admin_instance)
723
815
  user_instance = SNowInstance(snow_credentials=(username, password))
724
816
 
725
- for task, task_info in task_mapping.items():
817
+ for task, task_info in forms_to_setup.items():
726
818
  expected_fields_path = task_info["expected_fields_path"]
727
819
  with open(expected_fields_path, "r") as f:
728
820
  expected_fields = json.load(f)
@@ -764,6 +856,9 @@ def setup_form_fields():
764
856
  user_instance, task_info["url"], expected_fields=expected_fields, action="check"
765
857
  ), f"Error setting up form fields at {task_info['url']}"
766
858
 
859
+ # Mark this form as completed
860
+ mark_step_completed(f"form_fields_{task}")
861
+
767
862
  # Delete the user account
768
863
  logging.info("... Deleting the test user account")
769
864
  table_api_call(instance=admin_instance, table=f"sys_user/{usysid}", method="DELETE")
@@ -771,6 +866,7 @@ def setup_form_fields():
771
866
  logging.info("All form fields properly displayed.")
772
867
 
773
868
 
869
+ @retry_on_transient_error
774
870
  def check_instance_release_support():
775
871
  """
776
872
  Check that the instance is running a compatible version of ServiceNow.
@@ -793,6 +889,7 @@ def check_instance_release_support():
793
889
  return True
794
890
 
795
891
 
892
+ @retry_on_transient_error
796
893
  def enable_url_login():
797
894
  """
798
895
  Configure the instance to allow login via URL.
@@ -804,6 +901,7 @@ def enable_url_login():
804
901
  logging.info("URL login enabled.")
805
902
 
806
903
 
904
+ @retry_on_transient_error
807
905
  def disable_password_policies():
808
906
  """
809
907
  Disable password policies in the instance.
@@ -836,6 +934,7 @@ def disable_password_policies():
836
934
  logging.info("Password policies disabled.")
837
935
 
838
936
 
937
+ @retry_on_transient_error
839
938
  def disable_guided_tours():
840
939
  """
841
940
  Hide guided tour popups
@@ -852,6 +951,7 @@ def disable_guided_tours():
852
951
  logging.info("Guided tours disabled.")
853
952
 
854
953
 
954
+ @retry_on_transient_error
855
955
  def disable_welcome_help_popup():
856
956
  """
857
957
  Disable the welcome help popup
@@ -861,6 +961,7 @@ def disable_welcome_help_popup():
861
961
  logging.info("Welcome help popup disabled.")
862
962
 
863
963
 
964
+ @retry_on_transient_error
864
965
  def disable_analytics_popups():
865
966
  """
866
967
  Disable analytics popups (needs to be done through UI since Vancouver release)
@@ -872,6 +973,7 @@ def disable_analytics_popups():
872
973
  logging.info("Analytics popups disabled.")
873
974
 
874
975
 
976
+ @retry_on_transient_error
875
977
  def setup_ui_themes():
876
978
  """
877
979
  Install custom UI themes and set it as default
@@ -925,6 +1027,7 @@ def check_ui_themes_installed():
925
1027
  """
926
1028
 
927
1029
 
1030
+ @retry_on_transient_error
928
1031
  def set_home_page():
929
1032
  logging.info("Setting default home page")
930
1033
  set_sys_property(
@@ -932,6 +1035,7 @@ def set_home_page():
932
1035
  )
933
1036
 
934
1037
 
1038
+ @retry_on_transient_error
935
1039
  def wipe_system_admin_preferences():
936
1040
  """
937
1041
  Wipe all system admin preferences
@@ -964,16 +1068,142 @@ def is_report_filter_using_relative_time(filter):
964
1068
  return "javascript:gs." in filter or "@ago" in filter
965
1069
 
966
1070
 
967
- def patch_report_filters():
1071
+ @retry(
1072
+ stop=stop_after_attempt(5),
1073
+ retry=retry_if_exception_type(RETRYABLE_ERRORS),
1074
+ reraise=True,
1075
+ before_sleep=lambda retry_state: logging.info(
1076
+ f"Network error while patching report. Retrying (attempt {retry_state.attempt_number + 1}/5)..."
1077
+ ),
1078
+ )
1079
+ def _patch_single_report(instance, report, report_date_filter, report_time_filter):
1080
+ """
1081
+ Patch a single report with date filters. Retries on network errors.
1082
+ """
1083
+ # Find all sys_created_on columns of this record. Some have many.
1084
+ sys_created_on_cols = [
1085
+ c for c in table_column_info(instance, report["table"]).keys() if "sys_created_on" in c
1086
+ ]
1087
+
1088
+ # XXX: We purposely do not support reports with multiple filter conditions for simplicity
1089
+ if len(sys_created_on_cols) == 0 or "^NQ" in report["filter"]:
1090
+ logging.info(f"Discarding report {report['title']} {report['sys_id']}...")
1091
+ raise NotImplementedError() # Mark for deletion
1092
+
1093
+ if not is_report_filter_using_relative_time(report["filter"]):
1094
+ # That's a report we want to keep (use date cutoff filter)
1095
+ filter_date = report_date_filter
1096
+ filter_time = report_time_filter
1097
+ logging.info(
1098
+ f"Keeping report {report['title']} {report['sys_id']} (columns: {sys_created_on_cols})..."
1099
+ )
1100
+ else:
1101
+ # XXX: We do not support reports with filters that rely on relative time (e.g., last 10 days) because
1102
+ # there are not stable. In this case, we don't delete them but add a filter to make
1103
+ # them empty. They will be shown as "No data available".
1104
+ logging.info(
1105
+ f"Disabling report {report['title']} {report['sys_id']} because it uses time filters..."
1106
+ )
1107
+ filter_date = "1900-01-01"
1108
+ filter_time = "00:00:00"
1109
+
1110
+ # Format the filter
1111
+ filter = "".join(
1112
+ [
1113
+ f"^{col}<javascript:gs.dateGenerate('{filter_date}','{filter_time}')"
1114
+ for col in sys_created_on_cols
1115
+ ]
1116
+ ) + ("^" if len(report["filter"]) > 0 and not report["filter"].startswith("^") else "")
1117
+ # Patch the report with the new filter
1118
+ table_api_call(
1119
+ instance=instance,
1120
+ table=f"sys_report/{report['sys_id']}",
1121
+ method="PATCH",
1122
+ json={
1123
+ "filter": filter + report["filter"],
1124
+ "description": report["description"] + " " + REPORT_PATCH_FLAG,
1125
+ },
1126
+ )
1127
+ logging.info(f"... done")
1128
+
1129
+
1130
+ def _cleanup_patched_reports(instance):
1131
+ """
1132
+ Remove patch flags and date filters from already-patched reports to allow re-patching.
1133
+ Used when doing a fresh install.
1134
+ """
1135
+ logging.info("Cleaning up previously patched reports for fresh install...")
1136
+
1137
+ reports = table_api_call(
1138
+ instance=instance,
1139
+ table="sys_report",
1140
+ params={
1141
+ "sysparm_query": f"sys_class_name=sys_report^active=true^descriptionLIKE{REPORT_PATCH_FLAG}"
1142
+ },
1143
+ )["result"]
1144
+
1145
+ logging.info(f"Found {len(reports)} previously patched reports to clean up.")
1146
+
1147
+ for i, report in enumerate(reports):
1148
+ logging.info(f"Cleaning up report {i + 1}/{len(reports)}: {report['title']}")
1149
+
1150
+ # Remove the patch flag from description
1151
+ new_description = report["description"].replace(REPORT_PATCH_FLAG, "").strip()
1152
+
1153
+ # Remove the date filter prefix from the filter
1154
+ # The prefix looks like: ^col<javascript:gs.dateGenerate('YYYY-MM-DD','HH:MM:SS')
1155
+ # There might be multiple columns, so we use regex to remove all occurrences
1156
+ filter_pattern = r"\^?[a-z_\.]+<javascript:gs\.dateGenerate\('[^']+','[^']+'\)"
1157
+ new_filter = re.sub(filter_pattern, "", report["filter"])
1158
+ # Clean up any leading ^ that might remain
1159
+ new_filter = new_filter.lstrip("^")
1160
+
1161
+ try:
1162
+ table_api_call(
1163
+ instance=instance,
1164
+ table=f"sys_report/{report['sys_id']}",
1165
+ method="PATCH",
1166
+ json={
1167
+ "filter": new_filter,
1168
+ "description": new_description,
1169
+ },
1170
+ )
1171
+ logging.info(f"... cleaned up")
1172
+ except RETRYABLE_ERRORS:
1173
+ # Re-raise network errors so the outer retry can handle them
1174
+ raise
1175
+ except Exception as e:
1176
+ # For other errors (e.g., protected reports), log and continue
1177
+ logging.warning(f"... failed to clean up (skipping): {e}")
1178
+
1179
+
1180
+ @retry_on_transient_error
1181
+ def patch_report_filters(fresh: bool = False):
968
1182
  """
969
1183
  Add filters to reports to make sure they stay frozen in time and don't show new data
970
1184
  as then instance's life cycle progresses.
971
1185
 
1186
+ Parameters:
1187
+ -----------
1188
+ fresh: bool
1189
+ If True, reset the report date filter and re-patch all reports (including already-patched ones).
972
1190
  """
973
1191
  logging.info("Patching reports with date filter...")
974
1192
 
975
1193
  instance = SNowInstance()
976
- filter_config = instance.report_filter_config
1194
+
1195
+ # For fresh install, clean up previously patched reports and reset the date filter
1196
+ if fresh:
1197
+ _cleanup_patched_reports(instance)
1198
+ # Clear the existing filter config so a new one is generated
1199
+ logging.info("Clearing existing report filter config for fresh install...")
1200
+ try:
1201
+ set_sys_property(instance=instance, property_name=REPORT_FILTER_PROPERTY, value="")
1202
+ except:
1203
+ pass
1204
+ filter_config = None
1205
+ else:
1206
+ filter_config = instance.report_filter_config
977
1207
 
978
1208
  # If the report date filter is already set, we use the existing values (would be the case on reinstall)
979
1209
  if not filter_config:
@@ -1009,52 +1239,10 @@ def patch_report_filters():
1009
1239
  },
1010
1240
  )["result"]
1011
1241
 
1012
- for report in reports:
1013
- # Find all sys_created_on columns of this record. Some have many.
1014
- sys_created_on_cols = [
1015
- c for c in table_column_info(instance, report["table"]).keys() if "sys_created_on" in c
1016
- ]
1242
+ for i, report in enumerate(reports):
1243
+ logging.info(f"Processing report {i + 1}/{len(reports)}: {report['title']}")
1017
1244
  try:
1018
- # XXX: We purposely do not support reports with multiple filter conditions for simplicity
1019
- if len(sys_created_on_cols) == 0 or "^NQ" in report["filter"]:
1020
- logging.info(f"Discarding report {report['title']} {report['sys_id']}...")
1021
- raise NotImplementedError() # Mark for deletion
1022
-
1023
- if not is_report_filter_using_relative_time(report["filter"]):
1024
- # That's a report we want to keep (use date cutoff filter)
1025
- filter_date = report_date_filter
1026
- filter_time = report_time_filter
1027
- logging.info(
1028
- f"Keeping report {report['title']} {report['sys_id']} (columns: {sys_created_on_cols})..."
1029
- )
1030
- else:
1031
- # XXX: We do not support reports with filters that rely on relative time (e.g., last 10 days) because
1032
- # there are not stable. In this case, we don't delete them but add a filter to make
1033
- # them empty. They will be shown as "No data available".
1034
- logging.info(
1035
- f"Disabling report {report['title']} {report['sys_id']} because it uses time filters..."
1036
- )
1037
- filter_date = "1900-01-01"
1038
- filter_time = "00:00:00"
1039
-
1040
- # Format the filter
1041
- filter = "".join(
1042
- [
1043
- f"^{col}<javascript:gs.dateGenerate('{filter_date}','{filter_time}')"
1044
- for col in sys_created_on_cols
1045
- ]
1046
- ) + ("^" if len(report["filter"]) > 0 and not report["filter"].startswith("^") else "")
1047
- # Patch the report with the new filter
1048
- table_api_call(
1049
- instance=instance,
1050
- table=f"sys_report/{report['sys_id']}",
1051
- method="PATCH",
1052
- json={
1053
- "filter": filter + report["filter"],
1054
- "description": report["description"] + " " + REPORT_PATCH_FLAG,
1055
- },
1056
- )
1057
- logging.info(f"... done")
1245
+ _patch_single_report(instance, report, report_date_filter, report_time_filter)
1058
1246
 
1059
1247
  except (NotImplementedError, HTTPError):
1060
1248
  # HTTPError occurs when some reports simply cannot be patched because they are critical and protected
@@ -1071,52 +1259,76 @@ def patch_report_filters():
1071
1259
  logging.error(f"...... could not delete.")
1072
1260
 
1073
1261
 
1074
- @tenacity.retry(
1075
- stop=tenacity.stop_after_attempt(3),
1076
- reraise=True,
1077
- before_sleep=lambda _: logging.info("An error occurred. Retrying..."),
1078
- )
1079
- def setup():
1262
+ def run_step(step_name: str, step_func, resume: bool = True, **kwargs):
1263
+ """
1264
+ Run a setup step, skipping if already completed (when resuming).
1265
+
1266
+ Parameters:
1267
+ -----------
1268
+ step_name: str
1269
+ The name of the step (used for progress tracking)
1270
+ step_func: callable
1271
+ The function to run for this step
1272
+ resume: bool
1273
+ If True, skip steps that are already completed
1274
+ **kwargs:
1275
+ Additional arguments to pass to the step function
1276
+ """
1277
+ if resume and is_step_completed(step_name):
1278
+ logging.info(f"Skipping '{step_name}' (already completed)")
1279
+ return
1280
+
1281
+ logging.info(f"Running step: {step_name}")
1282
+ step_func(**kwargs)
1283
+ mark_step_completed(step_name)
1284
+
1285
+
1286
+ def setup(resume: bool = True):
1080
1287
  """
1081
1288
  Check that WorkArena is installed correctly in the instance.
1082
1289
 
1290
+ Parameters:
1291
+ -----------
1292
+ resume: bool
1293
+ If True, skip steps that have already been completed.
1294
+ If False, run all steps from the beginning.
1083
1295
  """
1296
+ if not resume:
1297
+ clear_installation_progress()
1298
+
1084
1299
  if not check_instance_release_support():
1085
1300
  return # Don't continue if the instance is not supported
1086
1301
 
1087
1302
  # Enable URL login (XXX: Do this first since other functions can use URL login)
1088
- enable_url_login()
1303
+ run_step("enable_url_login", enable_url_login, resume)
1089
1304
 
1090
1305
  # Disable password policies
1091
- disable_password_policies()
1306
+ run_step("disable_password_policies", disable_password_policies, resume)
1092
1307
 
1093
1308
  # Set default landing page
1094
- set_home_page()
1309
+ run_step("set_home_page", set_home_page, resume)
1095
1310
 
1096
1311
  # Disable popups for new users
1097
- # ... guided tours
1098
- disable_guided_tours()
1099
- # ... analytics
1100
- disable_analytics_popups()
1101
- # ... help
1102
- disable_welcome_help_popup()
1312
+ run_step("disable_guided_tours", disable_guided_tours, resume)
1313
+ run_step("disable_analytics_popups", disable_analytics_popups, resume)
1314
+ run_step("disable_welcome_help_popup", disable_welcome_help_popup, resume)
1103
1315
 
1104
1316
  # Install custom UI themes (needs to be after disabling popups)
1105
- setup_ui_themes()
1317
+ run_step("setup_ui_themes", setup_ui_themes, resume)
1106
1318
 
1107
1319
  # Clear all predefined system admin preferences (e.g., default list views, etc.)
1108
- wipe_system_admin_preferences()
1320
+ run_step("wipe_system_admin_preferences", wipe_system_admin_preferences, resume)
1109
1321
 
1110
- # Patch all reports to only show data <= April 1, 2024
1111
- patch_report_filters()
1322
+ # Patch all reports to only show data <= current date
1323
+ run_step("patch_report_filters", patch_report_filters, resume, fresh=not resume)
1112
1324
 
1113
1325
  # XXX: Install workflows first because they may automate some downstream installations
1114
- setup_workflows()
1115
- setup_knowledge_bases()
1326
+ run_step("setup_workflows", setup_workflows, resume)
1327
+ run_step("setup_knowledge_bases", setup_knowledge_bases, resume)
1116
1328
 
1117
1329
  # Setup the user list columns by displaying all columns and checking that the expected number are displayed
1118
- setup_form_fields()
1119
- setup_list_columns()
1330
+ run_step("setup_form_fields", setup_form_fields, resume)
1331
+ run_step("setup_list_columns", setup_list_columns, resume)
1120
1332
 
1121
1333
  # Save installation date
1122
1334
  logging.info("Saving installation date")
@@ -1126,6 +1338,9 @@ def setup():
1126
1338
  value=datetime.now().isoformat(),
1127
1339
  )
1128
1340
 
1341
+ # Clear progress tracking since installation is complete
1342
+ clear_installation_progress()
1343
+
1129
1344
  logging.info("WorkArena setup complete.")
1130
1345
 
1131
1346
 
@@ -1145,6 +1360,16 @@ def main():
1145
1360
  required=True,
1146
1361
  help="Password for the admin user on the target ServiceNow instance.",
1147
1362
  )
1363
+ parser.add_argument(
1364
+ "--fresh",
1365
+ action="store_true",
1366
+ help="Start a fresh installation, ignoring any previous progress.",
1367
+ )
1368
+ parser.add_argument(
1369
+ "--resume",
1370
+ action="store_true",
1371
+ help="Resume from previous progress without prompting.",
1372
+ )
1148
1373
  args = parser.parse_args()
1149
1374
 
1150
1375
  global _CLI_INSTANCE_URL, _CLI_INSTANCE_PASSWORD
@@ -1175,4 +1400,42 @@ Previous installation: {past_install_date}
1175
1400
 
1176
1401
  """
1177
1402
  )
1178
- setup()
1403
+
1404
+ # Determine whether to resume or start fresh
1405
+ if args.fresh:
1406
+ resume = False
1407
+ elif args.resume:
1408
+ resume = True
1409
+ else:
1410
+ # Check for existing progress and prompt user
1411
+ progress = get_installation_progress()
1412
+ completed_steps = [k for k, v in progress.items() if v.get("completed")]
1413
+
1414
+ if completed_steps:
1415
+ logging.info(
1416
+ f"Found incomplete installation with {len(completed_steps)} completed step(s):"
1417
+ )
1418
+ for step in completed_steps:
1419
+ timestamp = progress[step].get("timestamp", "unknown")
1420
+ logging.info(f" - {step} (completed at {timestamp})")
1421
+
1422
+ while True:
1423
+ choice = (
1424
+ input(
1425
+ "\nDo you want to [r]esume from where you left off, or [s]tart fresh? (r/s): "
1426
+ )
1427
+ .strip()
1428
+ .lower()
1429
+ )
1430
+ if choice in ("r", "resume"):
1431
+ resume = True
1432
+ break
1433
+ elif choice in ("s", "start", "fresh"):
1434
+ resume = False
1435
+ break
1436
+ else:
1437
+ print("Please enter 'r' to resume or 's' to start fresh.")
1438
+ else:
1439
+ resume = False # No previous progress, start fresh
1440
+
1441
+ setup(resume=resume)
@@ -395,6 +395,30 @@ class ServiceNowFormTask(AbstractServiceNowTask):
395
395
 
396
396
  runInGsftMainOnlyAndProtectByURL(removeAdditionalActionsButton, '{url_suffix}');
397
397
  """,
398
+ f"""
399
+ function removeContextMenus() {{
400
+ waLog('Setting up context menu removal observer...', 'removeContextMenus');
401
+ // Remove any existing context menus
402
+ document.querySelectorAll('.context_menu').forEach((menu) => {{
403
+ menu.remove();
404
+ }});
405
+ // Observe for new context menus being added
406
+ const observer = new MutationObserver((mutations) => {{
407
+ mutations.forEach((mutation) => {{
408
+ mutation.addedNodes.forEach((node) => {{
409
+ if (node.nodeType === 1 && node.classList && node.classList.contains('context_menu')) {{
410
+ node.remove();
411
+ waLog('Removed dynamically added context menu', 'removeContextMenus');
412
+ }}
413
+ }});
414
+ }});
415
+ }});
416
+ observer.observe(document.body, {{ childList: true, subtree: true }});
417
+ waLog('Context menu observer active', 'removeContextMenus');
418
+ }}
419
+
420
+ runInGsftMainOnlyAndProtectByURL(removeContextMenus, '{url_suffix}');
421
+ """,
398
422
  ]
399
423
 
400
424
  def start(self, page: Page) -> None:
@@ -116,6 +116,7 @@ class ServiceNowListTask(AbstractServiceNowTask):
116
116
  return super().get_init_scripts() + [
117
117
  "registerGsftMainLoaded();",
118
118
  self._get_remove_personalize_list_button_script(),
119
+ self._get_remove_context_menus_script(),
119
120
  ]
120
121
 
121
122
  def _get_remove_personalize_list_button_script(self):
@@ -136,6 +137,37 @@ class ServiceNowListTask(AbstractServiceNowTask):
136
137
  """
137
138
  return script
138
139
 
140
+ def _get_remove_context_menus_script(self):
141
+ """
142
+ Removes context menus that appear on right-click in list views.
143
+ These menus provide options that could be used to modify list data outside the task scope.
144
+ """
145
+ script = """
146
+ function removeContextMenus() {
147
+ waLog('Setting up context menu removal observer...', 'removeContextMenus');
148
+ // Remove any existing context menus
149
+ document.querySelectorAll('.context_menu').forEach((menu) => {
150
+ menu.remove();
151
+ });
152
+ // Observe for new context menus being added
153
+ const observer = new MutationObserver((mutations) => {
154
+ mutations.forEach((mutation) => {
155
+ mutation.addedNodes.forEach((node) => {
156
+ if (node.nodeType === 1 && node.classList && node.classList.contains('context_menu')) {
157
+ node.remove();
158
+ waLog('Removed dynamically added context menu', 'removeContextMenus');
159
+ }
160
+ });
161
+ });
162
+ });
163
+ observer.observe(document.body, { childList: true, subtree: true });
164
+ waLog('Context menu observer active', 'removeContextMenus');
165
+ }
166
+
167
+ runInGsftMainOnlyAndProtectByURL(removeContextMenus, '_list.do');
168
+ """
169
+ return script
170
+
139
171
  def _get_visible_list(self, page: Page):
140
172
  self._wait_for_ready(page)
141
173
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: browsergym-workarena
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: WorkArena benchmark for BrowserGym
5
5
  Project-URL: homepage, https://github.com/ServiceNow/WorkArena
6
6
  Author: Léo Boisvert, Alex Drouin, Maxime Gasse, Alex Lacoste, Manuel Del Verme, Megh Thakkar
@@ -1,6 +1,6 @@
1
- browsergym/workarena/__init__.py,sha256=ho1CCagiI_bHKbtdqNIG5SJNDdQHPEUaCf4xzjhtj_I,6676
1
+ browsergym/workarena/__init__.py,sha256=eTaNZq7KPn0T_fFH1KuaP_NuwCOC4R0VqJjVcB6kMpo,6676
2
2
  browsergym/workarena/config.py,sha256=n_nE6G08Edschfv9tKvJ1CWngpbaO3Uobxmbk9vfESU,8838
3
- browsergym/workarena/install.py,sha256=sgj8h0VXMqXue7xrFrvlXHm2XryvyWEf6v_SJSUd9yc,43197
3
+ browsergym/workarena/install.py,sha256=pfvZ1HSOjHpWeMkVO6V0NKbha_JP3FGEKWbjuZ9UdV8,52825
4
4
  browsergym/workarena/instance.py,sha256=nPDMCjdleQLlidpkyRGNm9VBSztAsz7bjeGAL9RO0M0,8396
5
5
  browsergym/workarena/utils.py,sha256=mD6RqVua-m1-mKM1RGGlUEu1s6un0ZI9a5ZTPN7g1hY,3199
6
6
  browsergym/workarena/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -78,9 +78,9 @@ browsergym/workarena/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
78
78
  browsergym/workarena/tasks/base.py,sha256=Ikh_A5I9_9acHFQCcnVMEnlBg3u3QHQD2I_NbGvD6SE,6411
79
79
  browsergym/workarena/tasks/comp_building_block.py,sha256=Lg3KbAWrxzAHe5XbPN6L8bvdu7mfJpmBvI7jXeSDwKE,194
80
80
  browsergym/workarena/tasks/dashboard.py,sha256=6ohpC40zpK1NLlfYM7RIqeenmuEuoIL9wOBUdG3JTFI,35842
81
- browsergym/workarena/tasks/form.py,sha256=eTITgbZnz0EpxIKlJNXpjZ2v5kUXLU9rtM5PUh2Qhk4,65352
81
+ browsergym/workarena/tasks/form.py,sha256=wMGbT-6-_KTUaaPDxYM0lMasCc8jZ4cD9Y7YFDkLEq8,66684
82
82
  browsergym/workarena/tasks/knowledge.py,sha256=zf-Rx6C8OhJcULEeWe4IVzN_SeDNgQ0jSGKi16GIJXk,13671
83
- browsergym/workarena/tasks/list.py,sha256=6Z8UypPFtvgpoT-Wm0pRpAfG-YgqXSz5Eya1P0z5KJQ,56628
83
+ browsergym/workarena/tasks/list.py,sha256=8yHpoOoPZIG5CcBNLEAUgEAmTJ91k1kNT3qUXc99PT0,58176
84
84
  browsergym/workarena/tasks/mark_duplicate_problem.py,sha256=2znPoyuC47hkIEz59jWR-KB2o4GKJ9z5K_C-mpBqBfE,7278
85
85
  browsergym/workarena/tasks/navigation.py,sha256=Y80DpL8xBA8u9zSudW0W6Vf4qaRZUgW-jQO7pl6gOFs,8729
86
86
  browsergym/workarena/tasks/send_chat_message.py,sha256=8yWSBEMDpv_reU4QH92rjtyPV6ZjhOAgby465Olc3jM,3854
@@ -132,8 +132,8 @@ browsergym/workarena/tasks/utils/js_utils.js,sha256=n97fmY2Jkr59rEcQSuSbCnn1L2ZN
132
132
  browsergym/workarena/tasks/utils/private_tasks.py,sha256=r7Z9SnBMuZdZ2i-tK6eULj0q8hclANXFSzdLl49KYHI,2128
133
133
  browsergym/workarena/tasks/utils/string.py,sha256=ir5_ASD9QSFMZ9kuHo2snSXRuSfv_wROH6nxBLOTP4I,330
134
134
  browsergym/workarena/tasks/utils/utils.py,sha256=xQD-njEwgN7qxfn1dLBN8MYfd3kl3TuVfpmI1yxML9k,955
135
- browsergym_workarena-0.5.2.dist-info/METADATA,sha256=DLprG6i689Q5htAHQTocxfXSLgVz2iKgeGZPGzwX1p8,10242
136
- browsergym_workarena-0.5.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
137
- browsergym_workarena-0.5.2.dist-info/entry_points.txt,sha256=1lCeAbQFCcU6UTFwS5QIA3TKhT2P9ZabaZKT7sIShKc,137
138
- browsergym_workarena-0.5.2.dist-info/licenses/LICENSE,sha256=sZLFiZHo_1hcxXRhXUDnQYVATUuWwRCdQjBxqxNnNEs,579
139
- browsergym_workarena-0.5.2.dist-info/RECORD,,
135
+ browsergym_workarena-0.5.3.dist-info/METADATA,sha256=FrJvAgWQBceRFM9YGWZiTxyBYqMgeDXbIfKZM7QE6js,10242
136
+ browsergym_workarena-0.5.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
137
+ browsergym_workarena-0.5.3.dist-info/entry_points.txt,sha256=1lCeAbQFCcU6UTFwS5QIA3TKhT2P9ZabaZKT7sIShKc,137
138
+ browsergym_workarena-0.5.3.dist-info/licenses/LICENSE,sha256=sZLFiZHo_1hcxXRhXUDnQYVATUuWwRCdQjBxqxNnNEs,579
139
+ browsergym_workarena-0.5.3.dist-info/RECORD,,