siesa-agents 2.1.72-qa.2 → 2.1.72-qa.21
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.
- package/bmad/bmm/config.yaml +33 -0
- package/claude/skills/sa-agent-sre-sentinel/SKILL.md +180 -0
- package/claude/skills/sa-aplicar/SKILL.md +268 -0
- package/claude/skills/sa-auditar-servicio/SKILL.md +255 -0
- package/claude/skills/sa-nueva-transversal/SKILL.md +317 -0
- package/claude/skills/sa-nuevo-ambiente/SKILL.md +147 -0
- package/claude/skills/sa-nuevo-servicio/SKILL.md +530 -0
- package/claude/skills/sa-onboard-db/SKILL.md +122 -0
- package/claude/skills/sa-qa-data-generator/SKILL.md +200 -51
- package/claude/skills/sa-quality-process/SKILL.md +6 -0
- package/claude/skills/sa-registrar-permisos/SKILL.md +233 -0
- package/package.json +1 -1
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_design_test.md +1037 -60
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_dor_gate.md +419 -379
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_e2e_executor.md +273 -0
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_playwright_impl.md +359 -355
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_test_plan.md +73 -111
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/workflow.md +1320 -997
- package/siesa-agents/resources/playwright-kit/.env.example +42 -0
- package/siesa-agents/resources/playwright-kit/README.md +228 -0
- package/siesa-agents/resources/playwright-kit/allure/categories.json +26 -0
- package/siesa-agents/resources/playwright-kit/global-setup.ts +34 -0
- package/siesa-agents/resources/playwright-kit/helpers/blazor-e2e-helpers.ts +417 -0
- package/siesa-agents/resources/playwright-kit/helpers/react-e2e-helpers.ts +297 -0
- package/siesa-agents/resources/playwright-kit/package-lock.json +850 -0
- package/siesa-agents/resources/playwright-kit/package.json +27 -0
- package/siesa-agents/resources/playwright-kit/playwright.config.ts +60 -0
- package/siesa-agents/resources/playwright-kit/proposed/.gitkeep +0 -0
- package/siesa-agents/resources/playwright-kit/reporters/agiletest-reporter.ts +252 -0
- package/siesa-agents/resources/playwright-kit/specs/.gitkeep +0 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-erp.md +85 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-react.md +109 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-template.md +69 -0
- package/siesa-agents/resources/playwright-kit/tests/seed-example.spec.ts +95 -0
- package/siesa-agents/resources/playwright-kit/tools/locator-overlay.js +1306 -0
- package/siesa-agents/resources/playwright-kit/tools/locator-recorder.spec.ts +986 -0
- package/siesa-agents/scripts/__pycache__/bmad_to_agiletest.cpython-36.pyc +0 -0
- package/siesa-agents/scripts/bmad_to_agiletest.py +352 -337
- package/siesa-agents/scripts/converters/convert_csv_to_test_design.py +327 -0
- package/siesa-agents/scripts/converters/convert_excel_to_test_design.py +360 -0
- package/siesa-agents/scripts/converters/convert_md_to_test_design.py +359 -0
- package/siesa-agents/scripts/converters/convert_pdf_to_test_design.py +412 -0
- package/siesa-agents/scripts/converters/export_yaml_to_csv.py +149 -0
- package/siesa-agents/scripts/converters/export_yaml_to_excel.py +233 -0
- package/siesa-agents/scripts/converters/export_yaml_to_markdown.py +172 -0
- package/siesa-agents/scripts/converters/export_yaml_to_pdf.py +297 -0
- package/siesa-agents/scripts/merge_test_design.py +106 -106
- package/siesa-agents/scripts/playwright/yaml-to-playwright-spec.ts +203 -0
- package/siesa-agents/scripts/requirements.txt +7 -0
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
"""
|
|
2
|
-
bmad_to_agiletest.py — Parser + Cargador de artefactos BMAD
|
|
2
|
+
bmad_to_agiletest.py — Parser + Cargador de artefactos BMAD V7.8 → AgileTest/Jira
|
|
3
3
|
|
|
4
|
-
Lee
|
|
5
|
-
- Stories
|
|
6
|
-
- FACs Gherkin desde
|
|
7
|
-
- Test Cases
|
|
8
|
-
- Traceability desde
|
|
9
|
-
Luego puede crear todo en Jira/AgileTest via API REST.
|
|
4
|
+
Lee el `test-design.yml` (fuente única de verdad, BMAD V7.8) con yaml.safe_load y extrae:
|
|
5
|
+
- Stories desde `gatekeeper[]`
|
|
6
|
+
- FACs Gherkin desde `features[].fac[]`
|
|
7
|
+
- Test Cases desde `test_matrix[]` (con `steps[]` atómicos: paso/accion/resultado/datos)
|
|
8
|
+
- Traceability desde `traceability[]`
|
|
9
|
+
Luego puede crear todo en Jira/AgileTest via API REST. Cada `step` lleva su `data` (datos por
|
|
10
|
+
paso del YAML), de modo que los Test Steps en AgileTest quedan con su testData real.
|
|
10
11
|
|
|
11
12
|
Uso:
|
|
12
13
|
python bmad_to_agiletest.py --dry-run # Parsea y muestra resumen
|
|
13
14
|
python bmad_to_agiletest.py --dry-run --detail # Muestra cada TC con steps
|
|
14
15
|
python bmad_to_agiletest.py --dry-run --export # Exporta a JSON
|
|
15
|
-
python bmad_to_agiletest.py --input
|
|
16
|
+
python bmad_to_agiletest.py --input test-design.yml --dry-run # Archivo custom (.yml)
|
|
16
17
|
python bmad_to_agiletest.py --create # Crea en Jira/AgileTest
|
|
17
18
|
|
|
19
|
+
Requiere: PyYAML (`pip install pyyaml`).
|
|
20
|
+
|
|
21
|
+
Codificación: toda la E/S de archivos (YAML in, JSON/state out) es UTF-8.
|
|
22
|
+
Salida: el progreso y los resúmenes van a stdout; para consumo por máquina usa
|
|
23
|
+
`--export` (escribe bmad_parsed_data.json) en vez de parsear stdout.
|
|
24
|
+
Exit codes: 0 = OK · 1 = error genérico/credenciales faltantes · 2 = entrada inválida
|
|
25
|
+
(archivo no encontrado / YAML mal formado) · 3 = error de state-dir.
|
|
26
|
+
|
|
18
27
|
Autor: Juan Manuel Reina Montoya — Lider QA, SIESA
|
|
19
|
-
Fecha: 29 marzo 2026 | Actualizado:
|
|
28
|
+
Fecha: 29 marzo 2026 | Actualizado: 11 junio 2026 (V7.8 — lectura YAML vía yaml.safe_load)
|
|
20
29
|
"""
|
|
21
30
|
|
|
22
31
|
import re
|
|
@@ -53,8 +62,8 @@ def _http_with_retry(fn, max_retries=4, base_delay=1.5):
|
|
|
53
62
|
# ============================================================
|
|
54
63
|
|
|
55
64
|
CONFIG = {
|
|
56
|
-
# Archivo BMAD
|
|
57
|
-
'input_file': '
|
|
65
|
+
# Archivo BMAD V7.8 — test-design.yml (ruta relativa al script)
|
|
66
|
+
'input_file': 'test-design.yml',
|
|
58
67
|
|
|
59
68
|
# Jira
|
|
60
69
|
'jira_base_url': 'https://siesa-team.atlassian.net',
|
|
@@ -82,8 +91,15 @@ CONFIG = {
|
|
|
82
91
|
'P2': 'Media',
|
|
83
92
|
'P3': 'Baja',
|
|
84
93
|
},
|
|
94
|
+
|
|
95
|
+
# Campos Jira obligatorios adicionales por proyecto (vacío = ninguno).
|
|
96
|
+
# Se llena desde config.yaml -> agiletest.extra_fields (ej. CON: timetracking).
|
|
97
|
+
'extra_fields': {},
|
|
85
98
|
}
|
|
86
99
|
|
|
100
|
+
# Diagnóstico detallado (--verbose)
|
|
101
|
+
VERBOSE = False
|
|
102
|
+
|
|
87
103
|
# Normalizacion de niveles de test
|
|
88
104
|
LEVEL_NORMALIZE = {
|
|
89
105
|
'BE-Integration': 'Integration (TI)',
|
|
@@ -134,93 +150,104 @@ class TestCase:
|
|
|
134
150
|
steps: list = field(default_factory=list)
|
|
135
151
|
|
|
136
152
|
# ============================================================
|
|
137
|
-
#
|
|
153
|
+
# Loader: test-design.yml (BMAD V7.8) via yaml.safe_load
|
|
138
154
|
# ============================================================
|
|
139
155
|
|
|
140
|
-
def
|
|
141
|
-
"""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
156
|
+
def load_design(yaml_path: str) -> dict:
|
|
157
|
+
"""Carga el test-design.yml (V7.8) como dict. Requiere PyYAML."""
|
|
158
|
+
try:
|
|
159
|
+
import yaml
|
|
160
|
+
except ImportError:
|
|
161
|
+
print(' ERROR: falta PyYAML. Instala con: pip install -r requirements.txt (o: pip install pyyaml)')
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
with open(yaml_path, 'r', encoding='utf-8') as f:
|
|
164
|
+
data = yaml.safe_load(f)
|
|
165
|
+
if not isinstance(data, dict):
|
|
166
|
+
raise ValueError('test-design.yml no tiene un mapeo (dict) en la raiz — revisa el YAML')
|
|
167
|
+
return data
|
|
147
168
|
|
|
148
|
-
m = re.search(r'\*\*Feature Code:\*\*\s*(.+)', content)
|
|
149
|
-
if m:
|
|
150
|
-
meta['feature_code'] = m.group(1).strip()
|
|
151
169
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if m:
|
|
156
|
-
meta['date'] = m.group(1).strip()
|
|
170
|
+
def validate_design(design: dict):
|
|
171
|
+
"""Validación blanda del test-design.yml (V7.8). Solo advierte, no aborta:
|
|
172
|
+
el procesamiento real decide qué hacer con secciones vacías.
|
|
157
173
|
|
|
158
|
-
|
|
174
|
+
- H-F5-11: avisa si la versión declarada no parece V7.8.
|
|
175
|
+
- H-F5-15: avisa si faltan las secciones de las que dependen los builders.
|
|
176
|
+
"""
|
|
177
|
+
md = design.get('metadata', {}) or {}
|
|
178
|
+
version = str(md.get('version') or md.get('schema_version') or design.get('version') or '').strip()
|
|
179
|
+
if version and '7.8' not in version:
|
|
180
|
+
safe_print(f' [WARN] El test-design.yml declara versión "{version}"; este script está calibrado para V7.8.')
|
|
181
|
+
|
|
182
|
+
required = ('gatekeeper', 'test_matrix')
|
|
183
|
+
faltantes = [k for k in required if not design.get(k)]
|
|
184
|
+
if faltantes:
|
|
185
|
+
safe_print(f' [WARN] Secciones ausentes o vacías en el YAML: {", ".join(faltantes)} '
|
|
186
|
+
f'(se esperan gatekeeper[] para Stories y test_matrix[] para Test Cases).')
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def build_metadata(design: dict) -> dict:
|
|
190
|
+
"""Deriva meta {feature, feature_code, date, author, mode} desde metadata + features."""
|
|
191
|
+
md = design.get('metadata', {}) or {}
|
|
192
|
+
features = design.get('features', []) or []
|
|
193
|
+
project = md.get('project_name') or md.get('project') or ''
|
|
194
|
+
# feature_code: un solo feature -> su code; varios (Modo Completo) -> el proyecto
|
|
195
|
+
if len(features) == 1 and features[0].get('code'):
|
|
196
|
+
feature_code = features[0]['code']
|
|
197
|
+
feature = features[0].get('name') or project
|
|
198
|
+
else:
|
|
199
|
+
feature_code = project or 'BMAD'
|
|
200
|
+
feature = project or 'BMAD'
|
|
201
|
+
return {
|
|
202
|
+
'feature': feature,
|
|
203
|
+
'feature_code': feature_code,
|
|
204
|
+
'date': str(md.get('generated_date', '')),
|
|
205
|
+
'author': '',
|
|
206
|
+
'mode': md.get('mode', ''),
|
|
207
|
+
}
|
|
159
208
|
|
|
160
209
|
# ============================================================
|
|
161
|
-
#
|
|
210
|
+
# Builder: gatekeeper[] -> Stories
|
|
162
211
|
# ============================================================
|
|
163
212
|
|
|
164
|
-
def
|
|
165
|
-
"""
|
|
213
|
+
def build_stories(design: dict) -> list:
|
|
214
|
+
"""Construye Stories desde `gatekeeper[]` del YAML V7.8.
|
|
166
215
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
- Quality-process: Story X.Y (ej: Story 2.3)
|
|
216
|
+
Cada entrada: {story_id, name, feature, classification, justification}.
|
|
217
|
+
Clasificacion BL ('BL' / 'Logica de Negocio' / '✅') -> risk_level High.
|
|
170
218
|
"""
|
|
171
219
|
stories = []
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
gk_match = re.search(
|
|
175
|
-
r'## I\.\s*REPORTE DEL GATEKEEPER.*?\n(.*?)(?=\n---\s*\n\n## II|\n## Resumen|\n# FASE 2|\Z)',
|
|
176
|
-
content, re.DOTALL
|
|
177
|
-
)
|
|
178
|
-
if not gk_match:
|
|
179
|
-
return stories
|
|
180
|
-
|
|
181
|
-
section = gk_match.group(1)
|
|
182
|
-
|
|
183
|
-
for line in section.split('\n'):
|
|
184
|
-
line = line.strip()
|
|
185
|
-
if not line.startswith('|'):
|
|
220
|
+
for e in design.get('gatekeeper', []) or []:
|
|
221
|
+
if not isinstance(e, dict):
|
|
186
222
|
continue
|
|
187
|
-
|
|
188
|
-
if
|
|
189
|
-
continue
|
|
190
|
-
story_id = cols[0].strip()
|
|
191
|
-
|
|
192
|
-
# Detectar formato
|
|
193
|
-
is_classic = bool(re.search(r'E\d+-S\d+', story_id)) # FEATURE-E001-S001
|
|
194
|
-
is_qp = bool(re.match(r'Story\s+\d+\.\d+', story_id)) # Story 2.3
|
|
195
|
-
|
|
196
|
-
if not is_classic and not is_qp:
|
|
223
|
+
story_id = str(e.get('story_id') or e.get('id') or '').strip()
|
|
224
|
+
if not story_id:
|
|
197
225
|
continue
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
classification =
|
|
201
|
-
|
|
226
|
+
title = str(e.get('name', '')).strip()
|
|
227
|
+
justification = str(e.get('justification', '')).strip()
|
|
228
|
+
classification = str(e.get('classification', '')).strip()
|
|
229
|
+
feature = str(e.get('feature', '')).strip()
|
|
202
230
|
|
|
203
231
|
# Epic ID
|
|
204
|
-
if
|
|
205
|
-
|
|
206
|
-
epic_id =
|
|
232
|
+
if re.search(r'E\d+-S\d+', story_id):
|
|
233
|
+
em = re.search(r'(.*-E\d+)', story_id)
|
|
234
|
+
epic_id = em.group(1) if em else ''
|
|
207
235
|
else:
|
|
208
|
-
|
|
209
|
-
epic_id = f'Epic {
|
|
236
|
+
em = re.match(r'(?:Story\s+)?(\d+)\.\d+', story_id)
|
|
237
|
+
epic_id = f'Epic {em.group(1)}' if em else (feature or '')
|
|
210
238
|
|
|
211
|
-
# Layer
|
|
239
|
+
# Layer (heuristica por contenido)
|
|
240
|
+
lower_all = (title + ' ' + justification + ' ' + feature).lower()
|
|
212
241
|
layer = 'Backend'
|
|
213
|
-
lower_all = (title + justification).lower()
|
|
214
242
|
if any(kw in lower_all for kw in [
|
|
215
243
|
'frontend', 'react', 'ui ', 'typescript', 'hook', 'page',
|
|
216
244
|
'drawer', 'form', 'navigation', 'fe+be', 'fe ',
|
|
217
245
|
]):
|
|
218
246
|
layer = 'Frontend'
|
|
219
247
|
|
|
220
|
-
# Clasificación BL: soporta "BL", "Lógica de Negocio", "Logica de Negocio", "✅"
|
|
221
248
|
is_bl = (
|
|
222
249
|
'BL' in classification or
|
|
223
|
-
'gica de Negocio' in classification or #
|
|
250
|
+
'gica de Negocio' in classification or # Logica / Lógica de Negocio
|
|
224
251
|
'✅' in classification
|
|
225
252
|
)
|
|
226
253
|
|
|
@@ -233,258 +260,178 @@ def parse_stories(content: str, feature_code: str) -> list:
|
|
|
233
260
|
layer=layer,
|
|
234
261
|
risk_level='High' if is_bl else 'Low',
|
|
235
262
|
))
|
|
236
|
-
|
|
237
263
|
return stories
|
|
238
264
|
|
|
239
265
|
# ============================================================
|
|
240
|
-
#
|
|
266
|
+
# Builder: features[].fac[] -> FACs Gherkin
|
|
241
267
|
# ============================================================
|
|
242
268
|
|
|
243
|
-
def
|
|
244
|
-
"""
|
|
269
|
+
def build_facs(design: dict) -> dict:
|
|
270
|
+
"""Construye dict fac_id -> texto Gherkin desde `features[].fac[]`."""
|
|
245
271
|
facs = {}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
# Separar scenarios
|
|
252
|
-
scenarios = re.split(r'\n\s*(?=Scenario(?:\s+Outline)?:)', block)
|
|
253
|
-
for scenario in scenarios:
|
|
254
|
-
scenario = scenario.strip()
|
|
255
|
-
if not scenario.startswith('Scenario'):
|
|
272
|
+
for feat in design.get('features', []) or []:
|
|
273
|
+
if not isinstance(feat, dict):
|
|
274
|
+
continue
|
|
275
|
+
for fac in feat.get('fac', []) or []:
|
|
276
|
+
if not isinstance(fac, dict):
|
|
256
277
|
continue
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if
|
|
260
|
-
|
|
261
|
-
facs[fac_id] = scenario
|
|
262
|
-
|
|
278
|
+
fid = str(fac.get('id', '')).strip()
|
|
279
|
+
gherkin = str(fac.get('gherkin', '')).strip()
|
|
280
|
+
if fid:
|
|
281
|
+
facs[fid] = gherkin
|
|
263
282
|
return facs
|
|
264
283
|
|
|
265
284
|
# ============================================================
|
|
266
|
-
#
|
|
285
|
+
# Builder: test_matrix[] -> Test Cases (con steps atomicos)
|
|
267
286
|
# ============================================================
|
|
268
287
|
|
|
269
|
-
def
|
|
270
|
-
"""
|
|
271
|
-
# Columnas: ID | Funcionalidad | Epicas | Nivel | Tecnica | Escenario |
|
|
272
|
-
# Precondiciones | Pasos | Resultado Esperado | Riesgo (IxP) | Prioridad | Estrategia
|
|
273
|
-
tc_id = cols[0].strip()
|
|
274
|
-
feature = cols[1].strip() if len(cols) > 1 else ''
|
|
275
|
-
epics = cols[2].strip() if len(cols) > 2 else ''
|
|
276
|
-
level_raw = cols[3].strip() if len(cols) > 3 else ''
|
|
277
|
-
technique = cols[4].strip() if len(cols) > 4 else ''
|
|
278
|
-
scenario = cols[5].strip() if len(cols) > 5 else ''
|
|
279
|
-
preconditions = cols[6].strip() if len(cols) > 6 else ''
|
|
280
|
-
pasos = cols[7].strip() if len(cols) > 7 else ''
|
|
281
|
-
expected = cols[8].strip() if len(cols) > 8 else ''
|
|
282
|
-
risk_score = cols[9].strip() if len(cols) > 9 else ''
|
|
283
|
-
priority = cols[10].strip() if len(cols) > 10 else ''
|
|
284
|
-
strategy = cols[11].strip() if len(cols) > 11 else ''
|
|
285
|
-
|
|
286
|
-
# Normalizar nivel
|
|
287
|
-
level = LEVEL_NORMALIZE.get(level_raw, level_raw)
|
|
288
|
-
|
|
289
|
-
# Construir steps: Precondiciones -> Setup, Pasos -> Action, Expected -> Result
|
|
290
|
-
steps = []
|
|
291
|
-
if preconditions and preconditions != '—' and preconditions != '-':
|
|
292
|
-
steps.append(TestStep(
|
|
293
|
-
action=f'SETUP: {preconditions}',
|
|
294
|
-
expected_result='Precondiciones verificadas',
|
|
295
|
-
))
|
|
296
|
-
if pasos:
|
|
297
|
-
# Separar pasos numerados (1. xxx 2. yyy)
|
|
298
|
-
numbered = re.split(r'\d+\.\s+', pasos)
|
|
299
|
-
numbered = [p.strip() for p in numbered if p.strip()]
|
|
300
|
-
if len(numbered) > 1:
|
|
301
|
-
# Multiples pasos: uno por cada numerado, el ultimo lleva el expected
|
|
302
|
-
for i, paso in enumerate(numbered):
|
|
303
|
-
if i < len(numbered) - 1:
|
|
304
|
-
steps.append(TestStep(
|
|
305
|
-
action=paso,
|
|
306
|
-
expected_result='Paso ejecutado correctamente',
|
|
307
|
-
))
|
|
308
|
-
else:
|
|
309
|
-
steps.append(TestStep(
|
|
310
|
-
action=paso,
|
|
311
|
-
expected_result=expected,
|
|
312
|
-
))
|
|
313
|
-
else:
|
|
314
|
-
steps.append(TestStep(
|
|
315
|
-
action=pasos,
|
|
316
|
-
expected_result=expected,
|
|
317
|
-
))
|
|
318
|
-
elif expected:
|
|
319
|
-
steps.append(TestStep(
|
|
320
|
-
action=scenario or 'Ejecutar escenario',
|
|
321
|
-
expected_result=expected,
|
|
322
|
-
))
|
|
323
|
-
|
|
324
|
-
# Extraer story refs del campo Epicas
|
|
325
|
-
source_stories = []
|
|
326
|
-
story_refs = re.findall(r'E\d+-S\d+', epics)
|
|
327
|
-
# Se enriquecen despues con feature_code prefix
|
|
328
|
-
|
|
329
|
-
# Construir titulo: escenario como titulo descriptivo
|
|
330
|
-
title = scenario if scenario else tc_id
|
|
331
|
-
|
|
332
|
-
# Raw body para descripcion en Jira
|
|
333
|
-
raw_body = (
|
|
334
|
-
f'Escenario: {scenario}\n'
|
|
335
|
-
f'Precondiciones: {preconditions}\n'
|
|
336
|
-
f'Pasos: {pasos}\n'
|
|
337
|
-
f'Resultado Esperado: {expected}\n'
|
|
338
|
-
f'Tecnica: {technique}\n'
|
|
339
|
-
f'Riesgo: {risk_score}'
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
return TestCase(
|
|
343
|
-
tc_id=tc_id,
|
|
344
|
-
title=title,
|
|
345
|
-
priority=priority,
|
|
346
|
-
level=level,
|
|
347
|
-
feature=feature,
|
|
348
|
-
epics=epics,
|
|
349
|
-
technique=technique,
|
|
350
|
-
scenario=scenario,
|
|
351
|
-
preconditions=preconditions,
|
|
352
|
-
risk_score=risk_score,
|
|
353
|
-
strategy=strategy,
|
|
354
|
-
source_stories=source_stories,
|
|
355
|
-
raw_body=raw_body,
|
|
356
|
-
steps=steps,
|
|
357
|
-
)
|
|
288
|
+
def build_test_cases(design: dict) -> list:
|
|
289
|
+
"""Construye Test Cases desde `test_matrix[]` (V7.8).
|
|
358
290
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
"""
|
|
291
|
+
Cada caso trae `steps[]` como array de {paso, accion, resultado, datos}. Cada step
|
|
292
|
+
se mapea a un TestStep con su `data` (datos por paso) — insumo del testData de AgileTest.
|
|
293
|
+
"""
|
|
362
294
|
test_cases = []
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
matrix_match = re.search(
|
|
366
|
-
r'## IV\.\s*MATRIZ INTEGRAL.*?\n(.*?)(?=\n---\s*\n\n## V|\Z)',
|
|
367
|
-
content, re.DOTALL
|
|
368
|
-
)
|
|
369
|
-
if not matrix_match:
|
|
370
|
-
return test_cases
|
|
371
|
-
|
|
372
|
-
section = matrix_match.group(1)
|
|
373
|
-
|
|
374
|
-
# Parsear todas las filas de tabla que empiecen con | TC-
|
|
375
|
-
for line in section.split('\n'):
|
|
376
|
-
line = line.strip()
|
|
377
|
-
if not line.startswith('|'):
|
|
295
|
+
for c in design.get('test_matrix', []) or []:
|
|
296
|
+
if not isinstance(c, dict):
|
|
378
297
|
continue
|
|
379
|
-
|
|
380
|
-
if
|
|
298
|
+
tc_id = str(c.get('id', '')).strip()
|
|
299
|
+
if not tc_id:
|
|
381
300
|
continue
|
|
382
301
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
302
|
+
feature = str(c.get('feature', '')).strip()
|
|
303
|
+
epics_raw = c.get('epics', [])
|
|
304
|
+
if isinstance(epics_raw, list):
|
|
305
|
+
epics = ', '.join(str(x) for x in epics_raw)
|
|
306
|
+
else:
|
|
307
|
+
epics = str(epics_raw or '')
|
|
308
|
+
modo = str(c.get('mode', '') or '').strip()
|
|
309
|
+
level_raw = str(c.get('level', '') or '').strip()
|
|
310
|
+
tipo_interfaz = str(c.get('interface_type', '') or '').strip()
|
|
311
|
+
technique = str(c.get('technique', '') or '').strip()
|
|
312
|
+
scenario = str(c.get('scenario', '') or '').strip()
|
|
313
|
+
preconditions = str(c.get('preconditions', '') or '').strip()
|
|
314
|
+
expected = str(c.get('expected_result', '') or '').strip()
|
|
315
|
+
risk_score = str(c.get('risk', '') or '').strip()
|
|
316
|
+
priority = str(c.get('priority', '') or '').strip()
|
|
317
|
+
level = LEVEL_NORMALIZE.get(level_raw, level_raw)
|
|
318
|
+
|
|
319
|
+
# Steps: precondiciones como SETUP + un step por cada paso atomico del YAML
|
|
320
|
+
steps = []
|
|
321
|
+
if preconditions and preconditions not in ('—', '-'):
|
|
322
|
+
steps.append(TestStep(
|
|
323
|
+
action=f'SETUP: {preconditions}',
|
|
324
|
+
expected_result='Precondiciones verificadas',
|
|
325
|
+
))
|
|
326
|
+
for s in c.get('steps', []) or []:
|
|
327
|
+
if isinstance(s, dict):
|
|
328
|
+
accion = str(s.get('accion', '') or '').strip()
|
|
329
|
+
resultado = str(s.get('resultado', '') or '').strip()
|
|
330
|
+
datos = str(s.get('datos', '') or '').strip()
|
|
331
|
+
else:
|
|
332
|
+
# tolerancia: step como string suelto
|
|
333
|
+
accion, resultado, datos = str(s).strip(), '', ''
|
|
334
|
+
if accion:
|
|
335
|
+
steps.append(TestStep(
|
|
336
|
+
action=accion,
|
|
337
|
+
expected_result=resultado or expected,
|
|
338
|
+
data=datos,
|
|
339
|
+
))
|
|
340
|
+
# Fallback: si el YAML no trajo pasos atomicos pero hay resultado esperado
|
|
341
|
+
if not any(not st.action.startswith('SETUP:') for st in steps) and expected:
|
|
342
|
+
steps.append(TestStep(
|
|
343
|
+
action=scenario or 'Ejecutar escenario',
|
|
344
|
+
expected_result=expected,
|
|
345
|
+
))
|
|
389
346
|
|
|
390
|
-
|
|
391
|
-
|
|
347
|
+
title = scenario if scenario else tc_id
|
|
348
|
+
raw_body = (
|
|
349
|
+
f'Escenario: {scenario}\n'
|
|
350
|
+
f'Precondiciones: {preconditions}\n'
|
|
351
|
+
f'Resultado Esperado: {expected}\n'
|
|
352
|
+
f'Tecnica: {technique}\n'
|
|
353
|
+
+ (f'Modo: {modo}\n' if modo else '')
|
|
354
|
+
+ (f'Tipo Interfaz: {tipo_interfaz}\n' if tipo_interfaz else '')
|
|
355
|
+
+ f'Riesgo: {risk_score}'
|
|
356
|
+
)
|
|
392
357
|
|
|
358
|
+
test_cases.append(TestCase(
|
|
359
|
+
tc_id=tc_id,
|
|
360
|
+
title=title,
|
|
361
|
+
priority=priority,
|
|
362
|
+
level=level,
|
|
363
|
+
feature=feature,
|
|
364
|
+
epics=epics,
|
|
365
|
+
technique=technique,
|
|
366
|
+
scenario=scenario,
|
|
367
|
+
preconditions=preconditions,
|
|
368
|
+
risk_score=risk_score,
|
|
369
|
+
strategy=tipo_interfaz,
|
|
370
|
+
source_stories=[],
|
|
371
|
+
raw_body=raw_body,
|
|
372
|
+
steps=steps,
|
|
373
|
+
))
|
|
393
374
|
return test_cases
|
|
394
375
|
|
|
395
376
|
# ============================================================
|
|
396
|
-
#
|
|
377
|
+
# Builder: traceability[] -> mapping requirement -> [tc_ids]
|
|
397
378
|
# ============================================================
|
|
398
379
|
|
|
399
|
-
def
|
|
400
|
-
"""
|
|
380
|
+
def build_traceability(design: dict) -> dict:
|
|
381
|
+
"""Construye mapping requisito/criterio -> [tc_ids] desde `traceability[]`."""
|
|
401
382
|
mapping = {}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
383
|
+
for t in design.get('traceability', []) or []:
|
|
384
|
+
if not isinstance(t, dict):
|
|
385
|
+
continue
|
|
386
|
+
req = str(t.get('requirement', '') or '').strip()
|
|
387
|
+
if not req:
|
|
388
|
+
continue
|
|
389
|
+
cases = t.get('cases', []) or []
|
|
390
|
+
mapping.setdefault(req, [])
|
|
391
|
+
for cid in cases:
|
|
392
|
+
cid = str(cid).strip()
|
|
393
|
+
if cid and cid not in mapping[req]:
|
|
394
|
+
mapping[req].append(cid)
|
|
413
395
|
return mapping
|
|
414
396
|
|
|
415
397
|
|
|
416
|
-
def
|
|
417
|
-
"""
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
├── Story 1.2 (Navigation)
|
|
421
|
-
│ ├── TC-F1-01 (desc)
|
|
422
|
-
└── ...
|
|
398
|
+
def augment_trace_with_stories(trace_map: dict, stories: list, test_cases: list):
|
|
399
|
+
"""Agrega al trace_map los vinculos story_id -> [tc_ids] inferidos de la columna
|
|
400
|
+
`epics` de cada caso, para que el link TC -> Requisito (gatekeeper) funcione aunque
|
|
401
|
+
el bloque `traceability` apunte a FACs en vez de a historias.
|
|
423
402
|
"""
|
|
424
|
-
|
|
425
|
-
current_story = None
|
|
426
|
-
current_tcs = []
|
|
427
|
-
|
|
428
|
-
for line in section.split('\n'):
|
|
429
|
-
# Detectar línea que menciona una Story
|
|
430
|
-
story_match = re.search(r'\bStory\s+(\d+\.\d+)\b', line)
|
|
431
|
-
if story_match:
|
|
432
|
-
# Cerrar story anterior
|
|
433
|
-
if current_story and current_tcs:
|
|
434
|
-
mapping.setdefault(current_story, [])
|
|
435
|
-
mapping[current_story] = list(set(mapping[current_story] + current_tcs))
|
|
436
|
-
|
|
437
|
-
current_story = f'Story {story_match.group(1)}'
|
|
438
|
-
current_tcs = []
|
|
439
|
-
|
|
440
|
-
# TCs en la misma línea: Story 2.1 → TC-F2-01, TC-F2-02
|
|
441
|
-
arrow_match = re.search(r'(?:→|->)\s*(.+)$', line)
|
|
442
|
-
if arrow_match:
|
|
443
|
-
current_tcs.extend(re.findall(r'TC-[A-Z0-9]+-\d+', arrow_match.group(1)))
|
|
444
|
-
|
|
445
|
-
elif current_story:
|
|
446
|
-
# TCs en líneas hijo: │ ├── TC-F1-01 (desc)
|
|
447
|
-
tcs = re.findall(r'TC-[A-Z0-9]+-\d+', line)
|
|
448
|
-
current_tcs.extend(tcs)
|
|
449
|
-
|
|
450
|
-
# Guardar el último story
|
|
451
|
-
if current_story and current_tcs:
|
|
452
|
-
mapping.setdefault(current_story, [])
|
|
453
|
-
mapping[current_story] = list(set(mapping[current_story] + current_tcs))
|
|
454
|
-
|
|
455
|
-
return mapping
|
|
403
|
+
story_ids = {s.story_id for s in stories}
|
|
456
404
|
|
|
405
|
+
def norm(sid):
|
|
406
|
+
m = re.search(r'(\d+\.\d+)', sid)
|
|
407
|
+
return m.group(1) if m else sid
|
|
457
408
|
|
|
458
|
-
|
|
459
|
-
"""Extrae mapping Story → TCs desde el Apéndice de Trazabilidad.
|
|
409
|
+
norm_index = {norm(sid): sid for sid in story_ids}
|
|
460
410
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
content, re.DOTALL
|
|
474
|
-
)
|
|
475
|
-
if not trace_match:
|
|
476
|
-
return {}
|
|
477
|
-
|
|
478
|
-
section = trace_match.group(1)
|
|
411
|
+
for tc in test_cases:
|
|
412
|
+
if not tc.epics:
|
|
413
|
+
continue
|
|
414
|
+
refs = re.findall(r'E\d+-S\d+', tc.epics) + re.findall(r'\b\d+\.\d+\b', tc.epics)
|
|
415
|
+
for ref in refs:
|
|
416
|
+
sid = ref if ref in story_ids else norm_index.get(norm(ref))
|
|
417
|
+
if not sid:
|
|
418
|
+
continue
|
|
419
|
+
trace_map.setdefault(sid, [])
|
|
420
|
+
if tc.tc_id not in trace_map[sid]:
|
|
421
|
+
trace_map[sid].append(tc.tc_id)
|
|
422
|
+
return trace_map
|
|
479
423
|
|
|
480
|
-
|
|
481
|
-
|
|
424
|
+
# ============================================================
|
|
425
|
+
# (legacy) Parser: Seccion II — FACs Gherkin
|
|
426
|
+
# ============================================================
|
|
482
427
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
428
|
+
# ============================================================
|
|
429
|
+
# (legacy) Parser: Seccion IV — Matriz 360° (Test Cases)
|
|
430
|
+
# ============================================================
|
|
486
431
|
|
|
487
|
-
|
|
432
|
+
# ============================================================
|
|
433
|
+
# (legacy) Parser: Apendice — Traceability
|
|
434
|
+
# ============================================================
|
|
488
435
|
|
|
489
436
|
# ============================================================
|
|
490
437
|
# Enriquecimiento: propagar traceability y FACs a TCs
|
|
@@ -503,12 +450,17 @@ def enrich_test_cases(test_cases: list, trace_map: dict, facs: dict, feature_cod
|
|
|
503
450
|
|
|
504
451
|
# 2. Inferir story refs del campo epics de cada TC
|
|
505
452
|
for tc in test_cases:
|
|
506
|
-
if tc.epics:
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
453
|
+
if not tc.epics:
|
|
454
|
+
continue
|
|
455
|
+
# Formato clasico: E001-S001
|
|
456
|
+
for ref in re.findall(r'E\d+-S\d+', tc.epics):
|
|
457
|
+
full_id = f'{feature_code}-{ref}'
|
|
458
|
+
if full_id not in tc.source_stories:
|
|
459
|
+
tc.source_stories.append(full_id)
|
|
460
|
+
# Formato V7.8: "Epic 2 / 2.1, 2.5" -> historias 2.1, 2.5 (numero pelado)
|
|
461
|
+
for ref in re.findall(r'\b\d+\.\d+\b', tc.epics):
|
|
462
|
+
if ref not in tc.source_stories:
|
|
463
|
+
tc.source_stories.append(ref)
|
|
512
464
|
|
|
513
465
|
# 3. Normalizar niveles
|
|
514
466
|
for tc in test_cases:
|
|
@@ -518,27 +470,24 @@ def enrich_test_cases(test_cases: list, trace_map: dict, facs: dict, feature_cod
|
|
|
518
470
|
# Enriquecimiento: epic names en stories
|
|
519
471
|
# ============================================================
|
|
520
472
|
|
|
521
|
-
def
|
|
522
|
-
"""
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
epic_names = {}
|
|
532
|
-
for m in re.finditer(r'-\s*(E\d+-S\d+)\s*\((.+?)\)', content):
|
|
533
|
-
epic_names[m.group(1)] = m.group(2).strip()
|
|
473
|
+
def enrich_stories_from_features(design: dict, stories: list):
|
|
474
|
+
"""Asigna epic_name a las stories usando los nombres de `features[]` del YAML."""
|
|
475
|
+
feature_names = {}
|
|
476
|
+
for feat in design.get('features', []) or []:
|
|
477
|
+
if not isinstance(feat, dict):
|
|
478
|
+
continue
|
|
479
|
+
code = str(feat.get('code', '') or '').strip()
|
|
480
|
+
name = str(feat.get('name', '') or '').strip()
|
|
481
|
+
if code and name:
|
|
482
|
+
feature_names[code] = name
|
|
534
483
|
|
|
535
484
|
for s in stories:
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
485
|
+
if s.epic_name:
|
|
486
|
+
continue
|
|
487
|
+
# epic_id tipo "VNT-E001" -> prefijo de feature antes del primer '-'
|
|
488
|
+
code = s.epic_id.split('-')[0] if s.epic_id else ''
|
|
489
|
+
if code in feature_names:
|
|
490
|
+
s.epic_name = feature_names[code]
|
|
542
491
|
|
|
543
492
|
# ============================================================
|
|
544
493
|
# API: Jira REST
|
|
@@ -550,6 +499,9 @@ class JiraAPI:
|
|
|
550
499
|
self.email = config['jira_user_email']
|
|
551
500
|
self.token = config['jira_api_token']
|
|
552
501
|
self.project = config['jira_project_key']
|
|
502
|
+
# Campos Jira obligatorios por proyecto (ej. CON exige timetracking.originalEstimate).
|
|
503
|
+
# Se mergean en cada create_issue; vacío por defecto (HCM no los requiere).
|
|
504
|
+
self.extra_fields = config.get('extra_fields') or {}
|
|
553
505
|
|
|
554
506
|
def _headers(self):
|
|
555
507
|
import base64
|
|
@@ -578,8 +530,12 @@ class JiraAPI:
|
|
|
578
530
|
except Exception:
|
|
579
531
|
return []
|
|
580
532
|
|
|
581
|
-
def create_issue(self, summary, description, issue_type_id, priority='Media', labels=None):
|
|
582
|
-
"""Crea un issue en Jira y retorna el ID y key.
|
|
533
|
+
def create_issue(self, summary, description, issue_type_id, priority='Media', labels=None, extra_fields=None):
|
|
534
|
+
"""Crea un issue en Jira y retorna el ID y key.
|
|
535
|
+
|
|
536
|
+
extra_fields: dict opcional de campos Jira adicionales (ej. timetracking).
|
|
537
|
+
Si es None, usa self.extra_fields (de config.yaml agiletest.extra_fields).
|
|
538
|
+
"""
|
|
583
539
|
import urllib.request
|
|
584
540
|
fields = {
|
|
585
541
|
'project': {'key': self.project},
|
|
@@ -592,6 +548,10 @@ class JiraAPI:
|
|
|
592
548
|
}
|
|
593
549
|
if priority:
|
|
594
550
|
fields['priority'] = {'name': priority}
|
|
551
|
+
# Mergear campos obligatorios por proyecto (ej. CON: timetracking.originalEstimate)
|
|
552
|
+
merged_extra = extra_fields if extra_fields is not None else self.extra_fields
|
|
553
|
+
if merged_extra:
|
|
554
|
+
fields.update(merged_extra)
|
|
595
555
|
payload = {'fields': fields}
|
|
596
556
|
if labels:
|
|
597
557
|
payload['fields']['labels'] = labels
|
|
@@ -857,7 +817,7 @@ def build_plan_tc_map(test_cases):
|
|
|
857
817
|
def print_summary(stories, test_cases, trace_map, facs, meta):
|
|
858
818
|
"""Muestra resumen del parseo."""
|
|
859
819
|
safe_print('\n' + '=' * 70)
|
|
860
|
-
safe_print(' BMAD
|
|
820
|
+
safe_print(' BMAD V7.8 (YAML) -> AgileTest -- Resumen del parseo')
|
|
861
821
|
safe_print('=' * 70)
|
|
862
822
|
|
|
863
823
|
safe_print(f'\n Feature: {meta.get("feature", "?")}')
|
|
@@ -1025,6 +985,14 @@ def create_in_agiletest(stories, test_cases, trace_map, meta, config, state_dir=
|
|
|
1025
985
|
feature_label = (meta.get('feature_code', '') or 'BMAD').replace('-', '_').replace(' ', '_').replace('&', 'and')
|
|
1026
986
|
if state_dir is None:
|
|
1027
987
|
state_dir = os.path.dirname(os.path.abspath(__file__))
|
|
988
|
+
# H-F5-13: garantizar que el state-dir existe y es escribible; fallar con mensaje claro
|
|
989
|
+
try:
|
|
990
|
+
os.makedirs(state_dir, exist_ok=True)
|
|
991
|
+
if not os.access(state_dir, os.W_OK):
|
|
992
|
+
raise OSError('sin permiso de escritura')
|
|
993
|
+
except OSError as e:
|
|
994
|
+
safe_print(f' ERROR: no se puede usar el state-dir "{state_dir}": {e}')
|
|
995
|
+
sys.exit(3)
|
|
1028
996
|
state_path = os.path.join(state_dir, f'bmad_state_{feature_label}.json')
|
|
1029
997
|
state = load_state(state_path)
|
|
1030
998
|
|
|
@@ -1425,15 +1393,32 @@ def load_bmm_config(config_path: str):
|
|
|
1425
1393
|
# Capa 1: config.yaml público (sin secretos)
|
|
1426
1394
|
try:
|
|
1427
1395
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|
1428
|
-
|
|
1396
|
+
content = f.read()
|
|
1397
|
+
_extract_agiletest_section(content, 'config.yaml', include_secrets=False)
|
|
1398
|
+
# extra_fields es una estructura anidada: se parsea con YAML, no con regex.
|
|
1399
|
+
try:
|
|
1400
|
+
import yaml
|
|
1401
|
+
cfg = yaml.safe_load(content)
|
|
1402
|
+
at = (cfg.get('agiletest') or {}) if isinstance(cfg, dict) else {}
|
|
1403
|
+
if isinstance(at, dict) and isinstance(at.get('extra_fields'), dict):
|
|
1404
|
+
CONFIG['extra_fields'] = at['extra_fields']
|
|
1405
|
+
except Exception:
|
|
1406
|
+
pass # extra_fields es opcional; si el YAML falla, se ignora
|
|
1429
1407
|
except Exception as e:
|
|
1430
1408
|
safe_print(f' [WARN] No se pudo leer {config_path}: {e}')
|
|
1431
1409
|
|
|
1432
1410
|
# Capa 2: config.secrets.yaml (gitignoreado, tiene los tokens)
|
|
1433
|
-
# Buscar
|
|
1411
|
+
# Buscar primero junto al --config provisto, luego relativo al script (_siesa-agents/bmm/).
|
|
1434
1412
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
1435
|
-
|
|
1436
|
-
|
|
1413
|
+
secrets_candidates = [
|
|
1414
|
+
os.path.join(config_dir, 'config.secrets.yaml'),
|
|
1415
|
+
os.path.normpath(os.path.join(script_dir, '..', 'bmm', 'config.secrets.yaml')),
|
|
1416
|
+
]
|
|
1417
|
+
secrets_path = next((p for p in secrets_candidates if os.path.exists(p)), None)
|
|
1418
|
+
if VERBOSE:
|
|
1419
|
+
safe_print(f' [VERBOSE] config.yaml: {config_path}')
|
|
1420
|
+
safe_print(f' [VERBOSE] config.secrets.yaml: {secrets_path or "(no encontrado)"}')
|
|
1421
|
+
if secrets_path:
|
|
1437
1422
|
try:
|
|
1438
1423
|
with open(secrets_path, 'r', encoding='utf-8') as f:
|
|
1439
1424
|
_extract_agiletest_section(f.read(), 'config.secrets.yaml', include_secrets=True)
|
|
@@ -1457,14 +1442,14 @@ def load_bmm_config(config_path: str):
|
|
|
1457
1442
|
|
|
1458
1443
|
def main():
|
|
1459
1444
|
parser = argparse.ArgumentParser(
|
|
1460
|
-
description='BMAD
|
|
1445
|
+
description='BMAD V7.8 (YAML) -> AgileTest: Parser + Cargador',
|
|
1461
1446
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1462
1447
|
epilog="""
|
|
1463
1448
|
Ejemplos:
|
|
1464
1449
|
python bmad_to_agiletest.py --dry-run Parsea y muestra resumen
|
|
1465
1450
|
python bmad_to_agiletest.py --dry-run --detail Muestra cada TC con steps
|
|
1466
1451
|
python bmad_to_agiletest.py --dry-run --export Exporta a JSON
|
|
1467
|
-
python bmad_to_agiletest.py --input
|
|
1452
|
+
python bmad_to_agiletest.py --input test-design.yml --dry-run
|
|
1468
1453
|
python bmad_to_agiletest.py --create Crea todo en Jira/AgileTest
|
|
1469
1454
|
""",
|
|
1470
1455
|
)
|
|
@@ -1477,45 +1462,69 @@ Ejemplos:
|
|
|
1477
1462
|
parser.add_argument('--feature-filter', default=None,
|
|
1478
1463
|
help='Filtrar por feature key (ej: F4, F1). Solo procesa TCs de esa feature')
|
|
1479
1464
|
parser.add_argument('--input', default=CONFIG['input_file'],
|
|
1480
|
-
help='Ruta al archivo test-design.
|
|
1465
|
+
help='Ruta al archivo test-design.yml generado por quality-process')
|
|
1481
1466
|
parser.add_argument('--config', default=None,
|
|
1482
1467
|
help='Ruta a _bmad/bmm/config.yaml (para leer sección agiletest)')
|
|
1468
|
+
parser.add_argument('--jira-url', default=None,
|
|
1469
|
+
help='URL base de Jira (sobreescribe config.yaml). Ej: https://tu-org.atlassian.net')
|
|
1470
|
+
parser.add_argument('--project-key', default=None,
|
|
1471
|
+
help='Project key de Jira (sobreescribe config.yaml). Ej: CON, COM, MAN')
|
|
1483
1472
|
parser.add_argument('--state-dir', default=None,
|
|
1484
1473
|
help='Directorio donde guardar el state file (default: directorio del input)')
|
|
1474
|
+
parser.add_argument('--verbose', action='store_true',
|
|
1475
|
+
help='Imprime detalle de diagnóstico (rutas de config/secrets resueltas, instancia destino)')
|
|
1485
1476
|
|
|
1486
1477
|
args = parser.parse_args()
|
|
1478
|
+
global VERBOSE
|
|
1479
|
+
VERBOSE = args.verbose
|
|
1487
1480
|
|
|
1488
1481
|
# Cargar config YAML si se proveyó (sobreescribe CONFIG con valores no-secretos)
|
|
1489
1482
|
if args.config:
|
|
1490
1483
|
load_bmm_config(args.config)
|
|
1491
1484
|
|
|
1485
|
+
# Flags CLI: tienen prioridad sobre config.yaml para identificar la instancia Jira destino
|
|
1486
|
+
if args.jira_url:
|
|
1487
|
+
CONFIG['jira_base_url'] = args.jira_url
|
|
1488
|
+
if args.project_key:
|
|
1489
|
+
CONFIG['jira_project_key'] = args.project_key
|
|
1490
|
+
|
|
1492
1491
|
# Resolver ruta
|
|
1493
1492
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
1494
1493
|
input_path = os.path.join(script_dir, args.input) if not os.path.isabs(args.input) else args.input
|
|
1495
1494
|
|
|
1496
1495
|
if not os.path.exists(input_path):
|
|
1497
1496
|
print(f' ERROR: Archivo no encontrado: {input_path}')
|
|
1498
|
-
sys.exit(
|
|
1497
|
+
sys.exit(2)
|
|
1499
1498
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
content = f.read()
|
|
1499
|
+
if VERBOSE:
|
|
1500
|
+
safe_print(f' [VERBOSE] Instancia destino: {CONFIG["jira_base_url"]} | proyecto {CONFIG["jira_project_key"]}')
|
|
1501
|
+
safe_print(f' [VERBOSE] Input: {input_path}')
|
|
1504
1502
|
|
|
1505
|
-
#
|
|
1506
|
-
|
|
1503
|
+
# Cargar el test-design.yml (V7.8) via yaml.safe_load
|
|
1504
|
+
print(f'Cargando YAML: {os.path.basename(input_path)}')
|
|
1505
|
+
try:
|
|
1506
|
+
design = load_design(input_path)
|
|
1507
|
+
except Exception as e:
|
|
1508
|
+
print(f' ERROR: YAML inválido en {os.path.basename(input_path)}: {e}')
|
|
1509
|
+
sys.exit(2)
|
|
1510
|
+
validate_design(design)
|
|
1511
|
+
|
|
1512
|
+
# Metadata
|
|
1513
|
+
meta = build_metadata(design)
|
|
1507
1514
|
feature_code = meta.get('feature_code', 'UNKNOWN')
|
|
1508
|
-
print(f' Feature: {meta.get("feature", "?")} ({feature_code})')
|
|
1515
|
+
print(f' Feature: {meta.get("feature", "?")} ({feature_code}) | Modo: {meta.get("mode", "?")}')
|
|
1509
1516
|
|
|
1510
|
-
#
|
|
1511
|
-
stories =
|
|
1512
|
-
facs =
|
|
1513
|
-
test_cases =
|
|
1514
|
-
trace_map =
|
|
1517
|
+
# Construir entidades desde el YAML
|
|
1518
|
+
stories = build_stories(design)
|
|
1519
|
+
facs = build_facs(design)
|
|
1520
|
+
test_cases = build_test_cases(design)
|
|
1521
|
+
trace_map = build_traceability(design)
|
|
1515
1522
|
|
|
1516
1523
|
# Enriquecer datos cruzados
|
|
1517
1524
|
enrich_test_cases(test_cases, trace_map, facs, feature_code)
|
|
1518
|
-
|
|
1525
|
+
enrich_stories_from_features(design, stories)
|
|
1526
|
+
# Link story -> TC inferido de la columna epics (para TC -> Requisito en el create)
|
|
1527
|
+
augment_trace_with_stories(trace_map, stories, test_cases)
|
|
1519
1528
|
|
|
1520
1529
|
# Filtrado por feature (--pilot usa F4, --feature-filter usa lo indicado)
|
|
1521
1530
|
feat_filter = None
|
|
@@ -1554,6 +1563,12 @@ Ejemplos:
|
|
|
1554
1563
|
export_json(stories, test_cases, trace_map, facs, meta, json_path)
|
|
1555
1564
|
|
|
1556
1565
|
if args.create:
|
|
1566
|
+
# H-F5-04: avisar si se creará en la instancia por defecto sin override explícito
|
|
1567
|
+
if not (args.config or args.jira_url or args.project_key):
|
|
1568
|
+
safe_print('\n [WARN] No se especificó --config, --jira-url ni --project-key.')
|
|
1569
|
+
safe_print(f' Se usará la instancia por defecto: {CONFIG["jira_base_url"]} (proyecto {CONFIG["jira_project_key"]}).')
|
|
1570
|
+
safe_print(' Si NO es tu instancia, cancela y re-ejecuta con --jira-url/--project-key o --config.')
|
|
1571
|
+
|
|
1557
1572
|
missing = []
|
|
1558
1573
|
if not CONFIG.get('jira_api_token'):
|
|
1559
1574
|
missing.append('jira_api_token (env: JIRA_API_TOKEN)')
|