alita-sdk 0.3.457__py3-none-any.whl → 0.3.486__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.
Potentially problematic release.
This version of alita-sdk might be problematic. Click here for more details.
- alita_sdk/cli/__init__.py +10 -0
- alita_sdk/cli/__main__.py +17 -0
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +155 -0
- alita_sdk/cli/agent_loader.py +194 -0
- alita_sdk/cli/agent_ui.py +228 -0
- alita_sdk/cli/agents.py +3592 -0
- alita_sdk/cli/callbacks.py +647 -0
- alita_sdk/cli/cli.py +168 -0
- alita_sdk/cli/config.py +306 -0
- alita_sdk/cli/context/__init__.py +30 -0
- alita_sdk/cli/context/cleanup.py +198 -0
- alita_sdk/cli/context/manager.py +731 -0
- alita_sdk/cli/context/message.py +285 -0
- alita_sdk/cli/context/strategies.py +289 -0
- alita_sdk/cli/context/token_estimation.py +127 -0
- alita_sdk/cli/formatting.py +182 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1256 -0
- alita_sdk/cli/mcp_loader.py +315 -0
- alita_sdk/cli/toolkit.py +327 -0
- alita_sdk/cli/toolkit_loader.py +85 -0
- alita_sdk/cli/tools/__init__.py +43 -0
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +1665 -0
- alita_sdk/cli/tools/planning.py +389 -0
- alita_sdk/cli/tools/terminal.py +414 -0
- alita_sdk/community/__init__.py +64 -8
- alita_sdk/community/inventory/__init__.py +224 -0
- alita_sdk/community/inventory/config.py +257 -0
- alita_sdk/community/inventory/enrichment.py +2137 -0
- alita_sdk/community/inventory/extractors.py +1469 -0
- alita_sdk/community/inventory/ingestion.py +3172 -0
- alita_sdk/community/inventory/knowledge_graph.py +1457 -0
- alita_sdk/community/inventory/parsers/__init__.py +218 -0
- alita_sdk/community/inventory/parsers/base.py +295 -0
- alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
- alita_sdk/community/inventory/parsers/go_parser.py +851 -0
- alita_sdk/community/inventory/parsers/html_parser.py +389 -0
- alita_sdk/community/inventory/parsers/java_parser.py +593 -0
- alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
- alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
- alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
- alita_sdk/community/inventory/parsers/python_parser.py +604 -0
- alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
- alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
- alita_sdk/community/inventory/parsers/text_parser.py +322 -0
- alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
- alita_sdk/community/inventory/patterns/__init__.py +61 -0
- alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
- alita_sdk/community/inventory/patterns/loader.py +348 -0
- alita_sdk/community/inventory/patterns/registry.py +198 -0
- alita_sdk/community/inventory/presets.py +535 -0
- alita_sdk/community/inventory/retrieval.py +1403 -0
- alita_sdk/community/inventory/toolkit.py +169 -0
- alita_sdk/community/inventory/visualize.py +1370 -0
- alita_sdk/configurations/bitbucket.py +0 -3
- alita_sdk/runtime/clients/client.py +99 -26
- alita_sdk/runtime/langchain/assistant.py +4 -2
- alita_sdk/runtime/langchain/constants.py +2 -1
- alita_sdk/runtime/langchain/langraph_agent.py +134 -31
- alita_sdk/runtime/langchain/utils.py +1 -1
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/toolkits/__init__.py +2 -0
- alita_sdk/runtime/toolkits/application.py +1 -1
- alita_sdk/runtime/toolkits/mcp.py +46 -36
- alita_sdk/runtime/toolkits/planning.py +171 -0
- alita_sdk/runtime/toolkits/tools.py +39 -6
- alita_sdk/runtime/tools/function.py +17 -5
- alita_sdk/runtime/tools/llm.py +249 -14
- alita_sdk/runtime/tools/planning/__init__.py +36 -0
- alita_sdk/runtime/tools/planning/models.py +246 -0
- alita_sdk/runtime/tools/planning/wrapper.py +607 -0
- alita_sdk/runtime/tools/vectorstore_base.py +41 -6
- alita_sdk/runtime/utils/mcp_oauth.py +80 -0
- alita_sdk/runtime/utils/streamlit.py +6 -10
- alita_sdk/runtime/utils/toolkit_utils.py +19 -4
- alita_sdk/tools/__init__.py +54 -27
- alita_sdk/tools/ado/repos/repos_wrapper.py +1 -2
- alita_sdk/tools/base_indexer_toolkit.py +150 -19
- alita_sdk/tools/bitbucket/__init__.py +2 -2
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +95 -6
- alita_sdk/tools/chunkers/universal_chunker.py +269 -0
- alita_sdk/tools/code_indexer_toolkit.py +55 -22
- alita_sdk/tools/elitea_base.py +86 -21
- alita_sdk/tools/jira/__init__.py +1 -1
- alita_sdk/tools/jira/api_wrapper.py +91 -40
- alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
- alita_sdk/tools/qtest/__init__.py +1 -1
- alita_sdk/tools/qtest/api_wrapper.py +871 -32
- alita_sdk/tools/sharepoint/api_wrapper.py +22 -2
- alita_sdk/tools/sharepoint/authorization_helper.py +17 -1
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +8 -2
- alita_sdk/tools/zephyr_essential/api_wrapper.py +12 -13
- {alita_sdk-0.3.457.dist-info → alita_sdk-0.3.486.dist-info}/METADATA +146 -2
- {alita_sdk-0.3.457.dist-info → alita_sdk-0.3.486.dist-info}/RECORD +102 -40
- alita_sdk-0.3.486.dist-info/entry_points.txt +2 -0
- {alita_sdk-0.3.457.dist-info → alita_sdk-0.3.486.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.457.dist-info → alita_sdk-0.3.486.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.457.dist-info → alita_sdk-0.3.486.dist-info}/top_level.txt +0 -0
|
@@ -78,6 +78,73 @@ Steps: Array of test steps with Description and Expected Result.
|
|
|
78
78
|
Json object
|
|
79
79
|
"""
|
|
80
80
|
|
|
81
|
+
# DQL Syntax Documentation - reusable across all DQL-based search tools
|
|
82
|
+
DQL_SYNTAX_DOCS = """
|
|
83
|
+
CRITICAL: USE SINGLE QUOTES ONLY - DQL does not support double quotes!
|
|
84
|
+
- ✓ CORRECT: Description ~ 'Forgot Password'
|
|
85
|
+
- ✗ WRONG: Description ~ "Forgot Password"
|
|
86
|
+
|
|
87
|
+
LIMITATION - CANNOT SEARCH BY LINKED OBJECTS:
|
|
88
|
+
- ✗ Searching by linked requirements, test cases, defects is NOT supported
|
|
89
|
+
- Use dedicated find_*_by_*_id tools for relationship queries
|
|
90
|
+
|
|
91
|
+
SEARCHABLE FIELDS:
|
|
92
|
+
- Direct fields: Id, Name, Description, Status, Type, Priority, etc.
|
|
93
|
+
- Custom fields: Use exact field name from project configuration
|
|
94
|
+
- Date fields: MUST use ISO DateTime format (e.g., '2024-01-01T00:00:00.000Z')
|
|
95
|
+
|
|
96
|
+
ENTITY-SPECIFIC NOTES:
|
|
97
|
+
- test-logs: Only support 'Execution Start Date' and 'Execution End Date' queries
|
|
98
|
+
- builds/test-cycles: Also support 'Created Date' and 'Last Modified Date'
|
|
99
|
+
- defects: Can use 'Affected Release/Build' and 'Fixed Release/Build'
|
|
100
|
+
|
|
101
|
+
SYNTAX RULES:
|
|
102
|
+
1. ALL string values MUST use single quotes (never double quotes)
|
|
103
|
+
2. Field names with spaces MUST be in single quotes: 'Created Date' > '2024-01-01T00:00:00.000Z'
|
|
104
|
+
3. Use ~ for 'contains', !~ for 'not contains': Description ~ 'login'
|
|
105
|
+
4. Use 'is not empty' for non-empty check: Name is 'not empty'
|
|
106
|
+
5. Operators: =, !=, <, >, <=, >=, in, ~, !~
|
|
107
|
+
|
|
108
|
+
EXAMPLES:
|
|
109
|
+
- Id = 'TC-123' or Id = 'RQ-15' or Id = 'DF-100' (depending on entity type)
|
|
110
|
+
- Description ~ 'Forgot Password'
|
|
111
|
+
- Status = 'New' and Priority = 'High'
|
|
112
|
+
- Name ~ 'login'
|
|
113
|
+
- 'Created Date' > '2024-01-01T00:00:00.000Z'
|
|
114
|
+
- 'Execution Start Date' > '2024-01-01T00:00:00.000Z' (for test-logs)
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
# Supported object types for DQL search (based on QTest Search API documentation)
|
|
118
|
+
# Note: Prefixes are configurable per-project but these are standard defaults
|
|
119
|
+
# Modules (MD) are NOT searchable via DQL - use get_modules tool instead
|
|
120
|
+
# Test-logs have NO prefix - they are internal records accessed via test runs
|
|
121
|
+
|
|
122
|
+
# Entity types with ID prefixes (can be looked up by ID like TC-123)
|
|
123
|
+
QTEST_OBJECT_TYPES = {
|
|
124
|
+
# Core test management entities
|
|
125
|
+
'test-cases': {'prefix': 'TC', 'name': 'Test Case', 'description': 'Test case definitions with steps'},
|
|
126
|
+
'test-runs': {'prefix': 'TR', 'name': 'Test Run', 'description': 'Execution instances of test cases'},
|
|
127
|
+
'defects': {'prefix': 'DF', 'name': 'Defect', 'description': 'Bugs/issues found during testing'},
|
|
128
|
+
'requirements': {'prefix': 'RQ', 'name': 'Requirement', 'description': 'Requirements to be tested'},
|
|
129
|
+
|
|
130
|
+
# Test organization entities
|
|
131
|
+
'test-suites': {'prefix': 'TS', 'name': 'Test Suite', 'description': 'Collections of test runs'},
|
|
132
|
+
'test-cycles': {'prefix': 'CL', 'name': 'Test Cycle', 'description': 'Test execution cycles'},
|
|
133
|
+
|
|
134
|
+
# Release management entities
|
|
135
|
+
'releases': {'prefix': 'RL', 'name': 'Release', 'description': 'Software releases'},
|
|
136
|
+
'builds': {'prefix': 'BL', 'name': 'Build', 'description': 'Builds within releases'},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Entity types searchable via DQL but without ID prefixes
|
|
140
|
+
# These can be searched by specific fields only, not by ID
|
|
141
|
+
QTEST_SEARCHABLE_ONLY_TYPES = {
|
|
142
|
+
'test-logs': {
|
|
143
|
+
'name': 'Test Log',
|
|
144
|
+
'description': "Execution logs. Only date queries supported (Execution Start Date, Execution End Date). For specific log details, use test run's 'Latest Test Log' field."
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
81
148
|
logger = logging.getLogger(__name__)
|
|
82
149
|
|
|
83
150
|
QtestDataQuerySearch = create_model(
|
|
@@ -154,6 +221,34 @@ FindTestCasesByRequirementId = create_model(
|
|
|
154
221
|
include_details=(Optional[bool], Field(description="If true, returns full test case details. If false (default), returns Id, QTest Id, Name, and Description fields.", default=False)),
|
|
155
222
|
)
|
|
156
223
|
|
|
224
|
+
FindRequirementsByTestCaseId = create_model(
|
|
225
|
+
"FindRequirementsByTestCaseId",
|
|
226
|
+
test_case_id=(str, Field(description="Test case ID in format TC-123. This will find all requirements linked to this test case.")),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
FindTestRunsByTestCaseId = create_model(
|
|
230
|
+
"FindTestRunsByTestCaseId",
|
|
231
|
+
test_case_id=(str, Field(description="Test case ID in format TC-123. This will find all test runs associated with this test case.")),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
FindDefectsByTestRunId = create_model(
|
|
235
|
+
"FindDefectsByTestRunId",
|
|
236
|
+
test_run_id=(str, Field(description="Test run ID in format TR-123. This will find all defects associated with this test run.")),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Generic search model for any entity type
|
|
240
|
+
GenericDqlSearch = create_model(
|
|
241
|
+
"GenericDqlSearch",
|
|
242
|
+
object_type=(str, Field(description="Entity type to search: 'test-cases', 'test-runs', 'defects', 'requirements', 'test-suites', 'test-cycles', 'test-logs', 'releases', or 'builds'. Note: test-logs only support date queries; modules are NOT searchable - use get_modules tool.")),
|
|
243
|
+
dql=(str, Field(description="QTest Data Query Language (DQL) query string")),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Generic find by ID model - only for entities with ID prefixes (NOT test-logs)
|
|
247
|
+
FindEntityById = create_model(
|
|
248
|
+
"FindEntityById",
|
|
249
|
+
entity_id=(str, Field(description="Entity ID with prefix: TC-123 (test case), RQ-15 (requirement), DF-100 (defect), TR-39 (test run), TS-5 (test suite), CL-3 (test cycle), RL-1 (release), or BL-2 (build). Note: test-logs and modules do NOT have ID prefixes.")),
|
|
250
|
+
)
|
|
251
|
+
|
|
157
252
|
NoInput = create_model(
|
|
158
253
|
"NoInput"
|
|
159
254
|
)
|
|
@@ -286,9 +381,18 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
286
381
|
field_def = field_definitions[field_name]
|
|
287
382
|
field_id = field_def['field_id']
|
|
288
383
|
is_multiple = field_def.get('multiple', False)
|
|
384
|
+
has_allowed_values = bool(field_def.get('values')) # True = dropdown, False = text
|
|
289
385
|
|
|
290
|
-
if
|
|
291
|
-
#
|
|
386
|
+
if not has_allowed_values:
|
|
387
|
+
# TEXT FIELD: can clear with empty string
|
|
388
|
+
props_dict[field_name] = {
|
|
389
|
+
'field_id': field_id,
|
|
390
|
+
'field_name': field_name,
|
|
391
|
+
'field_value': '',
|
|
392
|
+
'field_value_name': ''
|
|
393
|
+
}
|
|
394
|
+
elif is_multiple:
|
|
395
|
+
# MULTI-SELECT: can clear using empty array "[]"
|
|
292
396
|
props_dict[field_name] = {
|
|
293
397
|
'field_id': field_id,
|
|
294
398
|
'field_name': field_name,
|
|
@@ -296,7 +400,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
296
400
|
'field_value_name': None
|
|
297
401
|
}
|
|
298
402
|
else:
|
|
299
|
-
#
|
|
403
|
+
# SINGLE-SELECT: QTest API limitation - cannot clear to empty
|
|
300
404
|
# Note: Users CAN clear these fields from UI, but API doesn't expose this capability
|
|
301
405
|
validation_errors.append(
|
|
302
406
|
f"⚠️ Cannot clear single-select field '{field_name}' - this is a QTest API limitation "
|
|
@@ -421,14 +525,21 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
421
525
|
|
|
422
526
|
body = swagger_client.TestCaseWithCustomFieldResource(properties=props)
|
|
423
527
|
|
|
424
|
-
#
|
|
425
|
-
#
|
|
528
|
+
# Handle core fields: Name, Description, Precondition
|
|
529
|
+
# These are set if explicitly provided in the input
|
|
530
|
+
# None or empty string means "clear this field" (except Name which is required)
|
|
426
531
|
if 'Name' in test_case:
|
|
427
|
-
|
|
532
|
+
# Name is required - use 'Untitled' as fallback if null/empty
|
|
533
|
+
name_value = test_case['Name']
|
|
534
|
+
body.name = name_value if name_value else 'Untitled'
|
|
535
|
+
|
|
428
536
|
if 'Precondition' in test_case:
|
|
429
|
-
|
|
537
|
+
# Allow clearing with None or empty string
|
|
538
|
+
body.precondition = test_case['Precondition'] if test_case['Precondition'] is not None else ''
|
|
539
|
+
|
|
430
540
|
if 'Description' in test_case:
|
|
431
|
-
|
|
541
|
+
# Allow clearing with None or empty string
|
|
542
|
+
body.description = test_case['Description'] if test_case['Description'] is not None else ''
|
|
432
543
|
|
|
433
544
|
if parent_id:
|
|
434
545
|
body.parent_id = parent_id
|
|
@@ -678,16 +789,30 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
678
789
|
|
|
679
790
|
for field_name, field_info in sorted(field_definitions.items()):
|
|
680
791
|
required_marker = " (Required)" if field_info.get('required') else ""
|
|
681
|
-
|
|
792
|
+
has_values = bool(field_info.get('values'))
|
|
793
|
+
is_multiple = field_info.get('multiple', False)
|
|
794
|
+
|
|
795
|
+
# Determine field type label
|
|
796
|
+
if not has_values:
|
|
797
|
+
type_label = "Text"
|
|
798
|
+
elif is_multiple:
|
|
799
|
+
type_label = "Multi-select"
|
|
800
|
+
else:
|
|
801
|
+
type_label = "Single-select"
|
|
682
802
|
|
|
683
|
-
|
|
803
|
+
output.append(f"\n{field_name} ({type_label}{required_marker}):")
|
|
804
|
+
|
|
805
|
+
if has_values:
|
|
684
806
|
for value_name, value_id in sorted(field_info['values'].items()):
|
|
685
|
-
output.append(f" - {value_name}
|
|
807
|
+
output.append(f" - {value_name}")
|
|
686
808
|
else:
|
|
687
|
-
output.append("
|
|
809
|
+
output.append(" Free text input. Set to null to clear.")
|
|
688
810
|
|
|
689
|
-
output.append("\n\
|
|
690
|
-
|
|
811
|
+
output.append("\n\n--- Field Type Guide ---")
|
|
812
|
+
output.append("\nText fields: Use null to clear, provide string value to set.")
|
|
813
|
+
output.append("\nSingle-select: Provide exact value name from the list above. Cannot be cleared via API.")
|
|
814
|
+
output.append("\nMulti-select: Provide value as array [\"val1\", \"val2\"]. Use null to clear.")
|
|
815
|
+
return '\n'.join(output)
|
|
691
816
|
|
|
692
817
|
def get_all_test_cases_fields_for_project(self, force_refresh: bool = False) -> str:
|
|
693
818
|
"""
|
|
@@ -749,6 +874,37 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
749
874
|
raise ToolException(
|
|
750
875
|
f"Unable to create test case in project - {self.qtest_project_id} with the following content:\n{test_case_content}.\n\n Stacktrace was {stacktrace}") from e
|
|
751
876
|
|
|
877
|
+
def __format_property_value(self, prop: dict) -> Any:
|
|
878
|
+
"""Format property value for display, detecting field type from response structure.
|
|
879
|
+
|
|
880
|
+
Detection rules based on API response patterns:
|
|
881
|
+
- Text field: field_value_name is empty/None
|
|
882
|
+
- Multi-select: field_value_name starts with '[' and ends with ']'
|
|
883
|
+
- Single-select: field_value_name is plain text (no brackets)
|
|
884
|
+
|
|
885
|
+
Args:
|
|
886
|
+
prop: Property dict from API response with field_value and field_value_name
|
|
887
|
+
|
|
888
|
+
Returns:
|
|
889
|
+
Formatted value: list for multi-select, string for others
|
|
890
|
+
"""
|
|
891
|
+
field_value = prop.get('field_value') or ''
|
|
892
|
+
field_value_name = prop.get('field_value_name')
|
|
893
|
+
|
|
894
|
+
# Text field: no field_value_name, use field_value directly
|
|
895
|
+
if not field_value_name:
|
|
896
|
+
return field_value
|
|
897
|
+
|
|
898
|
+
# Multi-select: field_value_name is bracketed like '[value1, value2]'
|
|
899
|
+
if isinstance(field_value_name, str) and field_value_name.startswith('[') and field_value_name.endswith(']'):
|
|
900
|
+
inner = field_value_name[1:-1].strip() # Remove brackets
|
|
901
|
+
if inner:
|
|
902
|
+
return [v.strip() for v in inner.split(',')]
|
|
903
|
+
return [] # Empty multi-select
|
|
904
|
+
|
|
905
|
+
# Single-select: plain text value
|
|
906
|
+
return field_value_name
|
|
907
|
+
|
|
752
908
|
def __parse_data(self, response_to_parse: dict, parsed_data: list, extract_images: bool=False, prompt: str=None):
|
|
753
909
|
import html
|
|
754
910
|
|
|
@@ -775,10 +931,9 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
775
931
|
field_name = prop.get('field_name')
|
|
776
932
|
if not field_name:
|
|
777
933
|
continue
|
|
778
|
-
|
|
779
|
-
#
|
|
780
|
-
|
|
781
|
-
parsed_data_row[field_name] = field_value
|
|
934
|
+
|
|
935
|
+
# Format value based on field type (multi-select as array, etc.)
|
|
936
|
+
parsed_data_row[field_name] = self.__format_property_value(prop)
|
|
782
937
|
|
|
783
938
|
parsed_data.append(parsed_data_row)
|
|
784
939
|
|
|
@@ -837,38 +992,139 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
837
992
|
parsed_data = self.__perform_search_by_dql(dql)
|
|
838
993
|
return parsed_data[0]['QTest Id']
|
|
839
994
|
|
|
840
|
-
def
|
|
841
|
-
"""
|
|
995
|
+
def __find_qtest_internal_id(self, object_type: str, entity_id: str) -> int:
|
|
996
|
+
"""Generic search for an entity's internal QTest ID using its external ID (e.g., TR-xxx, DF-xxx, RQ-xxx).
|
|
997
|
+
|
|
998
|
+
This is the unified method for looking up internal IDs. Use this instead of
|
|
999
|
+
the entity-specific methods (__find_qtest_requirement_id_by_id, etc.).
|
|
842
1000
|
|
|
843
1001
|
Args:
|
|
844
|
-
|
|
1002
|
+
object_type: QTest object type ('test-runs', 'defects', 'requirements', etc.)
|
|
1003
|
+
entity_id: Entity ID in format TR-123, DF-456, etc.
|
|
845
1004
|
|
|
846
1005
|
Returns:
|
|
847
|
-
int: Internal QTest ID for the
|
|
1006
|
+
int: Internal QTest ID for the entity
|
|
848
1007
|
|
|
849
1008
|
Raises:
|
|
850
|
-
ValueError: If
|
|
1009
|
+
ValueError: If entity is not found
|
|
851
1010
|
"""
|
|
852
|
-
dql = f"Id = '{
|
|
1011
|
+
dql = f"Id = '{entity_id}'"
|
|
853
1012
|
search_instance: SearchApi = swagger_client.SearchApi(self._client)
|
|
854
|
-
body = swagger_client.ArtifactSearchParams(object_type=
|
|
1013
|
+
body = swagger_client.ArtifactSearchParams(object_type=object_type, fields=['*'], query=dql)
|
|
855
1014
|
|
|
856
1015
|
try:
|
|
857
1016
|
response = search_instance.search_artifact(self.qtest_project_id, body)
|
|
858
1017
|
if response['total'] == 0:
|
|
859
1018
|
raise ValueError(
|
|
860
|
-
f"
|
|
861
|
-
f"Please verify the
|
|
1019
|
+
f"{object_type.capitalize()} '{entity_id}' not found in project {self.qtest_project_id}. "
|
|
1020
|
+
f"Please verify the {entity_id} ID exists."
|
|
862
1021
|
)
|
|
863
1022
|
return response['items'][0]['id']
|
|
864
1023
|
except ApiException as e:
|
|
865
1024
|
stacktrace = format_exc()
|
|
866
|
-
logger.error(f"Exception when searching for
|
|
1025
|
+
logger.error(f"Exception when searching for '{object_type}': '{entity_id}': \n {stacktrace}")
|
|
867
1026
|
raise ToolException(
|
|
868
|
-
f"Unable to search for
|
|
1027
|
+
f"Unable to search for {object_type} '{entity_id}' in project {self.qtest_project_id}. "
|
|
869
1028
|
f"Exception: \n{stacktrace}"
|
|
870
1029
|
) from e
|
|
871
1030
|
|
|
1031
|
+
def __find_qtest_requirement_id_by_id(self, requirement_id: str) -> int:
|
|
1032
|
+
"""Search for requirement's internal QTest ID using requirement ID (RQ-xxx format).
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
requirement_id: Requirement ID in format RQ-123
|
|
1036
|
+
|
|
1037
|
+
Returns:
|
|
1038
|
+
int: Internal QTest ID for the requirement
|
|
1039
|
+
|
|
1040
|
+
Raises:
|
|
1041
|
+
ValueError: If requirement is not found
|
|
1042
|
+
"""
|
|
1043
|
+
return self.__find_qtest_internal_id('requirements', requirement_id)
|
|
1044
|
+
|
|
1045
|
+
def __find_qtest_defect_id_by_id(self, defect_id: str) -> int:
|
|
1046
|
+
"""Search for defect's internal QTest ID using defect ID (DF-xxx format).
|
|
1047
|
+
|
|
1048
|
+
Args:
|
|
1049
|
+
defect_id: Defect ID in format DF-123
|
|
1050
|
+
|
|
1051
|
+
Returns:
|
|
1052
|
+
int: Internal QTest ID for the defect
|
|
1053
|
+
|
|
1054
|
+
Raises:
|
|
1055
|
+
ValueError: If defect is not found
|
|
1056
|
+
"""
|
|
1057
|
+
return self.__find_qtest_internal_id('defects', defect_id)
|
|
1058
|
+
|
|
1059
|
+
def __search_entity_by_id(self, object_type: str, entity_id: str) -> dict:
|
|
1060
|
+
"""Generic search for any entity by its ID (RQ-xxx, DF-xxx, etc.).
|
|
1061
|
+
|
|
1062
|
+
Uses the unified __parse_entity_item method for consistent parsing.
|
|
1063
|
+
|
|
1064
|
+
Args:
|
|
1065
|
+
object_type: QTest object type ('requirements', 'defects', etc.)
|
|
1066
|
+
entity_id: Entity ID in format prefix-number (RQ-123, DF-456)
|
|
1067
|
+
|
|
1068
|
+
Returns:
|
|
1069
|
+
dict: Entity data with all parsed fields, or None if not found
|
|
1070
|
+
"""
|
|
1071
|
+
dql = f"Id = '{entity_id}'"
|
|
1072
|
+
search_instance: SearchApi = swagger_client.SearchApi(self._client)
|
|
1073
|
+
body = swagger_client.ArtifactSearchParams(object_type=object_type, fields=['*'], query=dql)
|
|
1074
|
+
|
|
1075
|
+
try:
|
|
1076
|
+
response = search_instance.search_artifact(self.qtest_project_id, body)
|
|
1077
|
+
if response['total'] == 0:
|
|
1078
|
+
return None # Not found, but don't raise - caller handles this
|
|
1079
|
+
|
|
1080
|
+
# Use the unified parser
|
|
1081
|
+
return self.__parse_entity_item(object_type, response['items'][0])
|
|
1082
|
+
|
|
1083
|
+
except ApiException as e:
|
|
1084
|
+
logger.warning(f"Could not fetch details for {entity_id}: {e}")
|
|
1085
|
+
return None
|
|
1086
|
+
|
|
1087
|
+
def __get_entity_pid_by_internal_id(self, object_type: str, internal_id: int) -> str:
|
|
1088
|
+
"""Reverse lookup: get entity PID (TC-xxx, TR-xxx, etc.) from internal QTest ID.
|
|
1089
|
+
|
|
1090
|
+
Args:
|
|
1091
|
+
object_type: QTest object type ('test-cases', 'test-runs', 'defects', 'requirements')
|
|
1092
|
+
internal_id: Internal QTest ID (numeric)
|
|
1093
|
+
|
|
1094
|
+
Returns:
|
|
1095
|
+
str: Entity PID in format prefix-number (TC-123, TR-456, etc.) or None if not found
|
|
1096
|
+
"""
|
|
1097
|
+
search_instance = swagger_client.SearchApi(self._client)
|
|
1098
|
+
# Note: 'id' needs quotes for DQL when searching by internal ID
|
|
1099
|
+
body = swagger_client.ArtifactSearchParams(
|
|
1100
|
+
object_type=object_type,
|
|
1101
|
+
fields=['id', 'pid'],
|
|
1102
|
+
query=f"'id' = '{internal_id}'"
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
try:
|
|
1106
|
+
response = search_instance.search_artifact(self.qtest_project_id, body)
|
|
1107
|
+
if response['total'] > 0:
|
|
1108
|
+
return response['items'][0].get('pid')
|
|
1109
|
+
return None
|
|
1110
|
+
except ApiException as e:
|
|
1111
|
+
logger.warning(f"Could not get PID for {object_type} internal ID {internal_id}: {e}")
|
|
1112
|
+
return None
|
|
1113
|
+
|
|
1114
|
+
def __find_qtest_test_run_id_by_id(self, test_run_id: str) -> int:
|
|
1115
|
+
"""Search for test run's internal QTest ID using test run ID (TR-xxx format).
|
|
1116
|
+
|
|
1117
|
+
Args:
|
|
1118
|
+
test_run_id: Test run ID in format TR-123
|
|
1119
|
+
|
|
1120
|
+
Returns:
|
|
1121
|
+
int: Internal QTest ID for the test run
|
|
1122
|
+
|
|
1123
|
+
Raises:
|
|
1124
|
+
ValueError: If test run is not found
|
|
1125
|
+
"""
|
|
1126
|
+
return self.__find_qtest_internal_id('test-runs', test_run_id)
|
|
1127
|
+
|
|
872
1128
|
def __is_jira_requirement_present(self, jira_issue_id: str) -> tuple[bool, dict]:
|
|
873
1129
|
""" Define if particular Jira requirement is present in qtest or not """
|
|
874
1130
|
dql = f"'External Id' = '{jira_issue_id}'"
|
|
@@ -1106,14 +1362,461 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
1106
1362
|
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
1107
1363
|
) from e
|
|
1108
1364
|
|
|
1365
|
+
def find_requirements_by_test_case_id(self, test_case_id: str) -> dict:
|
|
1366
|
+
"""Find all requirements linked to a test case.
|
|
1367
|
+
|
|
1368
|
+
This method uses the ObjectLinkApi.find() to discover requirements that are
|
|
1369
|
+
linked to a specific test case (reverse lookup).
|
|
1370
|
+
|
|
1371
|
+
Args:
|
|
1372
|
+
test_case_id: Test case ID in format TC-123
|
|
1373
|
+
|
|
1374
|
+
Returns:
|
|
1375
|
+
dict with test_case_id, total count, and requirements list
|
|
1376
|
+
|
|
1377
|
+
Raises:
|
|
1378
|
+
ValueError: If test case is not found
|
|
1379
|
+
ToolException: If API call fails
|
|
1380
|
+
"""
|
|
1381
|
+
# Get internal QTest ID for the test case
|
|
1382
|
+
qtest_test_case_id = self.__find_qtest_id_by_test_id(test_case_id)
|
|
1383
|
+
|
|
1384
|
+
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
1385
|
+
|
|
1386
|
+
try:
|
|
1387
|
+
# Use ObjectLinkApi.find() to get linked artifacts
|
|
1388
|
+
# type='test-cases' means we're searching from test cases
|
|
1389
|
+
response = link_object_api_instance.find(
|
|
1390
|
+
self.qtest_project_id,
|
|
1391
|
+
type='test-cases',
|
|
1392
|
+
ids=[qtest_test_case_id]
|
|
1393
|
+
)
|
|
1394
|
+
|
|
1395
|
+
# Parse the response to extract linked requirement IDs
|
|
1396
|
+
linked_requirement_ids = []
|
|
1397
|
+
if response and len(response) > 0:
|
|
1398
|
+
for container in response:
|
|
1399
|
+
container_data = container.to_dict() if hasattr(container, 'to_dict') else container
|
|
1400
|
+
objects = container_data.get('objects', []) if isinstance(container_data, dict) else []
|
|
1401
|
+
|
|
1402
|
+
for obj in objects:
|
|
1403
|
+
obj_data = obj.to_dict() if hasattr(obj, 'to_dict') else obj
|
|
1404
|
+
if isinstance(obj_data, dict):
|
|
1405
|
+
pid = obj_data.get('pid', '')
|
|
1406
|
+
# Requirements have RQ- prefix
|
|
1407
|
+
if pid and pid.startswith('RQ-'):
|
|
1408
|
+
linked_requirement_ids.append(pid)
|
|
1409
|
+
|
|
1410
|
+
if not linked_requirement_ids:
|
|
1411
|
+
return {
|
|
1412
|
+
'test_case_id': test_case_id,
|
|
1413
|
+
'total': 0,
|
|
1414
|
+
'requirements': [],
|
|
1415
|
+
'message': f"No requirements are linked to test case '{test_case_id}'"
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
# Fetch actual requirement details via DQL search
|
|
1419
|
+
requirements_result = []
|
|
1420
|
+
for req_id in linked_requirement_ids:
|
|
1421
|
+
req_data = self.__search_entity_by_id('requirements', req_id)
|
|
1422
|
+
if req_data:
|
|
1423
|
+
requirements_result.append(req_data)
|
|
1424
|
+
else:
|
|
1425
|
+
# Fallback if search fails
|
|
1426
|
+
requirements_result.append({
|
|
1427
|
+
'Id': req_id,
|
|
1428
|
+
'QTest Id': None,
|
|
1429
|
+
'Name': 'Unable to fetch',
|
|
1430
|
+
'Description': ''
|
|
1431
|
+
})
|
|
1432
|
+
|
|
1433
|
+
return {
|
|
1434
|
+
'test_case_id': test_case_id,
|
|
1435
|
+
'total': len(requirements_result),
|
|
1436
|
+
'requirements': requirements_result
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
except ApiException as e:
|
|
1440
|
+
stacktrace = format_exc()
|
|
1441
|
+
logger.error(f"Error finding requirements by test case: {stacktrace}")
|
|
1442
|
+
raise ToolException(
|
|
1443
|
+
f"Unable to find requirements linked to test case '{test_case_id}' "
|
|
1444
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
1445
|
+
) from e
|
|
1446
|
+
|
|
1447
|
+
def find_test_runs_by_test_case_id(self, test_case_id: str) -> dict:
|
|
1448
|
+
"""Find all test runs associated with a test case.
|
|
1449
|
+
|
|
1450
|
+
A test run represents an execution instance of a test case. Each test run
|
|
1451
|
+
tracks execution details, status, and any defects found during that run.
|
|
1452
|
+
|
|
1453
|
+
IMPORTANT: In QTest's data model, defects are linked to test runs, not directly
|
|
1454
|
+
to test cases. To find defects related to a test case:
|
|
1455
|
+
1. Use this tool to find test runs for the test case
|
|
1456
|
+
2. Use find_defects_by_test_run_id for each test run to get related defects
|
|
1457
|
+
|
|
1458
|
+
Each test run in the result includes 'Test Case Id' showing which test case
|
|
1459
|
+
it executes, and 'Latest Test Log' with execution status and log ID.
|
|
1460
|
+
|
|
1461
|
+
Args:
|
|
1462
|
+
test_case_id: Test case ID in format TC-123
|
|
1463
|
+
|
|
1464
|
+
Returns:
|
|
1465
|
+
dict with test_case_id, total count, and test_runs list with full details
|
|
1466
|
+
|
|
1467
|
+
Raises:
|
|
1468
|
+
ValueError: If test case is not found
|
|
1469
|
+
ToolException: If API call fails
|
|
1470
|
+
"""
|
|
1471
|
+
# Get internal QTest ID for the test case
|
|
1472
|
+
qtest_test_case_id = self.__find_qtest_id_by_test_id(test_case_id)
|
|
1473
|
+
|
|
1474
|
+
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
1475
|
+
|
|
1476
|
+
try:
|
|
1477
|
+
# Use ObjectLinkApi.find() to get linked artifacts
|
|
1478
|
+
response = link_object_api_instance.find(
|
|
1479
|
+
self.qtest_project_id,
|
|
1480
|
+
type='test-cases',
|
|
1481
|
+
ids=[qtest_test_case_id]
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
# Parse the response to extract linked test run IDs
|
|
1485
|
+
linked_test_run_ids = []
|
|
1486
|
+
if response and len(response) > 0:
|
|
1487
|
+
for container in response:
|
|
1488
|
+
container_data = container.to_dict() if hasattr(container, 'to_dict') else container
|
|
1489
|
+
objects = container_data.get('objects', []) if isinstance(container_data, dict) else []
|
|
1490
|
+
|
|
1491
|
+
for obj in objects:
|
|
1492
|
+
obj_data = obj.to_dict() if hasattr(obj, 'to_dict') else obj
|
|
1493
|
+
if isinstance(obj_data, dict):
|
|
1494
|
+
pid = obj_data.get('pid', '')
|
|
1495
|
+
# Test runs have TR- prefix
|
|
1496
|
+
if pid and pid.startswith('TR-'):
|
|
1497
|
+
linked_test_run_ids.append(pid)
|
|
1498
|
+
|
|
1499
|
+
if not linked_test_run_ids:
|
|
1500
|
+
return {
|
|
1501
|
+
'test_case_id': test_case_id,
|
|
1502
|
+
'total': 0,
|
|
1503
|
+
'test_runs': [],
|
|
1504
|
+
'message': f"No test runs are associated with test case '{test_case_id}'"
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
# Fetch actual test run details via DQL search
|
|
1508
|
+
test_runs_result = []
|
|
1509
|
+
for tr_id in linked_test_run_ids:
|
|
1510
|
+
tr_data = self.__search_entity_by_id('test-runs', tr_id)
|
|
1511
|
+
if tr_data:
|
|
1512
|
+
test_runs_result.append(tr_data)
|
|
1513
|
+
else:
|
|
1514
|
+
# Fallback if search fails
|
|
1515
|
+
test_runs_result.append({
|
|
1516
|
+
'Id': tr_id,
|
|
1517
|
+
'QTest Id': None,
|
|
1518
|
+
'Name': 'Unable to fetch',
|
|
1519
|
+
'Description': ''
|
|
1520
|
+
})
|
|
1521
|
+
|
|
1522
|
+
return {
|
|
1523
|
+
'test_case_id': test_case_id,
|
|
1524
|
+
'total': len(test_runs_result),
|
|
1525
|
+
'test_runs': test_runs_result,
|
|
1526
|
+
'hint': 'To find defects, use find_defects_by_test_run_id for each test run.'
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
except ApiException as e:
|
|
1530
|
+
stacktrace = format_exc()
|
|
1531
|
+
logger.error(f"Error finding test runs by test case: {stacktrace}")
|
|
1532
|
+
raise ToolException(
|
|
1533
|
+
f"Unable to find test runs associated with test case '{test_case_id}' "
|
|
1534
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
1535
|
+
) from e
|
|
1536
|
+
|
|
1537
|
+
def find_defects_by_test_run_id(self, test_run_id: str) -> dict:
|
|
1538
|
+
"""Find all defects associated with a test run.
|
|
1539
|
+
|
|
1540
|
+
In QTest, defects are linked to test runs (not directly to test cases).
|
|
1541
|
+
A test run executes a specific test case, so defects found here are
|
|
1542
|
+
related to that test case through the test run execution context.
|
|
1543
|
+
|
|
1544
|
+
Use this tool after find_test_runs_by_test_case_id to discover defects.
|
|
1545
|
+
The result includes source context (test run and test case IDs) for traceability.
|
|
1546
|
+
|
|
1547
|
+
Args:
|
|
1548
|
+
test_run_id: Test run ID in format TR-123
|
|
1549
|
+
|
|
1550
|
+
Returns:
|
|
1551
|
+
dict with test_run_id, source_test_case_id, total count, and defects list with full details
|
|
1552
|
+
|
|
1553
|
+
Raises:
|
|
1554
|
+
ValueError: If test run is not found
|
|
1555
|
+
ToolException: If API call fails
|
|
1556
|
+
"""
|
|
1557
|
+
# First, get test run details to get the source test case context
|
|
1558
|
+
test_run_data = self.__search_entity_by_id('test-runs', test_run_id)
|
|
1559
|
+
source_test_case_id = None
|
|
1560
|
+
if test_run_data:
|
|
1561
|
+
# testCaseId is the internal ID, we need the PID (TC-xxx format)
|
|
1562
|
+
internal_tc_id = test_run_data.get('Test Case Id')
|
|
1563
|
+
if internal_tc_id:
|
|
1564
|
+
source_test_case_id = self.__get_entity_pid_by_internal_id('test-cases', internal_tc_id)
|
|
1565
|
+
else:
|
|
1566
|
+
raise ValueError(f"Test run '{test_run_id}' not found")
|
|
1567
|
+
|
|
1568
|
+
# Get internal QTest ID for the test run from test_run_data (avoids duplicate API call)
|
|
1569
|
+
qtest_test_run_id = test_run_data.get('QTest Id')
|
|
1570
|
+
if not qtest_test_run_id:
|
|
1571
|
+
raise ValueError(f"QTest Id not found in test run data for '{test_run_id}'")
|
|
1572
|
+
|
|
1573
|
+
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
1574
|
+
|
|
1575
|
+
try:
|
|
1576
|
+
# Use ObjectLinkApi.find() to get linked artifacts
|
|
1577
|
+
response = link_object_api_instance.find(
|
|
1578
|
+
self.qtest_project_id,
|
|
1579
|
+
type='test-runs',
|
|
1580
|
+
ids=[qtest_test_run_id]
|
|
1581
|
+
)
|
|
1582
|
+
|
|
1583
|
+
# Parse the response to extract linked defect IDs
|
|
1584
|
+
linked_defect_ids = []
|
|
1585
|
+
if response and len(response) > 0:
|
|
1586
|
+
for container in response:
|
|
1587
|
+
container_data = container.to_dict() if hasattr(container, 'to_dict') else container
|
|
1588
|
+
objects = container_data.get('objects', []) if isinstance(container_data, dict) else []
|
|
1589
|
+
|
|
1590
|
+
for obj in objects:
|
|
1591
|
+
obj_data = obj.to_dict() if hasattr(obj, 'to_dict') else obj
|
|
1592
|
+
if isinstance(obj_data, dict):
|
|
1593
|
+
pid = obj_data.get('pid', '')
|
|
1594
|
+
# Defects have DF- prefix
|
|
1595
|
+
if pid and pid.startswith('DF-'):
|
|
1596
|
+
linked_defect_ids.append(pid)
|
|
1597
|
+
|
|
1598
|
+
if not linked_defect_ids:
|
|
1599
|
+
result = {
|
|
1600
|
+
'test_run_id': test_run_id,
|
|
1601
|
+
'total': 0,
|
|
1602
|
+
'defects': [],
|
|
1603
|
+
'message': f"No defects are associated with test run '{test_run_id}'"
|
|
1604
|
+
}
|
|
1605
|
+
if source_test_case_id:
|
|
1606
|
+
result['source_test_case_id'] = source_test_case_id
|
|
1607
|
+
return result
|
|
1608
|
+
|
|
1609
|
+
# Fetch actual defect details via DQL search
|
|
1610
|
+
defects_result = []
|
|
1611
|
+
for defect_id in linked_defect_ids:
|
|
1612
|
+
defect_data = self.__search_entity_by_id('defects', defect_id)
|
|
1613
|
+
if defect_data:
|
|
1614
|
+
defects_result.append(defect_data)
|
|
1615
|
+
else:
|
|
1616
|
+
# Fallback if search fails
|
|
1617
|
+
defects_result.append({
|
|
1618
|
+
'Id': defect_id,
|
|
1619
|
+
'QTest Id': None,
|
|
1620
|
+
'Name': 'Unable to fetch',
|
|
1621
|
+
'Description': ''
|
|
1622
|
+
})
|
|
1623
|
+
|
|
1624
|
+
result = {
|
|
1625
|
+
'test_run_id': test_run_id,
|
|
1626
|
+
'total': len(defects_result),
|
|
1627
|
+
'defects': defects_result
|
|
1628
|
+
}
|
|
1629
|
+
if source_test_case_id:
|
|
1630
|
+
result['source_test_case_id'] = source_test_case_id
|
|
1631
|
+
return result
|
|
1632
|
+
|
|
1633
|
+
except ApiException as e:
|
|
1634
|
+
stacktrace = format_exc()
|
|
1635
|
+
logger.error(f"Error finding defects by test run: {stacktrace}")
|
|
1636
|
+
raise ToolException(
|
|
1637
|
+
f"Unable to find defects associated with test run '{test_run_id}' "
|
|
1638
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
1639
|
+
) from e
|
|
1640
|
+
|
|
1109
1641
|
def search_by_dql(self, dql: str, extract_images:bool=False, prompt: str=None):
|
|
1110
1642
|
"""Search for the test cases in qTest using Data Query Language """
|
|
1111
1643
|
parsed_data = self.__perform_search_by_dql(dql, extract_images, prompt)
|
|
1112
1644
|
return "Found " + str(
|
|
1113
1645
|
len(parsed_data)) + f" Qtest test cases:\n" + str(parsed_data[:self.no_of_tests_shown_in_dql_search])
|
|
1114
1646
|
|
|
1647
|
+
def search_entities_by_dql(self, object_type: str, dql: str) -> dict:
|
|
1648
|
+
"""Generic DQL search for any entity type (test-cases, requirements, defects, test-runs, etc.).
|
|
1649
|
+
|
|
1650
|
+
This is the unified search method that works for all QTest searchable entity types.
|
|
1651
|
+
Each entity type has its own properties structure, but this method parses
|
|
1652
|
+
them consistently using the generic entity parser.
|
|
1653
|
+
|
|
1654
|
+
Args:
|
|
1655
|
+
object_type: Entity type to search (see QTEST_OBJECT_TYPES and QTEST_SEARCHABLE_ONLY_TYPES)
|
|
1656
|
+
dql: QTest Data Query Language query string
|
|
1657
|
+
|
|
1658
|
+
Returns:
|
|
1659
|
+
dict with object_type, total count, and items list with full entity details
|
|
1660
|
+
"""
|
|
1661
|
+
# Check if object_type is valid (either has prefix or is searchable-only)
|
|
1662
|
+
all_searchable = {**QTEST_OBJECT_TYPES, **QTEST_SEARCHABLE_ONLY_TYPES}
|
|
1663
|
+
if object_type not in all_searchable:
|
|
1664
|
+
raise ValueError(
|
|
1665
|
+
f"Invalid object_type '{object_type}'. "
|
|
1666
|
+
f"Must be one of: {', '.join(all_searchable.keys())}"
|
|
1667
|
+
)
|
|
1668
|
+
|
|
1669
|
+
entity_info = all_searchable[object_type]
|
|
1670
|
+
search_instance = swagger_client.SearchApi(self._client)
|
|
1671
|
+
body = swagger_client.ArtifactSearchParams(
|
|
1672
|
+
object_type=object_type,
|
|
1673
|
+
fields=['*'],
|
|
1674
|
+
query=dql
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
try:
|
|
1678
|
+
response = search_instance.search_artifact(self.qtest_project_id, body)
|
|
1679
|
+
|
|
1680
|
+
# Parse all items using the generic parser
|
|
1681
|
+
items = []
|
|
1682
|
+
for item in response.get('items', []):
|
|
1683
|
+
parsed = self.__parse_entity_item(object_type, item)
|
|
1684
|
+
items.append(parsed)
|
|
1685
|
+
|
|
1686
|
+
return {
|
|
1687
|
+
'object_type': object_type,
|
|
1688
|
+
'entity_name': entity_info['name'],
|
|
1689
|
+
'total': response.get('total', 0),
|
|
1690
|
+
'returned': len(items),
|
|
1691
|
+
'items': items[:self.no_of_tests_shown_in_dql_search]
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
except ApiException as e:
|
|
1695
|
+
stacktrace = format_exc()
|
|
1696
|
+
logger.error(f"Error searching {object_type} by DQL: {stacktrace}")
|
|
1697
|
+
raise ToolException(
|
|
1698
|
+
f"Unable to search {entity_info['name']}s with DQL '{dql}' "
|
|
1699
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
1700
|
+
) from e
|
|
1701
|
+
|
|
1702
|
+
def find_entity_by_id(self, entity_id: str) -> dict:
|
|
1703
|
+
"""Find any QTest entity by its ID (TC-xxx, RQ-xxx, DF-xxx, TR-xxx).
|
|
1704
|
+
|
|
1705
|
+
This is a universal lookup tool that works for any entity type.
|
|
1706
|
+
The entity type is automatically determined from the ID prefix.
|
|
1707
|
+
|
|
1708
|
+
Args:
|
|
1709
|
+
entity_id: Entity ID with prefix (TC-123, RQ-15, DF-100, TR-39, etc.)
|
|
1710
|
+
|
|
1711
|
+
Returns:
|
|
1712
|
+
dict with full entity details including all properties
|
|
1713
|
+
"""
|
|
1714
|
+
# Determine object type from prefix - dynamically built from registry
|
|
1715
|
+
prefix = entity_id.split('-')[0].upper() if '-' in entity_id else ''
|
|
1716
|
+
|
|
1717
|
+
# Build reverse mapping: prefix -> object_type from QTEST_OBJECT_TYPES
|
|
1718
|
+
prefix_to_type = {
|
|
1719
|
+
info['prefix']: obj_type
|
|
1720
|
+
for obj_type, info in QTEST_OBJECT_TYPES.items()
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
if prefix not in prefix_to_type:
|
|
1724
|
+
valid_prefixes = ', '.join(sorted(prefix_to_type.keys()))
|
|
1725
|
+
raise ValueError(
|
|
1726
|
+
f"Invalid entity ID format '{entity_id}'. "
|
|
1727
|
+
f"Expected prefix to be one of: {valid_prefixes}"
|
|
1728
|
+
)
|
|
1729
|
+
|
|
1730
|
+
object_type = prefix_to_type[prefix]
|
|
1731
|
+
result = self.__search_entity_by_id(object_type, entity_id)
|
|
1732
|
+
|
|
1733
|
+
if result is None:
|
|
1734
|
+
entity_name = QTEST_OBJECT_TYPES[object_type]['name']
|
|
1735
|
+
raise ValueError(
|
|
1736
|
+
f"{entity_name} '{entity_id}' not found in project {self.qtest_project_id}"
|
|
1737
|
+
)
|
|
1738
|
+
|
|
1739
|
+
return result
|
|
1740
|
+
|
|
1741
|
+
def __parse_entity_item(self, object_type: str, item: dict) -> dict:
|
|
1742
|
+
"""Generic parser for any entity type from DQL search response.
|
|
1743
|
+
|
|
1744
|
+
This parses the raw API response item into a clean dictionary,
|
|
1745
|
+
handling the differences between entity types (some have name at top level,
|
|
1746
|
+
some have it in properties as Summary, etc.)
|
|
1747
|
+
|
|
1748
|
+
Args:
|
|
1749
|
+
object_type: QTest object type
|
|
1750
|
+
item: Raw item from search response
|
|
1751
|
+
|
|
1752
|
+
Returns:
|
|
1753
|
+
dict with parsed entity data
|
|
1754
|
+
"""
|
|
1755
|
+
import html
|
|
1756
|
+
|
|
1757
|
+
result = {
|
|
1758
|
+
'Id': item.get('pid'),
|
|
1759
|
+
'QTest Id': item.get('id'),
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
# Add top-level fields if present
|
|
1763
|
+
if item.get('name'):
|
|
1764
|
+
result['Name'] = item.get('name')
|
|
1765
|
+
if item.get('description'):
|
|
1766
|
+
result['Description'] = html.unescape(strip_tags(item.get('description', '') or ''))
|
|
1767
|
+
if item.get('web_url'):
|
|
1768
|
+
result['Web URL'] = item.get('web_url')
|
|
1769
|
+
|
|
1770
|
+
# Test-case specific fields
|
|
1771
|
+
if object_type == 'test-cases':
|
|
1772
|
+
if item.get('precondition'):
|
|
1773
|
+
result['Precondition'] = html.unescape(strip_tags(item.get('precondition', '') or ''))
|
|
1774
|
+
if item.get('test_steps'):
|
|
1775
|
+
result['Steps'] = [
|
|
1776
|
+
{
|
|
1777
|
+
'Test Step Number': idx + 1,
|
|
1778
|
+
'Test Step Description': html.unescape(strip_tags(step.get('description', '') or '')),
|
|
1779
|
+
'Test Step Expected Result': html.unescape(strip_tags(step.get('expected', '') or ''))
|
|
1780
|
+
}
|
|
1781
|
+
for idx, step in enumerate(item.get('test_steps', []))
|
|
1782
|
+
]
|
|
1783
|
+
|
|
1784
|
+
# Test-run specific fields
|
|
1785
|
+
if object_type == 'test-runs':
|
|
1786
|
+
if item.get('testCaseId'):
|
|
1787
|
+
result['Test Case Id'] = item.get('testCaseId')
|
|
1788
|
+
if item.get('automation'):
|
|
1789
|
+
result['Automation'] = item.get('automation')
|
|
1790
|
+
if item.get('latest_test_log'):
|
|
1791
|
+
log = item.get('latest_test_log')
|
|
1792
|
+
result['Latest Test Log'] = {
|
|
1793
|
+
'Log Id': log.get('id'),
|
|
1794
|
+
'Status': log.get('status'),
|
|
1795
|
+
'Execution Start': log.get('exe_start_date'),
|
|
1796
|
+
'Execution End': log.get('exe_end_date')
|
|
1797
|
+
}
|
|
1798
|
+
if item.get('test_case_version'):
|
|
1799
|
+
result['Test Case Version'] = item.get('test_case_version')
|
|
1800
|
+
|
|
1801
|
+
# Parse all properties - works for all entity types
|
|
1802
|
+
for prop in item.get('properties', []):
|
|
1803
|
+
field_name = prop.get('field_name')
|
|
1804
|
+
if not field_name:
|
|
1805
|
+
continue
|
|
1806
|
+
|
|
1807
|
+
# Format value based on field type (multi-select as array, etc.)
|
|
1808
|
+
field_value = self.__format_property_value(prop)
|
|
1809
|
+
|
|
1810
|
+
# Strip HTML from text fields (strings only, not arrays)
|
|
1811
|
+
if isinstance(field_value, str) and ('<' in field_value or '&' in field_value):
|
|
1812
|
+
field_value = html.unescape(strip_tags(field_value))
|
|
1813
|
+
|
|
1814
|
+
result[field_name] = field_value
|
|
1815
|
+
|
|
1816
|
+
return result
|
|
1817
|
+
|
|
1115
1818
|
def create_test_cases(self, test_case_content: str, folder_to_place_test_cases_to: str) -> dict:
|
|
1116
|
-
""" Create the
|
|
1819
|
+
""" Create the test case based on the incoming content. The input should be in json format. """
|
|
1117
1820
|
test_cases_api_instance: TestCaseApi = self.__instantiate_test_api_instance()
|
|
1118
1821
|
input_obj = json.loads(test_case_content)
|
|
1119
1822
|
test_cases = input_obj if isinstance(input_obj, list) else [input_obj]
|
|
@@ -1207,11 +1910,11 @@ SEARCHABLE FIELDS:
|
|
|
1207
1910
|
- Direct fields: Id, Name, Description, Status, Type, Priority, Automation, etc.
|
|
1208
1911
|
- Module: Use 'Module in' syntax
|
|
1209
1912
|
- Custom fields: Use exact field name from project configuration
|
|
1210
|
-
- Date fields:
|
|
1913
|
+
- Date fields: MUST use ISO DateTime format (e.g., '2024-01-01T00:00:00.000Z')
|
|
1211
1914
|
|
|
1212
1915
|
SYNTAX RULES:
|
|
1213
1916
|
1. ALL string values MUST use single quotes (never double quotes)
|
|
1214
|
-
2. Field names with spaces MUST be in single quotes: 'Created Date' > '2024-01-
|
|
1917
|
+
2. Field names with spaces MUST be in single quotes: 'Created Date' > '2024-01-01T00:00:00.000Z'
|
|
1215
1918
|
3. Use ~ for 'contains', !~ for 'not contains': Description ~ 'login'
|
|
1216
1919
|
4. Use 'is not empty' for non-empty check: Name is 'not empty'
|
|
1217
1920
|
5. Operators: =, !=, <, >, <=, >=, in, ~, !~
|
|
@@ -1301,5 +2004,141 @@ Examples:
|
|
|
1301
2004
|
""",
|
|
1302
2005
|
"args_schema": FindTestCasesByRequirementId,
|
|
1303
2006
|
"ref": self.find_test_cases_by_requirement_id,
|
|
2007
|
+
},
|
|
2008
|
+
{
|
|
2009
|
+
"name": "find_requirements_by_test_case_id",
|
|
2010
|
+
"mode": "find_requirements_by_test_case_id",
|
|
2011
|
+
"description": """Find all requirements linked to a test case (direct link: test-case 'covers' requirements).
|
|
2012
|
+
|
|
2013
|
+
Use this tool to discover which requirements a specific test case covers.
|
|
2014
|
+
|
|
2015
|
+
Parameters:
|
|
2016
|
+
- test_case_id: Test case ID in format TC-123
|
|
2017
|
+
|
|
2018
|
+
Returns: List of linked requirements with Id, QTest Id, Name, and Description.
|
|
2019
|
+
|
|
2020
|
+
Examples:
|
|
2021
|
+
- Find requirements for TC-123: test_case_id='TC-123'
|
|
2022
|
+
""",
|
|
2023
|
+
"args_schema": FindRequirementsByTestCaseId,
|
|
2024
|
+
"ref": self.find_requirements_by_test_case_id,
|
|
2025
|
+
},
|
|
2026
|
+
{
|
|
2027
|
+
"name": "find_test_runs_by_test_case_id",
|
|
2028
|
+
"mode": "find_test_runs_by_test_case_id",
|
|
2029
|
+
"description": """Find all test runs associated with a test case.
|
|
2030
|
+
|
|
2031
|
+
IMPORTANT: In QTest, defects are NOT directly linked to test cases.
|
|
2032
|
+
Defects are linked to TEST RUNS. To find defects related to a test case:
|
|
2033
|
+
1. First use this tool to find test runs for the test case
|
|
2034
|
+
2. Then use find_defects_by_test_run_id for each test run
|
|
2035
|
+
|
|
2036
|
+
Parameters:
|
|
2037
|
+
- test_case_id: Test case ID in format TC-123
|
|
2038
|
+
|
|
2039
|
+
Returns: List of test runs with Id, QTest Id, Name, and Description.
|
|
2040
|
+
Also includes a hint about finding defects via test runs.
|
|
2041
|
+
|
|
2042
|
+
Examples:
|
|
2043
|
+
- Find test runs for TC-123: test_case_id='TC-123'
|
|
2044
|
+
""",
|
|
2045
|
+
"args_schema": FindTestRunsByTestCaseId,
|
|
2046
|
+
"ref": self.find_test_runs_by_test_case_id,
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
"name": "find_defects_by_test_run_id",
|
|
2050
|
+
"mode": "find_defects_by_test_run_id",
|
|
2051
|
+
"description": """Find all defects associated with a test run.
|
|
2052
|
+
|
|
2053
|
+
In QTest data model, defects are linked to test runs (not directly to test cases).
|
|
2054
|
+
A defect found here means it was reported during execution of this specific test run.
|
|
2055
|
+
|
|
2056
|
+
To find defects related to a test case:
|
|
2057
|
+
1. First use find_test_runs_by_test_case_id to get test runs
|
|
2058
|
+
2. Then use this tool for each test run
|
|
2059
|
+
|
|
2060
|
+
Parameters:
|
|
2061
|
+
- test_run_id: Test run ID in format TR-123
|
|
2062
|
+
|
|
2063
|
+
Returns: List of defects with Id, QTest Id, Name, and Description.
|
|
2064
|
+
|
|
2065
|
+
Examples:
|
|
2066
|
+
- Find defects for TR-39: test_run_id='TR-39'
|
|
2067
|
+
""",
|
|
2068
|
+
"args_schema": FindDefectsByTestRunId,
|
|
2069
|
+
"ref": self.find_defects_by_test_run_id,
|
|
2070
|
+
},
|
|
2071
|
+
{
|
|
2072
|
+
"name": "search_entities_by_dql",
|
|
2073
|
+
"mode": "search_entities_by_dql",
|
|
2074
|
+
"description": f"""Search any QTest entity type using Data Query Language (DQL).
|
|
2075
|
+
|
|
2076
|
+
This is a unified search tool for all searchable QTest entity types.
|
|
2077
|
+
|
|
2078
|
+
SUPPORTED ENTITY TYPES (object_type parameter):
|
|
2079
|
+
- 'test-cases' (TC-xxx): Test case definitions with steps
|
|
2080
|
+
- 'test-runs' (TR-xxx): Execution instances of test cases
|
|
2081
|
+
- 'defects' (DF-xxx): Bugs/issues found during testing
|
|
2082
|
+
- 'requirements' (RQ-xxx): Requirements to be tested
|
|
2083
|
+
- 'test-suites' (TS-xxx): Collections of test runs
|
|
2084
|
+
- 'test-cycles' (CL-xxx): Test execution cycles
|
|
2085
|
+
- 'test-logs': Execution logs (date queries ONLY - see notes)
|
|
2086
|
+
- 'releases' (RL-xxx): Software releases
|
|
2087
|
+
- 'builds' (BL-xxx): Builds within releases
|
|
2088
|
+
|
|
2089
|
+
NOTES:
|
|
2090
|
+
- Modules (MD-xxx) are NOT searchable via DQL. Use 'get_modules' tool instead.
|
|
2091
|
+
- Test-logs: Only date queries work (Execution Start Date, Execution End Date).
|
|
2092
|
+
For specific test log details, use find_test_runs_by_test_case_id -
|
|
2093
|
+
the test run includes 'Latest Test Log' with status and execution times.
|
|
2094
|
+
|
|
2095
|
+
{DQL_SYNTAX_DOCS}
|
|
2096
|
+
|
|
2097
|
+
EXAMPLES BY ENTITY TYPE:
|
|
2098
|
+
- Test cases: object_type='test-cases', dql="Name ~ 'login'"
|
|
2099
|
+
- Requirements: object_type='requirements', dql="Status = 'Baselined'"
|
|
2100
|
+
- Defects: object_type='defects', dql="Priority = 'High'"
|
|
2101
|
+
- Test runs: object_type='test-runs', dql="Status = 'Failed'"
|
|
2102
|
+
- Test logs: object_type='test-logs', dql="'Execution Start Date' > '2024-01-01T00:00:00.000Z'" (date queries only)
|
|
2103
|
+
- Releases: object_type='releases', dql="Name ~ '2024'"
|
|
2104
|
+
""",
|
|
2105
|
+
"args_schema": GenericDqlSearch,
|
|
2106
|
+
"ref": self.search_entities_by_dql,
|
|
2107
|
+
},
|
|
2108
|
+
{
|
|
2109
|
+
"name": "find_entity_by_id",
|
|
2110
|
+
"mode": "find_entity_by_id",
|
|
2111
|
+
"description": """Find any QTest entity by its ID.
|
|
2112
|
+
|
|
2113
|
+
This universal lookup tool works for entity types that have ID prefixes.
|
|
2114
|
+
The entity type is automatically determined from the ID prefix.
|
|
2115
|
+
|
|
2116
|
+
SUPPORTED ID FORMATS:
|
|
2117
|
+
- TC-123: Test Case
|
|
2118
|
+
- TR-39: Test Run
|
|
2119
|
+
- DF-100: Defect
|
|
2120
|
+
- RQ-15: Requirement
|
|
2121
|
+
- TS-5: Test Suite
|
|
2122
|
+
- CL-3: Test Cycle
|
|
2123
|
+
- RL-1: Release
|
|
2124
|
+
- BL-2: Build
|
|
2125
|
+
|
|
2126
|
+
NOT SUPPORTED (no ID prefix):
|
|
2127
|
+
- Test Logs: Get details from test run's 'Latest Test Log' field (contains Log Id, Status, Execution Start/End Date)
|
|
2128
|
+
- Modules: Use 'get_modules' tool instead
|
|
2129
|
+
|
|
2130
|
+
Parameters:
|
|
2131
|
+
- entity_id: Entity ID with prefix (e.g., TC-123, RQ-15, DF-100, TR-39)
|
|
2132
|
+
|
|
2133
|
+
Returns: Full entity details including all properties.
|
|
2134
|
+
|
|
2135
|
+
Examples:
|
|
2136
|
+
- Find test case: entity_id='TC-123'
|
|
2137
|
+
- Find requirement: entity_id='RQ-15'
|
|
2138
|
+
- Find defect: entity_id='DF-100'
|
|
2139
|
+
- Find test run: entity_id='TR-39'
|
|
2140
|
+
""",
|
|
2141
|
+
"args_schema": FindEntityById,
|
|
2142
|
+
"ref": self.find_entity_by_id,
|
|
1304
2143
|
}
|
|
1305
2144
|
]
|