From 4ae42e5eee04bea64b2923bc773c682a40ef1c7e Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Wed, 7 Oct 2026 15:53:37 +0800 Subject: [PATCH 1/2] TR-22340: Updated automation_id auto creation with output message and error handling --- trcli/api/api_request_handler.py | 44 +++++++++++++++++++++++ trcli/api/case_field_handler.py | 58 ++++++++++++++++++++++++++++++- trcli/api/project_based_client.py | 45 +++++++++++++++++++++--- trcli/constants.py | 10 ++++++ 4 files changed, 152 insertions(+), 5 deletions(-) diff --git a/trcli/api/api_request_handler.py b/trcli/api/api_request_handler.py index 0ff4ac3..47e4e63 100644 --- a/trcli/api/api_request_handler.py +++ b/trcli/api/api_request_handler.py @@ -152,6 +152,50 @@ def check_automation_id_field(self, project_id: int) -> Union[str, None]: else: return response.error_message + def automation_id_field_exists(self, project_id: int) -> bool: + """ + Checks whether the automation_id field exists at all (under either known system + name), regardless of whether it's active/in-scope for ``project_id``. + + Used to decide whether to offer field auto-creation (field genuinely absent) vs. + showing the static "fix its configuration" message (field exists but is inactive + or not scoped to this project - creating a duplicate wouldn't help and TestRail + does not allow two custom fields with the same system name anyway). + + :param project_id: the id of the project (unused directly here, kept for a + consistent signature alongside check_automation_id_field) + :return: True if a field with the automation_id system name exists, else False + """ + response = self.client.send_get("get_case_fields") + if response.error_message: + return False + fields: List = response.response_text + return any( + field["system_name"] in [OLD_SYSTEM_NAME_AUTOMATION_ID, UPDATED_SYSTEM_NAME_AUTOMATION_ID] + for field in fields + ) + + def add_automation_id_field(self, project_id: int) -> Tuple[dict, str, int]: + """ + Create the missing automation_id custom case field, scoped to ``project_id`` + (project-scoped, not global - the safer default: it only affects this one + project rather than silently mutating every project in the TestRail instance). + + Callers must only invoke this after explicit user confirmation. + + :param project_id: the id of the project to scope the new field to + :return: Tuple with (created_field_dict, error_message, status_code) + """ + # TestRail auto-prefixes "custom_" to the name we pass, so a literal + # "automation_id" here produces system_name "custom_automation_id" + return self.case_field_handler.add_case_field( + field_type="String", + name="automation_id", + label="Automation ID", + project_ids=[project_id], + is_global=False, + ) + def get_project_data(self, project_name: str, project_id: int = None) -> ProjectData: """ Send get_projects with project name diff --git a/trcli/api/case_field_handler.py b/trcli/api/case_field_handler.py index ba26d6b..031a36d 100644 --- a/trcli/api/case_field_handler.py +++ b/trcli/api/case_field_handler.py @@ -3,9 +3,11 @@ It manages case field query operations including: - Retrieving all available test case custom fields +- Creating new test case custom fields (e.g. auto-creating a missing + automation_id field - see ApiRequestHandler.add_automation_id_field) """ -from beartype.typing import Tuple +from beartype.typing import List, Optional, Tuple from trcli.api.api_client import APIClient @@ -31,3 +33,57 @@ def get_case_fields(self) -> Tuple[list, str]: if response.error_message: return [], response.error_message return response.response_text, "" + + def add_case_field( + self, + field_type: str, + name: str, + label: str, + project_ids: Optional[List[int]] = None, + is_global: bool = False, + ) -> Tuple[dict, str, int]: + """ + Create a new test case custom field via ``POST add_case_field``. + + This is an admin-level, instance-wide-visible action in TestRail (creating a + custom field affects every project it's scoped to) - callers must only invoke + this after explicit user confirmation (see + ``ProjectBasedClient.resolve_project``'s automation_id auto-creation prompt), + never silently. + + :param field_type: TestRail field type string. "String" confirmed valid against a + live TestRail instance (trcliaieval staging, 2026-10-05 spike) - creates a + working plain-text field. + :param name: system name for the field (without the "custom_" prefix TestRail + adds automatically), e.g. "automation_id". + :param label: human-readable label shown in the TestRail UI, e.g. "Automation ID". + :param project_ids: list of project IDs to scope this field to. Ignored when + ``is_global`` is True. A project-scoped field (``is_global=False``) is the + safer default - it only affects the specified project(s) rather than every + project in the TestRail instance. + :param is_global: when True, the field is created for every project instance-wide. + :returns: Tuple with (created_field_dict, error_message, status_code). On + failure, created_field_dict is {}. status_code lets callers distinguish a + permission error (403 - field management requires TestRail admin rights) + from other failures. + """ + payload = { + "type": field_type, + "name": name, + "label": label, + "configs": [ + { + "context": { + "is_global": is_global, + "project_ids": [] if is_global else (project_ids or []), + }, + "options": {"is_required": False}, + } + ], + # NOTE: must always be True, independent of is_global. + "include_all": True, + } + response = self.client.send_post("add_case_field", payload) + if response.error_message: + return {}, response.error_message, response.status_code + return response.response_text, "", response.status_code diff --git a/trcli/api/project_based_client.py b/trcli/api/project_based_client.py index 99d99cb..76c7699 100644 --- a/trcli/api/project_based_client.py +++ b/trcli/api/project_based_client.py @@ -79,10 +79,47 @@ def resolve_project(self): self._validate_project_id() if self.environment.auto_creation_response: if self.environment.case_matcher == MatchersParser.AUTO: - automation_id_error = self.api_request_handler.check_automation_id_field(self.project.project_id) - if automation_id_error: - self.environment.elog(automation_id_error) - exit(1) + self._check_or_create_automation_id_field() + self.environment.log("Done.") + + def _check_or_create_automation_id_field(self): + """ + Checks the automation_id field is properly configured for this project. + + Exits the process (code 1) if the field is unavailable and either the user + declines creation, lacks permission to create it, or creation fails for any + other reason. + """ + automation_id_error = self.api_request_handler.check_automation_id_field(self.project.project_id) + if not automation_id_error: + return + + if self.api_request_handler.automation_id_field_exists(self.project.project_id): + self.environment.elog(automation_id_error) + exit(1) + + prompt_message = PROMPT_MESSAGES["create_automation_id_field"].format( + project_name=self.environment.project or self.project.project_id, + ) + if not self.environment.get_prompt_response_for_auto_creation(prompt_message): + self.environment.elog(FAULT_MAPPING["no_user_agreement"].format(type="automation_id field")) + exit(1) + + self.environment.log("Creating automation_id field. ", new_line=False) + _, create_error, status_code = self.api_request_handler.add_automation_id_field(self.project.project_id) + if create_error: + if status_code == 403: + self.environment.elog(FAULT_MAPPING["automation_id_creation_permission_denied"]) + else: + self.environment.elog(FAULT_MAPPING["automation_id_creation_failed"].format(error_message=create_error)) + exit(1) + + # Re-check so _active_automation_id_field gets set on both self.api_request_handler + # and its case_handler before any case matching/creation happens. + automation_id_error = self.api_request_handler.check_automation_id_field(self.project.project_id) + if automation_id_error: + self.environment.elog(automation_id_error) + exit(1) self.environment.log("Done.") def _validate_project_id(self): diff --git a/trcli/constants.py b/trcli/constants.py index 0460684..5f0f6fa 100644 --- a/trcli/constants.py +++ b/trcli/constants.py @@ -56,6 +56,13 @@ f" - System Name: automation_id\n" f" - Type: Text (or String)\n" f" - Is Active: True", + automation_id_creation_permission_denied="Unable to create the automation_id field: TestRail reported a " + "permission error. Creating custom case fields requires TestRail Administrator rights. Please ask a " + "TestRail admin to create it for you, or configure it manually in the TestRail Administration under " + "Customizations > Case Fields (System Name: automation_id, Type: Text/String, Is Active: True).", + automation_id_creation_failed="Unable to create the automation_id field automatically: {error_message}\n" + "Please configure it manually in the TestRail Administration under Customizations > Case Fields " + "(System Name: automation_id, Type: Text/String, Is Active: True).", proxy_connection_error="Failed to connect to the proxy server. Please check the proxy settings and ensure the server is available.", proxy_authentication_failed="Proxy authentication failed for proxy. Please verify the username and password.", proxy_timeout="The connection to the proxy server timed out. Please try again later or check the proxy server's availability.", @@ -118,6 +125,9 @@ create_missing_test_cases="Some of the test cases in provided file are missing " "in TestRail or the IDs are not specified.\n" "Would you like to create missing test cases under project: '{project_name}'?", + create_automation_id_field="The automation_id field was not found in TestRail.\n" + "Would you like to create it now, scoped to project '{project_name}'? " + "(This requires TestRail Administrator rights.)", ) TOOL_VERSION = f"""TestRail CLI v{trcli.__version__} From e0437f6448031e7fb48ecee28c35599e52147eb7 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Wed, 7 Oct 2026 19:18:47 +0800 Subject: [PATCH 2/2] TR-22340: Fix auto create prompt not appearing due to gated auto_creation_response --- trcli/api/project_based_client.py | 6 +++--- trcli/api/results_uploader.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/trcli/api/project_based_client.py b/trcli/api/project_based_client.py index 76c7699..506453a 100644 --- a/trcli/api/project_based_client.py +++ b/trcli/api/project_based_client.py @@ -77,9 +77,9 @@ def resolve_project(self): self.environment.log("Checking project. ", new_line=False) self.project = self.api_request_handler.get_project_data(self.environment.project, self.environment.project_id) self._validate_project_id() - if self.environment.auto_creation_response: - if self.environment.case_matcher == MatchersParser.AUTO: - self._check_or_create_automation_id_field() + if self.environment.case_matcher == MatchersParser.AUTO: + # NOTE: must NOT be gated behind `self.environment.auto_creation_response` here. + self._check_or_create_automation_id_field() self.environment.log("Done.") def _check_or_create_automation_id_field(self): diff --git a/trcli/api/results_uploader.py b/trcli/api/results_uploader.py index 0922bb8..5fe5237 100644 --- a/trcli/api/results_uploader.py +++ b/trcli/api/results_uploader.py @@ -72,7 +72,8 @@ def upload_results(self): added_sections = None added_test_cases = None - if self.environment.auto_creation_response and not all_cases_have_ids: + if not all_cases_have_ids: + # NOTE: must NOT be gated behind `self.environment.auto_creation_response` here. added_sections, result_code = self.add_missing_sections(self.project.project_id) if result_code == -1: revert_logs = self.rollback_changes(