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.
Files changed (49) hide show
  1. package/bmad/bmm/config.yaml +33 -0
  2. package/claude/skills/sa-agent-sre-sentinel/SKILL.md +180 -0
  3. package/claude/skills/sa-aplicar/SKILL.md +268 -0
  4. package/claude/skills/sa-auditar-servicio/SKILL.md +255 -0
  5. package/claude/skills/sa-nueva-transversal/SKILL.md +317 -0
  6. package/claude/skills/sa-nuevo-ambiente/SKILL.md +147 -0
  7. package/claude/skills/sa-nuevo-servicio/SKILL.md +530 -0
  8. package/claude/skills/sa-onboard-db/SKILL.md +122 -0
  9. package/claude/skills/sa-qa-data-generator/SKILL.md +200 -51
  10. package/claude/skills/sa-quality-process/SKILL.md +6 -0
  11. package/claude/skills/sa-registrar-permisos/SKILL.md +233 -0
  12. package/package.json +1 -1
  13. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_design_test.md +1037 -60
  14. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_dor_gate.md +419 -379
  15. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_e2e_executor.md +273 -0
  16. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_playwright_impl.md +359 -355
  17. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_test_plan.md +73 -111
  18. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/workflow.md +1320 -997
  19. package/siesa-agents/resources/playwright-kit/.env.example +42 -0
  20. package/siesa-agents/resources/playwright-kit/README.md +228 -0
  21. package/siesa-agents/resources/playwright-kit/allure/categories.json +26 -0
  22. package/siesa-agents/resources/playwright-kit/global-setup.ts +34 -0
  23. package/siesa-agents/resources/playwright-kit/helpers/blazor-e2e-helpers.ts +417 -0
  24. package/siesa-agents/resources/playwright-kit/helpers/react-e2e-helpers.ts +297 -0
  25. package/siesa-agents/resources/playwright-kit/package-lock.json +850 -0
  26. package/siesa-agents/resources/playwright-kit/package.json +27 -0
  27. package/siesa-agents/resources/playwright-kit/playwright.config.ts +60 -0
  28. package/siesa-agents/resources/playwright-kit/proposed/.gitkeep +0 -0
  29. package/siesa-agents/resources/playwright-kit/reporters/agiletest-reporter.ts +252 -0
  30. package/siesa-agents/resources/playwright-kit/specs/.gitkeep +0 -0
  31. package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-erp.md +85 -0
  32. package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-react.md +109 -0
  33. package/siesa-agents/resources/playwright-kit/specs/architecture-brief-template.md +69 -0
  34. package/siesa-agents/resources/playwright-kit/tests/seed-example.spec.ts +95 -0
  35. package/siesa-agents/resources/playwright-kit/tools/locator-overlay.js +1306 -0
  36. package/siesa-agents/resources/playwright-kit/tools/locator-recorder.spec.ts +986 -0
  37. package/siesa-agents/scripts/__pycache__/bmad_to_agiletest.cpython-36.pyc +0 -0
  38. package/siesa-agents/scripts/bmad_to_agiletest.py +352 -337
  39. package/siesa-agents/scripts/converters/convert_csv_to_test_design.py +327 -0
  40. package/siesa-agents/scripts/converters/convert_excel_to_test_design.py +360 -0
  41. package/siesa-agents/scripts/converters/convert_md_to_test_design.py +359 -0
  42. package/siesa-agents/scripts/converters/convert_pdf_to_test_design.py +412 -0
  43. package/siesa-agents/scripts/converters/export_yaml_to_csv.py +149 -0
  44. package/siesa-agents/scripts/converters/export_yaml_to_excel.py +233 -0
  45. package/siesa-agents/scripts/converters/export_yaml_to_markdown.py +172 -0
  46. package/siesa-agents/scripts/converters/export_yaml_to_pdf.py +297 -0
  47. package/siesa-agents/scripts/merge_test_design.py +106 -106
  48. package/siesa-agents/scripts/playwright/yaml-to-playwright-spec.ts +203 -0
  49. package/siesa-agents/scripts/requirements.txt +7 -0
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ convert_csv_to_test_design.py — Convierte CSV a test-design YAML
4
+ Lee CSV con metadata en lineas # y pasos como filas estandar.
5
+ Uso: python convert_csv_to_test_design.py <input.csv> [output.yml]
6
+ """
7
+
8
+ import csv
9
+ import io
10
+ import re
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ try:
15
+ import yaml
16
+ except ImportError:
17
+ print("ERROR: pyyaml requerido. Instalar con: pip install pyyaml")
18
+ sys.exit(1)
19
+
20
+
21
+ # --- YAML formatting helpers ---
22
+
23
+ class LiteralStr(str):
24
+ """String que se serializa como bloque literal YAML (>)."""
25
+ pass
26
+
27
+
28
+ def literal_representer(dumper, data):
29
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=">")
30
+
31
+
32
+ yaml.add_representer(LiteralStr, literal_representer)
33
+
34
+
35
+ def yaml_str_representer(dumper, data):
36
+ """Fuerza comillas dobles solo cuando es necesario."""
37
+ if any(c in data for c in (":", "{", "}", "[", "]", ",", "&", "*", "?", "|", ">", "!", "%", "@", "`")):
38
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style='"')
39
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data)
40
+
41
+
42
+ yaml.add_representer(str, yaml_str_representer)
43
+
44
+
45
+ # --- Parsing ---
46
+
47
+ def parse_meta_lines(lines: list[str]) -> tuple[dict, list[tuple[str, str]]]:
48
+ """Parsea lineas # en un dict + lista de pares (para claves duplicadas)."""
49
+ meta = {}
50
+ pairs = []
51
+ for line in lines:
52
+ line = line.lstrip("# ").strip()
53
+ if not line or ":" not in line:
54
+ continue
55
+ key, _, value = line.partition(":")
56
+ key = key.strip()
57
+ value = value.strip()
58
+ meta[key] = value
59
+ pairs.append((key, value))
60
+ return meta, pairs
61
+
62
+
63
+ def get_meta(meta: dict, key: str, default: str = "") -> str:
64
+ return meta.get(key, default)
65
+
66
+
67
+ def parse_pipe_list(value: str) -> list[str]:
68
+ """Separa valores delimitados por |."""
69
+ if not value:
70
+ return []
71
+ return [v.strip() for v in value.split("|") if v.strip()]
72
+
73
+
74
+ def build_metadata_section(meta: dict) -> dict:
75
+ result = {}
76
+ for field in ("modulo", "version", "autor", "fecha", "stack"):
77
+ val = get_meta(meta, f"metadata.{field}")
78
+ if val:
79
+ result[field] = val
80
+ return result
81
+
82
+
83
+ def build_requisito_section(meta: dict) -> dict:
84
+ result = {}
85
+ for field in ("id", "nombre"):
86
+ val = get_meta(meta, f"requisito.{field}")
87
+ if val:
88
+ result[field] = val
89
+ desc = get_meta(meta, "requisito.descripcion")
90
+ if desc:
91
+ result["descripcion"] = LiteralStr(desc)
92
+ criterios = parse_pipe_list(get_meta(meta, "requisito.criterios_aceptacion"))
93
+ if criterios:
94
+ result["criterios_aceptacion"] = criterios
95
+ return result
96
+
97
+
98
+ def build_plan_section(meta: dict) -> dict:
99
+ result = {}
100
+ for field in ("id", "nombre", "framework"):
101
+ val = get_meta(meta, f"plan_pruebas.{field}")
102
+ if val:
103
+ result[field] = val
104
+ objetivo = get_meta(meta, "plan_pruebas.objetivo")
105
+ if objetivo:
106
+ result["objetivo"] = LiteralStr(objetivo)
107
+
108
+ # ambientes
109
+ ambientes = []
110
+ i = 0
111
+ while True:
112
+ nombre = get_meta(meta, f"plan_pruebas.ambiente_{i}.nombre")
113
+ if not nombre:
114
+ break
115
+ amb = {"nombre": nombre}
116
+ url = get_meta(meta, f"plan_pruebas.ambiente_{i}.url")
117
+ if url:
118
+ amb["url"] = url
119
+ usuario = get_meta(meta, f"plan_pruebas.ambiente_{i}.usuario")
120
+ if usuario:
121
+ amb["usuario"] = usuario
122
+ amb["validado"] = True
123
+ ambientes.append(amb)
124
+ i += 1
125
+ if ambientes:
126
+ result["ambientes"] = ambientes
127
+
128
+ # precondiciones
129
+ precondiciones = parse_pipe_list(get_meta(meta, "plan_pruebas.precondiciones"))
130
+ if precondiciones:
131
+ result["precondiciones"] = precondiciones
132
+
133
+ # tiempos
134
+ tiempos = {}
135
+ for key, val in meta.items():
136
+ if key.startswith("plan_pruebas.tiempos."):
137
+ t_key = key.replace("plan_pruebas.tiempos.", "")
138
+ tiempos[t_key] = val
139
+ if tiempos:
140
+ result["tiempos_referencia"] = tiempos
141
+
142
+ return result
143
+
144
+
145
+ def build_caso_prueba_section(meta: dict, pasos: list[dict]) -> dict:
146
+ result = {}
147
+ for field in ("id", "nombre", "prioridad", "tipo"):
148
+ val = get_meta(meta, f"caso_prueba.{field}")
149
+ if val:
150
+ result[field] = val
151
+ scripts = parse_pipe_list(get_meta(meta, "caso_prueba.scripts"))
152
+ if scripts:
153
+ result["scripts"] = scripts
154
+ result["pasos"] = pasos
155
+ return result
156
+
157
+
158
+ def build_ejecucion_section(meta: dict, pairs: list[tuple[str, str]]) -> dict:
159
+ result = {}
160
+ for field in ("id", "nombre"):
161
+ val = get_meta(meta, f"ejecucion.{field}")
162
+ if val:
163
+ result[field] = val
164
+
165
+ # variables_entorno (usa pairs para capturar claves duplicadas)
166
+ variables = []
167
+ for key, val in pairs:
168
+ if key == "ejecucion.variable":
169
+ parts = [p.strip() for p in val.split("|")]
170
+ if len(parts) >= 3:
171
+ variables.append({
172
+ "nombre": parts[0],
173
+ "descripcion": parts[1],
174
+ "default": parts[2],
175
+ })
176
+ if variables:
177
+ result["variables_entorno"] = variables
178
+
179
+ # comandos
180
+ comandos = {}
181
+ for key, val in meta.items():
182
+ if key.startswith("ejecucion.comando."):
183
+ cmd_key = key.replace("ejecucion.comando.", "")
184
+ comandos[cmd_key] = val
185
+ if comandos:
186
+ result["comandos"] = comandos
187
+
188
+ return result
189
+
190
+
191
+ def build_integraciones_section(meta: dict) -> dict:
192
+ result = {}
193
+ for key, val in meta.items():
194
+ if key.startswith("integracion."):
195
+ rest = key.replace("integracion.", "")
196
+ parts = rest.split(".", 1)
197
+ if len(parts) == 2:
198
+ integ_name, field = parts
199
+ if integ_name not in result:
200
+ result[integ_name] = {}
201
+ result[integ_name][field] = val
202
+ return result
203
+
204
+
205
+ def parse_csv_steps(csv_text: str) -> list[dict]:
206
+ """Parsea filas CSV como pasos de test."""
207
+ reader = csv.DictReader(io.StringIO(csv_text))
208
+ pasos = []
209
+ for row in reader:
210
+ paso_val = row.get("paso", "").strip()
211
+ # Determinar tipo de paso
212
+ if paso_val.isdigit():
213
+ paso_key = int(paso_val)
214
+ else:
215
+ paso_key = paso_val # "manual" u otro
216
+
217
+ step = {"paso": paso_key}
218
+ step["nombre"] = row.get("nombre", "").strip()
219
+ step["accion"] = row.get("accion", "").strip()
220
+ step["resultado"] = row.get("resultado_esperado", "").strip()
221
+
222
+ datos = row.get("datos", "").strip()
223
+ if datos:
224
+ step["datos"] = datos
225
+
226
+ script = row.get("script", "").strip()
227
+ if script:
228
+ step["script"] = script
229
+
230
+ auto_str = row.get("automatizado", "false").strip().lower()
231
+ step["automatizado"] = auto_str == "true"
232
+
233
+ bloqueado = row.get("bloqueado", "").strip()
234
+ if bloqueado:
235
+ step["bloqueado"] = bloqueado
236
+
237
+ nota = row.get("nota", "").strip()
238
+ if nota:
239
+ step["nota"] = nota
240
+
241
+ pasos.append(step)
242
+
243
+ return pasos
244
+
245
+
246
+ def main():
247
+ if len(sys.argv) < 2:
248
+ print("Uso: python convert_csv_to_test_design.py <input.csv> [output.yml]")
249
+ sys.exit(1)
250
+
251
+ input_path = Path(sys.argv[1])
252
+ if not input_path.exists():
253
+ print(f"ERROR: No se encontro {input_path}")
254
+ sys.exit(1)
255
+
256
+ if len(sys.argv) >= 3:
257
+ output_path = Path(sys.argv[2])
258
+ else:
259
+ output_path = input_path.with_name(
260
+ input_path.stem.replace("test-cases-", "test-design-") + ".yml"
261
+ )
262
+
263
+ # Leer archivo y separar metadata de CSV
264
+ with open(input_path, "r", encoding="utf-8") as f:
265
+ raw_lines = f.readlines()
266
+
267
+ meta_lines = []
268
+ csv_lines = []
269
+ for line in raw_lines:
270
+ if line.startswith("#"):
271
+ meta_lines.append(line)
272
+ else:
273
+ csv_lines.append(line)
274
+
275
+ meta, pairs = parse_meta_lines(meta_lines)
276
+ csv_text = "".join(csv_lines)
277
+ pasos = parse_csv_steps(csv_text)
278
+
279
+ # Construir YAML
280
+ autor = get_meta(meta, "metadata.autor", "QA Team")
281
+ doc = {}
282
+
283
+ metadata = build_metadata_section(meta)
284
+ if metadata:
285
+ doc["metadata"] = metadata
286
+
287
+ requisito = build_requisito_section(meta)
288
+ if requisito:
289
+ doc["requisito"] = requisito
290
+
291
+ plan = build_plan_section(meta)
292
+ if plan:
293
+ doc["plan_pruebas"] = plan
294
+
295
+ caso = build_caso_prueba_section(meta, pasos)
296
+ doc["caso_prueba"] = caso
297
+
298
+ ejecucion = build_ejecucion_section(meta, pairs)
299
+ if ejecucion:
300
+ doc["ejecucion"] = ejecucion
301
+
302
+ integraciones = build_integraciones_section(meta)
303
+ if integraciones:
304
+ doc["integraciones"] = integraciones
305
+
306
+ # Escribir YAML
307
+ header = (
308
+ f"# test-design generado desde {input_path.name}\n"
309
+ f"# Input para: AgileTest + Playwright + Agente de Datos\n"
310
+ f"# Autor: {autor}\n\n"
311
+ )
312
+
313
+ with open(output_path, "w", encoding="utf-8") as f:
314
+ f.write(header)
315
+ yaml.dump(
316
+ doc, f,
317
+ default_flow_style=False,
318
+ allow_unicode=True,
319
+ sort_keys=False,
320
+ width=120,
321
+ )
322
+
323
+ print(f"OK: {input_path.name} -> {output_path.name} ({len(pasos)} pasos convertidos)")
324
+
325
+
326
+ if __name__ == "__main__":
327
+ main()
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ convert_excel_to_test_design.py — Convierte Excel (.xlsx) a test-design YAML
4
+ Lee Excel con hoja "Metadata" (Seccion/Campo/Valor) + hoja "Pasos" (tabla).
5
+ Uso: python convert_excel_to_test_design.py <input.xlsx> [output.yml]
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ try:
12
+ import yaml
13
+ except ImportError:
14
+ print("ERROR: pyyaml requerido. Instalar con: pip install pyyaml")
15
+ sys.exit(1)
16
+
17
+ try:
18
+ from openpyxl import load_workbook
19
+ except ImportError:
20
+ print("ERROR: openpyxl requerido. Instalar con: pip install openpyxl")
21
+ sys.exit(1)
22
+
23
+
24
+ # --- YAML formatting helpers ---
25
+
26
+ class LiteralStr(str):
27
+ pass
28
+
29
+
30
+ def literal_representer(dumper, data):
31
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=">")
32
+
33
+
34
+ yaml.add_representer(LiteralStr, literal_representer)
35
+
36
+
37
+ def yaml_str_representer(dumper, data):
38
+ if any(c in data for c in (":", "{", "}", "[", "]", ",", "&", "*", "?", "|", ">", "!", "%", "@", "`")):
39
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style='"')
40
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data)
41
+
42
+
43
+ yaml.add_representer(str, yaml_str_representer)
44
+
45
+
46
+ # --- Parsing ---
47
+
48
+ def cell_str(val) -> str:
49
+ """Convierte valor de celda a string limpio."""
50
+ if val is None:
51
+ return ""
52
+ return str(val).strip()
53
+
54
+
55
+ def parse_pipe_list(value: str) -> list[str]:
56
+ if not value:
57
+ return []
58
+ return [v.strip() for v in value.split("|") if v.strip()]
59
+
60
+
61
+ def read_metadata_sheet(wb) -> tuple[dict, list[tuple[str, str, str]]]:
62
+ """Lee hoja Metadata y retorna dict + lista de tuplas para claves duplicadas."""
63
+ ws = wb["Metadata"]
64
+ meta = {}
65
+ triples = []
66
+
67
+ for row in ws.iter_rows(min_row=2, values_only=True):
68
+ if not row or not row[0]:
69
+ continue
70
+ seccion = cell_str(row[0])
71
+ campo = cell_str(row[1]) if len(row) > 1 else ""
72
+ valor = cell_str(row[2]) if len(row) > 2 else ""
73
+
74
+ key = f"{seccion}.{campo}" if campo else seccion
75
+ meta[key] = valor
76
+ triples.append((seccion, campo, valor))
77
+
78
+ return meta, triples
79
+
80
+
81
+ def read_pasos_sheet(wb) -> list[dict]:
82
+ """Lee hoja Pasos y retorna lista de dicts de pasos."""
83
+ ws = wb["Pasos"]
84
+ pasos = []
85
+
86
+ # Leer headers de la primera fila
87
+ headers_raw = [cell_str(c) for c in next(ws.iter_rows(min_row=1, max_row=1, values_only=True))]
88
+ # Normalizar headers
89
+ header_map = {
90
+ "paso": "paso", "nombre": "nombre", "accion": "accion",
91
+ "resultado esperado": "resultado", "datos": "datos",
92
+ "script": "script", "automatizado": "automatizado",
93
+ "bloqueado": "bloqueado", "nota": "nota",
94
+ }
95
+ headers = []
96
+ for h in headers_raw:
97
+ normalized = h.lower().strip()
98
+ headers.append(header_map.get(normalized, normalized))
99
+
100
+ for row in ws.iter_rows(min_row=2, values_only=True):
101
+ if not row or row[0] is None:
102
+ continue
103
+
104
+ row_dict = {}
105
+ for i, val in enumerate(row):
106
+ if i < len(headers):
107
+ row_dict[headers[i]] = val
108
+
109
+ paso_val = row_dict.get("paso", "")
110
+ if isinstance(paso_val, (int, float)):
111
+ paso_key = int(paso_val)
112
+ else:
113
+ paso_key = cell_str(paso_val)
114
+ if paso_key.isdigit():
115
+ paso_key = int(paso_key)
116
+
117
+ step = {"paso": paso_key}
118
+ step["nombre"] = cell_str(row_dict.get("nombre", ""))
119
+ step["accion"] = cell_str(row_dict.get("accion", ""))
120
+ step["resultado"] = cell_str(row_dict.get("resultado", ""))
121
+
122
+ datos = cell_str(row_dict.get("datos", ""))
123
+ if datos:
124
+ step["datos"] = datos
125
+
126
+ script = cell_str(row_dict.get("script", ""))
127
+ if script:
128
+ step["script"] = script
129
+
130
+ auto_val = row_dict.get("automatizado", False)
131
+ if isinstance(auto_val, bool):
132
+ step["automatizado"] = auto_val
133
+ else:
134
+ step["automatizado"] = cell_str(auto_val).lower() == "true"
135
+
136
+ bloqueado = cell_str(row_dict.get("bloqueado", ""))
137
+ if bloqueado:
138
+ step["bloqueado"] = bloqueado
139
+
140
+ nota = cell_str(row_dict.get("nota", ""))
141
+ if nota:
142
+ step["nota"] = nota
143
+
144
+ pasos.append(step)
145
+
146
+ return pasos
147
+
148
+
149
+ # --- YAML builders ---
150
+
151
+ def get_meta(meta: dict, key: str, default: str = "") -> str:
152
+ return meta.get(key, default)
153
+
154
+
155
+ def build_metadata_section(meta: dict) -> dict:
156
+ result = {}
157
+ for field in ("modulo", "version", "autor", "fecha", "stack"):
158
+ val = get_meta(meta, f"metadata.{field}")
159
+ if val:
160
+ result[field] = val
161
+ return result
162
+
163
+
164
+ def build_requisito_section(meta: dict) -> dict:
165
+ result = {}
166
+ for field in ("id", "nombre"):
167
+ val = get_meta(meta, f"requisito.{field}")
168
+ if val:
169
+ result[field] = val
170
+ desc = get_meta(meta, "requisito.descripcion")
171
+ if desc:
172
+ result["descripcion"] = LiteralStr(desc)
173
+ criterios = parse_pipe_list(get_meta(meta, "requisito.criterios_aceptacion"))
174
+ if criterios:
175
+ result["criterios_aceptacion"] = criterios
176
+ return result
177
+
178
+
179
+ def build_plan_section(meta: dict) -> dict:
180
+ result = {}
181
+ for field in ("id", "nombre", "framework"):
182
+ val = get_meta(meta, f"plan_pruebas.{field}")
183
+ if val:
184
+ result[field] = val
185
+ objetivo = get_meta(meta, "plan_pruebas.objetivo")
186
+ if objetivo:
187
+ result["objetivo"] = LiteralStr(objetivo)
188
+
189
+ # ambientes
190
+ ambientes = []
191
+ i = 0
192
+ while True:
193
+ nombre = get_meta(meta, f"plan_pruebas.ambiente_{i}.nombre")
194
+ if not nombre:
195
+ break
196
+ amb = {"nombre": nombre}
197
+ url = get_meta(meta, f"plan_pruebas.ambiente_{i}.url")
198
+ if url:
199
+ amb["url"] = url
200
+ usuario = get_meta(meta, f"plan_pruebas.ambiente_{i}.usuario")
201
+ if usuario:
202
+ amb["usuario"] = usuario
203
+ amb["validado"] = True
204
+ ambientes.append(amb)
205
+ i += 1
206
+ if ambientes:
207
+ result["ambientes"] = ambientes
208
+
209
+ precondiciones = parse_pipe_list(get_meta(meta, "plan_pruebas.precondiciones"))
210
+ if precondiciones:
211
+ result["precondiciones"] = precondiciones
212
+
213
+ tiempos = {}
214
+ for key, val in meta.items():
215
+ if key.startswith("plan_pruebas.tiempos."):
216
+ t_key = key.replace("plan_pruebas.tiempos.", "")
217
+ tiempos[t_key] = val
218
+ if tiempos:
219
+ result["tiempos_referencia"] = tiempos
220
+
221
+ return result
222
+
223
+
224
+ def build_caso_prueba_section(meta: dict, pasos: list[dict]) -> dict:
225
+ result = {}
226
+ for field in ("id", "nombre", "prioridad", "tipo"):
227
+ val = get_meta(meta, f"caso_prueba.{field}")
228
+ if val:
229
+ result[field] = val
230
+ scripts = parse_pipe_list(get_meta(meta, "caso_prueba.scripts"))
231
+ if scripts:
232
+ result["scripts"] = scripts
233
+ result["pasos"] = pasos
234
+ return result
235
+
236
+
237
+ def build_ejecucion_section(meta: dict, triples: list[tuple[str, str, str]]) -> dict:
238
+ result = {}
239
+ for field in ("id", "nombre"):
240
+ val = get_meta(meta, f"ejecucion.{field}")
241
+ if val:
242
+ result[field] = val
243
+
244
+ # variables (usa triples para capturar duplicados)
245
+ variables = []
246
+ for seccion, campo, valor in triples:
247
+ if seccion == "ejecucion.variable":
248
+ parts = [p.strip() for p in valor.split("|")]
249
+ if len(parts) >= 2:
250
+ var = {"nombre": campo, "descripcion": parts[0]}
251
+ if len(parts) >= 2:
252
+ var["default"] = parts[1]
253
+ variables.append(var)
254
+ if variables:
255
+ result["variables_entorno"] = variables
256
+
257
+ # comandos
258
+ comandos = {}
259
+ for seccion, campo, valor in triples:
260
+ if seccion == "ejecucion.comando":
261
+ comandos[campo] = valor
262
+ if comandos:
263
+ result["comandos"] = comandos
264
+
265
+ return result
266
+
267
+
268
+ def build_integraciones_section(meta: dict) -> dict:
269
+ result = {}
270
+ for key, val in meta.items():
271
+ if key.startswith("integracion."):
272
+ rest = key.replace("integracion.", "")
273
+ parts = rest.split(".", 1)
274
+ if len(parts) == 2:
275
+ integ_name, field = parts
276
+ if integ_name not in result:
277
+ result[integ_name] = {}
278
+ result[integ_name][field] = val
279
+ return result
280
+
281
+
282
+ def main():
283
+ if len(sys.argv) < 2:
284
+ print("Uso: python convert_excel_to_test_design.py <input.xlsx> [output.yml]")
285
+ sys.exit(1)
286
+
287
+ input_path = Path(sys.argv[1])
288
+ if not input_path.exists():
289
+ print(f"ERROR: No se encontro {input_path}")
290
+ sys.exit(1)
291
+
292
+ if len(sys.argv) >= 3:
293
+ output_path = Path(sys.argv[2])
294
+ else:
295
+ output_path = input_path.with_name(
296
+ input_path.stem.replace("test-cases-", "test-design-") + ".yml"
297
+ )
298
+
299
+ wb = load_workbook(input_path, read_only=True, data_only=True)
300
+
301
+ # Validar hojas requeridas
302
+ required = {"Metadata", "Pasos"}
303
+ found = set(wb.sheetnames)
304
+ missing = required - found
305
+ if missing:
306
+ print(f"ERROR: Hojas faltantes: {missing}. Encontradas: {found}")
307
+ sys.exit(1)
308
+
309
+ meta, triples = read_metadata_sheet(wb)
310
+ pasos = read_pasos_sheet(wb)
311
+ wb.close()
312
+
313
+ # Construir YAML
314
+ autor = get_meta(meta, "metadata.autor", "QA Team")
315
+ doc = {}
316
+
317
+ metadata = build_metadata_section(meta)
318
+ if metadata:
319
+ doc["metadata"] = metadata
320
+
321
+ requisito = build_requisito_section(meta)
322
+ if requisito:
323
+ doc["requisito"] = requisito
324
+
325
+ plan = build_plan_section(meta)
326
+ if plan:
327
+ doc["plan_pruebas"] = plan
328
+
329
+ caso = build_caso_prueba_section(meta, pasos)
330
+ doc["caso_prueba"] = caso
331
+
332
+ ejecucion = build_ejecucion_section(meta, triples)
333
+ if ejecucion:
334
+ doc["ejecucion"] = ejecucion
335
+
336
+ integraciones = build_integraciones_section(meta)
337
+ if integraciones:
338
+ doc["integraciones"] = integraciones
339
+
340
+ header = (
341
+ f"# test-design generado desde {input_path.name}\n"
342
+ f"# Input para: AgileTest + Playwright + Agente de Datos\n"
343
+ f"# Autor: {autor}\n\n"
344
+ )
345
+
346
+ with open(output_path, "w", encoding="utf-8") as f:
347
+ f.write(header)
348
+ yaml.dump(
349
+ doc, f,
350
+ default_flow_style=False,
351
+ allow_unicode=True,
352
+ sort_keys=False,
353
+ width=120,
354
+ )
355
+
356
+ print(f"OK: {input_path.name} -> {output_path.name} ({len(pasos)} pasos convertidos)")
357
+
358
+
359
+ if __name__ == "__main__":
360
+ main()