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