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
|
@@ -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()
|