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,412 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ convert_pdf_to_test_design.py — Convierte PDF a test-design YAML
4
+ Lee PDF con secciones [SECCION] (metadata key-value) + tabla de pasos.
5
+ Uso: python convert_pdf_to_test_design.py <input.pdf> [output.yml]
6
+ """
7
+
8
+ import re
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ try:
13
+ import yaml
14
+ except ImportError:
15
+ print("ERROR: pyyaml requerido. Instalar con: pip install pyyaml")
16
+ sys.exit(1)
17
+
18
+ try:
19
+ import pdfplumber
20
+ except ImportError:
21
+ print("ERROR: pdfplumber requerido. Instalar con: pip install pdfplumber")
22
+ sys.exit(1)
23
+
24
+
25
+ # --- YAML formatting helpers ---
26
+
27
+ class LiteralStr(str):
28
+ pass
29
+
30
+
31
+ def literal_representer(dumper, data):
32
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=">")
33
+
34
+
35
+ yaml.add_representer(LiteralStr, literal_representer)
36
+
37
+
38
+ def yaml_str_representer(dumper, data):
39
+ if any(c in data for c in (":", "{", "}", "[", "]", ",", "&", "*", "?", "|", ">", "!", "%", "@", "`")):
40
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style='"')
41
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data)
42
+
43
+
44
+ yaml.add_representer(str, yaml_str_representer)
45
+
46
+
47
+ # --- PDF extraction ---
48
+
49
+ def extract_all_text(pdf) -> str:
50
+ """Extrae todo el texto de paginas sin tabla de pasos."""
51
+ texts = []
52
+ for page in pdf.pages:
53
+ tables = page.extract_tables() or []
54
+ if tables:
55
+ # Pagina con tabla: solo extraer texto antes de [PASOS]
56
+ text = page.extract_text() or ""
57
+ idx = text.find("[PASOS]")
58
+ if idx >= 0:
59
+ texts.append(text[:idx])
60
+ # La tabla se procesa aparte
61
+ else:
62
+ texts.append(page.extract_text() or "")
63
+ return "\n".join(texts)
64
+
65
+
66
+ def extract_steps_table(pdf) -> list[list[str]]:
67
+ """Extrae la tabla de pasos del PDF."""
68
+ for page in pdf.pages:
69
+ tables = page.extract_tables() or []
70
+ for table in tables:
71
+ if table and table[0] and "Paso" in str(table[0][0]):
72
+ return table
73
+ return []
74
+
75
+
76
+ def parse_sections(text: str) -> dict[str, list[str]]:
77
+ """Divide el texto en secciones por marcadores [SECCION]."""
78
+ sections = {}
79
+ current = "_header"
80
+ sections[current] = []
81
+
82
+ for line in text.split("\n"):
83
+ line = line.strip()
84
+ if not line:
85
+ continue
86
+ match = re.match(r"^\[([A-Z_]+)\]$", line)
87
+ if match:
88
+ current = match.group(1).lower()
89
+ sections[current] = []
90
+ else:
91
+ sections[current].append(line)
92
+
93
+ return sections
94
+
95
+
96
+ _KNOWN_KEYS = re.compile(
97
+ r"^("
98
+ r"modulo|version|autor|fecha|stack|"
99
+ r"id|nombre|descripcion|criterios_aceptacion|"
100
+ r"objetivo|framework|precondiciones|"
101
+ r"prioridad|tipo|scripts|"
102
+ r"ambiente_\d+\.\w+|tiempos\.\w+|"
103
+ r"variable\.\w+|comando\.\w+|"
104
+ r"\w+\.\w+" # fallback para keys con punto (integraciones)
105
+ r")\s"
106
+ )
107
+
108
+
109
+ def parse_kv_lines(lines: list[str]) -> list[tuple[str, str]]:
110
+ """Parsea lineas key-value con soporte para continuacion de valores largos."""
111
+ pairs = []
112
+ for line in lines:
113
+ match = re.match(r"^(\S+)\s+(.+)$", line)
114
+ if match and _KNOWN_KEYS.match(line):
115
+ pairs.append((match.group(1), match.group(2).strip()))
116
+ elif pairs:
117
+ # Linea de continuacion: anexar al valor anterior
118
+ prev_key, prev_val = pairs[-1]
119
+ pairs[-1] = (prev_key, prev_val + " " + line.strip())
120
+ return pairs
121
+
122
+
123
+ def clean_cell(val) -> str:
124
+ """Limpia valor de celda PDF, corrigiendo artefactos de word-wrap."""
125
+ if val is None:
126
+ return ""
127
+ text = str(val).replace("\n", " ")
128
+ text = re.sub(r" {2,}", " ", text).strip()
129
+ return fixup_pdf_wordwrap(text)
130
+
131
+
132
+ def fixup_pdf_wordwrap(text: str) -> str:
133
+ """Corrige artefactos comunes de word-wrap en PDFs."""
134
+ # Fix split file extensions: "xxx.sp ec.ts" → "xxx.spec.ts"
135
+ text = re.sub(r"\.sp ec\.ts", ".spec.ts", text)
136
+ # Fix split URL paths: "/BLSurveyDocumentSearch Results/" → sin espacio
137
+ text = re.sub(r"(/[A-Za-z]+) ([A-Za-z]+/)", lambda m: m.group(0).replace(" ", ""), text)
138
+ return text
139
+
140
+
141
+ def parse_pipe_list(value: str) -> list[str]:
142
+ if not value:
143
+ return []
144
+ return [v.strip() for v in value.split("|") if v.strip()]
145
+
146
+
147
+ # --- YAML builders ---
148
+
149
+ def build_metadata(kv: list[tuple[str, str]]) -> dict:
150
+ d = dict(kv)
151
+ result = {}
152
+ for field in ("modulo", "version", "autor", "fecha", "stack"):
153
+ if field in d:
154
+ result[field] = d[field]
155
+ return result
156
+
157
+
158
+ def build_requisito(kv: list[tuple[str, str]]) -> dict:
159
+ d = dict(kv)
160
+ result = {}
161
+ for field in ("id", "nombre"):
162
+ if field in d:
163
+ result[field] = d[field]
164
+ desc = d.get("descripcion", "")
165
+ if desc:
166
+ result["descripcion"] = LiteralStr(desc)
167
+ criterios = parse_pipe_list(d.get("criterios_aceptacion", ""))
168
+ if criterios:
169
+ result["criterios_aceptacion"] = criterios
170
+ return result
171
+
172
+
173
+ def build_plan_pruebas(kv: list[tuple[str, str]]) -> dict:
174
+ d = dict(kv)
175
+ result = {}
176
+ for field in ("id", "nombre", "framework"):
177
+ if field in d:
178
+ result[field] = d[field]
179
+ objetivo = d.get("objetivo", "")
180
+ if objetivo:
181
+ result["objetivo"] = LiteralStr(objetivo)
182
+
183
+ precondiciones = parse_pipe_list(d.get("precondiciones", ""))
184
+ if precondiciones:
185
+ result["precondiciones"] = precondiciones
186
+
187
+ # ambientes
188
+ ambientes = []
189
+ i = 0
190
+ while True:
191
+ nombre = d.get(f"ambiente_{i}.nombre", "")
192
+ if not nombre:
193
+ break
194
+ amb = {"nombre": nombre}
195
+ url = d.get(f"ambiente_{i}.url", "")
196
+ if url:
197
+ amb["url"] = url
198
+ usuario = d.get(f"ambiente_{i}.usuario", "")
199
+ if usuario:
200
+ amb["usuario"] = usuario
201
+ amb["validado"] = True
202
+ ambientes.append(amb)
203
+ i += 1
204
+ if ambientes:
205
+ result["ambientes"] = ambientes
206
+
207
+ # tiempos
208
+ tiempos = {}
209
+ for key, val in kv:
210
+ if key.startswith("tiempos."):
211
+ t_key = key.replace("tiempos.", "")
212
+ tiempos[t_key] = val
213
+ if tiempos:
214
+ result["tiempos_referencia"] = tiempos
215
+
216
+ return result
217
+
218
+
219
+ def build_caso_prueba(kv: list[tuple[str, str]], pasos: list[dict]) -> dict:
220
+ d = dict(kv)
221
+ result = {}
222
+ for field in ("id", "nombre", "prioridad", "tipo"):
223
+ if field in d:
224
+ result[field] = d[field]
225
+ scripts = parse_pipe_list(d.get("scripts", ""))
226
+ if scripts:
227
+ result["scripts"] = scripts
228
+ result["pasos"] = pasos
229
+ return result
230
+
231
+
232
+ def build_ejecucion(kv: list[tuple[str, str]]) -> dict:
233
+ d = dict(kv)
234
+ result = {}
235
+ for field in ("id", "nombre"):
236
+ if field in d:
237
+ result[field] = d[field]
238
+
239
+ variables = []
240
+ comandos = {}
241
+ for key, val in kv:
242
+ if key.startswith("variable."):
243
+ var_name = key.replace("variable.", "")
244
+ parts = [p.strip() for p in val.split("|")]
245
+ var = {"nombre": var_name}
246
+ if len(parts) >= 1:
247
+ var["descripcion"] = parts[0]
248
+ if len(parts) >= 2:
249
+ var["default"] = parts[1]
250
+ variables.append(var)
251
+ elif key.startswith("comando."):
252
+ cmd_name = key.replace("comando.", "")
253
+ comandos[cmd_name] = val
254
+
255
+ if variables:
256
+ result["variables_entorno"] = variables
257
+ if comandos:
258
+ result["comandos"] = comandos
259
+ return result
260
+
261
+
262
+ def build_integraciones(kv: list[tuple[str, str]]) -> dict:
263
+ result = {}
264
+ for key, val in kv:
265
+ parts = key.split(".", 1)
266
+ if len(parts) == 2:
267
+ integ_name, field = parts
268
+ if integ_name not in result:
269
+ result[integ_name] = {}
270
+ result[integ_name][field] = val
271
+ return result
272
+
273
+
274
+ def parse_table_to_steps(table: list[list[str]]) -> list[dict]:
275
+ """Convierte tabla PDF a lista de pasos."""
276
+ if not table or len(table) < 2:
277
+ return []
278
+
279
+ headers_raw = [clean_cell(c).lower() for c in table[0]]
280
+ header_map = {
281
+ "paso": "paso", "nombre": "nombre", "accion": "accion",
282
+ "resultado esperado": "resultado", "datos": "datos",
283
+ "script": "script", "auto": "automatizado",
284
+ "bloqueado": "bloqueado", "nota": "nota",
285
+ }
286
+ headers = [header_map.get(h, h) for h in headers_raw]
287
+
288
+ pasos = []
289
+ for row in table[1:]:
290
+ if not row or not row[0]:
291
+ continue
292
+ row_dict = {}
293
+ for i, val in enumerate(row):
294
+ if i < len(headers):
295
+ row_dict[headers[i]] = clean_cell(val)
296
+
297
+ paso_val = row_dict.get("paso", "").strip()
298
+ if paso_val.isdigit():
299
+ paso_key = int(paso_val)
300
+ else:
301
+ paso_key = paso_val
302
+
303
+ step = {"paso": paso_key}
304
+ step["nombre"] = row_dict.get("nombre", "")
305
+ step["accion"] = row_dict.get("accion", "")
306
+ step["resultado"] = row_dict.get("resultado", "")
307
+
308
+ datos = row_dict.get("datos", "")
309
+ if datos:
310
+ step["datos"] = datos
311
+
312
+ script = row_dict.get("script", "")
313
+ if script:
314
+ step["script"] = script
315
+
316
+ auto_str = row_dict.get("automatizado", "False").strip()
317
+ step["automatizado"] = auto_str.lower() == "true"
318
+
319
+ bloqueado = row_dict.get("bloqueado", "")
320
+ if bloqueado:
321
+ step["bloqueado"] = bloqueado
322
+
323
+ nota = row_dict.get("nota", "")
324
+ if nota:
325
+ step["nota"] = nota
326
+
327
+ pasos.append(step)
328
+
329
+ return pasos
330
+
331
+
332
+ def main():
333
+ if len(sys.argv) < 2:
334
+ print("Uso: python convert_pdf_to_test_design.py <input.pdf> [output.yml]")
335
+ sys.exit(1)
336
+
337
+ input_path = Path(sys.argv[1])
338
+ if not input_path.exists():
339
+ print(f"ERROR: No se encontro {input_path}")
340
+ sys.exit(1)
341
+
342
+ if len(sys.argv) >= 3:
343
+ output_path = Path(sys.argv[2])
344
+ else:
345
+ output_path = input_path.with_name(
346
+ input_path.stem.replace("test-cases-", "test-design-") + ".yml"
347
+ )
348
+
349
+ with pdfplumber.open(input_path) as pdf:
350
+ full_text = extract_all_text(pdf)
351
+ table = extract_steps_table(pdf)
352
+
353
+ sections = parse_sections(full_text)
354
+
355
+ # Parse cada seccion
356
+ meta_kv = parse_kv_lines(sections.get("metadata", []))
357
+ req_kv = parse_kv_lines(sections.get("requisito", []))
358
+ plan_kv = parse_kv_lines(sections.get("plan_pruebas", []))
359
+ caso_kv = parse_kv_lines(sections.get("caso_prueba", []))
360
+ ejec_kv = parse_kv_lines(sections.get("ejecucion", []))
361
+ integ_kv = parse_kv_lines(sections.get("integraciones", []))
362
+
363
+ pasos = parse_table_to_steps(table)
364
+
365
+ # Construir YAML
366
+ autor = dict(meta_kv).get("autor", "QA Team")
367
+ doc = {}
368
+
369
+ metadata = build_metadata(meta_kv)
370
+ if metadata:
371
+ doc["metadata"] = metadata
372
+
373
+ requisito = build_requisito(req_kv)
374
+ if requisito:
375
+ doc["requisito"] = requisito
376
+
377
+ plan = build_plan_pruebas(plan_kv)
378
+ if plan:
379
+ doc["plan_pruebas"] = plan
380
+
381
+ caso = build_caso_prueba(caso_kv, pasos)
382
+ doc["caso_prueba"] = caso
383
+
384
+ ejecucion = build_ejecucion(ejec_kv)
385
+ if ejecucion:
386
+ doc["ejecucion"] = ejecucion
387
+
388
+ integraciones = build_integraciones(integ_kv)
389
+ if integraciones:
390
+ doc["integraciones"] = integraciones
391
+
392
+ header = (
393
+ f"# test-design generado desde {input_path.name}\n"
394
+ f"# Input para: AgileTest + Playwright + Agente de Datos\n"
395
+ f"# Autor: {autor}\n\n"
396
+ )
397
+
398
+ with open(output_path, "w", encoding="utf-8") as f:
399
+ f.write(header)
400
+ yaml.dump(
401
+ doc, f,
402
+ default_flow_style=False,
403
+ allow_unicode=True,
404
+ sort_keys=False,
405
+ width=120,
406
+ )
407
+
408
+ print(f"OK: {input_path.name} -> {output_path.name} ({len(pasos)} pasos convertidos)")
409
+
410
+
411
+ if __name__ == "__main__":
412
+ main()
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ export_yaml_to_csv.py — Exporta test-design YAML a CSV
4
+ Formato CSV:
5
+ - Metadata en lineas comentario (#) con formato: # seccion.campo: valor
6
+ - Pasos como filas CSV estandar
7
+ Uso: python export_yaml_to_csv.py <input.yml> [output.csv]
8
+ """
9
+
10
+ import csv
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
+ def write_meta_line(f, section: str, key: str, value: str):
22
+ """Escribe una linea de metadata como comentario CSV."""
23
+ clean = str(value).replace("\n", " ").strip()
24
+ f.write(f"# {section}.{key}: {clean}\n")
25
+
26
+
27
+ def export_metadata(f, data: dict):
28
+ """Exporta todas las secciones de metadata como comentarios."""
29
+ # metadata
30
+ meta = data.get("metadata", {})
31
+ for key in ("modulo", "version", "autor", "fecha", "stack"):
32
+ if key in meta:
33
+ write_meta_line(f, "metadata", key, meta[key])
34
+
35
+ # requisito
36
+ req = data.get("requisito", {})
37
+ for key in ("id", "nombre", "descripcion"):
38
+ if key in req:
39
+ write_meta_line(f, "requisito", key, req[key])
40
+ criterios = req.get("criterios_aceptacion", [])
41
+ if criterios:
42
+ write_meta_line(f, "requisito", "criterios_aceptacion", " | ".join(criterios))
43
+
44
+ # plan_pruebas
45
+ plan = data.get("plan_pruebas", {})
46
+ for key in ("id", "nombre", "objetivo", "framework"):
47
+ if key in plan:
48
+ write_meta_line(f, "plan_pruebas", key, plan[key])
49
+ precondiciones = plan.get("precondiciones", [])
50
+ if precondiciones:
51
+ write_meta_line(f, "plan_pruebas", "precondiciones", " | ".join(precondiciones))
52
+ ambientes = plan.get("ambientes", [])
53
+ for i, amb in enumerate(ambientes):
54
+ prefix = f"plan_pruebas.ambiente_{i}"
55
+ for key in ("nombre", "url", "usuario"):
56
+ if key in amb:
57
+ f.write(f"# {prefix}.{key}: {amb[key]}\n")
58
+ tiempos = plan.get("tiempos_referencia", {})
59
+ for key, val in tiempos.items():
60
+ write_meta_line(f, "plan_pruebas.tiempos", key, val)
61
+
62
+ # caso_prueba (sin pasos)
63
+ caso = data.get("caso_prueba", {})
64
+ for key in ("id", "nombre", "prioridad", "tipo"):
65
+ if key in caso:
66
+ write_meta_line(f, "caso_prueba", key, caso[key])
67
+ scripts = caso.get("scripts", [])
68
+ if scripts:
69
+ write_meta_line(f, "caso_prueba", "scripts", " | ".join(scripts))
70
+
71
+ # ejecucion
72
+ ejec = data.get("ejecucion", {})
73
+ for key in ("id", "nombre"):
74
+ if key in ejec:
75
+ write_meta_line(f, "ejecucion", key, ejec[key])
76
+ variables = ejec.get("variables_entorno", [])
77
+ for var in variables:
78
+ nombre = var.get("nombre", "")
79
+ desc = var.get("descripcion", "")
80
+ default = var.get("default", "")
81
+ f.write(f"# ejecucion.variable: {nombre} | {desc} | {default}\n")
82
+ comandos = ejec.get("comandos", {})
83
+ for key, val in comandos.items():
84
+ write_meta_line(f, "ejecucion.comando", key, val)
85
+
86
+ # integraciones
87
+ integ = data.get("integraciones", {})
88
+ for nombre_integ, info in integ.items():
89
+ for key, val in info.items():
90
+ write_meta_line(f, f"integracion.{nombre_integ}", key, val)
91
+
92
+
93
+ def export_steps(f, data: dict):
94
+ """Exporta los pasos como filas CSV."""
95
+ pasos = data.get("caso_prueba", {}).get("pasos", [])
96
+ if not pasos:
97
+ print("WARN: No se encontraron pasos en caso_prueba.pasos")
98
+ return
99
+
100
+ headers = [
101
+ "paso", "nombre", "accion", "resultado_esperado",
102
+ "datos", "script", "automatizado", "bloqueado", "nota"
103
+ ]
104
+ writer = csv.DictWriter(f, fieldnames=headers, quoting=csv.QUOTE_MINIMAL)
105
+ writer.writeheader()
106
+
107
+ for p in pasos:
108
+ writer.writerow({
109
+ "paso": p.get("paso", ""),
110
+ "nombre": p.get("nombre", ""),
111
+ "accion": p.get("accion", ""),
112
+ "resultado_esperado": p.get("resultado", ""),
113
+ "datos": p.get("datos", ""),
114
+ "script": p.get("script", ""),
115
+ "automatizado": str(p.get("automatizado", False)).lower(),
116
+ "bloqueado": p.get("bloqueado", ""),
117
+ "nota": p.get("nota", ""),
118
+ })
119
+
120
+
121
+ def main():
122
+ if len(sys.argv) < 2:
123
+ print("Uso: python export_yaml_to_csv.py <input.yml> [output.csv]")
124
+ sys.exit(1)
125
+
126
+ input_path = Path(sys.argv[1])
127
+ if not input_path.exists():
128
+ print(f"ERROR: No se encontro {input_path}")
129
+ sys.exit(1)
130
+
131
+ if len(sys.argv) >= 3:
132
+ output_path = Path(sys.argv[2])
133
+ else:
134
+ output_path = input_path.with_suffix(".csv")
135
+
136
+ with open(input_path, "r", encoding="utf-8") as f:
137
+ data = yaml.safe_load(f)
138
+
139
+ with open(output_path, "w", encoding="utf-8", newline="") as f:
140
+ export_metadata(f, data)
141
+ f.write("#\n") # separador visual
142
+ export_steps(f, data)
143
+
144
+ n_pasos = len(data.get("caso_prueba", {}).get("pasos", []))
145
+ print(f"OK: {input_path.name} -> {output_path.name} ({n_pasos} pasos exportados)")
146
+
147
+
148
+ if __name__ == "__main__":
149
+ main()